Runner: Build-Auftraege Review-Korrekturen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:53:41 +00:00
1 parent 4ed7e5d7a5
commit 4fffd8b218
2 files changed
+119 -32

No files matched your search

+48 -31
View File
@@ -46,18 +46,23 @@ async function dirSize(fs, p) {
const defaultFs = { ...nodeFs, chownR, copyTree, dirSize: (p) => dirSize(nodeFs, p) };
// ---- git (R11) ----
export function createGit(config) {
return function git({ repo, branch, dir, onLine }) {
const CLONE_MAX_MS = 5 * 60e3;
export function createGit(config, { spawnFn = spawn } = {}) {
return function git({ repo, branch, dir, onLine, timeoutMs = CLONE_MAX_MS }) {
const b64 = Buffer.from(`x-access-token:${config.giteaToken}`).toString('base64');
const url = `${String(config.giteaUrl).replace(/\/+$/, '')}/${repo}.git`;
const args = ['-c', `http.extraHeader=Authorization: Basic ${b64}`, 'clone', '--depth', '1', '--branch', branch, '--', url, dir];
const env = { PATH: process.env.PATH || '/usr/bin:/bin', GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', HOME: tmpdir() };
const args = ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=60', 'clone', '--depth', '1', '--branch', branch, '--', url, dir];
const env = { PATH: process.env.PATH || '/usr/bin:/bin', GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', HOME: tmpdir(),
// Token nur über die Umgebung des git-Prozesses, nie in argv/.git/config (R11)
GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'http.extraHeader', GIT_CONFIG_VALUE_0: `Authorization: Basic ${b64}` };
return new Promise((resolve, reject) => {
const child = spawn('git', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
const child = spawnFn('git', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; try { child.kill('SIGKILL'); } catch { /* beendet */ } reject(new Error('git clone: Zeitlimit überschritten')); }, Math.max(1000, Math.min(timeoutMs, CLONE_MAX_MS)));
const feed = (s) => { let buf = ''; s.setEncoding('utf8'); s.on('data', (d) => { const parts = (buf + d).split(/\r?\n|\r/); buf = parts.pop(); parts.forEach((l) => l && onLine?.(l)); }); s.on('end', () => buf && onLine?.(buf)); };
feed(child.stdout); feed(child.stderr);
child.on('error', reject);
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`git clone fehlgeschlagen (Exit-Code ${code})`))));
child.on('error', (e) => { clearTimeout(timer); reject(e); });
child.on('close', (code) => { clearTimeout(timer); if (!timedOut) { if (code === 0) resolve(); else reject(new Error(`git clone fehlgeschlagen (Exit-Code ${code})`)); } });
});
};
}
@@ -78,7 +83,9 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
// ---- Warteschlange mit genau einem aktiven Slot und Speicherwächter ----
function pump() {
if (active || queue.length === 0) return;
if (mem() < config.minFreeBytes) {
let free = 0;
try { free = mem(); } catch { /* Fehler = zu wenig Speicher */ }
if (!(free >= config.minFreeBytes)) {
if (!retry) { retry = true; setTimer(() => { retry = false; pump(); }, MEM_RETRY_MS); }
return;
}
@@ -114,6 +121,8 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
// ---- Container-Schritt ----
async function containerStep({ job, id, step, cmd, pm, workDir, cacheDir, env, onLine, deadline }) {
await fs.mkdir(cacheDir, { recursive: true });
const t = new Date(now());
await fs.utimes(cacheDir, t, t).catch(() => {}); // Alter = letzte Nutzung
await fs.chownR(workDir);
await fs.chownR(cacheDir); // R7
const remaining = Math.max(1000, deadline - now());
@@ -125,42 +134,48 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
// Klonen + Erkennung + Installation (R1); Aufrufer hält den Slot.
async function prepare(job, { onLine, workDir, id = crypto.randomUUID(), deadline = now() + config.buildTimeoutMs }) {
const cacheDir = join(config.cacheRoot, cacheName(job.repo));
activeCaches.add(cacheName(job.repo));
try {
await fs.rm(workDir, { recursive: true, force: true });
await fs.mkdir(dirname(workDir), { recursive: true });
await gitFn({ repo: job.repo, branch: job.branch, dir: workDir, onLine });
const detected = detectFromDir(workDir, { mode: job.mode, index: job.index });
if (detected.error || detected.kind === 'static' || !detected.install?.length) return { workDir, detected };
const r = await containerStep({ job, id, step: 'install', cmd: detected.install, pm: detected.pm, workDir, cacheDir, onLine, deadline });
if (r.code !== 0) return { workDir, detected, error: failText('Installation', r.code, config.buildTimeoutMs) };
return { workDir, detected };
} finally {
activeCaches.delete(cacheName(job.repo));
}
await fs.rm(workDir, { recursive: true, force: true });
await fs.mkdir(dirname(workDir), { recursive: true });
await gitFn({ repo: job.repo, branch: job.branch, dir: workDir, onLine, timeoutMs: Math.max(1000, deadline - now()) });
const detected = detectFromDir(workDir, { mode: job.mode, index: job.index });
if (detected.error || detected.kind === 'static' || !detected.install?.length) return { workDir, detected };
const r = await containerStep({ job, id, step: 'install', cmd: detected.install, pm: detected.pm, workDir, cacheDir, onLine, deadline });
if (r.code !== 0) return { workDir, detected, error: failText('Installation', r.code, config.buildTimeoutMs) };
return { workDir, detected };
}
// Cache für die gesamte Laufzeit vor pruneCache schützen.
async function holdCache(repo, fn) {
const n = cacheName(repo);
activeCaches.add(n);
try { return await fn(); } finally { activeCaches.delete(n); }
}
// ---- Atomare Veröffentlichung (R11) ----
async function publish(job, srcDir, { skipGit }) {
async function publish(job, srcDir, { skipGit, onLine }) {
const target = join(config.previewsRoot, job.sub);
const fresh = `${target}.new`;
const old = `${target}.old`;
await fs.rm(fresh, { recursive: true, force: true });
// Abgebrochener früherer Tausch: fehlt <sub>, ist <sub>.old die letzte gute Kopie – zurückholen.
const exists = (p) => fs.lstat(p).then(() => true, () => false);
if (!(await exists(target)) && (await exists(old))) await fs.rename(old, target);
await fs.rm(old, { recursive: true, force: true });
let hadOld = true;
try {
await fs.mkdir(config.previewsRoot, { recursive: true });
await fs.copyTree(fs, srcDir, fresh, { skipGit });
let hadOld = true;
try { await fs.rename(target, old); } catch (e) { if (e.code !== 'ENOENT') throw e; hadOld = false; }
try { await fs.rename(fresh, target); } catch (e) {
if (hadOld) await fs.rename(old, target).catch(() => {});
if (hadOld) await fs.rename(old, target).catch((e2) => onLine?.(`Rollback fehlgeschlagen: ${e2.message}`));
throw e;
}
await fs.rm(old, { recursive: true, force: true });
} catch (e) {
await fs.rm(fresh, { recursive: true, force: true }).catch(() => {});
throw e;
}
// Ab hier ist der Preview live; Aufräumen darf den Lauf nicht scheitern lassen.
await fs.rm(old, { recursive: true, force: true }).catch((e) => onLine?.(`Aufräumen fehlgeschlagen: ${e.message}`));
}
async function findOutput(workDir, dirs) {
@@ -179,7 +194,8 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
}
// ---- Ablauf eines Build-Laufs ----
async function execute(run, job) {
const execute = (run, job) => holdCache(job.repo, () => executeRun(run, job));
async function executeRun(run, job) {
const onLine = makeLog(run.log, masker(job));
const workDir = join(config.workRoot, 'build', run.runId);
const deadline = now() + config.buildTimeoutMs;
@@ -192,7 +208,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
if (prep.error) throw new Error(prep.error);
if (d.kind === 'static') {
if (d.hint) onLine(d.hint);
await publish(job, workDir, { skipGit: true });
await publish(job, workDir, { skipGit: true, onLine });
run.result = 'static';
} else if (d.kind === 'dev') {
run.result = 'dev';
@@ -210,7 +226,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
if (job.mode === 'auto' && d.devCmd) { onLine(`${problem} – Dev-Server wird verwendet`); run.result = 'dev'; }
else throw new Error(problem);
} else {
await publish(job, out, { skipGit: false });
await publish(job, out, { skipGit: false, onLine });
run.result = 'build';
}
}
@@ -245,12 +261,13 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
// Für Dev (Task 5): gleicher Slot, Klon + Installation in workDir.
function prepareWorkspace(job, { onLine = () => {}, workDir } = {}) {
const log = (l) => onLine(masker(job)(l));
return enqueue(async () => {
const mask = masker(job);
const log = (l) => onLine(mask(l));
return enqueue(() => holdCache(job.repo, async () => {
const r = await prepare(job, { onLine: log, workDir });
if (r.error) throw new Error(r.error);
return { workDir: r.workDir, detected: r.detected };
});
}));
}
async function pruneCache() {