Test modülü eklendi
This commit is contained in:
@@ -3,7 +3,7 @@ FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
RUN apk add --no-cache git openssh-client && npm install
|
||||
|
||||
COPY tsconfig.json .
|
||||
COPY src ./src
|
||||
|
||||
@@ -7,6 +7,8 @@ import authRoutes from "./routes/auth.js";
|
||||
import jobsRoutes from "./routes/jobs.js";
|
||||
import { config } from "./config/env.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { jobService } from "./services/jobService.js";
|
||||
import { Job } from "./models/job.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -34,6 +36,8 @@ const io = new Server(server, {
|
||||
}
|
||||
});
|
||||
|
||||
jobService.setSocket(io);
|
||||
|
||||
let counter = 0;
|
||||
let counterTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -90,12 +94,37 @@ io.on("connection", (socket) => {
|
||||
socket.on("counter:status", (ack?: (payload: { running: boolean; value: number }) => void) => {
|
||||
ack?.({ running: !!counterTimer, value: counter });
|
||||
});
|
||||
|
||||
socket.on("job:subscribe", async ({ jobId }: { jobId: string }) => {
|
||||
if (!jobId) return;
|
||||
socket.join(`job:${jobId}`);
|
||||
try {
|
||||
const job = await Job.findById(jobId);
|
||||
if (job) {
|
||||
socket.emit("job:status", {
|
||||
jobId,
|
||||
status: job.status,
|
||||
lastRunAt: job.lastRunAt,
|
||||
lastDurationMs: job.lastDurationMs,
|
||||
lastMessage: job.lastMessage
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// sessizce geç
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("job:unsubscribe", ({ jobId }: { jobId: string }) => {
|
||||
if (!jobId) return;
|
||||
socket.leave(`job:${jobId}`);
|
||||
});
|
||||
});
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
await mongoose.connect(config.mongoUri);
|
||||
console.log("MongoDB'ye bağlanıldı");
|
||||
await jobService.bootstrap();
|
||||
|
||||
server.listen(config.port, () => {
|
||||
console.log(`Sunucu ${config.port} portunda çalışıyor`);
|
||||
|
||||
@@ -8,6 +8,10 @@ export interface JobDocument extends Document {
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
status: "idle" | "running" | "success" | "failed";
|
||||
lastRunAt?: Date;
|
||||
lastDurationMs?: number;
|
||||
lastMessage?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -18,7 +22,11 @@ const JobSchema = new Schema<JobDocument>(
|
||||
repoUrl: { type: String, required: true, trim: true },
|
||||
testCommand: { type: String, required: true, trim: true },
|
||||
checkValue: { type: Number, required: true, min: 1 },
|
||||
checkUnit: { type: String, required: true, enum: ["dakika", "saat", "gün"] }
|
||||
checkUnit: { type: String, required: true, enum: ["dakika", "saat", "gün"] },
|
||||
status: { type: String, enum: ["idle", "running", "success", "failed"], default: "idle" },
|
||||
lastRunAt: { type: Date },
|
||||
lastDurationMs: { type: Number },
|
||||
lastMessage: { type: String }
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { authMiddleware } from "../middleware/authMiddleware.js";
|
||||
import { Job } from "../models/job.js";
|
||||
import { jobService } from "../services/jobService.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -11,6 +12,13 @@ router.get("/", async (_req, res) => {
|
||||
res.json(jobs);
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const job = await Job.findById(id).lean();
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
return res.json(job);
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit } = req.body;
|
||||
if (!name || !repoUrl || !testCommand || !checkValue || !checkUnit) {
|
||||
@@ -18,6 +26,7 @@ router.post("/", async (req, res) => {
|
||||
}
|
||||
try {
|
||||
const job = await Job.create({ name, repoUrl, testCommand, checkValue, checkUnit });
|
||||
jobService.scheduleJob(job);
|
||||
return res.status(201).json(job);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job oluşturulamadı", error: (err as Error).message });
|
||||
@@ -34,6 +43,7 @@ router.put("/:id", async (req, res) => {
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
jobService.scheduleJob(job);
|
||||
return res.json(job);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job güncellenemedi", error: (err as Error).message });
|
||||
@@ -45,10 +55,19 @@ router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const job = await Job.findByIdAndDelete(id);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
jobService.clearJob(id);
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job silinemedi", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/:id/run", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const job = await Job.findById(id);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
jobService.runJob(id).catch(() => undefined);
|
||||
return res.json({ queued: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
181
backend/src/services/jobService.ts
Normal file
181
backend/src/services/jobService.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { Server } from "socket.io";
|
||||
import { Job, JobDocument, TimeUnit } from "../models/job.js";
|
||||
|
||||
const repoBaseDir = path.join(process.cwd(), "test-runs");
|
||||
|
||||
function unitToMs(unit: TimeUnit) {
|
||||
if (unit === "dakika") return 60_000;
|
||||
if (unit === "saat") return 60 * 60_000;
|
||||
return 24 * 60 * 60_000;
|
||||
}
|
||||
|
||||
function ensureDir(dir: string) {
|
||||
return fs.promises.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function cleanOutput(input: string) {
|
||||
// ANSI escape sequences temizleme
|
||||
return input.replace(
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function runCommand(command: string, cwd: string, onData: (chunk: string) => void) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(command, {
|
||||
cwd,
|
||||
shell: true,
|
||||
env: { ...process.env, CI: process.env.CI || "1" }
|
||||
});
|
||||
|
||||
child.stdout.on("data", (data) => onData(cleanOutput(data.toString())));
|
||||
child.stderr.on("data", (data) => onData(cleanOutput(data.toString())));
|
||||
|
||||
child.on("error", (err) => {
|
||||
onData(`Hata: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Komut kod ${code} ile kapandı`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
||||
const repoDir = path.join(repoBaseDir, job._id.toString());
|
||||
await ensureDir(repoDir);
|
||||
const gitDir = path.join(repoDir, ".git");
|
||||
const exists = fs.existsSync(gitDir);
|
||||
|
||||
if (!exists) {
|
||||
onData(`Repo klonlanıyor: ${job.repoUrl}`);
|
||||
await runCommand(`git clone ${job.repoUrl} ${repoDir}`, process.cwd(), onData);
|
||||
} else {
|
||||
onData("Repo güncelleniyor (git pull)...");
|
||||
await runCommand("git pull", repoDir, onData);
|
||||
}
|
||||
|
||||
return repoDir;
|
||||
}
|
||||
|
||||
async function ensureDependencies(repoDir: string, onData: (chunk: string) => void) {
|
||||
const nodeModules = path.join(repoDir, "node_modules");
|
||||
const hasPackageJson = fs.existsSync(path.join(repoDir, "package.json"));
|
||||
if (!hasPackageJson) {
|
||||
onData("package.json bulunamadı, npm install atlanıyor");
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(nodeModules)) {
|
||||
onData("Bağımlılıklar mevcut, npm install atlanıyor");
|
||||
return;
|
||||
}
|
||||
onData("npm install çalıştırılıyor...");
|
||||
await runCommand("npm install", repoDir, (line) => onData(line));
|
||||
}
|
||||
|
||||
class JobService {
|
||||
private timers: Map<string, NodeJS.Timeout> = new Map();
|
||||
private io: Server | null = null;
|
||||
|
||||
setSocket(io: Server) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
private emitStatus(jobId: string, payload: Partial<JobDocument>) {
|
||||
if (!this.io) return;
|
||||
const body = {
|
||||
jobId,
|
||||
status: payload.status,
|
||||
lastRunAt: payload.lastRunAt,
|
||||
lastDurationMs: payload.lastDurationMs,
|
||||
lastMessage: payload.lastMessage
|
||||
};
|
||||
this.io.to(`job:${jobId}`).emit("job:status", body);
|
||||
this.io.emit("job:status", body);
|
||||
}
|
||||
|
||||
private emitLog(jobId: string, line: string) {
|
||||
if (!this.io) return;
|
||||
this.io.to(`job:${jobId}`).emit("job:log", { jobId, line });
|
||||
}
|
||||
|
||||
async runJob(jobId: string) {
|
||||
const job = await Job.findById(jobId);
|
||||
if (!job) return;
|
||||
|
||||
const startedAt = Date.now();
|
||||
await Job.findByIdAndUpdate(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." });
|
||||
this.emitStatus(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." } as JobDocument);
|
||||
|
||||
try {
|
||||
const repoDir = await cloneOrPull(job, (line) => this.emitLog(jobId, line));
|
||||
await ensureDependencies(repoDir, (line) => this.emitLog(jobId, line));
|
||||
this.emitLog(jobId, `Test komutu çalıştırılıyor: ${job.testCommand}`);
|
||||
await runCommand(job.testCommand, repoDir, (line) => this.emitLog(jobId, line));
|
||||
this.emitLog(jobId, "Test tamamlandı: Başarılı");
|
||||
const duration = Date.now() - startedAt;
|
||||
await Job.findByIdAndUpdate(jobId, {
|
||||
status: "success",
|
||||
lastRunAt: new Date(),
|
||||
lastDurationMs: duration,
|
||||
lastMessage: "Başarılı"
|
||||
});
|
||||
this.emitStatus(jobId, {
|
||||
status: "success",
|
||||
lastRunAt: new Date(),
|
||||
lastDurationMs: duration,
|
||||
lastMessage: "Başarılı"
|
||||
} as JobDocument);
|
||||
} catch (err) {
|
||||
const duration = Date.now() - startedAt;
|
||||
await Job.findByIdAndUpdate(jobId, {
|
||||
status: "failed",
|
||||
lastRunAt: new Date(),
|
||||
lastDurationMs: duration,
|
||||
lastMessage: (err as Error).message
|
||||
});
|
||||
this.emitLog(jobId, `Hata: ${(err as Error).message}`);
|
||||
this.emitStatus(jobId, {
|
||||
status: "failed",
|
||||
lastRunAt: new Date(),
|
||||
lastDurationMs: duration,
|
||||
lastMessage: (err as Error).message
|
||||
} as JobDocument);
|
||||
this.emitLog(jobId, "Test tamamlandı: Hata");
|
||||
}
|
||||
}
|
||||
|
||||
scheduleJob(job: JobDocument) {
|
||||
const intervalMs = job.checkValue * unitToMs(job.checkUnit);
|
||||
if (!intervalMs || Number.isNaN(intervalMs)) return;
|
||||
|
||||
this.clearJob(job._id.toString());
|
||||
const timer = setInterval(() => this.runJob(job._id.toString()), intervalMs);
|
||||
this.timers.set(job._id.toString(), timer);
|
||||
}
|
||||
|
||||
clearJob(jobId: string) {
|
||||
const timer = this.timers.get(jobId);
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
this.timers.delete(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
const jobs = await Job.find();
|
||||
jobs.forEach((job) => this.scheduleJob(job));
|
||||
}
|
||||
}
|
||||
|
||||
export const jobService = new JobService();
|
||||
Reference in New Issue
Block a user