Runner: Build-Auftraege Review-Korrekturen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ed7e5d7a5
commit
4fffd8b218
2 files changed
+119
-32
No files matched your search
+48
-31
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user