feat(deployments): docker tabanlı proje yönetim ve otomatik deploy sistemi ekle
Docker Compose projeleri için tam kapsamlı yönetim paneli ve otomatik deployment altyapısı eklendi. Sistem özellikleri: - Belirtilen root dizin altındaki docker-compose dosyası içeren projeleri tarama - Git repo bağlantısı ile branch yönetimi ve klonlama/pull işlemleri - Docker compose up/down komutları ile otomatik deploy - Gitea webhook entegrasyonu ile commit bazlı tetikleme - Deploy geçmişi, log kayıtları ve durum takibi (running/success/failed) - Deploy metrikleri ve dashboard görselleştirmesi - Webhook token ve secret yönetimi ile güvenlik - Proje favicon servisi Teknik değişiklikler: - Backend: deploymentProject, deploymentRun ve settings modelleri eklendi - Backend: deploymentService ile git ve docker işlemleri otomatize edildi - Backend: webhook doğrulaması için signature kontrolü eklendi - Docker: docker-cli ve docker-compose bağımlılıkları eklendi - Frontend: deployments ve settings sayfaları eklendi - Frontend: dashboard'a deploy metrikleri ve aktivite akışı eklendi - API: /api/deployments ve /api/settings yolları eklendi
This commit is contained in:
193
backend/src/routes/deployments.ts
Normal file
193
backend/src/routes/deployments.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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";
|
||||
|
||||
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("/scan", async (req, res) => {
|
||||
authMiddleware(req, res, async () => {
|
||||
try {
|
||||
const candidates = await deploymentService.scanRoot();
|
||||
return res.json(candidates);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ message: "Root taraması yapılamadı" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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("/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, rootPath, repoUrl, branch, composeFile, port } = req.body;
|
||||
if (!name || !rootPath || !repoUrl || !branch || !composeFile) {
|
||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||
}
|
||||
try {
|
||||
const created = await deploymentService.createProject({
|
||||
name,
|
||||
rootPath,
|
||||
repoUrl,
|
||||
branch,
|
||||
composeFile,
|
||||
port
|
||||
});
|
||||
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 deleted = await DeploymentProject.findByIdAndDelete(id);
|
||||
if (!deleted) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||||
await DeploymentRun.deleteMany({ project: id });
|
||||
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).catch(() => undefined);
|
||||
return res.json({ queued: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user