import { spawnSync } from "node:child_process"; import { lstat } from "node:fs/promises"; import path from "node:path"; import { ciCheckoutIdentityFailures, ciBuildEnvironmentFailures, isValidCommitSha, isValidSourceDateEpoch, } from "./lib/build-environment.ts"; import { classifyGateStepResult } from "./lib/ci-step-result.ts"; import { withCiGatePreflight } from "./contracts/ci-gates.ts"; import { validateCiArtifact } from "./lib/ci-artifact-validator.ts"; import { writeCiGateLogAtomic } from "./lib/ci-gate-log.ts"; const requestedGateId = process.argv .slice(2) .find((argument) => /^FE-GATE-\d{3}$/.test(argument)); await withCiGatePreflight( process.cwd(), requestedGateId, async ({ contractIndex, gateId, gate }) => { const logArtifact = contractIndex.artifacts.get(gate.logArtifactId); if (!logArtifact) throw new TypeError(`CI gate log artifact disappeared: ${gate.logArtifactId}`); const logSchema = contractIndex.artifactSchemas.get(logArtifact.schemaId); if (!logSchema || logSchema.kind !== "text") { throw new TypeError(`CI gate log schema must be bounded text: ${logArtifact.schemaId}`); } const output: string[] = []; let outputBytes = 0; let passed = true; const DEFAULT_STEP_TIMEOUT_MS = 30 * 60 * 1_000; const MAX_STEP_OUTPUT_BYTES = 16 * 1024 * 1_024; const LOG_DIAGNOSTIC_RESERVE_BYTES = 4_096; const freshlyProducedArtifactIds = new Set(); const commandGeneratedEvidence = gate.evidenceArtifactIds .map((artifactId) => contractIndex.artifacts.get(artifactId)) .filter((artifact) => artifact?.production === "command-generated"); async function observeArtifactGeneration(relativePath: string): Promise { try { const metadata = await lstat(path.join(process.cwd(), relativePath), { bigint: true, }); return [ metadata.dev, metadata.ino, metadata.size, metadata.mtimeNs, metadata.ctimeNs, ].join(":"); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return "missing"; throw error; } } const appendOutput = (...values: readonly string[]): boolean => { for (const value of values.filter(Boolean)) { const addedBytes = Buffer.byteLength(value, "utf8") + 1; if (outputBytes + addedBytes > logSchema.maxBytes) return false; output.push(value); outputBytes += addedBytes; } return true; }; const gateEnvironment = { ...process.env }; if (gateEnvironment.CI === "true") { const commitMetadata = spawnSync( "git", ["show", "-s", "--format=%H%n%ct", "HEAD"], { encoding: "utf8" }, ); const [commitSha = "", sourceDateEpoch = ""] = commitMetadata.stdout.trim().split(/\r?\n/); if ( commitMetadata.status === 0 && isValidCommitSha(commitSha) && isValidSourceDateEpoch(sourceDateEpoch) ) { if (!gateEnvironment.SOURCE_DATE_EPOCH?.trim()) { gateEnvironment.SOURCE_DATE_EPOCH = sourceDateEpoch; appendOutput(`derived SOURCE_DATE_EPOCH=${sourceDateEpoch} from HEAD`); } for (const failure of ciCheckoutIdentityFailures(gateEnvironment, { commitSha, sourceDateEpoch, })) { appendOutput(failure); passed = false; } } else { appendOutput( "unable to resolve the checked-out commit identity and timestamp", commitMetadata.stderr, ); passed = false; } } for (const failure of ciBuildEnvironmentFailures(gateEnvironment)) { appendOutput(failure); passed = false; } for (const variable of gate.requiresEnvironment ?? []) { if (!gateEnvironment[variable]) { appendOutput(`missing required environment: ${variable}`); passed = false; } } if (passed) { for (const commandId of gate.commandIds) { const step = contractIndex.commands.get(commandId); if (!step) throw new TypeError(`CI gate command disappeared after validation: ${commandId}`); const producedArtifacts = commandGeneratedEvidence.filter((artifact) => artifact.producerCommandIds.includes(commandId) ); const generationBefore = new Map( await Promise.all( producedArtifacts.map(async (artifact) => [ artifact.id, await observeArtifactGeneration(artifact.path), ] as const), ), ); const commandLine = `$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim(); if (!appendOutput(commandLine) || logSchema.maxBytes - outputBytes <= LOG_DIAGNOSTIC_RESERVE_BYTES) { appendOutput("gate aggregate output budget exhausted before command execution"); passed = false; break; } const result = spawnSync( "corepack", ["pnpm", step.script, ...(step.args ?? [])], { encoding: "utf8", env: gateEnvironment, timeout: step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS, maxBuffer: Math.min( MAX_STEP_OUTPUT_BYTES, logSchema.maxBytes - outputBytes - LOG_DIAGNOSTIC_RESERVE_BYTES, ), }, ); const stdout = result.stdout ?? ""; const stderr = result.stderr ?? ""; if (!appendOutput(stdout, stderr)) { appendOutput("gate aggregate output exceeded the bounded log schema"); passed = false; break; } const expectation = step.expect === "pass" ? ({ kind: "pass" } as const) : step.expectedExitCode !== undefined && step.expectedDiagnosticId !== undefined ? ({ kind: "fail", expectedExitCode: step.expectedExitCode, expectedDiagnosticId: step.expectedDiagnosticId, } as const) : (() => { throw new TypeError(`negative command lost its validated identity: ${step.id}`); })(); const classification = classifyGateStepResult(expectation, { status: result.status, signal: result.signal, stdout, stderr, ...(result.error ? { error: { code: (result.error as NodeJS.ErrnoException).code } } : {}), }); if (!appendOutput(`classification: ${classification.kind}`)) passed = false; if (!classification.expectationMet) { appendOutput( `expectation failed: expected ${step.expect}, exit=${result.status}, signal=${result.signal ?? "none"}`, ...(step.expect === "fail" ? [ `expected negative fixture identity: exit=${step.expectedExitCode}, diagnostic=${JSON.stringify(step.expectedDiagnosticId)}`, ] : []), ...(classification.kind === "INFRASTRUCTURE_FAILURE" ? [`infrastructure failure: ${classification.detail}`] : []), ); passed = false; break; } for (const artifact of producedArtifacts) { const generationAfter = await observeArtifactGeneration(artifact.path); if (generationAfter !== generationBefore.get(artifact.id)) { freshlyProducedArtifactIds.add(artifact.id); } } } } if (passed) { for (const artifact of commandGeneratedEvidence) { if (!freshlyProducedArtifactIds.has(artifact.id)) { appendOutput(`command-generated evidence was not freshly produced: ${artifact.path}`); passed = false; } } } await writeCiGateLogAtomic({ root: process.cwd(), relativePath: logArtifact.path, content: `${output.filter(Boolean).join("\n")}\n`, maxBytes: logSchema.maxBytes, }); if (passed) { const validationIds = [...new Set([gate.logArtifactId, ...gate.evidenceArtifactIds])]; for (const artifactId of validationIds) { const artifact = contractIndex.artifacts.get(artifactId); if (!artifact) throw new TypeError(`CI artifact disappeared: ${artifactId}`); const schema = contractIndex.artifactSchemas.get(artifact.schemaId); if (!schema) throw new TypeError(`CI artifact schema disappeared: ${artifact.schemaId}`); try { await validateCiArtifact({ root: process.cwd(), artifact, schema }); if (!appendOutput(`validated evidence: ${artifact.path} (${schema.id})`)) passed = false; } catch (error) { appendOutput( `invalid evidence: ${artifact.path}: ${error instanceof Error ? error.message : String(error)}`, ); passed = false; } } if (!passed) { await writeCiGateLogAtomic({ root: process.cwd(), relativePath: logArtifact.path, content: `${output.filter(Boolean).join("\n")}\n`, maxBytes: logSchema.maxBytes, }); } } if (!passed) { process.stderr.write(`${gateId} ${gate.name}: FAIL\n`); process.exit(1); } process.stdout.write( `${gateId} ${gate.name}: PASS (${gate.retentionClassId})\n`, ); }, );