chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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<string>();
|
||||
const commandGeneratedEvidence = gate.evidenceArtifactIds
|
||||
.map((artifactId) => contractIndex.artifacts.get(artifactId))
|
||||
.filter((artifact) => artifact?.production === "command-generated");
|
||||
|
||||
async function observeArtifactGeneration(relativePath: string): Promise<string> {
|
||||
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`,
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user