fix(runtime): remove build staging folders left over by killed builds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-09 10:33:55 +01:00
1 parent 76f88163d1
commit 4fbb2e84c8
2 files changed
+43 -1

No files matched your search

+17 -1
View File
@@ -1,5 +1,5 @@
import { build } from 'astro';
import { cp, mkdir, rm } from 'node:fs/promises';
import { cp, mkdir, readdir, rm, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -10,6 +10,21 @@ import { clashingPostWarnings, reservedPostWarnings } from '../site/data.ts';
export const APP_ROOT = fileURLToPath(new URL('../..', import.meta.url));
const STALE_STAGING_MS = 3_600_000;
/**
* Removes `build-*` staging folders in `dir` older than an hour: leftovers of builds that were killed
* (timeout, container stop) before their own cleanup ran. Younger ones may belong to a running build.
*/
export async function removeStaleStaging(dir: string, now = Date.now()): Promise<void> {
const names = await readdir(dir).catch(() => [] as string[]);
for (const name of names.filter((n) => n.startsWith('build-'))) {
const path = join(dir, name);
const info = await stat(path).catch(() => null);
if (info?.isDirectory() && now - info.mtimeMs > STALE_STAGING_MS) await rm(path, { recursive: true, force: true });
}
}
/**
* Validates a content directory, fetches the external sources once, and renders the static site
* into `outDir`. `cacheDir` holds the per-source caches (so an outage falls back to the last good
@@ -25,6 +40,7 @@ export async function buildSite(contentDir: string, outDir: string, opts: { cach
const cacheDir = resolve(opts.cacheDir ?? join(APP_ROOT, '.tmp', 'sources'));
const { content, errors } = await loadContent(source);
if (!content) throw new ContentError(errors);
await removeStaleStaging(join(APP_ROOT, '.tmp'));
await mkdir(cacheDir, { recursive: true });
const sources = await loadSources({ cacheDir, env: process.env });
+26
View File
@@ -0,0 +1,26 @@
import { expect, test } from 'bun:test';
import { existsSync, mkdirSync, mkdtempSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { removeStaleStaging } from '../../src/runtime/build.ts';
test('removes build staging folders older than an hour, keeps fresh ones and everything else', async () => {
const dir = mkdtempSync(join(tmpdir(), 'site-tmp-'));
const now = Date.now();
const make = (name: string, ageMs: number) => {
const p = join(dir, name);
mkdirSync(p);
writeFileSync(join(p, 'index.html'), 'x');
const t = new Date(now - ageMs);
utimesSync(p, t, t);
return p;
};
const stale = make('build-old', 2 * 3_600_000);
const fresh = make('build-new', 10 * 60_000);
const other = make('sources', 5 * 3_600_000);
await removeStaleStaging(dir, now);
expect(existsSync(stale)).toBe(false);
expect(existsSync(fresh)).toBe(true);
expect(existsSync(other)).toBe(true);
await removeStaleStaging(join(dir, 'missing'), now); // no folder yet: nothing to do
});