- Socket.IO tabanlı gerçek zamanlı deployment log ve durum bildirimleri ekle - deployment:subscribe ve deployment:unsubscribe soket olaylarını destekle - DeploymentService'e anlık durum ve log yayınlama özelliği ekle - Deployment silinirken docker kaynaklarını temizle - Ortam değişkenlerini tek bir .env.example dosyasında birleştir - Docker compose yapılandırmasını güncelle (PWD ve DEPLOYMENTS_ROOT kullan) - Repo URL'sinden proje adını otomatik öner - Güvensiz bağlamlar için clipboard kopya fallback mekanizması ekle - Socket.IO path'ini /api/socket.io olarak ayarla
207 lines
6.4 KiB
TypeScript
207 lines
6.4 KiB
TypeScript
import { Router } from "express";
|
||
import fs from "fs";
|
||
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("/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 } = 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
|
||
});
|
||
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 } = 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
|
||
});
|
||
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ı" });
|
||
deploymentService
|
||
.runDeployment(id, { message: "Elle deploy tetikleme" })
|
||
.catch(() => undefined);
|
||
return res.json({ queued: true });
|
||
});
|
||
});
|
||
|
||
export default router;
|