From b04ac03739f090427f6c6e489ac2a5fb27304039 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Tue, 3 Feb 2026 08:53:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(deployments):=20deployment=20restart=20?= =?UTF-8?q?=C3=B6zelli=C4=9Fi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deployment projeleri için yeniden başlatma (restart) yeteneği eklendi. Backend servisi, API endpoint'i ve kullanıcı arayüzü butonları güncellendi. --- backend/src/routes/deployments.ts | 14 ++++ backend/src/services/deploymentService.ts | 91 +++++++++++++++++++++ frontend/src/api/deployments.ts | 4 + frontend/src/pages/DeploymentDetailPage.tsx | 22 ++++- frontend/src/pages/DeploymentsPage.tsx | 25 +++++- 5 files changed, 154 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/deployments.ts b/backend/src/routes/deployments.ts index 71bed75..9aa800f 100644 --- a/backend/src/routes/deployments.ts +++ b/backend/src/routes/deployments.ts @@ -232,4 +232,18 @@ router.post("/:id/run", async (req, res) => { }); }); +router.post("/:id/restart", async (req, res) => { + authMiddleware(req, res, async () => { + const { id } = req.params; + const project = await DeploymentProject.findById(id); + if (!project) return res.status(404).json({ message: "Deployment bulunamadı" }); + const rawMessage = typeof req.body?.message === "string" ? req.body.message.trim() : ""; + const message = rawMessage || "restart"; + deploymentService + .restartDeployment(id, { message }) + .catch(() => undefined); + return res.json({ queued: true }); + }); +}); + export default router; diff --git a/backend/src/services/deploymentService.ts b/backend/src/services/deploymentService.ts index ccf875a..7eb0d48 100644 --- a/backend/src/services/deploymentService.ts +++ b/backend/src/services/deploymentService.ts @@ -706,6 +706,97 @@ class DeploymentService { } } + async restartDeployment(projectId: string, options?: { message?: string }) { + if (this.running.get(projectId)) { + return; + } + this.running.set(projectId, true); + + const project = await DeploymentProject.findById(projectId); + if (!project) { + this.running.delete(projectId); + return; + } + + const normalizedMessage = normalizeCommitMessage(options?.message); + const startedAt = Date.now(); + const runLogs: string[] = []; + const pushLog = (line: string) => { + runLogs.push(line); + this.emitLog(projectId, line); + }; + + const runDoc = await DeploymentRun.create({ + project: projectId, + status: "running", + startedAt: new Date(), + message: normalizedMessage ?? options?.message + }); + this.emitRun(projectId, runDoc); + await writeRunFile(project.rootPath, runDoc); + + await DeploymentProject.findByIdAndUpdate(projectId, { + lastStatus: "running", + lastMessage: normalizedMessage ?? options?.message ?? "Restart başlıyor..." + }); + await this.emitStatus(projectId, { + lastStatus: "running", + lastMessage: normalizedMessage ?? options?.message ?? "Restart başlıyor..." + } as DeploymentProjectDocument); + + try { + pushLog("Restart komutları çalıştırılıyor..."); + await runCompose(project, (line) => pushLog(line)); + const duration = Date.now() - startedAt; + await DeploymentProject.findByIdAndUpdate(projectId, { + lastStatus: "success", + lastDeployAt: new Date(), + lastMessage: normalizedMessage ?? options?.message ?? "Restart başarılı" + }); + await this.emitStatus(projectId, { + lastStatus: "success", + lastDeployAt: new Date(), + lastMessage: normalizedMessage ?? options?.message ?? "Restart başarılı" + } as DeploymentProjectDocument); + await DeploymentRun.findByIdAndUpdate(runDoc._id, { + status: "success", + finishedAt: new Date(), + durationMs: duration, + logs: runLogs, + message: normalizedMessage ?? options?.message + }); + const updatedRun = await DeploymentRun.findById(runDoc._id); + if (updatedRun) this.emitRun(projectId, updatedRun); + if (updatedRun) await writeRunFile(project.rootPath, updatedRun); + pushLog("Restart tamamlandı: Başarılı"); + } catch (err) { + const duration = Date.now() - startedAt; + await DeploymentProject.findByIdAndUpdate(projectId, { + lastStatus: "failed", + lastDeployAt: new Date(), + lastMessage: (err as Error).message + }); + await this.emitStatus(projectId, { + lastStatus: "failed", + lastDeployAt: new Date(), + lastMessage: (err as Error).message + } as DeploymentProjectDocument); + await DeploymentRun.findByIdAndUpdate(runDoc._id, { + status: "failed", + finishedAt: new Date(), + durationMs: duration, + logs: runLogs, + message: normalizedMessage ?? options?.message + }); + const updatedRun = await DeploymentRun.findById(runDoc._id); + if (updatedRun) this.emitRun(projectId, updatedRun); + if (updatedRun) await writeRunFile(project.rootPath, updatedRun); + pushLog(`Hata: ${(err as Error).message}`); + } finally { + this.running.delete(projectId); + } + } + async cleanupProjectResources(project: DeploymentProjectDocument) { const composePath = path.join(project.rootPath, project.composeFile); if (!fs.existsSync(composePath)) { diff --git a/frontend/src/api/deployments.ts b/frontend/src/api/deployments.ts index b8f61c3..91b049c 100644 --- a/frontend/src/api/deployments.ts +++ b/frontend/src/api/deployments.ts @@ -96,6 +96,10 @@ export async function runDeployment(id: string, message?: string): Promise await apiClient.post(`/deployments/${id}/run`, message ? { message } : {}); } +export async function restartDeployment(id: string, message?: string): Promise { + await apiClient.post(`/deployments/${id}/restart`, message ? { message } : {}); +} + export async function fetchDeployment(id: string): Promise { const { data } = await apiClient.get(`/deployments/${id}`); return data as DeploymentDetailResponse; diff --git a/frontend/src/pages/DeploymentDetailPage.tsx b/frontend/src/pages/DeploymentDetailPage.tsx index caab574..5366e92 100644 --- a/frontend/src/pages/DeploymentDetailPage.tsx +++ b/frontend/src/pages/DeploymentDetailPage.tsx @@ -7,7 +7,8 @@ import { faCopy, faEye, faEyeSlash, - faHistory + faHistory, + faRotate } from "@fortawesome/free-solid-svg-icons"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; @@ -25,6 +26,7 @@ import { fetchDeploymentBranches, fetchDeploymentComposeFiles, fetchDeploymentEnvExamples, + restartDeployment, runDeployment, updateDeployment } from "../api/deployments"; @@ -49,6 +51,7 @@ export function DeploymentDetailPage() { const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); const [triggering, setTriggering] = useState(false); + const [restarting, setRestarting] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ @@ -169,6 +172,19 @@ export function DeploymentDetailPage() { } }; + const handleRestart = async () => { + if (!id) return; + setRestarting(true); + try { + await restartDeployment(id, "restart"); + toast.success("Restart tetiklendi"); + } catch { + toast.error("Restart tetiklenemedi"); + } finally { + setRestarting(false); + } + }; + useEffect(() => { const repoUrl = form.repoUrl.trim(); if (!repoUrl) { @@ -341,6 +357,10 @@ export function DeploymentDetailPage() { > Düzenle + +