import { readFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import path from "node:path"; import { z } from "zod"; import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts"; import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts"; import { RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH, } from "../lib/release-candidate.ts"; import { validateInstallScriptPolicy, validateNpmScopeEnvironment, validatePackageScriptGraph, } from "../lib/package-script-graph.ts"; import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts"; const ciActionRegistrationSchema = z .object({ repository: z .string() .regex( /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u, "CI action repository must be an absolute upstream GitHub URL", ), revision: z .string() .regex(/^[0-9a-f]{40}$/u, "CI action revision must be a full 40-hex commit SHA"), version: z.string().min(1).max(64), }) .strict() .readonly(); const ciActionRegistrySchema = z .object({ checkout: ciActionRegistrationSchema, setupNode: ciActionRegistrationSchema, uploadArtifact: ciActionRegistrationSchema, downloadArtifact: ciActionRegistrationSchema, }) .strict() .readonly(); export type CiActionRegistry = z.infer; export type CiActionId = keyof CiActionRegistry; export function parseCiActionRegistry(value: unknown): CiActionRegistry { const result = ciActionRegistrySchema.safeParse(value); if (!result.success) { const diagnostic = result.error.issues .map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`) .join("\n"); throw new TypeError(`CI action registry invalid:\n${diagnostic}`); } return result.data; } export const CI_ACTION_REGISTRY = parseCiActionRegistry({ checkout: { repository: "https://github.com/actions/checkout", revision: "34e114876b0b11c390a56381ad16ebd13914f8d5", version: "v4.3.1", }, setupNode: { repository: "https://github.com/actions/setup-node", revision: "49933ea5288caeca8642d1e84afbd3f7d6820020", version: "v4.4.0", }, uploadArtifact: { repository: "https://github.com/ChristopherHX/gitea-upload-artifact", revision: "81f940d004763f986ba3582c007fd842dd5cb0d7", version: "v4 branch", }, downloadArtifact: { repository: "https://github.com/ChristopherHX/gitea-download-artifact", revision: "75635f32b4c1c41c4b3d64e8f85210112ed4c9c7", version: "v4 branch", }, }); const actionIdByStepKind = Object.freeze({ checkout: "checkout", "setup-node": "setupNode", upload: "uploadArtifact", download: "downloadArtifact", } as const satisfies Readonly>); export function resolveCiActionUses(actionId: CiActionId): string { const action = CI_ACTION_REGISTRY[actionId]; if (!action) throw new TypeError(`unknown CI action: ${String(actionId)}`); return `${action.repository}@${action.revision}`; } export function resolveCiStepActionUses(stepKind: string): string | null { const actionId = actionIdByStepKind[stepKind as keyof typeof actionIdByStepKind]; return actionId ? resolveCiActionUses(actionId) : null; } function hasAsciiControl(value: string): boolean { return [...value].some((character) => { const codePoint = character.codePointAt(0)!; return codePoint <= 0x1f || codePoint === 0x7f; }); } function hasForbiddenLineOrControl(value: string): boolean { return [...value].some((character) => { const codePoint = character.codePointAt(0)!; return ( codePoint <= 0x1f || codePoint === 0x7f || codePoint === 0x85 || codePoint === 0x2028 || codePoint === 0x2029 ); }); } const nonEmpty = z .string() .min(1) .max(4_096) .refine((value) => !hasForbiddenLineOrControl(value), "control and Unicode line-break characters are forbidden") .refine((value) => value === value.trim(), "leading/trailing whitespace is forbidden"); const id = nonEmpty.max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u); const repositoryPath = nonEmpty.superRefine((value, context) => { if ( value.includes("\\") || value.includes("\0") || path.posix.isAbsolute(value) || path.posix.normalize(value) !== value || value === "." || value === ".." || value.startsWith("../") || value.includes("/../") ) { context.addIssue({ code: "custom", message: `unsafe repository path: ${value}` }); } }); const workflowValue = nonEmpty.refine( (value) => !value.includes("\0") && !value.includes("\r"), "workflow values must not contain NUL or carriage returns", ); const workflowPath = workflowValue.superRefine((value, context) => { if ( value.includes("\\") || path.posix.isAbsolute(value) || path.posix.normalize(value) !== value || value === "." || value === ".." || value.startsWith("../") || value.includes("/../") ) { context.addIssue({ code: "custom", message: `unsafe workflow path: ${value}` }); } }); const commandSchema = z .object({ id, script: nonEmpty.regex(/^[A-Za-z0-9:_-]+$/u), args: z.array(z.string().max(512).refine((value) => !hasAsciiControl(value), "command arguments contain controls")).max(32).optional(), timeoutMs: z.number().int().min(1_000).max(3_600_000).optional(), expect: z.enum(["pass", "fail"]), expectedExitCode: z.number().int().min(1).max(255).optional(), expectedDiagnosticId: nonEmpty.max(256).optional(), }) .strict() .superRefine((command, context) => { const hasNegativeIdentity = command.expectedExitCode !== undefined || command.expectedDiagnosticId !== undefined; if (command.expect === "pass" && hasNegativeIdentity) { context.addIssue({ code: "custom", message: `pass command carries negative identity: ${command.id}`, }); } if ( command.expect === "fail" && (command.expectedExitCode === undefined || !command.expectedDiagnosticId) ) { context.addIssue({ code: "custom", message: `negative command lacks exact identity: ${command.id}`, }); } if ( command.expectedDiagnosticId && /[\n\r\0]/u.test(command.expectedDiagnosticId) ) { context.addIssue({ code: "custom", message: `negative command diagnostic is unsafe: ${command.id}`, }); } }); const artifactSchemaSchema = z.discriminatedUnion("kind", [ z.object({ id, kind: z.literal("text"), maxBytes: z.number().int().min(1).max(268_435_456) }).strict(), z .object({ id, kind: z.literal("json"), maxBytes: z.number().int().min(1).max(268_435_456), executableSchemaId: z.enum([ "automated-a11y", "manual-a11y", "architecture-dependency-report", "design-system-contract", "i18n-contract", "diagnostics-contract", "realtime-boundaries", "optional-recipes", "optional-recipe-fixtures", "registry-compatibility-fixtures", "reproducible-build", "supply-chain-fixtures", "supply-chain-provider-fixtures", "compatibility-fixtures", "documentation-review", "hosting-headers", "coverage-summary-v8", "risk-coverage-v3", "build-manifest", "module-inventory", "dependency-inventory", "registry-snapshot", "registry-governance-run", "bundle-performance", "sbom", "provenance", "dependency-diff", "license-report", "vulnerability-report", "field-web-vitals", "lab-performance", "release-verification", "runbook-record", "supply-chain-verification", "release-candidate", "supply-chain-coherence", "http-scenario-receipt", "test-evidence-report", "provider-vulnerability", "provider-provenance", "provider-verification", "ci-contract-report", "deployment-admission", ]), }) .strict(), z .object({ id, kind: z.literal("json-schema"), maxBytes: z.number().int().min(1).max(268_435_456), }) .strict(), z.object({ id, kind: z.literal("junit"), maxBytes: z.number().int().min(1).max(268_435_456) }).strict(), z.object({ id, kind: z.literal("html"), maxBytes: z.number().int().min(1).max(268_435_456) }).strict(), z.object({ id, kind: z.literal("markdown"), maxBytes: z.number().int().min(1).max(268_435_456) }).strict(), z.object({ id, kind: z.literal("sarif"), maxBytes: z.number().int().min(1).max(268_435_456) }).strict(), z .object({ id, kind: z.literal("candidate-archive"), maxBytes: z.number().int().min(1).max(268_435_456) }) .strict(), ]); const artifactBaseShape = { id, path: repositoryPath, schemaId: id } as const; const artifactSchema = z.discriminatedUnion("production", [ z.object({ ...artifactBaseShape, production: z.literal("source-controlled") }).strict(), z .object({ ...artifactBaseShape, production: z.literal("command-generated"), producerCommandIds: z.array(id).min(1).max(32), }) .strict(), z.object({ ...artifactBaseShape, production: z.literal("runner-generated") }).strict(), ]); const gateSchema = z .object({ id: z.string().regex(/^FE-GATE-\d{3}$/u), name: nonEmpty.regex(/^[a-z0-9][a-z0-9-]*$/u), commandIds: z.array(id).min(1).max(128), logArtifactId: id, evidenceArtifactIds: z.array(id).min(1).max(128), retentionClassId: id, requiresEnvironment: z.array(z.string().max(128).regex(/^[A-Z][A-Z0-9_]*$/u)).max(32).optional(), }) .strict(); const stageSchema = z .object({ id, readiness: z.enum([ "MERGE_READY", "RELEASE_READY", "PROD_PROMOTION_READY", "FIELD_SLO_READY", "DOCUMENTATION_READY", ]), needs: z.array(id).max(16), gateIds: z.array(id).min(1).max(64), }) .strict(); const environmentBindingSchema = z .object({ name: z.string().regex(/^[A-Z][A-Z0-9_]*$/u), value: workflowValue }) .strict(); const checkoutStep = z.object({ kind: z.literal("checkout") }).strict(); const setupNodeStep = z.object({ kind: z.literal("setup-node") }).strict(); const frozenInstallStep = z.object({ kind: z.literal("frozen-install") }).strict(); const browserInstallStep = z.object({ kind: z.literal("browser-install") }).strict(); const runGateStep = z.object({ kind: z.literal("run-gate") }).strict(); const archiveCandidateStep = z .object({ kind: z.literal("archive-candidate"), stepId: id, archivePath: workflowPath, members: z.array(repositoryPath).min(1).max(128), archiveOutputName: id, distOutputName: id, }) .strict(); const uploadStep = z .object({ kind: z.literal("upload"), transferId: id, name: workflowValue, paths: z.array(workflowPath).min(1).max(128), always: z.boolean().optional(), }) .strict(); const downloadStep = z .object({ kind: z.literal("download"), transferId: id, path: workflowPath }) .strict(); const extractStep = z .object({ kind: z.literal("extract"), archivePath: workflowPath, targetRoot: workflowPath, }) .strict(); const providerStep = z .object({ kind: z.literal("run-provider"), provider: z.enum(["vulnerability", "provenance"]), stepId: id, }) .strict(); const validateProviderStep = z .object({ kind: z.literal("validate-provider-evidence"), provider: z.enum(["vulnerability", "provenance"]), }) .strict(); const promotionStep = z .object({ kind: z.literal("verify-promotion"), stepId: id }) .strict(); const cleanupPromotionStep = z .object({ kind: z.literal("cleanup-promotion"), finalizerStepId: id }) .strict(); const jobStepSchema = z.discriminatedUnion("kind", [ checkoutStep, setupNodeStep, frozenInstallStep, browserInstallStep, runGateStep, archiveCandidateStep, uploadStep, downloadStep, extractStep, providerStep, validateProviderStep, promotionStep, cleanupPromotionStep, ]); const jobSchema = z .object({ id, displayName: workflowValue, kind: z.enum(["gate-matrix", "gate-single", "immutable", "provider", "promotion"]), needs: z.array(id).max(32), condition: z.enum(["always", "needs-success", "merge", "release", "production", "field", "documentation"]), timeoutMinutes: z.number().int().positive(), gateIds: z.array(id).max(64), browserGateIds: z.array(id).max(64), environment: z.array(environmentBindingSchema).max(32), steps: z.array(jobStepSchema).min(1).max(64), }) .strict(); const retentionSchema = z .object({ durationStatus: nonEmpty, classes: z.array(z.object({ id, policy: nonEmpty }).strict()).min(1).max(32), }) .strict(); const ciGateContractBaseSchema = z .object({ schemaVersion: z.literal(2), providerAdapter: repositoryPath, commands: z.array(commandSchema).min(1).max(256), artifactSchemas: z.array(artifactSchemaSchema).min(1).max(128), artifacts: z.array(artifactSchema).min(1).max(512), gates: z.array(gateSchema).min(1).max(64), stages: z.array(stageSchema).min(1).max(16), jobs: z.array(jobSchema).min(1).max(32), retention: retentionSchema, }) .strict(); const ciGateContractSchema = ciGateContractBaseSchema.superRefine( (contract, context) => validateContractSemantics(contract, context), ); export type CiGateContract = z.infer; export type CiGateCommand = CiGateContract["commands"][number]; export type CiGateArtifactSchema = CiGateContract["artifactSchemas"][number]; export type CiGateArtifact = CiGateContract["artifacts"][number]; export type CiGate = CiGateContract["gates"][number]; export type CiWorkflowJob = CiGateContract["jobs"][number]; export type CiWorkflowStep = CiWorkflowJob["steps"][number]; export type CiGateContractIndex = Readonly<{ commands: ReadonlyMap; artifactSchemas: ReadonlyMap; artifacts: ReadonlyMap; gates: ReadonlyMap; stages: ReadonlyMap; jobs: ReadonlyMap; retentionClasses: ReadonlyMap; }>; export type LoadCiGateContractOptions = Readonly<{ mode?: "canonical" | "removal-fixture"; }>; const CANONICAL_GATE_SHAPE_SHA256 = // Template merge. Both sides carried a digest of their own gate set; neither // describes the merged one. Recomputed from the merged config/ci/gates.json. // Task 11: recomputed again after FE-GATE-009 gained the // TECH_LOG_STUDIO_ASSETS manual accessibility evidence artifact. // Final fix wave item 1: recomputed again after FE-GATE-010 gained // `check-tech-log-contract`. Recomputed with `canonicalGateShapeSha256` // below, verified by first reproducing the previous constant from the // previous `config/ci/gates.json` before hashing the new one. // Alignment follow-up item 2: recomputed again after FE-GATE-007 gained // `test-tech-log` and its junit evidence, by the same method — the previous // constant f3cc9075… was reproduced from the previous gates.json first, so // the transcription that produced this value is known to be the real one. // Dev release manifest drift fix, item 2: recomputed again after FE-GATE-010 // gained `check-dev-release-manifest`. Same method — 98d19911… was first // reproduced from the previous gates.json before this value was hashed. "b40962448e617883060e09eb7183837cfea6833519f435f11713efd083210dbe"; function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string { const normalized = gates.map( ({ id, name, commandIds, logArtifactId, evidenceArtifactIds, retentionClassId, requiresEnvironment, }) => ({ id, name, commandIds, logArtifactId, evidenceArtifactIds, retentionClassId, requiresEnvironment: requiresEnvironment ?? [], }), ); return createHash("sha256").update(JSON.stringify(normalized)).digest("hex"); } function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] { const failures: string[] = []; const commandReferenceCount = contract.gates.reduce( (total, gate) => total + gate.commandIds.length, 0, ); if (contract.gates.length !== 27) { failures.push(`gate authority baseline must contain exactly 27 gates; received ${contract.gates.length}`); } // Final fix wave, item 1: FE-GATE-010 gained `check-tech-log-contract`, the // drift gate that pins the vendored canonical Studio contract to its digest. // Until it was referenced by a gate it ran only when typed by hand. // Alignment follow-up, item 2: FE-GATE-007 gained `test-tech-log`. The suite // already ran inside `test:coverage`'s combined vitest invocation, so a // TechLog failure was reported as a coverage-gate failure with no junit of // its own to name it. // Dev release manifest drift fix, item 2: FE-GATE-010 gained // `check-dev-release-manifest`. No gate read `public/*.json` at all, so a // fixture that did not declare the compiled contract set broke `pnpm dev` // outright while every static gate stayed green. if (contract.commands.length !== 85 || commandReferenceCount !== 97) { failures.push( `command authority baseline must contain exactly 85 definitions and 97 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, ); } // Template merge. 126 product artifacts plus the two the template added. // Task 11 added one more: the TECH_LOG_STUDIO_ASSETS manual a11y evidence file. // Alignment follow-up, item 2 added the TechLog junit report. if (contract.artifacts.length !== 130) { failures.push(`artifact authority baseline must contain exactly 130 artifacts; received ${contract.artifacts.length}`); } if (contract.stages.length !== 5) { failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`); } const expectedRetentionClasses = [ { id: "merge-cycle", policy: "at least through pull-request readiness decision" }, { id: "release-coherence", policy: "at least until the next release is promoted" }, { id: "prod-drill", policy: "at least until the next production promotion decision" }, { id: "field", policy: "through the 28-day window and aggregation" }, { id: "documentation", policy: "through documentation readiness review" }, ]; if (JSON.stringify(contract.retention.classes) !== JSON.stringify(expectedRetentionClasses)) { failures.push("retention registry must contain exactly the five canonical retention classes"); } return failures; } export function parseCiGateContract( value: unknown, options: LoadCiGateContractOptions = {}, ): CiGateContract { const result = ciGateContractSchema.safeParse(value); if (!result.success) { const diagnostic = result.error.issues .map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`) .join("\n"); throw new TypeError(`CI gate contract invalid:\n${diagnostic}`); } if ((options.mode ?? defaultCiContractMode()) === "canonical") { const failures = canonicalAuthorityBaselineFailures(result.data); if (failures.length > 0) { throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`); } } return result.data; } /** * A removal fixture runs the whole suite against a deliberately *reduced* CI * contract: the removed capability's gates, commands and artifacts are pruned. * Loading that contract in canonical mode re-imposes the full exact-count * authority on it, so the fixture failed on the very reduction it exists to * prove. `runRemovalFixturePnpm` marks those runs, and this is where the mark * is honoured. */ export function defaultCiContractMode(): "canonical" | "removal-fixture" { return process.env.CI_CONTRACT_MODE === "removal-fixture" ? "removal-fixture" : "canonical"; } export function isReducedCiContractRun(): boolean { return defaultCiContractMode() === "removal-fixture"; } export async function loadCiGateContract( root = process.cwd(), options: LoadCiGateContractOptions = {}, ): Promise { const mode = options.mode ?? defaultCiContractMode(); const [rawContract, rawPackage] = await Promise.all([ readFile(path.join(root, "config/ci/gates.json"), "utf8"), readFile(path.join(root, "package.json"), "utf8"), ]); const contract = parseCiGateContract(JSON.parse(rawContract), { mode }); if ( mode === "canonical" && canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256 ) { throw new TypeError("CI gate contract canonical gate semantic shape drift"); } const packageDocument = z .object({ scripts: z.record(z.string(), z.string()).default({}) }) .passthrough() .parse(JSON.parse(rawPackage)); const missing = contract.commands .map((command) => command.script) .filter((script, index, scripts) => scripts.indexOf(script) === index) .filter((script) => !packageDocument.scripts[script]); if (missing.length > 0) { throw new TypeError(`CI gate contract missing package scripts: ${missing.join(", ")}`); } const expectedCheckCi = mode === "canonical" ? "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow" : "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow"; if (packageDocument.scripts["check:ci"] !== expectedCheckCi) { throw new TypeError("check:ci must use the exact canonical non-recursive orchestration"); } const canonicalCheckCiDependencies = { "check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check", "check:ci-workflow": mode === "canonical" ? "node scripts/generate-ci-workflow.ts --check" : "node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture", } as const; for (const [script, expected] of Object.entries(canonicalCheckCiDependencies)) { if (packageDocument.scripts[script] !== expected) { throw new TypeError(`canonical check:ci dependency drift: ${script}`); } } const contractEntryScripts = [ ...new Set(contract.commands.map((command) => command.script)), ]; const graphFailures = [ ...validatePackageScriptGraph(packageDocument.scripts, "check:ci"), ...contractEntryScripts.flatMap((script) => validatePackageScriptGraph(packageDocument.scripts, script) ), ]; if (graphFailures.length > 0) { throw new TypeError( `CI package script graph invalid:\n${[...new Set(graphFailures)].join("\n")}`, ); } const installPolicyFailures = validateInstallScriptPolicy( packageDocument.scripts, contractEntryScripts, ); if (installPolicyFailures.length > 0) { throw new TypeError( `CI package script install policy invalid:\n${installPolicyFailures.join("\n")}`, ); } return contract; } export async function withCiGatePreflight( root: string, gateId: string | undefined, execute: (context: Readonly<{ contract: CiGateContract; contractIndex: CiGateContractIndex; gateId: string; gate: CiGate; }>) => Promise | Result, ): Promise { const npmScopeEnvironmentFailures = validateNpmScopeEnvironment(process.env); if (npmScopeEnvironmentFailures.length > 0) { throw new TypeError( `CI runner npm scope environment invalid:\n${npmScopeEnvironmentFailures.join("\n")}`, ); } const contract = await loadCiGateContract(root); const contractIndex = indexCiGateContract(contract); const gate = gateId ? contractIndex.gates.get(gateId) : undefined; if (!gateId || !gate) { throw new TypeError("CI gate id must be FE-GATE-001..FE-GATE-026"); } return execute({ contract, contractIndex, gateId, gate }); } export function indexCiGateContract(contract: CiGateContract): CiGateContractIndex { return Object.freeze({ commands: new Map(contract.commands.map((entry) => [entry.id, entry])), artifactSchemas: new Map(contract.artifactSchemas.map((entry) => [entry.id, entry])), artifacts: new Map(contract.artifacts.map((entry) => [entry.id, entry])), gates: new Map(contract.gates.map((entry) => [entry.id, entry])), stages: new Map(contract.stages.map((entry) => [entry.id, entry])), jobs: new Map(contract.jobs.map((entry) => [entry.id, entry])), retentionClasses: new Map(contract.retention.classes.map((entry) => [entry.id, entry])), }); } function validateContractSemantics( contract: z.infer, context: z.RefinementCtx, ): void { const issue = (message: string, path: PropertyKey[] = []) => context.addIssue({ code: "custom", message, path }); if (contract.providerAdapter !== ".gitea/workflows/quality-gates.yml") { issue("providerAdapter must target the canonical generated workflow"); } const registries = [ ["command", contract.commands], ["artifact schema", contract.artifactSchemas], ["artifact", contract.artifacts], ["gate", contract.gates], ["stage", contract.stages], ["job", contract.jobs], ["retention class", contract.retention.classes], ] as const; for (const [label, entries] of registries) { const seen = new Set(); for (const entry of entries) { if (seen.has(entry.id)) issue(`duplicate ${label} id: ${entry.id}`); seen.add(entry.id); } } const commandTuples = new Map(); for (const command of contract.commands) { const { id: _id, ...tuple } = command; const key = JSON.stringify(tuple); const previous = commandTuples.get(key); if (previous) issue(`duplicate command tuple: ${previous}, ${command.id}`); commandTuples.set(key, command.id); } const artifactPaths = new Map(); for (const artifact of contract.artifacts) { const previous = artifactPaths.get(artifact.path); if (previous) issue(`duplicate artifact path: ${previous}, ${artifact.id}`); artifactPaths.set(artifact.path, artifact.id); } const commandIds = new Set(contract.commands.map(({ id }) => id)); const schemaIds = new Set(contract.artifactSchemas.map(({ id }) => id)); const artifactIds = new Set(contract.artifacts.map(({ id }) => id)); const gateIds = new Set(contract.gates.map(({ id }) => id)); const stageIds = new Set(contract.stages.map(({ id }) => id)); const jobIds = new Set(contract.jobs.map(({ id }) => id)); const retentionIds = new Set(contract.retention.classes.map(({ id }) => id)); for (const artifact of contract.artifacts) { if (!schemaIds.has(artifact.schemaId)) { issue(`unknown artifact schema ${artifact.schemaId} for ${artifact.id}`); } if (artifact.production === "command-generated") { if (new Set(artifact.producerCommandIds).size !== artifact.producerCommandIds.length) { issue(`duplicate producer command reference for artifact: ${artifact.id}`); } for (const producerCommandId of artifact.producerCommandIds) { if (!commandIds.has(producerCommandId)) { issue(`unknown producer command ${producerCommandId} for ${artifact.id}`); } } } } for (const gate of contract.gates) { if (new Set(gate.commandIds).size !== gate.commandIds.length) { issue(`duplicate command reference within gate: ${gate.id}`); } if (new Set(gate.evidenceArtifactIds).size !== gate.evidenceArtifactIds.length) { issue(`duplicate artifact reference within gate: ${gate.id}`); } for (const commandId of gate.commandIds) { if (!commandIds.has(commandId)) issue(`unknown command ${commandId} for ${gate.id}`); } for (const artifactId of [gate.logArtifactId, ...gate.evidenceArtifactIds]) { if (!artifactIds.has(artifactId)) issue(`unknown artifact ${artifactId} for ${gate.id}`); } const logArtifact = contract.artifacts.find(({ id }) => id === gate.logArtifactId); if (logArtifact && logArtifact.production !== "runner-generated") { issue(`gate log must be runner-generated: ${gate.id}`); } for (const artifactId of gate.evidenceArtifactIds) { const artifact = contract.artifacts.find(({ id }) => id === artifactId); if ( artifact?.production === "command-generated" && !artifact.producerCommandIds.some((producerCommandId) => gate.commandIds.includes(producerCommandId) ) ) { issue(`gate lacks a bound producer command for ${artifact.id}: ${gate.id}`); } } if (!retentionIds.has(gate.retentionClassId)) { issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`); } } const accessibilityGate = contract.gates.find( ({ id: gateId }) => gateId === "FE-GATE-009", ); const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({ id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`, path: `artifacts/tests/a11y-manual/${routeId}.md`, })); const expectedAccessibilityEvidenceArtifactIds = [ "artifact-artifacts-tests-a11y-json", ...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId), "artifact-artifacts-tests-a11y-manual-report-json", ]; if ( !accessibilityGate || JSON.stringify(accessibilityGate.evidenceArtifactIds) !== JSON.stringify(expectedAccessibilityEvidenceArtifactIds) ) { issue( "FE-GATE-009 manual accessibility evidence must exactly match the installed route scope", ); } for (const expected of manualAccessibilityArtifacts) { const artifact = contract.artifacts.find( ({ id: artifactId }) => artifactId === expected.id, ); if ( !artifact || artifact.path !== expected.path || artifact.schemaId !== "markdown" || artifact.production !== "source-controlled" ) { issue( `FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`, ); } } const referencedRetentionClasses = new Set( contract.gates.map(({ retentionClassId }) => retentionClassId), ); for (const retentionClass of contract.retention.classes) { if (!referencedRetentionClasses.has(retentionClass.id)) { issue(`orphan retention class: ${retentionClass.id}`); } } const referencedCommands = new Set(contract.gates.flatMap(({ commandIds }) => commandIds)); for (const command of contract.commands) { if (!referencedCommands.has(command.id)) issue(`orphan command: ${command.id}`); } const referencedArtifacts = new Set( contract.gates.flatMap((gate) => [gate.logArtifactId, ...gate.evidenceArtifactIds]), ); for (const artifact of contract.artifacts) { if (!referencedArtifacts.has(artifact.id)) issue(`orphan artifact: ${artifact.id}`); } const referencedSchemas = new Set(contract.artifacts.map(({ schemaId }) => schemaId)); for (const schema of contract.artifactSchemas) { if (!referencedSchemas.has(schema.id)) issue(`orphan artifact schema: ${schema.id}`); } for (const stage of contract.stages) { for (const dependency of stage.needs) { if (dependency === stage.id) issue(`stage self dependency: ${stage.id}`); else if (!stageIds.has(dependency)) issue(`unknown stage dependency ${dependency} for ${stage.id}`); } for (const gateId of stage.gateIds) { if (!gateIds.has(gateId)) issue(`unknown gate ${gateId} for stage ${stage.id}`); } } for (const cycle of findCycles(contract.stages.map((stage) => [stage.id, stage.needs] as const))) { issue(`stage dependency cycle: ${cycle.join(" -> ")}`); } const expectedGateIds = Array.from( { length: 27 }, (_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`, ); if (JSON.stringify(contract.gates.map(({ id }) => id)) !== JSON.stringify(expectedGateIds)) { issue("gate registry must contain FE-GATE-001..027 in canonical order"); } const expectedStages: ReadonlyArray = [ ["merge", "MERGE_READY", [], PROMOTION_FORMULA.MERGE_READY], ["release", "RELEASE_READY", ["merge"], PROMOTION_FORMULA.RELEASE_READY], ["production", "PROD_PROMOTION_READY", ["release"], PROMOTION_FORMULA.PROD_PROMOTION_READY], ["field", "FIELD_SLO_READY", ["production"], PROMOTION_FORMULA.FIELD_SLO_READY], ["documentation", "DOCUMENTATION_READY", [], PROMOTION_FORMULA.DOCUMENTATION_READY], ]; const stageShape = contract.stages.map(({ id, readiness, needs, gateIds }) => [id, readiness, needs, gateIds]); if (JSON.stringify(stageShape) !== JSON.stringify(expectedStages)) { issue("stage formula/order/ownership drift"); } const stageOwners = new Map(); for (const stage of contract.stages) { for (const gateId of stage.gateIds) { stageOwners.set(gateId, [...(stageOwners.get(gateId) ?? []), stage.id]); } } for (const gateId of expectedGateIds) { if ((stageOwners.get(gateId) ?? []).length !== 1) issue(`gate must belong to exactly one stage: ${gateId}`); } const owners = new Map(); for (const job of contract.jobs) { for (const [label, values] of [ ["needs", job.needs], ["gateIds", job.gateIds], ["browserGateIds", job.browserGateIds], ] as const) { if (new Set(values).size !== values.length) { issue(`duplicate ${label} reference in job: ${job.id}`); } } if (job.timeoutMinutes !== 45) issue(`job timeout must be 45 minutes: ${job.id}`); for (const dependency of job.needs) { if (dependency === job.id) issue(`job self dependency: ${job.id}`); else if (!jobIds.has(dependency)) issue(`unknown job dependency ${dependency} for ${job.id}`); } for (const gateId of job.gateIds) { if (!gateIds.has(gateId)) issue(`unknown gate ${gateId} for job ${job.id}`); owners.set(gateId, [...(owners.get(gateId) ?? []), job.id]); } for (const browserGateId of job.browserGateIds) { if (!job.gateIds.includes(browserGateId)) { issue(`browser gate ${browserGateId} is not owned by ${job.id}`); } } validateJobStepKinds(job, issue); for (const step of job.steps) { const uses = resolveCiStepActionUses(step.kind); if (uses && !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/u.test(uses)) { issue(`CI action ref is not an absolute upstream URL pinned to a full commit SHA: ${step.kind}`); } } const envNames = new Set(job.environment.map(({ name }) => name)); for (const gateId of job.gateIds) { const gate = contract.gates.find(({ id }) => id === gateId); for (const required of gate?.requiresEnvironment ?? []) { if (!envNames.has(required)) issue(`job ${job.id} lacks environment ${required} for ${gateId}`); } } } for (const gate of contract.gates) { const gateOwners = owners.get(gate.id) ?? []; if (gateOwners.length === 0) issue(`unowned gate: ${gate.id}`); if (gateOwners.length > 1) issue(`multiply owned gate: ${gate.id} by ${gateOwners.join(", ")}`); } const expectedJobOwnership: Readonly> = { merge_gate: ["FE-GATE-001", "FE-GATE-002", "FE-GATE-003", "FE-GATE-004", "FE-GATE-005", "FE-GATE-006", "FE-GATE-007", "FE-GATE-008", "FE-GATE-009", "FE-GATE-010", "FE-GATE-011", "FE-GATE-013", "FE-GATE-020"], release_gate: ["FE-GATE-012", "FE-GATE-014", "FE-GATE-019", "FE-GATE-026"], immutable_build: ["FE-GATE-015", "FE-GATE-027"], vulnerability_provider: [], provenance_provider: [], promotion: [], production_gate: ["FE-GATE-016", "FE-GATE-021", "FE-GATE-022", "FE-GATE-023", "FE-GATE-024", "FE-GATE-025"], field_gate: ["FE-GATE-018"], documentation_gate: ["FE-GATE-017"], }; if (JSON.stringify(contract.jobs.map(({ id }) => id)) !== JSON.stringify(Object.keys(expectedJobOwnership))) { issue("job registry must contain the exact nine canonical jobs in semantic order"); } for (const [jobId, gateIds] of Object.entries(expectedJobOwnership)) { const job = contract.jobs.find(({ id }) => id === jobId); if (!job || JSON.stringify(job.gateIds) !== JSON.stringify(gateIds)) { issue(`exact gate execution ownership drift: ${jobId}`); } } const expectedJobGraph: Readonly> = { merge_gate: ["gate-matrix", [], "merge"], release_gate: ["gate-matrix", ["merge_gate"], "release"], immutable_build: ["immutable", ["release_gate"], "release"], vulnerability_provider: ["provider", ["immutable_build"], "always"], provenance_provider: ["provider", ["immutable_build"], "always"], promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "needs-success"], production_gate: ["gate-matrix", ["promotion"], "production"], field_gate: ["gate-single", ["production_gate"], "field"], documentation_gate: ["gate-single", [], "documentation"], }; for (const [jobId, [kind, needs, condition]] of Object.entries(expectedJobGraph)) { const job = contract.jobs.find(({ id }) => id === jobId); if (!job || job.kind !== kind || JSON.stringify(job.needs) !== JSON.stringify(needs) || job.condition !== condition) { issue(`job graph drift: ${jobId}`); } } const expectedStepKinds: Readonly> = { merge_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"], release_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"], immutable_build: ["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"], vulnerability_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"], provenance_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"], promotion: ["checkout", "setup-node", "frozen-install", "download", "download", "download", "verify-promotion", "upload", "cleanup-promotion"], production_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"], field_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"], documentation_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"], }; for (const [jobId, expected] of Object.entries(expectedStepKinds)) { const actual = contract.jobs.find(({ id }) => id === jobId)?.steps.map(({ kind }) => kind); if (JSON.stringify(actual) !== JSON.stringify(expected)) { issue(`canonical job step sequence drift: ${jobId}`); } } const expectedEnvironmentBindings: Readonly []>> = { merge_gate: [], release_gate: [{ name: "HOSTING_BASE_URL", value: "${{ vars.HOSTING_BASE_URL }}" }], immutable_build: [ // FE-GATE-027 admits the built artifact to a named environment, so both // the profile it was built from and the destination it is claimed for are // declared inputs. An absent RELEASE_TARGET is a refusal, not a default. { name: "APP_PROFILE", value: "${{ vars.APP_PROFILE }}" }, { name: "RELEASE_TARGET", value: "${{ vars.RELEASE_TARGET }}" }, ], vulnerability_provider: [ { name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" }, { name: "CANDIDATE_ARCHIVE_PATH", value: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" }, { name: "CI_RUN_ID", value: "${{ gitea.run_id }}" }, { name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" }, { name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" }, { name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" }, { name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" }, { name: "VULNERABILITY_PROVIDER_COMMAND", value: "${{ vars.VULNERABILITY_PROVIDER_COMMAND }}" }, { name: "VULNERABILITY_REPORT_PATH", value: "provider-evidence/untrusted/vulnerability-report.json" }, { name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/vulnerability-report.json" }, ], provenance_provider: [ { name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" }, { name: "CANDIDATE_ARCHIVE_PATH", value: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" }, { name: "CI_RUN_ID", value: "${{ gitea.run_id }}" }, { name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" }, { name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" }, { name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" }, { name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" }, { name: "PROVENANCE_PROVIDER_COMMAND", value: "${{ vars.PROVENANCE_PROVIDER_COMMAND }}" }, { name: "PROVENANCE_ATTESTATION_PATH", value: "provider-evidence/untrusted/provenance-attestation.json" }, { name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/provenance-attestation.json" }, ], promotion: [ { name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" }, { name: "CANDIDATE_ARCHIVE_PATH", value: ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" }, { name: "CI_RUN_ID", value: "${{ gitea.run_id }}" }, { name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" }, { name: "VULNERABILITY_REPORT_PATH", value: "${{ gitea.workspace }}/.release/vulnerability/vulnerability-report.json" }, { name: "PROVENANCE_ATTESTATION_PATH", value: "${{ gitea.workspace }}/.release/provenance/provenance-attestation.json" }, { name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" }, { name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" }, { name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" }, { name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" }, { name: "VULNERABILITY_INVOCATION_NONCE", value: "${{ needs.vulnerability_provider.outputs.invocation_nonce }}" }, { name: "PROVENANCE_INVOCATION_NONCE", value: "${{ needs.provenance_provider.outputs.invocation_nonce }}" }, ], production_gate: [], field_gate: [ { name: "FIELD_WEB_VITALS_INPUT", value: "${{ vars.FIELD_WEB_VITALS_INPUT }}" }, { name: "MIN_ELIGIBLE_SAMPLES", value: "${{ vars.MIN_ELIGIBLE_SAMPLES }}" }, ], documentation_gate: [], }; for (const job of contract.jobs) { const environmentNames = job.environment.map(({ name }) => name); if (new Set(environmentNames).size !== environmentNames.length || JSON.stringify(job.environment) !== JSON.stringify(expectedEnvironmentBindings[job.id])) { issue(`job environment binding drift: ${job.id}`); } } if (owners.get("FE-GATE-015")?.[0] !== "immutable_build") { issue("FE-GATE-015 must be owned only by immutable_build"); } for (const job of contract.jobs.filter(({ kind }) => kind === "gate-matrix")) { if (job.gateIds.includes("FE-GATE-015")) issue(`release matrix duplicates FE-GATE-015: ${job.id}`); } const browserGateIds = contract.jobs.flatMap(({ browserGateIds }) => browserGateIds).sort(asciiCompare); if (JSON.stringify(browserGateIds) !== JSON.stringify(["FE-GATE-008", "FE-GATE-009", "FE-GATE-026"])) { issue(`browser gate set drift: ${browserGateIds.join(",")}`); } for (const job of contract.jobs) { const browserSteps = job.steps.filter(({ kind }) => kind === "browser-install").length; const expected = job.browserGateIds.length > 0 ? 1 : 0; if (browserSteps !== expected) issue(`browser install step drift: ${job.id}`); } for (const cycle of findCycles(contract.jobs.map((job) => [job.id, job.needs] as const))) { issue(`job dependency cycle: ${cycle.join(" -> ")}`); } const uploads = new Map }>(); for (const job of contract.jobs) { for (const step of job.steps) { if (step.kind !== "upload") continue; const previous = uploads.get(step.transferId); if (previous) issue(`duplicate upload transfer ${step.transferId}: ${previous.producer}, ${job.id}`); uploads.set(step.transferId, { producer: job.id, step }); } } const dependencies = new Map(contract.jobs.map((job) => [job.id, job.needs])); for (const job of contract.jobs) { for (const step of job.steps) { if (step.kind !== "download") continue; const upload = uploads.get(step.transferId); if (!upload) issue(`unknown download transfer ${step.transferId} for ${job.id}`); else if (!isDependencyReachable(job.id, upload.producer, dependencies)) { issue(`download producer ${upload.producer} is unreachable from ${job.id}`); } } } const promotion = contract.jobs.find(({ id }) => id === "promotion"); if (promotion) { if (promotion.kind !== "promotion") issue("promotion job kind drift"); if (promotion.gateIds.length > 0 || promotion.steps.some(({ kind }) => kind === "run-gate")) { issue("promotion job must not own or run a gate"); } const forbidden = promotion.steps.filter(({ kind }) => ["archive-candidate", "run-provider"].includes(kind), ); if (forbidden.length > 0) issue("promotion job must not build or rebuild candidate bytes"); const order = promotion.steps.map(({ kind }) => kind); const verificationIndex = order.indexOf("verify-promotion"); const uploadIndex = order.indexOf("upload"); if (verificationIndex < 0 || uploadIndex !== verificationIndex + 1) { issue("promotion verification and upload must be immediately adjacent"); } const upload = promotion.steps[uploadIndex]; if (upload?.kind === "upload" && upload.always) { issue("promotion upload must not use always"); } const cleanupIndex = order.indexOf("cleanup-promotion"); if ( order.includes("extract") || verificationIndex < order.lastIndexOf("download") || uploadIndex < verificationIndex || cleanupIndex !== uploadIndex + 1 ) { issue("promotion formula order must download, finalize, upload, then cleanup without extraction"); } } const immutable = contract.jobs.find(({ id }) => id === "immutable_build"); const archive = immutable?.steps.find(({ kind }) => kind === "archive-candidate"); const expectedArchiveMembers = ["dist", ...RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH]; if ( !archive || archive.kind !== "archive-candidate" || JSON.stringify(archive.members) !== JSON.stringify(expectedArchiveMembers) ) { issue("immutable candidate archive member set drift"); } if ( archive?.kind === "archive-candidate" && (archive.stepId !== "candidate" || archive.archivePath !== ".release/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" || archive.archiveOutputName !== "archive_sha256" || archive.distOutputName !== "dist_sha256") ) { issue("immutable candidate output identity drift"); } const promotionUpload = promotion?.steps.find( (step) => step.kind === "upload" && step.transferId === "promoted-release", ); if (!promotionUpload || promotionUpload.kind !== "upload" || JSON.stringify(promotionUpload.paths) !== JSON.stringify(PROMOTED_UPLOAD_PATHS)) { issue("promotion upload bundle must contain the exact five typed paths"); } validateCanonicalStepFields(contract, issue); } function validateCanonicalStepFields( contract: z.infer, issue: (message: string, path?: PropertyKey[]) => void, ): void { const immutable = contract.jobs.find(({ id }) => id === "immutable_build"); const immutableArchive = immutable?.steps.find(({ kind }) => kind === "archive-candidate"); const immutableUpload = immutable?.steps.find( (step) => step.kind === "upload" && step.transferId === "release-candidate", ); if ( !immutableArchive || immutableArchive.kind !== "archive-candidate" || !immutableUpload || immutableUpload.kind !== "upload" || JSON.stringify(immutableUpload.paths) !== JSON.stringify([immutableArchive.archivePath]) ) { issue("immutable archive and upload fields must remain linked"); } const providerExpectations = { vulnerability_provider: { provider: "vulnerability", stepId: "supervise_vulnerability", downloadPath: ".release/vulnerability-candidate", archivePath: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz", rawPath: "provider-evidence/untrusted/vulnerability-report.json", rawName: "VULNERABILITY_REPORT_PATH", sealedPath: "provider-evidence/vulnerability-report.json", transferId: "vulnerability-provider-evidence", }, provenance_provider: { provider: "provenance", stepId: "supervise_provenance", downloadPath: ".release/provenance-candidate", archivePath: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz", rawPath: "provider-evidence/untrusted/provenance-attestation.json", rawName: "PROVENANCE_ATTESTATION_PATH", sealedPath: "provider-evidence/provenance-attestation.json", transferId: "provenance-provider-evidence", }, } as const; for (const [jobId, expected] of Object.entries(providerExpectations)) { const job = contract.jobs.find(({ id }) => id === jobId); const environment = new Map(job?.environment.map(({ name, value }) => [name, value])); const download = job?.steps.find(({ kind }) => kind === "download"); const runProvider = job?.steps.find(({ kind }) => kind === "run-provider"); const validateProvider = job?.steps.find(({ kind }) => kind === "validate-provider-evidence"); const upload = job?.steps.find( (step) => step.kind === "upload" && step.transferId === expected.transferId, ); if ( !download || download.kind !== "download" || download.transferId !== "release-candidate" || download.path !== expected.downloadPath || !runProvider || runProvider.kind !== "run-provider" || runProvider.provider !== expected.provider || runProvider.stepId !== expected.stepId || !validateProvider || validateProvider.kind !== "validate-provider-evidence" || validateProvider.provider !== expected.provider || environment.get("CANDIDATE_ARCHIVE_PATH") !== expected.archivePath || environment.get(expected.rawName) !== expected.rawPath || environment.get("VALIDATED_PROVIDER_REPORT_PATH") !== expected.sealedPath || !upload || upload.kind !== "upload" || JSON.stringify(upload.paths) !== JSON.stringify([expected.sealedPath]) ) { issue(`provider archive, extraction, evidence, and upload fields must remain linked: ${jobId}`); } } const promotion = contract.jobs.find(({ id }) => id === "promotion"); const promotionDownloads = promotion?.steps.filter(({ kind }) => kind === "download"); const expectedDownloads = [ { kind: "download", transferId: "release-candidate", path: ".release/candidate" }, { kind: "download", transferId: "vulnerability-provider-evidence", path: ".release/vulnerability" }, { kind: "download", transferId: "provenance-provider-evidence", path: ".release/provenance" }, ]; const promotionFinalizer = promotion?.steps.find(({ kind }) => kind === "verify-promotion"); const promotionCleanup = promotion?.steps.find(({ kind }) => kind === "cleanup-promotion"); if ( JSON.stringify(promotionDownloads) !== JSON.stringify(expectedDownloads) || !promotionFinalizer || promotionFinalizer.kind !== "verify-promotion" || promotionFinalizer.stepId !== "finalize" || !promotionCleanup || promotionCleanup.kind !== "cleanup-promotion" || promotionCleanup.finalizerStepId !== "finalize" ) { issue("promotion download fields and finalizer/cleanup step identities must remain linked"); } } function validateJobStepKinds( job: z.infer, issue: (message: string, path?: PropertyKey[]) => void, ): void { const allowed: Readonly["kind"], ReadonlySet>> = { "gate-matrix": new Set(["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"]), "gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]), immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]), provider: new Set(["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"]), promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]), }; for (const step of job.steps) { if (!allowed[job.kind].has(step.kind)) { issue(`step kind ${step.kind} is forbidden for ${job.kind} job ${job.id}`); } } const kinds = job.steps.map(({ kind }) => kind); if (job.kind === "provider") { const providerIndex = kinds.indexOf("run-provider"); const validateProviderIndex = kinds.indexOf("validate-provider-evidence"); const uploadIndex = kinds.indexOf("upload"); if ( providerIndex < kinds.lastIndexOf("download") || validateProviderIndex < providerIndex || uploadIndex < validateProviderIndex ) { issue(`provider execution/validation/upload order drift: ${job.id}`); } } } function findCycles(entries: readonly (readonly [string, readonly string[]])[]): string[][] { const graph = new Map(entries); const visiting = new Set(); const visited = new Set(); const stack: string[] = []; const cycles: string[][] = []; const visit = (node: string): void => { if (visiting.has(node)) { const start = stack.indexOf(node); cycles.push([...stack.slice(start), node]); return; } if (visited.has(node) || !graph.has(node)) return; visiting.add(node); stack.push(node); for (const next of graph.get(node) ?? []) visit(next); stack.pop(); visiting.delete(node); visited.add(node); }; for (const node of graph.keys()) visit(node); return cycles; } function isDependencyReachable( consumer: string, producer: string, graph: ReadonlyMap, ): boolean { const pending = [...(graph.get(consumer) ?? [])]; const visited = new Set(); while (pending.length > 0) { const current = pending.shift()!; if (current === producer) return true; if (visited.has(current)) continue; visited.add(current); pending.push(...(graph.get(current) ?? [])); } return false; } export function asciiCompare(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; }