feat(jobs): işler için env konfigürasyonu ekle
İşlere .env dosyası konfigürasyonu özelliği eklendi. Kullanıcılar artık depodan .env.example dosyalarını listeleyebilir, seçebilir ve içeriklerini düzenleyebilir. Backend: - Job modeline envContent ve envExampleName alanları eklendi - /jobs/env-examples endpoint'i eklendi - cloneOrPull ile .env dosyaları korunur - İş çalıştırma sırasında .env otomatik oluşturulur - Dockerfile'a bash, curl, jq eklendi Frontend: - İş formlarına Environment sekmesi eklendi - .env.example dosyaları seçilebilir - Env içeriği düzenlenebilir ve gizlenebilir - Log görüntüleme iyileştirildi (progress bar desteği)
This commit is contained in:
@@ -3,7 +3,7 @@ FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json .
|
||||
RUN apk add --no-cache git openssh-client docker-cli docker-cli-compose && npm install
|
||||
RUN apk add --no-cache bash curl jq git openssh-client docker-cli docker-cli-compose && npm install
|
||||
|
||||
COPY tsconfig.json .
|
||||
COPY src ./src
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface JobDocument extends Document {
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
envContent?: string;
|
||||
envExampleName?: string;
|
||||
status: "idle" | "running" | "success" | "failed";
|
||||
lastRunAt?: Date;
|
||||
lastDurationMs?: number;
|
||||
@@ -23,6 +25,8 @@ const JobSchema = new Schema<JobDocument>(
|
||||
testCommand: { type: String, required: true, trim: true },
|
||||
checkValue: { type: Number, required: true, min: 1 },
|
||||
checkUnit: { type: String, required: true, enum: ["dakika", "saat", "gün"] },
|
||||
envContent: { type: String },
|
||||
envExampleName: { type: String },
|
||||
status: { type: String, enum: ["idle", "running", "success", "failed"], default: "idle" },
|
||||
lastRunAt: { type: Date },
|
||||
lastDurationMs: { type: Number },
|
||||
|
||||
@@ -79,6 +79,19 @@ router.get("/metrics/summary", async (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/env-examples", async (req, res) => {
|
||||
const repoUrl = req.query.repoUrl as string | undefined;
|
||||
if (!repoUrl) {
|
||||
return res.status(400).json({ message: "repoUrl gerekli" });
|
||||
}
|
||||
try {
|
||||
const examples = await jobService.listRemoteEnvExamples(repoUrl);
|
||||
return res.json({ examples });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Env example alınamadı", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const job = await Job.findById(id).lean();
|
||||
@@ -89,12 +102,20 @@ router.get("/:id", async (req, res) => {
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit } = req.body;
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit, envContent, envExampleName } = req.body;
|
||||
if (!name || !repoUrl || !testCommand || !checkValue || !checkUnit) {
|
||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||
}
|
||||
try {
|
||||
const job = await Job.create({ name, repoUrl, testCommand, checkValue, checkUnit });
|
||||
const job = await Job.create({
|
||||
name,
|
||||
repoUrl,
|
||||
testCommand,
|
||||
checkValue,
|
||||
checkUnit,
|
||||
envContent,
|
||||
envExampleName
|
||||
});
|
||||
await jobService.persistMetadata(job);
|
||||
jobService.scheduleJob(job);
|
||||
// Yeni job oluşturulduğunda ilk test otomatik tetiklensin
|
||||
@@ -107,11 +128,11 @@ router.post("/", async (req, res) => {
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit } = req.body;
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit, envContent, envExampleName } = req.body;
|
||||
try {
|
||||
const job = await Job.findByIdAndUpdate(
|
||||
id,
|
||||
{ name, repoUrl, testCommand, checkValue, checkUnit },
|
||||
{ name, repoUrl, testCommand, checkValue, checkUnit, envContent, envExampleName },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
|
||||
@@ -25,6 +25,8 @@ type JobMetadata = {
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
envContent?: string;
|
||||
envExampleName?: string;
|
||||
};
|
||||
|
||||
type StoredJobRun = {
|
||||
@@ -165,6 +167,27 @@ function runCommand(
|
||||
});
|
||||
}
|
||||
|
||||
async function listRemoteEnvExamples(repoUrl: string) {
|
||||
await fs.promises.mkdir(repoBaseDir, { recursive: true });
|
||||
const tmpBase = await fs.promises.mkdtemp(path.join(repoBaseDir, ".tmp-"));
|
||||
try {
|
||||
await runCommand(`git clone --depth 1 ${repoUrl} ${tmpBase}`, process.cwd(), () => undefined);
|
||||
const entries = await fs.promises.readdir(tmpBase, { withFileTypes: true });
|
||||
const files = entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => name.toLowerCase().endsWith(".env.example"));
|
||||
const items = await Promise.all(
|
||||
files.map(async (name) => ({
|
||||
name,
|
||||
content: await fs.promises.readFile(path.join(tmpBase, name), "utf8")
|
||||
}))
|
||||
);
|
||||
return items;
|
||||
} finally {
|
||||
await fs.promises.rm(tmpBase, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
||||
const repoDir = path.join(repoBaseDir, job._id.toString());
|
||||
await ensureDir(repoDir);
|
||||
@@ -173,7 +196,7 @@ async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
||||
|
||||
if (!exists) {
|
||||
const entries = await fs.promises.readdir(repoDir);
|
||||
const allowed = new Set<string>([jobMetadataFileName, jobRunsDirName]);
|
||||
const allowed = new Set<string>([jobMetadataFileName, jobRunsDirName, ".env", ".env.local"]);
|
||||
const blocking = entries.filter((name) => !allowed.has(name));
|
||||
if (blocking.length > 0) {
|
||||
throw new Error("Repo klasoru git olmayan dosyalar iceriyor");
|
||||
@@ -185,6 +208,12 @@ async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
||||
metadataBackup = await fs.promises.readFile(metadataPath, "utf8");
|
||||
}
|
||||
|
||||
let envBackup: string | null = null;
|
||||
const envPath = path.join(repoDir, ".env");
|
||||
if (fs.existsSync(envPath)) {
|
||||
envBackup = await fs.promises.readFile(envPath, "utf8");
|
||||
}
|
||||
|
||||
let runsBackupPath: string | null = null;
|
||||
const runsDir = path.join(repoDir, jobRunsDirName);
|
||||
if (fs.existsSync(runsDir)) {
|
||||
@@ -205,6 +234,9 @@ async function cloneOrPull(job: JobDocument, onData: (chunk: string) => void) {
|
||||
if (metadataBackup) {
|
||||
await fs.promises.writeFile(metadataPath, metadataBackup, "utf8");
|
||||
}
|
||||
if (envBackup) {
|
||||
await fs.promises.writeFile(envPath, envBackup, "utf8");
|
||||
}
|
||||
if (runsBackupPath) {
|
||||
await fs.promises.rename(runsBackupPath, runsDir);
|
||||
}
|
||||
@@ -282,11 +314,15 @@ class JobService {
|
||||
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);
|
||||
|
||||
try {
|
||||
const repoDir = await cloneOrPull(job, (line) => pushLog(line));
|
||||
await ensureDependencies(repoDir, (line) => pushLog(line));
|
||||
pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`);
|
||||
await runCommand(job.testCommand, repoDir, (line) => pushLog(line), 180_000);
|
||||
try {
|
||||
const repoDir = await cloneOrPull(job, (line) => pushLog(line));
|
||||
await ensureDependencies(repoDir, (line) => pushLog(line));
|
||||
if (job.envContent) {
|
||||
await fs.promises.writeFile(path.join(repoDir, ".env"), job.envContent, "utf8");
|
||||
pushLog(".env güncellendi");
|
||||
}
|
||||
pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`);
|
||||
await runCommand(job.testCommand, repoDir, (line) => pushLog(line), 180_000);
|
||||
pushLog("Test tamamlandı: Başarılı");
|
||||
const duration = Date.now() - startedAt;
|
||||
await Job.findByIdAndUpdate(jobId, {
|
||||
@@ -336,6 +372,10 @@ class JobService {
|
||||
}
|
||||
}
|
||||
|
||||
async listRemoteEnvExamples(repoUrl: string) {
|
||||
return listRemoteEnvExamples(repoUrl);
|
||||
}
|
||||
|
||||
scheduleJob(job: JobDocument) {
|
||||
const intervalMs = job.checkValue * unitToMs(job.checkUnit);
|
||||
if (!intervalMs || Number.isNaN(intervalMs)) return;
|
||||
@@ -364,7 +404,9 @@ class JobService {
|
||||
repoUrl: job.repoUrl,
|
||||
testCommand: job.testCommand,
|
||||
checkValue: job.checkValue,
|
||||
checkUnit: job.checkUnit
|
||||
checkUnit: job.checkUnit,
|
||||
envContent: job.envContent,
|
||||
envExampleName: job.envExampleName
|
||||
});
|
||||
}
|
||||
|
||||
@@ -392,12 +434,20 @@ class JobService {
|
||||
const existing = await Job.findOne({ repoUrl: metadata.repoUrl });
|
||||
if (existing) continue;
|
||||
|
||||
let envContent = metadata.envContent;
|
||||
const envPath = path.join(jobDir, ".env");
|
||||
if (!envContent && fs.existsSync(envPath)) {
|
||||
envContent = await fs.promises.readFile(envPath, "utf8");
|
||||
}
|
||||
|
||||
const created = await Job.create({
|
||||
name: metadata.name,
|
||||
repoUrl: metadata.repoUrl,
|
||||
testCommand: metadata.testCommand,
|
||||
checkValue: metadata.checkValue,
|
||||
checkUnit: metadata.checkUnit
|
||||
checkUnit: metadata.checkUnit,
|
||||
envContent,
|
||||
envExampleName: metadata.envExampleName
|
||||
});
|
||||
await this.persistMetadata(created);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user