personal-homepage/scripts/rebuild.ts

187 lines
4.7 KiB
TypeScript

import { spawn } from 'node:child_process';
import process from 'node:process';
import { REBUILD_EXIT_CODES, getRebuildExitSymbol } from './rebuild-codes.ts';
type RebuildStage = 'sync' | 'build' | 'deploy' | 'bootstrap';
type StepResult = {
ok: boolean;
exitCode: number;
stage: RebuildStage;
durationMs: number;
tail: string[];
};
async function main() {
const startedAt = new Date();
console.log(`[rebuild] start ${startedAt.toISOString()}`);
const syncResult = await runNpmStep({
stage: 'sync',
script: 'content:sync',
failureCode: REBUILD_EXIT_CODES.SYNC_FAILED,
});
if (!syncResult.ok) {
return finish(syncResult, startedAt);
}
const buildResult = await runNpmStep({
stage: 'build',
script: 'build',
failureCode: REBUILD_EXIT_CODES.BUILD_FAILED,
});
return finish(buildResult, startedAt);
}
async function runNpmStep(input: {
stage: RebuildStage;
script: string;
failureCode: number;
}): Promise<StepResult> {
console.log(`[rebuild] stage=${input.stage} command="npm run ${input.script}"`);
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const startedAt = Date.now();
const tailBuffer = createTailBuffer(40);
return new Promise((resolve) => {
const child = spawn(command, ['run', input.script], {
cwd: process.cwd(),
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.on('data', (chunk) => {
const text = chunk.toString();
process.stdout.write(text);
tailBuffer.push(text);
});
child.stderr.on('data', (chunk) => {
const text = chunk.toString();
process.stderr.write(text);
tailBuffer.push(text);
});
child.on('error', (error) => {
const durationMs = Date.now() - startedAt;
const exitCode = classifySpawnError(error);
tailBuffer.push(`[rebuild] failed to spawn command: ${error.message}\n`);
resolve({
ok: false,
exitCode,
stage: input.stage,
durationMs,
tail: tailBuffer.read(),
});
});
child.on('close', (code) => {
const durationMs = Date.now() - startedAt;
const normalizedExitCode =
code === 0 ? REBUILD_EXIT_CODES.SUCCESS : normalizeStageExitCode(input.failureCode, code);
resolve({
ok: normalizedExitCode === REBUILD_EXIT_CODES.SUCCESS,
exitCode: normalizedExitCode,
stage: input.stage,
durationMs,
tail: tailBuffer.read(),
});
});
});
}
function finish(result: StepResult, startedAt: Date) {
const finishedAt = new Date();
const payload = {
ok: result.ok,
stage: result.stage,
code: result.exitCode,
symbol: getRebuildExitSymbol(result.exitCode),
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationMs: finishedAt.getTime() - startedAt.getTime(),
failedStepDurationMs: result.durationMs,
logTail: result.tail,
};
console.log(`REBUILD_RESULT ${JSON.stringify(payload)}`);
if (!result.ok) {
console.error(
`[rebuild] failed stage=${result.stage} code=${result.exitCode} symbol=${payload.symbol}`,
);
} else {
console.log(`[rebuild] success code=0 symbol=${payload.symbol}`);
}
process.exitCode = result.exitCode;
}
function normalizeStageExitCode(fallbackCode: number, childCode: number | null) {
if (childCode == null) {
return fallbackCode;
}
if (Object.values(REBUILD_EXIT_CODES).includes(childCode as never)) {
return childCode;
}
return fallbackCode;
}
function classifySpawnError(error: NodeJS.ErrnoException) {
if (error.code === 'ENOENT') {
return REBUILD_EXIT_CODES.CONFIG_INVALID;
}
return REBUILD_EXIT_CODES.UNKNOWN_ERROR;
}
function createTailBuffer(maxLines: number) {
const lines: string[] = [];
return {
push(chunk: string) {
const normalized = chunk.replace(/\r\n/g, '\n');
for (const line of normalized.split('\n')) {
if (!line.trim()) {
continue;
}
lines.push(line);
}
while (lines.length > maxLines) {
lines.shift();
}
},
read() {
return [...lines];
},
};
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error('[rebuild] unhandled failure');
console.error(message);
console.log(
`REBUILD_RESULT ${JSON.stringify({
ok: false,
stage: 'bootstrap',
code: REBUILD_EXIT_CODES.UNKNOWN_ERROR,
symbol: getRebuildExitSymbol(REBUILD_EXIT_CODES.UNKNOWN_ERROR),
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
durationMs: 0,
failedStepDurationMs: 0,
logTail: [message],
})}`,
);
process.exitCode = REBUILD_EXIT_CODES.UNKNOWN_ERROR;
});