chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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 {
|
||||
CANONICAL_VITE_MANIFEST_PATH,
|
||||
verifyBuildManifestOutputs,
|
||||
} from "./lib/build-manifest-outputs.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<unknown>;
|
||||
|
||||
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<unknown> {
|
||||
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<void> {
|
||||
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(CANONICAL_VITE_MANIFEST_PATH, "utf8");
|
||||
const viteManifestObject = parseViteManifest(JSON.parse(viteManifest));
|
||||
const actualAssetManifestHash = createHash("sha256")
|
||||
.update(viteManifest)
|
||||
.digest("hex");
|
||||
|
||||
const artifactMismatches: string[] = [...artifactComparison.mismatches];
|
||||
artifactMismatches.push(...(await verifyBuildManifestOutputs(buildManifest)));
|
||||
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<Record<string, { moduleId: string }>> =
|
||||
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: release.builtAt,
|
||||
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<Record<string, ViteManifestEntry>> {
|
||||
const document = requireRecord(value, "Vite manifest");
|
||||
const entries: Record<string, ViteManifestEntry> = {};
|
||||
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<string, unknown> {
|
||||
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
Reference in New Issue
Block a user