import { createHash } from "node:crypto"; import { mkdir, readFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import { verifyCompatibilityTuple, type CompatibilityTuple, } from "../src/application/policies/compatibility.ts"; import type { InstalledContractPackageIdentity } from "../src/contracts/external-contract-runtime.ts"; import { parseBuildManifestArtifact, parseReleaseArtifact, parseRuntimeConfigArtifact, projectReleaseTokens, type BuildManifestArtifact, type ReleaseArtifact, type RuntimeConfigArtifact, } from "../src/contracts/release-artifacts.ts"; import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts"; import { ROUTE_REGISTRY, ROUTE_RUNTIME_CONTRACT, } from "../src/features/installed-feature-contracts.ts"; import { assertMatchesJsonSchema } from "./lib/json-schema.ts"; import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; import { releaseVerificationArtifactSchema } from "./contracts/release-artifacts.ts"; type CoherenceFixture = Readonly<{ name: string; expectedCompatible: boolean; frontend: CompatibilityTuple; runtime: CompatibilityTuple; }>; type RuntimeConfigDocument = RuntimeConfigArtifact & Readonly<{ BUILD_ID: string; RELEASE_ID: string }>; type ViteManifestEntry = Readonly<{ file: string; name?: string; isDynamicEntry?: boolean; }>; export type ReleaseArtifactReader = (path: string) => Promise; export type ReleaseArtifactsCoherenceOptions = Readonly<{ readArtifact?: ReleaseArtifactReader; contractPackages?: readonly InstalledContractPackageIdentity[]; paths?: Readonly<{ release: string; runtime: string }>; }>; const DEFAULT_RELEASE_COHERENCE_PATHS = Object.freeze({ release: "dist/release-manifest.json", runtime: "dist/config.json", }); async function readJsonArtifact(path: string): Promise { return JSON.parse(await readFile(path, "utf8")); } export async function verifyReleaseArtifactsCoherence( options: ReleaseArtifactsCoherenceOptions = {}, ) { const readArtifact = options.readArtifact ?? readJsonArtifact; const paths = options.paths ?? DEFAULT_RELEASE_COHERENCE_PATHS; const release = parseReleaseDocument(await readArtifact(paths.release)); const runtime = parseRuntimeConfigDocument( await readArtifact(paths.runtime), ); const coherence = await verifyReleaseRuntimeCoherence({ release, runtime, contractPackages: options.contractPackages ?? EXPECTED_CONTRACT_SET_PACKAGES, }); return Object.freeze({ release, runtime, coherence }); } async function main(): Promise { const fixturesDocument = parseFixturesDocument( JSON.parse( await readFile("config/release/coherence-fixtures.json", "utf8"), ), ); const verifiedRuntime = await verifyReleaseArtifactsCoherence(); const { release, runtime: runtimeConfig, coherence: artifactComparison, } = verifiedRuntime; const buildManifestDocument: unknown = JSON.parse( await readFile("artifacts/release/build-manifest.json", "utf8"), ); assertMatchesJsonSchema( JSON.parse( await readFile("schemas/artifacts/build-manifest.schema.json", "utf8"), ), buildManifestDocument, "build manifest", ); const buildManifest = parseBuildManifestDocument(buildManifestDocument); const runtimeConfigJsonSchema = requireRecord( JSON.parse(await readFile("dist/runtime-config.schema.json", "utf8")), "runtime config JSON schema", ); const viteManifest = await readFile("dist/.vite/manifest.json", "utf8"); const viteManifestObject = parseViteManifest(JSON.parse(viteManifest)); const actualAssetManifestHash = createHash("sha256") .update(viteManifest) .digest("hex"); const artifactMismatches: string[] = [...artifactComparison.mismatches]; for (const [token, value] of Object.entries(projectReleaseTokens(release))) { if (token !== "schemaVersion" && (typeof value !== "string" || value.length === 0)) { artifactMismatches.push(`releaseToken:${token}`); } } if ( typeof release.builtAt !== "string" || !Number.isFinite(Date.parse(release.builtAt)) ) { artifactMismatches.push("releaseToken:builtAtFormat"); } if (release.assetManifestHash !== actualAssetManifestHash) { artifactMismatches.push("assetManifestContent"); } if ( runtimeConfigJsonSchema.$schema !== "https://json-schema.org/draft/2020-12/schema" || runtimeConfigJsonSchema.type !== "object" || !runtimeConfigJsonSchema.properties ) { artifactMismatches.push("runtimeConfigSchema"); } if ( buildManifest.outputs.runtimeConfigSchema !== "dist/runtime-config.schema.json" ) { artifactMismatches.push("buildManifest:runtimeConfigSchema"); } for (const [token, buildValue, releaseValue] of [ ["buildId", buildManifest.buildId, release.buildId], ["commitSha", buildManifest.commitSha, release.commitSha], ["releaseId", buildManifest.releaseId, release.releaseId], ["generatedAt", buildManifest.generatedAt, release.builtAt], ] as const) { if (buildValue !== releaseValue) { artifactMismatches.push(`buildManifest:${token}`); } } const expectedChunkIds = new Set( Object.values(ROUTE_REGISTRY).map((definition) => definition.chunkId), ); const actualChunkIds = new Set(Object.keys(release.routeChunks)); for (const chunkId of expectedChunkIds) { if (!actualChunkIds.has(chunkId)) { artifactMismatches.push(`routeChunk:missing:${chunkId}`); } } for (const chunkId of actualChunkIds) { if (!expectedChunkIds.has(chunkId)) { artifactMismatches.push(`routeChunk:orphan:${chunkId}`); } } const runtimeContracts: Readonly> = ROUTE_RUNTIME_CONTRACT; for (const definition of Object.values(ROUTE_REGISTRY)) { const runtime = runtimeContracts[definition.routeId]; const viteEntry = Object.values(viteManifestObject).find( (entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry, ); const routeAsset = release.routeChunks[definition.chunkId]; if (!runtime || !viteEntry || routeAsset !== viteEntry.file) { artifactMismatches.push(`routeChunk:mismatch:${definition.chunkId}`); continue; } if ( buildManifest.outputs.routeChunks[definition.chunkId] !== routeAsset ) { artifactMismatches.push(`buildManifest:routeChunk:${definition.chunkId}`); } try { await readFile(`dist/${routeAsset}`); } catch { artifactMismatches.push(`routeChunk:file:${definition.chunkId}`); } } const fixtures = fixturesDocument.fixtures.map((fixture) => { const result = verifyCompatibilityTuple({ frontend: fixture.frontend, runtime: fixture.runtime, }); return { name: fixture.name, expectedCompatible: fixture.expectedCompatible, actualCompatible: result.compatible, mismatches: result.mismatches, passed: result.compatible === fixture.expectedCompatible, }; }); const artifact = { checked: true, compatible: artifactComparison.compatible && artifactMismatches.length === 0, mismatches: artifactMismatches, releaseId: release.releaseId, }; const passed = artifact.compatible && fixtures.every((fixture) => fixture.passed); const report = { schemaVersion: 1, generatedAt: new Date().toISOString(), artifact, fixtures, passed, }; await mkdir("artifacts/release", { recursive: true }); await writeValidatedJsonArtifact({ path: "artifacts/release/verification.json", schema: releaseVerificationArtifactSchema, value: report, }); if (!passed) { process.stderr.write( `Release coherence failed: ${artifactMismatches.join(", ") || "fixture"}\n`, ); process.exit(1); } process.stdout.write( `Release coherence: PASS (${fixtures.length - 1} mixed fixtures rejected)\n`, ); } const invokedPath = process.argv[1]; if ( invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href ) { await main(); } function parseFixturesDocument(value: unknown): Readonly<{ fixtures: readonly CoherenceFixture[]; }> { const document = requireRecord(value, "release coherence fixtures"); if (!Array.isArray(document.fixtures)) { throw new TypeError("release coherence fixtures must be an array"); } return { fixtures: document.fixtures.map((candidate, index) => { const fixture = requireRecord(candidate, `release fixture ${index}`); if ( typeof fixture.name !== "string" || typeof fixture.expectedCompatible !== "boolean" ) { throw new TypeError(`Invalid release fixture metadata: ${index}`); } return { name: fixture.name, expectedCompatible: fixture.expectedCompatible, frontend: parseCompatibilityTuple( fixture.frontend, `release fixture ${index}.frontend`, ), runtime: parseCompatibilityTuple( fixture.runtime, `release fixture ${index}.runtime`, ), }; }), }; } function parseReleaseDocument(value: unknown): ReleaseArtifact { return parseReleaseArtifact(value); } function parseRuntimeConfigDocument(value: unknown): RuntimeConfigDocument { const document = parseRuntimeConfigArtifact(value); return { ...document, BUILD_ID: requireString(document.BUILD_ID, "runtime config BUILD_ID"), RELEASE_ID: requireString(document.RELEASE_ID, "runtime config RELEASE_ID"), }; } function parseBuildManifestDocument(value: unknown): BuildManifestArtifact { return parseBuildManifestArtifact(value); } function parseCompatibilityTuple(value: unknown, label: string): CompatibilityTuple { const document = requireRecord(value, label); return { buildId: requireString(document.buildId, `${label}.buildId`), configSchemaVersion: requireString( document.configSchemaVersion, `${label}.configSchemaVersion`, ), apiContractVersion: requireString( document.apiContractVersion, `${label}.apiContractVersion`, ), assetManifestHash: requireString( document.assetManifestHash, `${label}.assetManifestHash`, ), releaseId: requireString(document.releaseId, `${label}.releaseId`), }; } function parseViteManifest( value: unknown, ): Readonly> { const document = requireRecord(value, "Vite manifest"); const entries: Record = {}; for (const [key, candidate] of Object.entries(document)) { const entry = requireRecord(candidate, `Vite manifest entry ${key}`); entries[key] = { file: requireString(entry.file, `Vite manifest entry ${key}.file`), ...(typeof entry.name === "string" ? { name: entry.name } : {}), ...(typeof entry.isDynamicEntry === "boolean" ? { isDynamicEntry: entry.isDynamicEntry } : {}), }; } return entries; } function requireString(value: unknown, label: string): string { if (typeof value !== "string" || value.length === 0) { throw new TypeError(`${label} must be a non-empty string`); } return value; } function requireRecord( value: unknown, label: string, ): Record { if (!isRecord(value)) throw new TypeError(`${label} must be an object`); return value; } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }