feat(backend): dosya sistemi tabanlı veri kalıcılığı ekle
Deployment ve job verilerinin dosya sisteminde JSON formatında saklanması ve uygulama başladığında bu verilerin otomatik olarak yüklenmesi özelliği eklendi. - Deployment ve job metadata'ları dosya sisteminde saklanır - Run geçmişi dosya sisteminde JSON olarak tutulur - Uygulama başlangıcında dosya sistemi taranır ve eksik veriler yüklenir - Git'ten repo URL ve branch bilgileri çıkarılabilir - Commit mesajları normalize edilir - Ayarlar (webhook token/secret) dosya sisteminde saklanır
This commit is contained in:
@@ -126,7 +126,10 @@ async function start() {
|
|||||||
try {
|
try {
|
||||||
await mongoose.connect(config.mongoUri);
|
await mongoose.connect(config.mongoUri);
|
||||||
console.log("MongoDB'ye bağlanıldı");
|
console.log("MongoDB'ye bağlanıldı");
|
||||||
|
await jobService.bootstrapFromFilesystem();
|
||||||
await jobService.bootstrap();
|
await jobService.bootstrap();
|
||||||
|
await deploymentService.normalizeExistingCommitMessages();
|
||||||
|
await deploymentService.bootstrapFromFilesystem();
|
||||||
|
|
||||||
server.listen(config.port, () => {
|
server.listen(config.port, () => {
|
||||||
console.log(`Sunucu ${config.port} portunda çalışıyor`);
|
console.log(`Sunucu ${config.port} portunda çalışıyor`);
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ router.get("/metrics/summary", async (req, res) => {
|
|||||||
|
|
||||||
router.get("/", async (_req, res) => {
|
router.get("/", async (_req, res) => {
|
||||||
authMiddleware(_req, res, async () => {
|
authMiddleware(_req, res, async () => {
|
||||||
const projects = await DeploymentProject.find().sort({ createdAt: -1 }).lean();
|
let projects = await DeploymentProject.find().sort({ createdAt: -1 }).lean();
|
||||||
|
if (projects.length === 0) {
|
||||||
|
await deploymentService.bootstrapFromFilesystem();
|
||||||
|
projects = await DeploymentProject.find().sort({ createdAt: -1 }).lean();
|
||||||
|
}
|
||||||
return res.json(projects);
|
return res.json(projects);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -216,7 +220,7 @@ router.post("/:id/run", async (req, res) => {
|
|||||||
const project = await DeploymentProject.findById(id);
|
const project = await DeploymentProject.findById(id);
|
||||||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||||||
const rawMessage = typeof req.body?.message === "string" ? req.body.message.trim() : "";
|
const rawMessage = typeof req.body?.message === "string" ? req.body.message.trim() : "";
|
||||||
const message = rawMessage || "Elle deploy tetikleme";
|
const message = rawMessage || "manual deploy trigger";
|
||||||
deploymentService
|
deploymentService
|
||||||
.runDeployment(id, { message })
|
.runDeployment(id, { message })
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ const router = Router();
|
|||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
router.get("/", async (_req, res) => {
|
router.get("/", async (_req, res) => {
|
||||||
const jobs = await Job.find().sort({ createdAt: -1 }).lean();
|
let jobs = await Job.find().sort({ createdAt: -1 }).lean();
|
||||||
|
if (jobs.length === 0) {
|
||||||
|
await jobService.bootstrapFromFilesystem();
|
||||||
|
jobs = await Job.find().sort({ createdAt: -1 }).lean();
|
||||||
|
}
|
||||||
const counts = await JobRun.aggregate([
|
const counts = await JobRun.aggregate([
|
||||||
{ $group: { _id: "$job", runCount: { $sum: 1 } } }
|
{ $group: { _id: "$job", runCount: { $sum: 1 } } }
|
||||||
]);
|
]);
|
||||||
@@ -87,6 +91,7 @@ router.post("/", async (req, res) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const job = await Job.create({ name, repoUrl, testCommand, checkValue, checkUnit });
|
const job = await Job.create({ name, repoUrl, testCommand, checkValue, checkUnit });
|
||||||
|
await jobService.persistMetadata(job);
|
||||||
jobService.scheduleJob(job);
|
jobService.scheduleJob(job);
|
||||||
// Yeni job oluşturulduğunda ilk test otomatik tetiklensin
|
// Yeni job oluşturulduğunda ilk test otomatik tetiklensin
|
||||||
jobService.runJob(job._id.toString()).catch(() => undefined);
|
jobService.runJob(job._id.toString()).catch(() => undefined);
|
||||||
@@ -106,6 +111,7 @@ router.put("/:id", async (req, res) => {
|
|||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
);
|
);
|
||||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||||
|
await jobService.persistMetadata(job);
|
||||||
jobService.scheduleJob(job);
|
jobService.scheduleJob(job);
|
||||||
return res.json(job);
|
return res.json(job);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router, Request } from "express";
|
import { Router, Request } from "express";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { deploymentService } from "../services/deploymentService.js";
|
import { deploymentService, normalizeCommitMessage } from "../services/deploymentService.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -18,6 +18,12 @@ function verifySignature(rawBody: Buffer, secret: string, signature: string) {
|
|||||||
return crypto.timingSafeEqual(Buffer.from(cleaned), Buffer.from(expected));
|
return crypto.timingSafeEqual(Buffer.from(cleaned), Buffer.from(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBranch(value: string | undefined) {
|
||||||
|
const raw = (value || "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
return raw.startsWith("refs/heads/") ? raw.replace("refs/heads/", "") : raw;
|
||||||
|
}
|
||||||
|
|
||||||
router.post("/api/deployments/webhook/:token", async (req, res) => {
|
router.post("/api/deployments/webhook/:token", async (req, res) => {
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
const settings = await deploymentService.ensureSettings();
|
const settings = await deploymentService.ensureSettings();
|
||||||
@@ -46,14 +52,16 @@ router.post("/api/deployments/webhook/:token", async (req, res) => {
|
|||||||
|
|
||||||
const payload = req.body as { ref?: string; head_commit?: { message?: string }; commits?: Array<{ message?: string }> };
|
const payload = req.body as { ref?: string; head_commit?: { message?: string }; commits?: Array<{ message?: string }> };
|
||||||
const ref = payload?.ref || "";
|
const ref = payload?.ref || "";
|
||||||
const branch = ref.startsWith("refs/heads/") ? ref.replace("refs/heads/", "") : ref;
|
const branch = normalizeBranch(ref);
|
||||||
const commitMessage =
|
const commitMessageRaw =
|
||||||
payload?.head_commit?.message || payload?.commits?.[payload.commits.length - 1]?.message;
|
payload?.head_commit?.message || payload?.commits?.[payload.commits.length - 1]?.message;
|
||||||
|
const commitMessage = normalizeCommitMessage(commitMessageRaw);
|
||||||
|
|
||||||
const project = await deploymentService.findByWebhookToken(token);
|
const project = await deploymentService.findByWebhookToken(token);
|
||||||
if (!project) return res.status(404).json({ message: "Deployment bulunamadi" });
|
if (!project) return res.status(404).json({ message: "Deployment bulunamadi" });
|
||||||
|
|
||||||
if (branch && branch !== project.branch) {
|
const projectBranch = normalizeBranch(project.branch);
|
||||||
|
if (projectBranch && projectBranch !== "*" && branch && branch !== projectBranch) {
|
||||||
return res.json({ ignored: true });
|
return res.json({ ignored: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,191 @@ import {
|
|||||||
ComposeFile,
|
ComposeFile,
|
||||||
DeploymentEnv
|
DeploymentEnv
|
||||||
} from "../models/deploymentProject.js";
|
} from "../models/deploymentProject.js";
|
||||||
import { DeploymentRun } from "../models/deploymentRun.js";
|
import { DeploymentRun, DeploymentRunDocument } from "../models/deploymentRun.js";
|
||||||
import { Settings } from "../models/settings.js";
|
import { Settings } from "../models/settings.js";
|
||||||
|
|
||||||
const composeFileCandidates: ComposeFile[] = ["docker-compose.yml", "docker-compose.dev.yml"];
|
const composeFileCandidates: ComposeFile[] = ["docker-compose.yml", "docker-compose.dev.yml"];
|
||||||
|
|
||||||
const deploymentsRoot = config.deploymentsRoot;
|
const deploymentsRoot = config.deploymentsRoot;
|
||||||
|
const metadataFileName = ".wisecolt-ci.json";
|
||||||
|
const settingsFileName = ".wisecolt-ci-settings.json";
|
||||||
|
const runsDirName = ".wisecolt-ci-runs";
|
||||||
|
|
||||||
|
export function normalizeCommitMessage(message?: string) {
|
||||||
|
if (!message) return undefined;
|
||||||
|
const firstLine = message.split(/\r?\n/)[0]?.trim();
|
||||||
|
return firstLine || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeploymentMetadata = {
|
||||||
|
name: string;
|
||||||
|
repoUrl: string;
|
||||||
|
branch: string;
|
||||||
|
composeFile: ComposeFile;
|
||||||
|
webhookToken: string;
|
||||||
|
env: DeploymentEnv;
|
||||||
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingsMetadata = {
|
||||||
|
webhookToken: string;
|
||||||
|
webhookSecret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StoredRun = {
|
||||||
|
status: "running" | "success" | "failed";
|
||||||
|
message?: string;
|
||||||
|
logs: string[];
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readMetadata(repoDir: string): Promise<DeploymentMetadata | null> {
|
||||||
|
const filePath = path.join(repoDir, metadataFileName);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(filePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as DeploymentMetadata;
|
||||||
|
if (!parsed?.repoUrl || !parsed?.composeFile) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeMetadata(repoDir: string, data: DeploymentMetadata) {
|
||||||
|
const filePath = path.join(repoDir, metadataFileName);
|
||||||
|
const payload = JSON.stringify(data, null, 2);
|
||||||
|
await fs.promises.writeFile(filePath, payload, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRunsDir(repoDir: string) {
|
||||||
|
return path.join(repoDir, runsDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeRun(run: DeploymentRunDocument) {
|
||||||
|
return {
|
||||||
|
status: run.status,
|
||||||
|
message: run.message,
|
||||||
|
logs: run.logs || [],
|
||||||
|
startedAt: new Date(run.startedAt).toISOString(),
|
||||||
|
finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : undefined,
|
||||||
|
durationMs: run.durationMs,
|
||||||
|
createdAt: new Date(run.createdAt).toISOString(),
|
||||||
|
updatedAt: new Date(run.updatedAt).toISOString()
|
||||||
|
} satisfies StoredRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRunFile(repoDir: string, run: DeploymentRunDocument) {
|
||||||
|
const dir = getRunsDir(repoDir);
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
const data = serializeRun(run);
|
||||||
|
const name = `${new Date(data.startedAt).getTime()}-${run._id.toString()}.json`;
|
||||||
|
const filePath = path.join(dir, name);
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStoredRuns(repoDir: string): Promise<StoredRun[]> {
|
||||||
|
const dir = getRunsDir(repoDir);
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
const entries = await fs.promises.readdir(dir);
|
||||||
|
const items: StoredRun[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.endsWith(".json")) continue;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(path.join(dir, entry), "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as StoredRun;
|
||||||
|
if (!parsed?.startedAt || !parsed?.status) continue;
|
||||||
|
items.push(parsed);
|
||||||
|
} catch {
|
||||||
|
// ignore invalid file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSettingsFile(): Promise<SettingsMetadata | null> {
|
||||||
|
const filePath = path.join(deploymentsRoot, settingsFileName);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(filePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as SettingsMetadata;
|
||||||
|
if (!parsed?.webhookToken || !parsed?.webhookSecret) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSettingsFile(data: SettingsMetadata) {
|
||||||
|
await fs.promises.mkdir(deploymentsRoot, { recursive: true });
|
||||||
|
const filePath = path.join(deploymentsRoot, settingsFileName);
|
||||||
|
const payload = JSON.stringify(data, null, 2);
|
||||||
|
await fs.promises.writeFile(filePath, payload, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferComposeFile(repoDir: string): ComposeFile | null {
|
||||||
|
const prod = path.join(repoDir, "docker-compose.yml");
|
||||||
|
if (fs.existsSync(prod)) return "docker-compose.yml";
|
||||||
|
const dev = path.join(repoDir, "docker-compose.dev.yml");
|
||||||
|
if (fs.existsSync(dev)) return "docker-compose.dev.yml";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inferRepoUrlFromGit(repoDir: string): Promise<string | null> {
|
||||||
|
const gitConfig = path.join(repoDir, ".git", "config");
|
||||||
|
if (!fs.existsSync(gitConfig)) return null;
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(gitConfig, "utf8");
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
let inOrigin = false;
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith("[remote \"")) {
|
||||||
|
inOrigin = trimmed === "[remote \"origin\"]";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inOrigin) continue;
|
||||||
|
if (trimmed.startsWith("url")) {
|
||||||
|
const parts = trimmed.split("=");
|
||||||
|
const value = parts.slice(1).join("=").trim();
|
||||||
|
return value || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inferBranchFromGit(repoDir: string): Promise<string | null> {
|
||||||
|
const headPath = path.join(repoDir, ".git", "HEAD");
|
||||||
|
if (!fs.existsSync(headPath)) return null;
|
||||||
|
try {
|
||||||
|
const head = (await fs.promises.readFile(headPath, "utf8")).trim();
|
||||||
|
if (!head.startsWith("ref:")) return null;
|
||||||
|
const ref = head.replace("ref:", "").trim();
|
||||||
|
const prefix = "refs/heads/";
|
||||||
|
if (ref.startsWith(prefix)) {
|
||||||
|
return ref.slice(prefix.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferName(repoUrl: string, rootPath: string) {
|
||||||
|
const normalized = repoUrl.replace(/\/+$/, "");
|
||||||
|
const lastPart = normalized.split("/").pop() || "";
|
||||||
|
const cleaned = lastPart.replace(/\.git$/i, "");
|
||||||
|
return cleaned || path.basename(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
function slugify(value: string) {
|
function slugify(value: string) {
|
||||||
return value
|
return value
|
||||||
@@ -120,11 +299,37 @@ async function ensureRepo(project: DeploymentProjectDocument, onData: (line: str
|
|||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
const entries = await fs.promises.readdir(repoDir);
|
const entries = await fs.promises.readdir(repoDir);
|
||||||
if (entries.length > 0) {
|
const allowed = new Set<string>([metadataFileName, ".env", ".env.local", runsDirName]);
|
||||||
|
const blocking = entries.filter((name) => !allowed.has(name));
|
||||||
|
if (blocking.length > 0) {
|
||||||
throw new Error("Repo klasoru git olmayan dosyalar iceriyor");
|
throw new Error("Repo klasoru git olmayan dosyalar iceriyor");
|
||||||
}
|
}
|
||||||
|
let envBackup: string | null = null;
|
||||||
|
const envPath = path.join(repoDir, ".env");
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
envBackup = await fs.promises.readFile(envPath, "utf8");
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
entries
|
||||||
|
.filter((name) => allowed.has(name))
|
||||||
|
.map((name) => fs.promises.rm(path.join(repoDir, name), { force: true }))
|
||||||
|
);
|
||||||
onData(`Repo klonlanıyor: ${project.repoUrl}`);
|
onData(`Repo klonlanıyor: ${project.repoUrl}`);
|
||||||
await runCommand(`git clone --branch ${project.branch} ${project.repoUrl} .`, repoDir, onData);
|
await runCommand(`git clone --branch ${project.branch} ${project.repoUrl} .`, repoDir, onData);
|
||||||
|
if (envBackup) {
|
||||||
|
await fs.promises.writeFile(envPath, envBackup, "utf8");
|
||||||
|
}
|
||||||
|
await writeMetadata(repoDir, {
|
||||||
|
name: project.name,
|
||||||
|
repoUrl: project.repoUrl,
|
||||||
|
branch: project.branch,
|
||||||
|
composeFile: project.composeFile,
|
||||||
|
webhookToken: project.webhookToken,
|
||||||
|
env: project.env,
|
||||||
|
port: project.port,
|
||||||
|
envContent: project.envContent,
|
||||||
|
envExampleName: project.envExampleName
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
onData("Repo güncelleniyor (git fetch/pull)...");
|
onData("Repo güncelleniyor (git fetch/pull)...");
|
||||||
await runCommand(`git fetch origin ${project.branch}`, repoDir, onData);
|
await runCommand(`git fetch origin ${project.branch}`, repoDir, onData);
|
||||||
@@ -247,10 +452,24 @@ class DeploymentService {
|
|||||||
async ensureSettings() {
|
async ensureSettings() {
|
||||||
const existing = await Settings.findOne();
|
const existing = await Settings.findOne();
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const fileSettings = await readSettingsFile();
|
||||||
|
if (fileSettings) {
|
||||||
|
const createdFromFile = await Settings.create({
|
||||||
|
webhookToken: fileSettings.webhookToken,
|
||||||
|
webhookSecret: fileSettings.webhookSecret
|
||||||
|
});
|
||||||
|
return createdFromFile;
|
||||||
|
}
|
||||||
|
|
||||||
const created = await Settings.create({
|
const created = await Settings.create({
|
||||||
webhookToken: generateApiToken(),
|
webhookToken: generateApiToken(),
|
||||||
webhookSecret: generateSecret()
|
webhookSecret: generateSecret()
|
||||||
});
|
});
|
||||||
|
await writeSettingsFile({
|
||||||
|
webhookToken: created.webhookToken,
|
||||||
|
webhookSecret: created.webhookSecret
|
||||||
|
});
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +477,10 @@ class DeploymentService {
|
|||||||
const settings = await this.ensureSettings();
|
const settings = await this.ensureSettings();
|
||||||
settings.webhookToken = generateApiToken();
|
settings.webhookToken = generateApiToken();
|
||||||
await settings.save();
|
await settings.save();
|
||||||
|
await writeSettingsFile({
|
||||||
|
webhookToken: settings.webhookToken,
|
||||||
|
webhookSecret: settings.webhookSecret
|
||||||
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +488,10 @@ class DeploymentService {
|
|||||||
const settings = await this.ensureSettings();
|
const settings = await this.ensureSettings();
|
||||||
settings.webhookSecret = generateSecret();
|
settings.webhookSecret = generateSecret();
|
||||||
await settings.save();
|
await settings.save();
|
||||||
|
await writeSettingsFile({
|
||||||
|
webhookToken: settings.webhookToken,
|
||||||
|
webhookSecret: settings.webhookSecret
|
||||||
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +528,7 @@ class DeploymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const env = deriveEnv(input.composeFile);
|
const env = deriveEnv(input.composeFile);
|
||||||
return DeploymentProject.create({
|
const created = await DeploymentProject.create({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
rootPath,
|
rootPath,
|
||||||
repoUrl,
|
repoUrl,
|
||||||
@@ -313,6 +540,18 @@ class DeploymentService {
|
|||||||
envContent: input.envContent,
|
envContent: input.envContent,
|
||||||
envExampleName: input.envExampleName
|
envExampleName: input.envExampleName
|
||||||
});
|
});
|
||||||
|
await writeMetadata(rootPath, {
|
||||||
|
name: created.name,
|
||||||
|
repoUrl: created.repoUrl,
|
||||||
|
branch: created.branch,
|
||||||
|
composeFile: created.composeFile,
|
||||||
|
webhookToken: created.webhookToken,
|
||||||
|
env: created.env,
|
||||||
|
port: created.port,
|
||||||
|
envContent: created.envContent,
|
||||||
|
envExampleName: created.envExampleName
|
||||||
|
});
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProject(
|
async updateProject(
|
||||||
@@ -355,6 +594,19 @@ class DeploymentService {
|
|||||||
},
|
},
|
||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
);
|
);
|
||||||
|
if (updated) {
|
||||||
|
await writeMetadata(updated.rootPath, {
|
||||||
|
name: updated.name,
|
||||||
|
repoUrl: updated.repoUrl,
|
||||||
|
branch: updated.branch,
|
||||||
|
composeFile: updated.composeFile,
|
||||||
|
webhookToken: updated.webhookToken,
|
||||||
|
env: updated.env,
|
||||||
|
port: updated.port,
|
||||||
|
envContent: updated.envContent,
|
||||||
|
envExampleName: updated.envExampleName
|
||||||
|
});
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,6 +622,7 @@ class DeploymentService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedMessage = normalizeCommitMessage(options?.message);
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const runLogs: string[] = [];
|
const runLogs: string[] = [];
|
||||||
const pushLog = (line: string) => {
|
const pushLog = (line: string) => {
|
||||||
@@ -381,17 +634,18 @@ class DeploymentService {
|
|||||||
project: projectId,
|
project: projectId,
|
||||||
status: "running",
|
status: "running",
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
message: options?.message
|
message: normalizedMessage ?? options?.message
|
||||||
});
|
});
|
||||||
this.emitRun(projectId, runDoc);
|
this.emitRun(projectId, runDoc);
|
||||||
|
await writeRunFile(project.rootPath, runDoc);
|
||||||
|
|
||||||
await DeploymentProject.findByIdAndUpdate(projectId, {
|
await DeploymentProject.findByIdAndUpdate(projectId, {
|
||||||
lastStatus: "running",
|
lastStatus: "running",
|
||||||
lastMessage: options?.message || "Deploy başlıyor..."
|
lastMessage: normalizedMessage ?? options?.message ?? "Deploy başlıyor..."
|
||||||
});
|
});
|
||||||
await this.emitStatus(projectId, {
|
await this.emitStatus(projectId, {
|
||||||
lastStatus: "running",
|
lastStatus: "running",
|
||||||
lastMessage: options?.message || "Deploy başlıyor..."
|
lastMessage: normalizedMessage ?? options?.message ?? "Deploy başlıyor..."
|
||||||
} as DeploymentProjectDocument);
|
} as DeploymentProjectDocument);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -406,22 +660,23 @@ class DeploymentService {
|
|||||||
await DeploymentProject.findByIdAndUpdate(projectId, {
|
await DeploymentProject.findByIdAndUpdate(projectId, {
|
||||||
lastStatus: "success",
|
lastStatus: "success",
|
||||||
lastDeployAt: new Date(),
|
lastDeployAt: new Date(),
|
||||||
lastMessage: options?.message || "Başarılı"
|
lastMessage: normalizedMessage ?? options?.message ?? "Başarılı"
|
||||||
});
|
});
|
||||||
await this.emitStatus(projectId, {
|
await this.emitStatus(projectId, {
|
||||||
lastStatus: "success",
|
lastStatus: "success",
|
||||||
lastDeployAt: new Date(),
|
lastDeployAt: new Date(),
|
||||||
lastMessage: options?.message || "Başarılı"
|
lastMessage: normalizedMessage ?? options?.message ?? "Başarılı"
|
||||||
} as DeploymentProjectDocument);
|
} as DeploymentProjectDocument);
|
||||||
await DeploymentRun.findByIdAndUpdate(runDoc._id, {
|
await DeploymentRun.findByIdAndUpdate(runDoc._id, {
|
||||||
status: "success",
|
status: "success",
|
||||||
finishedAt: new Date(),
|
finishedAt: new Date(),
|
||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
logs: runLogs,
|
logs: runLogs,
|
||||||
message: options?.message
|
message: normalizedMessage ?? options?.message
|
||||||
});
|
});
|
||||||
const updatedRun = await DeploymentRun.findById(runDoc._id);
|
const updatedRun = await DeploymentRun.findById(runDoc._id);
|
||||||
if (updatedRun) this.emitRun(projectId, updatedRun);
|
if (updatedRun) this.emitRun(projectId, updatedRun);
|
||||||
|
if (updatedRun) await writeRunFile(project.rootPath, updatedRun);
|
||||||
pushLog("Deploy tamamlandı: Başarılı");
|
pushLog("Deploy tamamlandı: Başarılı");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const duration = Date.now() - startedAt;
|
const duration = Date.now() - startedAt;
|
||||||
@@ -440,10 +695,11 @@ class DeploymentService {
|
|||||||
finishedAt: new Date(),
|
finishedAt: new Date(),
|
||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
logs: runLogs,
|
logs: runLogs,
|
||||||
message: options?.message
|
message: normalizedMessage ?? options?.message
|
||||||
});
|
});
|
||||||
const updatedRun = await DeploymentRun.findById(runDoc._id);
|
const updatedRun = await DeploymentRun.findById(runDoc._id);
|
||||||
if (updatedRun) this.emitRun(projectId, updatedRun);
|
if (updatedRun) this.emitRun(projectId, updatedRun);
|
||||||
|
if (updatedRun) await writeRunFile(project.rootPath, updatedRun);
|
||||||
pushLog(`Hata: ${(err as Error).message}`);
|
pushLog(`Hata: ${(err as Error).message}`);
|
||||||
} finally {
|
} finally {
|
||||||
this.running.delete(projectId);
|
this.running.delete(projectId);
|
||||||
@@ -465,6 +721,131 @@ class DeploymentService {
|
|||||||
async findByWebhookToken(token: string) {
|
async findByWebhookToken(token: string) {
|
||||||
return DeploymentProject.findOne({ webhookToken: token });
|
return DeploymentProject.findOne({ webhookToken: token });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async normalizeExistingCommitMessages() {
|
||||||
|
const projects = await DeploymentProject.find({
|
||||||
|
lastMessage: { $regex: /[\r\n]/ }
|
||||||
|
});
|
||||||
|
for (const project of projects) {
|
||||||
|
const normalized = normalizeCommitMessage(project.lastMessage);
|
||||||
|
if (normalized && normalized !== project.lastMessage) {
|
||||||
|
project.lastMessage = normalized;
|
||||||
|
await project.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runs = await DeploymentRun.find({
|
||||||
|
message: { $regex: /[\r\n]/ }
|
||||||
|
});
|
||||||
|
for (const run of runs) {
|
||||||
|
const normalized = normalizeCommitMessage(run.message);
|
||||||
|
if (normalized && normalized !== run.message) {
|
||||||
|
run.message = normalized;
|
||||||
|
await run.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async bootstrapFromFilesystem() {
|
||||||
|
const candidateRoots = [
|
||||||
|
deploymentsRoot,
|
||||||
|
path.resolve(process.cwd(), "deployments"),
|
||||||
|
path.resolve(process.cwd(), "..", "deployments"),
|
||||||
|
path.resolve(process.cwd(), "..", "..", "deployments"),
|
||||||
|
"/root/Wisecolt-CI/deployments"
|
||||||
|
];
|
||||||
|
const roots = Array.from(
|
||||||
|
new Set(candidateRoots.filter((root) => root && fs.existsSync(root)))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const root of roots) {
|
||||||
|
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
||||||
|
const dirs = entries.filter((entry) => entry.isDirectory());
|
||||||
|
|
||||||
|
for (const entry of dirs) {
|
||||||
|
const rootPath = path.join(root, entry.name);
|
||||||
|
const existing = await DeploymentProject.findOne({ rootPath });
|
||||||
|
if (existing) continue;
|
||||||
|
|
||||||
|
const metadata = await readMetadata(rootPath);
|
||||||
|
const repoUrlRaw = metadata?.repoUrl || (await inferRepoUrlFromGit(rootPath));
|
||||||
|
if (!repoUrlRaw) continue;
|
||||||
|
const repoUrl = normalizeRepoUrl(repoUrlRaw);
|
||||||
|
const repoExisting = await DeploymentProject.findOne({ repoUrl });
|
||||||
|
if (repoExisting) continue;
|
||||||
|
|
||||||
|
const composeFile = metadata?.composeFile || inferComposeFile(rootPath);
|
||||||
|
if (!composeFile) continue;
|
||||||
|
const branch = metadata?.branch || (await inferBranchFromGit(rootPath)) || "main";
|
||||||
|
const name = metadata?.name || inferName(repoUrl, rootPath);
|
||||||
|
|
||||||
|
let webhookToken = metadata?.webhookToken || generateWebhookToken();
|
||||||
|
while (await DeploymentProject.findOne({ webhookToken })) {
|
||||||
|
webhookToken = generateWebhookToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
let envContent = metadata?.envContent;
|
||||||
|
const envPath = path.join(rootPath, ".env");
|
||||||
|
if (!envContent && fs.existsSync(envPath)) {
|
||||||
|
envContent = await fs.promises.readFile(envPath, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const envExampleName = metadata?.envExampleName;
|
||||||
|
const env = deriveEnv(composeFile);
|
||||||
|
|
||||||
|
const created = await DeploymentProject.create({
|
||||||
|
name,
|
||||||
|
rootPath,
|
||||||
|
repoUrl,
|
||||||
|
branch,
|
||||||
|
composeFile,
|
||||||
|
webhookToken,
|
||||||
|
env,
|
||||||
|
port: metadata?.port,
|
||||||
|
envContent,
|
||||||
|
envExampleName
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeMetadata(rootPath, {
|
||||||
|
name: created.name,
|
||||||
|
repoUrl: created.repoUrl,
|
||||||
|
branch: created.branch,
|
||||||
|
composeFile: created.composeFile,
|
||||||
|
webhookToken: created.webhookToken,
|
||||||
|
env: created.env,
|
||||||
|
port: created.port,
|
||||||
|
envContent: created.envContent,
|
||||||
|
envExampleName: created.envExampleName
|
||||||
|
});
|
||||||
|
|
||||||
|
const storedRuns = await readStoredRuns(rootPath);
|
||||||
|
if (storedRuns.length > 0) {
|
||||||
|
storedRuns.sort(
|
||||||
|
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
||||||
|
);
|
||||||
|
await DeploymentRun.insertMany(
|
||||||
|
storedRuns.map((run) => ({
|
||||||
|
project: created._id,
|
||||||
|
status: run.status,
|
||||||
|
message: run.message,
|
||||||
|
logs: run.logs || [],
|
||||||
|
startedAt: new Date(run.startedAt),
|
||||||
|
finishedAt: run.finishedAt ? new Date(run.finishedAt) : undefined,
|
||||||
|
durationMs: run.durationMs,
|
||||||
|
createdAt: new Date(run.createdAt),
|
||||||
|
updatedAt: new Date(run.updatedAt)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const latest = storedRuns[0];
|
||||||
|
await DeploymentProject.findByIdAndUpdate(created._id, {
|
||||||
|
lastStatus: latest.status,
|
||||||
|
lastDeployAt: new Date(latest.finishedAt || latest.startedAt),
|
||||||
|
lastMessage: latest.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deploymentService = new DeploymentService();
|
export const deploymentService = new DeploymentService();
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import path from "path";
|
|||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { Job, JobDocument, TimeUnit } from "../models/job.js";
|
import { Job, JobDocument, TimeUnit } from "../models/job.js";
|
||||||
import { JobRun } from "../models/jobRun.js";
|
import { JobRun, JobRunDocument } from "../models/jobRun.js";
|
||||||
|
|
||||||
const repoBaseDir = path.join(process.cwd(), "test-runs");
|
const repoBaseDir = path.join(process.cwd(), "test-runs");
|
||||||
|
const jobMetadataFileName = ".wisecolt-ci-job.json";
|
||||||
|
const jobRunsDirName = ".wisecolt-ci-job-runs";
|
||||||
|
|
||||||
function unitToMs(unit: TimeUnit) {
|
function unitToMs(unit: TimeUnit) {
|
||||||
if (unit === "dakika") return 60_000;
|
if (unit === "dakika") return 60_000;
|
||||||
@@ -17,6 +19,91 @@ function ensureDir(dir: string) {
|
|||||||
return fs.promises.mkdir(dir, { recursive: true });
|
return fs.promises.mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JobMetadata = {
|
||||||
|
name: string;
|
||||||
|
repoUrl: string;
|
||||||
|
testCommand: string;
|
||||||
|
checkValue: number;
|
||||||
|
checkUnit: TimeUnit;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StoredJobRun = {
|
||||||
|
status: "running" | "success" | "failed";
|
||||||
|
logs: string[];
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getJobDir(jobId: string) {
|
||||||
|
return path.join(repoBaseDir, jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJobRunsDir(jobDir: string) {
|
||||||
|
return path.join(jobDir, jobRunsDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJobMetadata(jobDir: string): Promise<JobMetadata | null> {
|
||||||
|
const filePath = path.join(jobDir, jobMetadataFileName);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(filePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as JobMetadata;
|
||||||
|
if (!parsed?.repoUrl || !parsed?.testCommand) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeJobMetadata(jobDir: string, data: JobMetadata) {
|
||||||
|
await ensureDir(jobDir);
|
||||||
|
const filePath = path.join(jobDir, jobMetadataFileName);
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeJobRun(run: JobRunDocument) {
|
||||||
|
return {
|
||||||
|
status: run.status,
|
||||||
|
logs: run.logs || [],
|
||||||
|
startedAt: new Date(run.startedAt).toISOString(),
|
||||||
|
finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : undefined,
|
||||||
|
durationMs: run.durationMs,
|
||||||
|
createdAt: new Date(run.createdAt).toISOString(),
|
||||||
|
updatedAt: new Date(run.updatedAt).toISOString()
|
||||||
|
} satisfies StoredJobRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeJobRunFile(jobDir: string, run: JobRunDocument) {
|
||||||
|
const dir = getJobRunsDir(jobDir);
|
||||||
|
await ensureDir(dir);
|
||||||
|
const data = serializeJobRun(run);
|
||||||
|
const name = `${new Date(data.startedAt).getTime()}-${run._id.toString()}.json`;
|
||||||
|
const filePath = path.join(dir, name);
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStoredJobRuns(jobDir: string): Promise<StoredJobRun[]> {
|
||||||
|
const dir = getJobRunsDir(jobDir);
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
const entries = await fs.promises.readdir(dir);
|
||||||
|
const items: StoredJobRun[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.endsWith(".json")) continue;
|
||||||
|
try {
|
||||||
|
const raw = await fs.promises.readFile(path.join(dir, entry), "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as StoredJobRun;
|
||||||
|
if (!parsed?.startedAt || !parsed?.status) continue;
|
||||||
|
items.push(parsed);
|
||||||
|
} catch {
|
||||||
|
// ignore invalid file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
function cleanOutput(input: string) {
|
function cleanOutput(input: string) {
|
||||||
// ANSI escape sequences temizleme
|
// ANSI escape sequences temizleme
|
||||||
return input.replace(
|
return input.replace(
|
||||||
@@ -85,8 +172,42 @@ async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
|||||||
const exists = fs.existsSync(gitDir);
|
const exists = fs.existsSync(gitDir);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
|
const entries = await fs.promises.readdir(repoDir);
|
||||||
|
const allowed = new Set<string>([jobMetadataFileName, jobRunsDirName]);
|
||||||
|
const blocking = entries.filter((name) => !allowed.has(name));
|
||||||
|
if (blocking.length > 0) {
|
||||||
|
throw new Error("Repo klasoru git olmayan dosyalar iceriyor");
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadataBackup: string | null = null;
|
||||||
|
const metadataPath = path.join(repoDir, jobMetadataFileName);
|
||||||
|
if (fs.existsSync(metadataPath)) {
|
||||||
|
metadataBackup = await fs.promises.readFile(metadataPath, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
let runsBackupPath: string | null = null;
|
||||||
|
const runsDir = path.join(repoDir, jobRunsDirName);
|
||||||
|
if (fs.existsSync(runsDir)) {
|
||||||
|
const tmpBase = await fs.promises.mkdtemp(path.join(repoBaseDir, ".tmp-"));
|
||||||
|
runsBackupPath = path.join(tmpBase, jobRunsDirName);
|
||||||
|
await fs.promises.rename(runsDir, runsBackupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
entries
|
||||||
|
.filter((name) => allowed.has(name))
|
||||||
|
.map((name) => fs.promises.rm(path.join(repoDir, name), { recursive: true, force: true }))
|
||||||
|
);
|
||||||
|
|
||||||
onData(`Repo klonlanıyor: ${job.repoUrl}`);
|
onData(`Repo klonlanıyor: ${job.repoUrl}`);
|
||||||
await runCommand(`git clone ${job.repoUrl} ${repoDir}`, process.cwd(), onData);
|
await runCommand(`git clone ${job.repoUrl} ${repoDir}`, process.cwd(), onData);
|
||||||
|
|
||||||
|
if (metadataBackup) {
|
||||||
|
await fs.promises.writeFile(metadataPath, metadataBackup, "utf8");
|
||||||
|
}
|
||||||
|
if (runsBackupPath) {
|
||||||
|
await fs.promises.rename(runsBackupPath, runsDir);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
onData("Repo güncelleniyor (git pull)...");
|
onData("Repo güncelleniyor (git pull)...");
|
||||||
await runCommand("git pull", repoDir, onData);
|
await runCommand("git pull", repoDir, onData);
|
||||||
@@ -156,6 +277,7 @@ class JobService {
|
|||||||
status: "running",
|
status: "running",
|
||||||
startedAt: new Date()
|
startedAt: new Date()
|
||||||
});
|
});
|
||||||
|
await writeJobRunFile(getJobDir(jobId), runDoc);
|
||||||
|
|
||||||
await Job.findByIdAndUpdate(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." });
|
await Job.findByIdAndUpdate(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." });
|
||||||
await this.emitStatus(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." } as JobDocument);
|
await this.emitStatus(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." } as JobDocument);
|
||||||
@@ -179,6 +301,8 @@ class JobService {
|
|||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
logs: runLogs
|
logs: runLogs
|
||||||
});
|
});
|
||||||
|
const updatedRun = await JobRun.findById(runDoc._id);
|
||||||
|
if (updatedRun) await writeJobRunFile(getJobDir(jobId), updatedRun);
|
||||||
await this.emitStatus(jobId, {
|
await this.emitStatus(jobId, {
|
||||||
status: "success",
|
status: "success",
|
||||||
lastRunAt: new Date(),
|
lastRunAt: new Date(),
|
||||||
@@ -199,6 +323,8 @@ class JobService {
|
|||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
logs: runLogs
|
logs: runLogs
|
||||||
});
|
});
|
||||||
|
const updatedRun = await JobRun.findById(runDoc._id);
|
||||||
|
if (updatedRun) await writeJobRunFile(getJobDir(jobId), updatedRun);
|
||||||
pushLog(`Hata: ${(err as Error).message}`);
|
pushLog(`Hata: ${(err as Error).message}`);
|
||||||
await this.emitStatus(jobId, {
|
await this.emitStatus(jobId, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
@@ -231,6 +357,78 @@ class JobService {
|
|||||||
const jobs = await Job.find();
|
const jobs = await Job.find();
|
||||||
jobs.forEach((job) => this.scheduleJob(job));
|
jobs.forEach((job) => this.scheduleJob(job));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async persistMetadata(job: JobDocument) {
|
||||||
|
await writeJobMetadata(getJobDir(job._id.toString()), {
|
||||||
|
name: job.name,
|
||||||
|
repoUrl: job.repoUrl,
|
||||||
|
testCommand: job.testCommand,
|
||||||
|
checkValue: job.checkValue,
|
||||||
|
checkUnit: job.checkUnit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async bootstrapFromFilesystem() {
|
||||||
|
const candidateRoots = [
|
||||||
|
repoBaseDir,
|
||||||
|
path.resolve(process.cwd(), "test-runs"),
|
||||||
|
path.resolve(process.cwd(), "..", "test-runs"),
|
||||||
|
path.resolve(process.cwd(), "..", "..", "test-runs"),
|
||||||
|
"/root/Wisecolt-CI/test-runs"
|
||||||
|
];
|
||||||
|
const roots = Array.from(
|
||||||
|
new Set(candidateRoots.filter((root) => root && fs.existsSync(root)))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const root of roots) {
|
||||||
|
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
||||||
|
const dirs = entries.filter((entry) => entry.isDirectory());
|
||||||
|
|
||||||
|
for (const entry of dirs) {
|
||||||
|
const jobDir = path.join(root, entry.name);
|
||||||
|
const metadata = await readJobMetadata(jobDir);
|
||||||
|
if (!metadata) continue;
|
||||||
|
|
||||||
|
const existing = await Job.findOne({ repoUrl: metadata.repoUrl });
|
||||||
|
if (existing) continue;
|
||||||
|
|
||||||
|
const created = await Job.create({
|
||||||
|
name: metadata.name,
|
||||||
|
repoUrl: metadata.repoUrl,
|
||||||
|
testCommand: metadata.testCommand,
|
||||||
|
checkValue: metadata.checkValue,
|
||||||
|
checkUnit: metadata.checkUnit
|
||||||
|
});
|
||||||
|
await this.persistMetadata(created);
|
||||||
|
|
||||||
|
const storedRuns = await readStoredJobRuns(jobDir);
|
||||||
|
if (storedRuns.length > 0) {
|
||||||
|
storedRuns.sort(
|
||||||
|
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
||||||
|
);
|
||||||
|
await JobRun.insertMany(
|
||||||
|
storedRuns.map((run) => ({
|
||||||
|
job: created._id,
|
||||||
|
status: run.status,
|
||||||
|
logs: run.logs || [],
|
||||||
|
startedAt: new Date(run.startedAt),
|
||||||
|
finishedAt: run.finishedAt ? new Date(run.finishedAt) : undefined,
|
||||||
|
durationMs: run.durationMs,
|
||||||
|
createdAt: new Date(run.createdAt),
|
||||||
|
updatedAt: new Date(run.updatedAt)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const latest = storedRuns[0];
|
||||||
|
await Job.findByIdAndUpdate(created._id, {
|
||||||
|
status: latest.status === "running" ? "idle" : latest.status,
|
||||||
|
lastRunAt: new Date(latest.finishedAt || latest.startedAt),
|
||||||
|
lastDurationMs: latest.durationMs,
|
||||||
|
lastMessage: latest.status === "success" ? "Başarılı" : "Hata"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobService = new JobService();
|
export const jobService = new JobService();
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export function DeploymentDetailPage() {
|
|||||||
const updated = await updateDeployment(form._id, payload);
|
const updated = await updateDeployment(form._id, payload);
|
||||||
setProject(updated);
|
setProject(updated);
|
||||||
try {
|
try {
|
||||||
await runDeployment(updated._id, "Update deploy");
|
await runDeployment(updated._id, "update deploy");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Deploy tetiklenemedi");
|
toast.error("Deploy tetiklenemedi");
|
||||||
}
|
}
|
||||||
@@ -435,7 +435,7 @@ export function DeploymentDetailPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="max-h-72 overflow-auto rounded-md border border-border bg-black px-3 py-2 font-mono text-xs text-green-100">
|
<div className="max-h-72 overflow-auto rounded-md border border-border bg-black px-3 py-2 font-mono text-xs text-green-100">
|
||||||
{currentLogs.length ? (
|
{currentLogs.length ? (
|
||||||
currentLogs.map((line, idx) => (
|
[...currentLogs].reverse().map((line, idx) => (
|
||||||
<div key={idx} className="whitespace-pre-wrap">
|
<div key={idx} className="whitespace-pre-wrap">
|
||||||
{decorateLogLine(line)}
|
{decorateLogLine(line)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export function DeploymentsPage() {
|
|||||||
});
|
});
|
||||||
setDeployments((prev) => prev.map((d) => (d._id === updated._id ? updated : d)));
|
setDeployments((prev) => prev.map((d) => (d._id === updated._id ? updated : d)));
|
||||||
try {
|
try {
|
||||||
await runDeployment(updated._id, "Update deploy");
|
await runDeployment(updated._id, "update deploy");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Deploy tetiklenemedi");
|
toast.error("Deploy tetiklenemedi");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export function HomePage() {
|
|||||||
<RepoIcon repoUrl={run.repoUrl} />
|
<RepoIcon repoUrl={run.repoUrl} />
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold text-foreground">
|
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
|
<span>{run.title}</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${
|
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${
|
||||||
run.type === "test"
|
run.type === "test"
|
||||||
@@ -286,7 +287,6 @@ export function HomePage() {
|
|||||||
/>
|
/>
|
||||||
{run.type === "test" ? "Test" : "Deploy"}
|
{run.type === "test" ? "Test" : "Deploy"}
|
||||||
</span>
|
</span>
|
||||||
<span>{run.title}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{new Date(run.startedAt).toLocaleString()} · Süre:{" "}
|
{new Date(run.startedAt).toLocaleString()} · Süre:{" "}
|
||||||
|
|||||||
Reference in New Issue
Block a user