import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js"; import { compareReleaseToRuntime, RELEASE_TOKEN_REGISTRY, } from "../src/contracts/release-tokens.js"; import { ROUTE_RUNTIME_CONTRACT } from "../src/contracts/route-runtime-contract.js"; import { ROUTE_REGISTRY } from "../src/contracts/routes.js"; const fixturesDocument = /** @type {{ * fixtures: Array<{ * name: string, * expectedCompatible: boolean, * frontend: { * buildId: string, * configSchemaVersion: string, * apiContractVersion: string, * assetManifestHash: string, * releaseId: string * }, * runtime: { * buildId: string, * configSchemaVersion: string, * apiContractVersion: string, * assetManifestHash: string, * releaseId: string * } * }> * }} */ ( JSON.parse( await readFile("config/release/coherence-fixtures.json", "utf8"), ) ); const release = JSON.parse(await readFile("dist/release-manifest.json", "utf8")); const runtimeConfig = JSON.parse(await readFile("dist/config.json", "utf8")); const buildManifest = JSON.parse( await readFile("artifacts/release/build-manifest.json", "utf8"), ); const runtimeConfigJsonSchema = JSON.parse( await readFile("dist/runtime-config.schema.json", "utf8"), ); const viteManifest = await readFile("dist/.vite/manifest.json", "utf8"); const viteManifestObject = /** @type {Record} */ ( JSON.parse(viteManifest) ); const actualAssetManifestHash = createHash("sha256") .update(viteManifest) .digest("hex"); const artifactComparison = compareReleaseToRuntime(release, runtimeConfig); const artifactMismatches = [...artifactComparison.mismatches]; for (const token of Object.keys(RELEASE_TOKEN_REGISTRY)) { if (typeof release[token] !== "string" || release[token].length === 0) { artifactMismatches.push(`releaseToken:${token}`); } } if (!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"); } 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}`); } } for (const definition of Object.values(ROUTE_REGISTRY)) { const runtime = /** @type {Record} */ ( ROUTE_RUNTIME_CONTRACT )[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 writeFile( "artifacts/release/verification.json", `${JSON.stringify(report, null, 2)}\n`, ); 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`, );