import { mkdir, readFile } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { z } from "zod"; import { buildManifestArtifactSchema, dependencyInventoryArtifactSchema, jsonSchemaDocumentArtifactSchema, registrySnapshotArtifactSchema, supplyChainVerificationArtifactSchema, } from "./contracts/release-artifacts.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; const ARTIFACT_SCHEMA_DEFINITIONS = Object.freeze([ { relativePath: "schemas/artifacts/build-manifest.schema.json", id: undefined, schema: buildManifestArtifactSchema, }, { relativePath: "schemas/artifacts/dependency-inventory.schema.json", id: "https://tech-log-frontend.local/schemas/dependency-inventory.schema.json", schema: dependencyInventoryArtifactSchema, }, { relativePath: "schemas/artifacts/registry-snapshot.schema.json", id: undefined, schema: registrySnapshotArtifactSchema, }, { relativePath: "schemas/artifacts/supply-chain-verification.schema.json", id: "https://tech-log-frontend.local/schemas/supply-chain-verification.schema.json", schema: supplyChainVerificationArtifactSchema, }, ] as const); type GenerateArtifactSchemasOptions = Readonly<{ root?: string; check?: boolean; }>; function canonicalize(value: unknown): unknown { if (Array.isArray(value)) return value.map(canonicalize); if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, item]) => [key, canonicalize(item)]), ); } return value; } function generatedSchema( definition: (typeof ARTIFACT_SCHEMA_DEFINITIONS)[number], ): Readonly> { const schema = z.toJSONSchema(definition.schema, { target: "draft-2020-12", }); return canonicalize({ ...schema, ...(definition.id === undefined ? {} : { $id: definition.id }), }) as Readonly>; } export async function generateArtifactSchemas( options: GenerateArtifactSchemasOptions = {}, ): Promise { const root = path.resolve(options.root ?? process.cwd()); const documents = ARTIFACT_SCHEMA_DEFINITIONS.map((definition) => ({ definition, document: generatedSchema(definition), })); const drift: string[] = []; for (const { definition, document } of documents) { const destination = path.join(root, definition.relativePath); const validatedDocument = jsonSchemaDocumentArtifactSchema.parse(document); const expected = `${JSON.stringify(validatedDocument, null, 2)}\n`; if (options.check) { let actual: string | null = null; try { actual = await readFile(destination, "utf8"); } catch { // A missing or unreadable checked-in schema is drift. } if (actual !== expected) drift.push(definition.relativePath); continue; } await mkdir(path.dirname(destination), { recursive: true }); await writeValidatedJsonArtifact({ path: destination, schema: jsonSchemaDocumentArtifactSchema, value: document, }); } if (drift.length > 0) { throw new Error(`Artifact JSON Schema drift:\n- ${drift.join("\n- ")}`); } } function argumentValue(name: string): string | undefined { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } const invokedPath = process.argv[1]; if ( invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href ) { try { await generateArtifactSchemas({ root: argumentValue("--root"), check: process.argv.includes("--check"), }); process.stdout.write( process.argv.includes("--check") ? "Artifact JSON Schemas: PASS\n" : "Artifact JSON Schemas: GENERATED\n", ); } catch (error) { process.stderr.write( `${error instanceof Error ? error.message : String(error)}\n`, ); process.exitCode = 1; } }