Deployment detay sayfasında düzenleme modalı eklendi. Repo URL, branch, compose dosyası ve environment değişkenleri inline düzenlenebilir hale getirildi. Deploy tetikleme işlemi için özel mesaj parametresi desteği eklendi. Düzenleme sonrası otomatik deploy tetikleme özelliği aktif edildi.
228 lines
7.3 KiB
TypeScript
228 lines
7.3 KiB
TypeScript
import { Router } from "express";
|
||
import path from "path";
|
||
import { authMiddleware } from "../middleware/authMiddleware.js";
|
||
import { deploymentService } from "../services/deploymentService.js";
|
||
import { DeploymentProject } from "../models/deploymentProject.js";
|
||
import { DeploymentRun } from "../models/deploymentRun.js";
|
||
import fs from "fs";
|
||
|
||
const router = Router();
|
||
|
||
const faviconCandidates = [
|
||
"favicon.ico",
|
||
"public/favicon.ico",
|
||
"public/favicon.png",
|
||
"public/favicon.svg",
|
||
"assets/favicon.ico"
|
||
];
|
||
|
||
function getContentType(filePath: string) {
|
||
if (filePath.endsWith(".svg")) return "image/svg+xml";
|
||
if (filePath.endsWith(".png")) return "image/png";
|
||
return "image/x-icon";
|
||
}
|
||
|
||
router.get("/:id/favicon", async (req, res) => {
|
||
const { id } = req.params;
|
||
const project = await DeploymentProject.findById(id).lean();
|
||
if (!project) return res.status(404).end();
|
||
const rootPath = path.resolve(project.rootPath);
|
||
|
||
for (const candidate of faviconCandidates) {
|
||
const filePath = path.join(rootPath, candidate);
|
||
if (!fs.existsSync(filePath)) continue;
|
||
res.setHeader("Content-Type", getContentType(filePath));
|
||
res.setHeader("Cache-Control", "public, max-age=300");
|
||
return fs.createReadStream(filePath).pipe(res);
|
||
}
|
||
|
||
return res.status(404).end();
|
||
});
|
||
|
||
router.get("/branches", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const repoUrl = req.query.repoUrl as string | undefined;
|
||
if (!repoUrl) {
|
||
return res.status(400).json({ message: "repoUrl gerekli" });
|
||
}
|
||
try {
|
||
const branches = await deploymentService.listRemoteBranches(repoUrl);
|
||
return res.json({ branches });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Branch listesi alınamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.get("/compose-files", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const repoUrl = req.query.repoUrl as string | undefined;
|
||
const branch = req.query.branch as string | undefined;
|
||
if (!repoUrl || !branch) {
|
||
return res.status(400).json({ message: "repoUrl ve branch gerekli" });
|
||
}
|
||
try {
|
||
const files = await deploymentService.listRemoteComposeFiles(repoUrl, branch);
|
||
return res.json({ files });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Compose listesi alınamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.get("/env-examples", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const repoUrl = req.query.repoUrl as string | undefined;
|
||
const branch = req.query.branch as string | undefined;
|
||
if (!repoUrl || !branch) {
|
||
return res.status(400).json({ message: "repoUrl ve branch gerekli" });
|
||
}
|
||
try {
|
||
const examples = await deploymentService.listRemoteEnvExamples(repoUrl, branch);
|
||
return res.json({ examples });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Env example alınamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.get("/metrics/summary", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const since = new Date();
|
||
since.setDate(since.getDate() - 7);
|
||
|
||
const dailyStats = await DeploymentRun.aggregate([
|
||
{ $match: { startedAt: { $gte: since } } },
|
||
{
|
||
$group: {
|
||
_id: { $dateToString: { format: "%Y-%m-%d", date: "$startedAt" } },
|
||
total: { $sum: 1 },
|
||
success: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$status", "success"] }, 1, 0]
|
||
}
|
||
},
|
||
failed: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$status", "failed"] }, 1, 0]
|
||
}
|
||
},
|
||
avgDurationMs: { $avg: "$durationMs" }
|
||
}
|
||
},
|
||
{ $sort: { _id: 1 } }
|
||
]);
|
||
|
||
const recentRuns = await DeploymentRun.find()
|
||
.sort({ startedAt: -1 })
|
||
.limit(10)
|
||
.populate("project", "name repoUrl rootPath")
|
||
.lean();
|
||
return res.json({ recentRuns, dailyStats });
|
||
});
|
||
});
|
||
|
||
router.get("/", async (_req, res) => {
|
||
authMiddleware(_req, res, async () => {
|
||
const projects = await DeploymentProject.find().sort({ createdAt: -1 }).lean();
|
||
return res.json(projects);
|
||
});
|
||
});
|
||
|
||
router.get("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
const project = await DeploymentProject.findById(id).lean();
|
||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
const runs = await DeploymentRun.find({ project: id })
|
||
.sort({ startedAt: -1 })
|
||
.limit(20)
|
||
.lean();
|
||
return res.json({ project, runs });
|
||
});
|
||
});
|
||
|
||
router.post("/", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { name, repoUrl, branch, composeFile, port, envContent, envExampleName } = req.body;
|
||
if (!name || !repoUrl || !branch || !composeFile) {
|
||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||
}
|
||
try {
|
||
const created = await deploymentService.createProject({
|
||
name,
|
||
repoUrl,
|
||
branch,
|
||
composeFile,
|
||
port,
|
||
envContent,
|
||
envExampleName
|
||
});
|
||
deploymentService
|
||
.runDeployment(created._id.toString(), { message: "First deployment" })
|
||
.catch(() => undefined);
|
||
return res.status(201).json(created);
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment oluşturulamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.put("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
const { name, repoUrl, branch, composeFile, port, envContent, envExampleName } = req.body;
|
||
if (!name || !repoUrl || !branch || !composeFile) {
|
||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||
}
|
||
try {
|
||
const updated = await deploymentService.updateProject(id, {
|
||
name,
|
||
repoUrl,
|
||
branch,
|
||
composeFile,
|
||
port,
|
||
envContent,
|
||
envExampleName
|
||
});
|
||
if (!updated) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
return res.json(updated);
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment güncellenemedi", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.delete("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
try {
|
||
const project = await DeploymentProject.findById(id);
|
||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
await deploymentService.cleanupProjectResources(project);
|
||
await DeploymentProject.findByIdAndDelete(id);
|
||
await DeploymentRun.deleteMany({ project: id });
|
||
await fs.promises.rm(project.rootPath, { recursive: true, force: true });
|
||
return res.json({ success: true });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment silinemedi", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.post("/:id/run", 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 || "Elle deploy tetikleme";
|
||
deploymentService
|
||
.runDeployment(id, { message })
|
||
.catch(() => undefined);
|
||
return res.json({ queued: true });
|
||
});
|
||
});
|
||
|
||
export default router;
|