132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
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<Record<string, unknown>> {
|
|
const schema = z.toJSONSchema(definition.schema, {
|
|
target: "draft-2020-12",
|
|
});
|
|
return canonicalize({
|
|
...schema,
|
|
...(definition.id === undefined ? {} : { $id: definition.id }),
|
|
}) as Readonly<Record<string, unknown>>;
|
|
}
|
|
|
|
export async function generateArtifactSchemas(
|
|
options: GenerateArtifactSchemasOptions = {},
|
|
): Promise<void> {
|
|
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;
|
|
}
|
|
}
|