fix: certify generated symlink state

This commit is contained in:
2026-07-31 19:20:39 -05:00
parent 7ae3f97789
commit df7530aeac
6 changed files with 269 additions and 29 deletions
+39 -9
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { createHash, randomUUID } from 'node:crypto';
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { sourceFingerprint } from './preflight.mjs';
import { generatedSymlinkManifest, sourceFingerprint } from './preflight.mjs';
const scriptRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -26,6 +26,18 @@ function run(command, args, options) {
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function requireRealDirectory(target, { allowMissing = false } = {}) {
try {
const stats = await lstat(target);
if (!stats.isDirectory() || stats.isSymbolicLink()) {
throw new Error(`${target} must be a real directory, not a symbolic link.`);
}
} catch (error) {
if (allowMissing && error.code === 'ENOENT') return;
throw error;
}
}
async function acquireBuildLock(root) {
const workRoot = path.join(root, '.mosaic-test-work');
const lock = path.join(workRoot, 'web-build.lock');
@@ -89,12 +101,19 @@ export async function buildWeb({
try {
const webDir = path.join(root, 'apps', 'web');
const nextDir = path.join(webDir, '.next');
const marker = path.join(nextDir, '.mosaic-source-hash');
const temporary = `${marker}.${randomUUID()}.tmp`;
const certificationMarker = path.join(nextDir, '.mosaic-source-hash');
const symlinkManifest = path.join(nextDir, '.mosaic-symlink-manifest');
const certificationTemporary = `${certificationMarker}.${randomUUID()}.tmp`;
const manifestTemporary = `${symlinkManifest}.${randomUUID()}.tmp`;
const before = await fingerprint(root);
await rm(marker, { force: true });
await requireRealDirectory(nextDir, { allowMissing: true });
await Promise.all([
rm(certificationMarker, { force: true }),
rm(symlinkManifest, { force: true }),
]);
await runBuild(webDir);
await requireRealDirectory(nextDir);
const after = await fingerprint(root);
if (after !== before) {
@@ -103,9 +122,20 @@ export async function buildWeb({
);
}
await mkdir(nextDir, { recursive: true });
await writeFile(temporary, `${before}\n`, { mode: 0o600 });
await rename(temporary, marker);
const manifestContents = await generatedSymlinkManifest(nextDir);
const certificationContents = `${JSON.stringify({
version: 1,
sourceFingerprint: before,
symlinkManifestHash: createHash('sha256').update(manifestContents).digest('hex'),
})}\n`;
await Promise.all([
writeFile(certificationTemporary, certificationContents, { mode: 0o600 }),
writeFile(manifestTemporary, manifestContents, { mode: 0o600 }),
]);
// The certification marker is the commit point. Publishing the manifest first
// leaves interrupted builds untrusted because the marker remains absent.
await rename(manifestTemporary, symlinkManifest);
await rename(certificationTemporary, certificationMarker);
} finally {
await releaseLock();
}