import { mkdir, readFile, writeFile } from "node:fs/promises"; import { evaluatePromotionReadiness, PROMOTION_FORMULA, type GateResult, } from "../src/application/policies/promotion-readiness.ts"; type GateDefinition = Readonly<{ steps?: readonly unknown[]; evidence?: readonly string[]; retentionClass?: string; }>; type CiContractDocument = Readonly<{ providerAdapter: string; stages: Readonly>>; gates: Readonly>; retention: Readonly<{ durationStatus: unknown }>; }>; const document = parseCiContractDocument( JSON.parse(await readFile("config/ci/gates.json", "utf8")), ); const workflow = await readFile(document.providerAdapter, "utf8"); const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as { scripts?: Record; }; const packageScripts = packageDocument.scripts ?? {}; const nodeVersion = (await readFile(".nvmrc", "utf8")).trim(); const gateRunner = await readFile("scripts/run-ci-gate.ts", "utf8"); const drillRunner = await readFile("scripts/drill-runbook.ts", "utf8"); const buildManifestGenerator = await readFile( "scripts/generate-build-manifest.ts", "utf8", ); const failures: string[] = []; if (!/^\d+\.\d+\.\d+$/.test(nodeVersion)) { failures.push(".nvmrc must contain one exact Node.js semantic version"); } const setupNodeCount = workflow.match(/uses:\s*actions\/setup-node@v4/g)?.length ?? 0; const nodeVersionFileCount = workflow.match(/node-version-file:\s*\.nvmrc/g)?.length ?? 0; if (setupNodeCount === 0 || nodeVersionFileCount !== setupNodeCount) { failures.push("every setup-node step must use node-version-file: .nvmrc"); } if (/node-version\s*:/.test(workflow) || /NODE_VERSION\s*:/.test(workflow)) { failures.push("workflow must not override the exact .nvmrc Node.js pin"); } const stageFormula: Readonly> = { merge: PROMOTION_FORMULA.MERGE_READY, release: PROMOTION_FORMULA.RELEASE_READY, production: PROMOTION_FORMULA.PROD_PROMOTION_READY, field: PROMOTION_FORMULA.FIELD_SLO_READY, documentation: PROMOTION_FORMULA.DOCUMENTATION_READY, }; for (const [stage, expectedGates] of Object.entries(stageFormula)) { const actual = document.stages[stage]?.gates; if (JSON.stringify(actual) !== JSON.stringify(expectedGates)) { failures.push(`${stage} gate formula drift`); } } const configuredGateIds = Object.keys(document.gates).sort(); const expectedGateIds = Array.from( { length: 26 }, (_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`, ); if (JSON.stringify(configuredGateIds) !== JSON.stringify(expectedGateIds)) { failures.push("gate registry must contain FE-GATE-001..026 exactly once"); } for (const [gateId, gate] of Object.entries(document.gates)) { if (!gate.steps?.length || !gate.evidence?.length || !gate.retentionClass) { failures.push(`${gateId} lacks command, evidence, or retention wiring`); } for (const [index, step] of (gate.steps ?? []).entries()) { if (!isRecord(step) || (step.expect !== "pass" && step.expect !== "fail")) { failures.push(`${gateId}[${index}] has an invalid step expectation`); continue; } if (step.expect === "pass") { if ( step.expectedExitCode !== undefined || step.expectedDiagnosticId !== undefined ) { failures.push( `${gateId}[${index}] passing step declares a negative fixture identity`, ); } continue; } if ( typeof step.expectedExitCode !== "number" || !Number.isSafeInteger(step.expectedExitCode) || step.expectedExitCode < 1 || step.expectedExitCode > 255 ) { failures.push(`${gateId}[${index}] lacks an exact expected exit code`); } const diagnosticId = step.expectedDiagnosticId; if ( typeof diagnosticId !== "string" || diagnosticId.trim().length === 0 || diagnosticId.length > 256 || ["\r", "\n", "\0"].some( (character) => typeof diagnosticId === "string" && diagnosticId.includes(character), ) ) { failures.push( `${gateId}[${index}] lacks a bounded expected diagnostic identity`, ); } } } const runbookGateEvidence = Object.freeze({ "FE-GATE-016": "artifacts/runbooks/FE-RB-005/record.json", "FE-GATE-021": "artifacts/runbooks/FE-RB-001/record.json", "FE-GATE-022": "artifacts/runbooks/FE-RB-002/record.json", "FE-GATE-023": "artifacts/runbooks/FE-RB-003/record.json", "FE-GATE-024": "artifacts/runbooks/FE-RB-004/record.json", "FE-GATE-025": "artifacts/runbooks/FE-RB-005/record.json", }); for (const [gateId, evidencePath] of Object.entries(runbookGateEvidence)) { const evidence = document.gates[gateId]?.evidence; if ( !Array.isArray(evidence) || evidence.length !== 1 || evidence[0] !== evidencePath ) { failures.push(`${gateId} runbook evidence path drift`); } } if ( !drillRunner.includes( "const artifactDirectory = `artifacts/runbooks/${runbookId}`", ) || drillRunner.includes( "artifacts/runbooks/${runbookId}/${release.releaseId}", ) ) { failures.push( "runbook evidence path must be stable while releaseId stays in the record", ); } const forbiddenWorkflowPatterns = [ /continue-on-error\s*:/, /retention-days\s*:/, /allow_failure\s*:/, ]; for (const pattern of forbiddenWorkflowPatterns) { if (pattern.test(workflow)) { failures.push(`workflow contains forbidden downgrade/unsupported setting ${pattern}`); } } const jobTimeoutCount = workflow.match(/timeout-minutes:\s*45/g)?.length ?? 0; if (jobTimeoutCount !== 9) { failures.push("every CI gate job must declare timeout-minutes: 45"); } if (/if-no-files-found:\s*warn/.test(workflow)) { failures.push("CI evidence upload must fail when artifacts are absent"); } for (const requiredToken of [ "merge_gate:", "release_gate:", "production_gate:", "field_gate:", "documentation_gate:", "immutable_build:", "vulnerability_provider:", "provenance_provider:", "promotion:", "needs: merge_gate", "needs: release_gate", "needs: production_gate", "needs: immutable_build", "needs: [immutable_build, vulnerability_provider, provenance_provider]", "actions/download-artifact@v4", "actions/upload-artifact@v4", "if: always()", "permissions:", "contents: read", 'CI: "true"', 'VITE_BUILD_ID: "gitea-${{ gitea.run_id }}-${{ gitea.run_attempt }}"', 'VITE_COMMIT_SHA: "${{ gitea.sha }}"', 'RELEASE_ID: "${{ gitea.ref }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}"', 'CI_RUNNER_IMAGE: "${{ vars.RUNNER_IMAGE_DIGEST }}"', "VULNERABILITY_REPORT_PATH:", "PROVENANCE_ATTESTATION_PATH:", "VULNERABILITY_PUBLIC_KEY_PATH:", "PROVENANCE_PUBLIC_KEY_PATH:", "release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}", "corepack pnpm verify:provider-evidence", "corepack pnpm verify:promotion", ]) { if (!workflow.includes(requiredToken)) { failures.push(`workflow missing ${requiredToken}`); } } for (const script of [ "build:release-candidate", "verify:local-evidence", "verify:provider-evidence", "verify:promotion", ]) { if (!packageScripts[script]) failures.push(`package script missing ${script}`); } for (const removedScript of [ "build:release", "verify:supply-chain", "verify:supply-chain:promotion", ]) { if (packageScripts[removedScript]) { failures.push(`legacy package script remains ${removedScript}`); } } if (/\b(?:build|rebuild)(?::[\w-]+)?\b/u.test(packageScripts["verify:promotion"] ?? "")) { failures.push("verify:promotion must not build or rebuild candidate bytes"); } const promotionWorkflow = workflow.match( /\n {2}promotion:\n(?[\s\S]*?)\n {2}production_gate:/u, )?.groups?.body; if (!promotionWorkflow) { failures.push("workflow promotion job is missing or misplaced"); } else if ( /\b(?:build|build:[\w-]+|rebuild)\b/u.test( promotionWorkflow.replaceAll("immutable_build", ""), ) ) { failures.push("workflow promotion job must not build or rebuild candidate bytes"); } const immutableGateSteps = document.gates["FE-GATE-015"]?.steps; if ( JSON.stringify(immutableGateSteps) !== JSON.stringify([ { script: "build:release-candidate", expect: "pass" }, { script: "verify:local-evidence", expect: "pass" }, ]) ) { failures.push("FE-GATE-015 must build the candidate once and verify local evidence only"); } for (const requiredToken of [ "ciCheckoutIdentityFailures", "ciBuildEnvironmentFailures", "SOURCE_DATE_EPOCH", '"--format=%H%n%ct"', "env: gateEnvironment", "classifyGateStepResult", "timeout: step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS", ]) { if (!gateRunner.includes(requiredToken)) { failures.push(`CI gate runner missing ${requiredToken}`); } } for (const requiredToken of [ "assertCiBuildEnvironment(process.env)", "releaseId", "sourceDateEpoch", ]) { if (!buildManifestGenerator.includes(requiredToken)) { failures.push(`build manifest generator missing ${requiredToken}`); } } const passingResults: Record = {}; for (const gateId of expectedGateIds) passingResults[gateId] = "PASS"; const allPass = evaluatePromotionReadiness(passingResults); const negativeFixtures: Array<{ readiness: keyof typeof PROMOTION_FORMULA; failedGate: string; passed: boolean; }> = []; for (const readiness of Object.keys(PROMOTION_FORMULA) as Array< keyof typeof PROMOTION_FORMULA >) { const gateIds = PROMOTION_FORMULA[readiness]; const failedGate = gateIds[0]; if (!failedGate) throw new Error(`${readiness} has no configured gates`); const result = evaluatePromotionReadiness({ ...passingResults, [failedGate]: "FAIL", }); const passed = result[readiness] === false; negativeFixtures.push({ readiness, failedGate, passed }); if (!passed) failures.push(`${readiness} did not fail closed`); } if (!Object.values(allPass).every(Boolean)) { failures.push("all-PASS formula did not produce every readiness state"); } const report = { schemaVersion: 1, generatedAt: new Date().toISOString(), providerAdapter: document.providerAdapter, nodeVersion, gateCount: configuredGateIds.length, noDowngrade: failures.every( (failure) => !failure.includes("downgrade"), ), durationStatus: document.retention.durationStatus, negativeFixtures, failures, passed: failures.length === 0, }; await mkdir("artifacts/quality", { recursive: true }); await writeFile( "artifacts/quality/ci-contract.json", `${JSON.stringify(report, null, 2)}\n`, ); if (failures.length > 0) { process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`); process.exit(1); } process.stdout.write("CI contract: 26 blocking gates and 4-tier graph PASS\n"); function parseCiContractDocument(value: unknown): CiContractDocument { if (!isRecord(value)) throw new TypeError("CI gate config must be an object"); if ( typeof value.providerAdapter !== "string" || !isRecord(value.stages) || !isRecord(value.gates) || !isRecord(value.retention) ) { throw new TypeError("CI gate config is missing required registries"); } const stages: Record = {}; for (const [stage, candidate] of Object.entries(value.stages)) { if (!isRecord(candidate)) throw new TypeError(`Invalid CI stage: ${stage}`); if ( candidate.gates !== undefined && (!Array.isArray(candidate.gates) || !candidate.gates.every((gate) => typeof gate === "string")) ) { throw new TypeError(`Invalid gate list for CI stage: ${stage}`); } stages[stage] = { gates: candidate.gates as readonly string[] | undefined, }; } const gates: Record = {}; for (const [gateId, candidate] of Object.entries(value.gates)) { if (!isRecord(candidate)) throw new TypeError(`Invalid CI gate: ${gateId}`); if ( candidate.evidence !== undefined && (!Array.isArray(candidate.evidence) || !candidate.evidence.every((path) => typeof path === "string")) ) { throw new TypeError(`Invalid evidence list for CI gate: ${gateId}`); } gates[gateId] = { steps: Array.isArray(candidate.steps) ? candidate.steps : undefined, evidence: candidate.evidence as readonly string[] | undefined, retentionClass: typeof candidate.retentionClass === "string" ? candidate.retentionClass : undefined, }; } return { providerAdapter: value.providerAdapter, stages, gates, retention: { durationStatus: value.retention.durationStatus }, }; } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }