refactor: validate generated evidence artifacts
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
verifyRegistryBaselineApproval,
|
||||
} from "./lib/registry-compatibility.ts";
|
||||
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
|
||||
import { registrySnapshotArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type RegistryRow = Record<string, unknown>;
|
||||
type RegistryRows = Record<string, RegistryRow>;
|
||||
@@ -675,7 +676,11 @@ if (usesRepositoryBaseline && failures.length === 0) {
|
||||
}
|
||||
}
|
||||
await mkdir(path.dirname(artifactPath), { recursive: true });
|
||||
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: artifactPath,
|
||||
schema: registrySnapshotArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
evaluateFieldBudget,
|
||||
percentile75,
|
||||
} from "../src/application/policies/performance-budgets.ts";
|
||||
import { fieldWebVitalsArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
const inputPath =
|
||||
process.env.FIELD_WEB_VITALS_INPUT ||
|
||||
@@ -94,10 +96,11 @@ const report = {
|
||||
};
|
||||
|
||||
await mkdir("artifacts/performance", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/performance/field-web-vitals.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/performance/field-web-vitals.json",
|
||||
schema: fieldWebVitalsArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
if (!passed) {
|
||||
process.stderr.write(
|
||||
`Field Web Vitals: ${status} (approved threshold decision and valid 28-day production evidence are required)\n`,
|
||||
|
||||
@@ -1 +1,425 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export * from "../../src/contracts/release-artifacts.ts";
|
||||
|
||||
const nonEmptyString = z.string().min(1);
|
||||
const timestamp = z.iso.datetime();
|
||||
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const jsonObject = z.record(z.string(), z.json());
|
||||
|
||||
export const moduleInventoryArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
chunks: z.array(
|
||||
z
|
||||
.object({
|
||||
fileName: nonEmptyString,
|
||||
modules: z.array(nonEmptyString),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const jsonSchemaDocumentArtifactSchema = z
|
||||
.object({
|
||||
$schema: z.literal("https://json-schema.org/draft/2020-12/schema"),
|
||||
})
|
||||
.catchall(z.json());
|
||||
|
||||
const dependencyInventoryRowSchema = z
|
||||
.object({
|
||||
name: nonEmptyString,
|
||||
version: nonEmptyString,
|
||||
direct: z.boolean(),
|
||||
scope: z.enum(["production", "development"]),
|
||||
optional: z.boolean(),
|
||||
license: nonEmptyString,
|
||||
integrity: z.string().regex(/^sha512-/u),
|
||||
dependencies: z.array(nonEmptyString),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const dependencyInventoryArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(2),
|
||||
packageManager: nonEmptyString,
|
||||
lockfileSha256: sha256,
|
||||
dependencyCount: z.int().nonnegative(),
|
||||
directDependencyCount: z.int().nonnegative(),
|
||||
dependencies: z.array(dependencyInventoryRowSchema),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((inventory, context) => {
|
||||
if (inventory.dependencyCount !== inventory.dependencies.length) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["dependencyCount"],
|
||||
message: "must equal dependencies.length",
|
||||
});
|
||||
}
|
||||
const actualDirect = inventory.dependencies.filter(
|
||||
(dependency) => dependency.direct,
|
||||
).length;
|
||||
if (inventory.directDependencyCount !== actualDirect) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["directDependencyCount"],
|
||||
message: "must equal the number of direct dependencies",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const dependencyUpgradeSchema = z
|
||||
.object({
|
||||
name: nonEmptyString,
|
||||
from: nonEmptyString,
|
||||
to: nonEmptyString,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const dependencyDiffSchema = z
|
||||
.object({
|
||||
added: z.array(nonEmptyString),
|
||||
removed: z.array(nonEmptyString),
|
||||
changed: z.array(nonEmptyString),
|
||||
upgrades: z.array(dependencyUpgradeSchema),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const supplyChainVerificationArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
localStatus: z.enum(["PASS", "FAIL"]),
|
||||
promotionStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
|
||||
lockfileSha256: sha256,
|
||||
sourceSetSha256: sha256,
|
||||
distSha256: sha256,
|
||||
sbomSha256: sha256,
|
||||
dependencyDiff: dependencyDiffSchema,
|
||||
highRiskReview: z.array(nonEmptyString),
|
||||
vulnerabilityStatus: z.enum(["PASS", "FAIL", "FAIL_UNVERIFIED"]),
|
||||
provenanceAttestationStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
|
||||
failures: z.array(z.string()),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const registryChangeSchema = z
|
||||
.object({
|
||||
changeId: nonEmptyString,
|
||||
registryId: nonEmptyString,
|
||||
rowName: nonEmptyString,
|
||||
field: nonEmptyString,
|
||||
kind: nonEmptyString,
|
||||
impact: z.enum(["none", "additive", "behavior-change", "breaking"]),
|
||||
before: z.json().optional(),
|
||||
after: z.json().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const registryArtifactRowSchema = z
|
||||
.object({
|
||||
registryId: nonEmptyString,
|
||||
owner: nonEmptyString,
|
||||
source: nonEmptyString,
|
||||
rowCount: z.int().nonnegative(),
|
||||
contract: jsonObject,
|
||||
rows: jsonObject,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const registrySnapshotArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(2),
|
||||
generatedAt: timestamp,
|
||||
baselineDigest: sha256.nullable(),
|
||||
currentDigest: sha256,
|
||||
compatibility: z
|
||||
.object({
|
||||
impact: z.enum([
|
||||
"not-evaluated",
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking",
|
||||
]),
|
||||
changes: z.array(registryChangeSchema),
|
||||
})
|
||||
.strict(),
|
||||
failures: z.array(z.string()),
|
||||
registries: z.array(registryArtifactRowSchema),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const outputDigestSchema = z
|
||||
.object({
|
||||
path: nonEmptyString,
|
||||
bytes: z.int().nonnegative(),
|
||||
gzipBytes: z.int().nonnegative(),
|
||||
sha256,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const bundlePerformanceArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
generatedAt: timestamp,
|
||||
context: z
|
||||
.object({
|
||||
nodeVersion: nonEmptyString,
|
||||
packageManager: nonEmptyString,
|
||||
runnerImage: nonEmptyString,
|
||||
})
|
||||
.strict(),
|
||||
outputs: z.array(outputDigestSchema).min(1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const cyclonedxComponentSchema = z
|
||||
.object({
|
||||
type: z.literal("library"),
|
||||
"bom-ref": nonEmptyString,
|
||||
name: nonEmptyString,
|
||||
version: nonEmptyString,
|
||||
scope: z.enum(["optional", "required"]),
|
||||
hashes: z.array(
|
||||
z.object({ alg: z.literal("SHA-512"), content: nonEmptyString }).strict(),
|
||||
),
|
||||
licenses: z.array(
|
||||
z.object({ expression: nonEmptyString }).strict(),
|
||||
),
|
||||
properties: z.array(
|
||||
z.object({ name: nonEmptyString, value: nonEmptyString }).strict(),
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const sbomArtifactSchema = z
|
||||
.object({
|
||||
bomFormat: z.literal("CycloneDX"),
|
||||
specVersion: z.literal("1.6"),
|
||||
serialNumber: nonEmptyString,
|
||||
version: z.literal(1),
|
||||
metadata: z
|
||||
.object({
|
||||
component: z
|
||||
.object({
|
||||
type: z.literal("application"),
|
||||
name: nonEmptyString,
|
||||
version: nonEmptyString,
|
||||
})
|
||||
.strict(),
|
||||
properties: z.array(
|
||||
z.object({ name: nonEmptyString, value: nonEmptyString }).strict(),
|
||||
),
|
||||
})
|
||||
.strict(),
|
||||
components: z.array(cyclonedxComponentSchema),
|
||||
dependencies: z.array(
|
||||
z
|
||||
.object({ ref: nonEmptyString, dependsOn: z.array(nonEmptyString) })
|
||||
.strict(),
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const provenanceArtifactSchema = z
|
||||
.object({
|
||||
_type: z.literal("https://in-toto.io/Statement/v1"),
|
||||
subject: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.literal("dist"),
|
||||
digest: z.object({ sha256 }).strict(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.length(1),
|
||||
predicateType: z.literal("https://slsa.dev/provenance/v1"),
|
||||
predicate: z
|
||||
.object({
|
||||
buildDefinition: z
|
||||
.object({
|
||||
buildType: nonEmptyString,
|
||||
externalParameters: jsonObject,
|
||||
internalParameters: jsonObject,
|
||||
resolvedDependencies: z.array(
|
||||
z
|
||||
.object({ uri: nonEmptyString, digest: z.object({ sha256 }).strict() })
|
||||
.strict(),
|
||||
),
|
||||
})
|
||||
.strict(),
|
||||
runDetails: z
|
||||
.object({
|
||||
builder: z.object({ id: nonEmptyString }).strict(),
|
||||
metadata: z.object({ invocationId: nonEmptyString }).strict(),
|
||||
})
|
||||
.strict(),
|
||||
materials: z
|
||||
.object({ lockfileSha256: sha256, sourceSetSha256: sha256, sbomSha256: sha256 })
|
||||
.strict(),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const dependencyDiffArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(2),
|
||||
baselineDigest: sha256.nullable(),
|
||||
currentDigest: sha256,
|
||||
...dependencyDiffSchema.shape,
|
||||
highRisk: z.array(nonEmptyString),
|
||||
reviewFailures: z.array(z.string()),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const licenseReportArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
status: z.enum(["PASS", "FAIL"]),
|
||||
dependencyCount: z.int().nonnegative(),
|
||||
results: z.array(
|
||||
z
|
||||
.object({
|
||||
package: nonEmptyString,
|
||||
license: nonEmptyString,
|
||||
passed: z.boolean(),
|
||||
reason: z.string().nullable(),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
failures: z.array(z.string()),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const vulnerabilityReportArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
provider: nonEmptyString,
|
||||
scannedLockfileSha256: sha256,
|
||||
status: z.enum(["PASS", "FAIL", "FAIL_UNVERIFIED"]),
|
||||
findings: z.array(jsonObject),
|
||||
exceptionsApplied: z.array(jsonObject),
|
||||
failures: z.array(z.string()),
|
||||
blocking: z.array(z.string()),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const fieldWebVitalsArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
generatedAt: timestamp,
|
||||
window: z
|
||||
.object({ days: z.literal(28), start: timestamp, end: timestamp })
|
||||
.strict(),
|
||||
context: z
|
||||
.object({
|
||||
source: nonEmptyString,
|
||||
sourceSystem: z.string().nullable(),
|
||||
exportId: z.string().nullable(),
|
||||
network: z.literal("production-real-user"),
|
||||
routeAggregation: z.literal("route-id-only"),
|
||||
releaseId: z.string().nullable(),
|
||||
privacyApprovalRef: z.string().nullable(),
|
||||
thresholdDecisionRef: z.string().nullable(),
|
||||
validationFailures: z.array(z.string()),
|
||||
})
|
||||
.strict(),
|
||||
metrics: z
|
||||
.object({
|
||||
p75LcpMs: z.number().finite().nonnegative().nullable(),
|
||||
p75Cls: z.number().finite().nonnegative().nullable(),
|
||||
p75InpMs: z.number().finite().nonnegative().nullable(),
|
||||
})
|
||||
.strict(),
|
||||
thresholds: z
|
||||
.object({
|
||||
p75LcpMs: z.number().finite().nonnegative(),
|
||||
p75Cls: z.number().finite().nonnegative(),
|
||||
p75InpMs: z.number().finite().nonnegative(),
|
||||
minimumEligibleSamples: z.int().positive().nullable(),
|
||||
})
|
||||
.strict(),
|
||||
eligibility: z
|
||||
.object({
|
||||
consentRequired: z.literal(true),
|
||||
totalSamples: z.int().nonnegative(),
|
||||
eligibleSamples: z.int().nonnegative(),
|
||||
minimumEligibleSamples: z.int().positive().nullable(),
|
||||
routeSamples: z.record(z.string(), z.int().nonnegative()),
|
||||
})
|
||||
.strict(),
|
||||
status: z.enum(["PASS", "FAIL_THRESHOLD", "FAIL_UNVERIFIED"]),
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const labPerformanceArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
generatedAt: timestamp,
|
||||
context: jsonObject,
|
||||
metrics: jsonObject,
|
||||
thresholds: jsonObject,
|
||||
fixtures: z.array(
|
||||
z.object({ name: nonEmptyString, passed: z.boolean() }).strict(),
|
||||
),
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const releaseVerificationArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
generatedAt: timestamp,
|
||||
artifact: z
|
||||
.object({
|
||||
checked: z.boolean(),
|
||||
compatible: z.boolean(),
|
||||
mismatches: z.array(z.string()),
|
||||
releaseId: nonEmptyString,
|
||||
})
|
||||
.strict(),
|
||||
fixtures: z.array(
|
||||
z
|
||||
.object({
|
||||
name: nonEmptyString,
|
||||
expectedCompatible: z.boolean(),
|
||||
actualCompatible: z.boolean(),
|
||||
mismatches: z.array(z.string()),
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const runbookRecordArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
runbookId: z.string().regex(/^FE-RB-00[1-5]$/u),
|
||||
releaseId: nonEmptyString,
|
||||
drillTimestamp: timestamp,
|
||||
triggerInjected: nonEmptyString,
|
||||
triggerAsserted: z.boolean(),
|
||||
containmentAsserted: z.boolean(),
|
||||
escalationPathAsserted: z.boolean(),
|
||||
recoveryAssertions: z.array(
|
||||
z
|
||||
.object({
|
||||
assertion: nonEmptyString,
|
||||
evidence: nonEmptyString,
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
negativeFixtureFailedAsExpected: z.boolean(),
|
||||
windowObservedBucket: nonEmptyString,
|
||||
providerVerificationRequired: z.boolean(),
|
||||
passed: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { access, mkdir, readFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
} from "../src/contracts/release-artifacts.ts";
|
||||
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
|
||||
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
|
||||
import { runbookRecordArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type RecoveryAssertion = Readonly<{
|
||||
assertion: string;
|
||||
@@ -354,10 +356,11 @@ async function main(): Promise<void> {
|
||||
};
|
||||
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
|
||||
await mkdir(artifactDirectory, { recursive: true });
|
||||
await writeFile(
|
||||
`${artifactDirectory}/record.json`,
|
||||
`${JSON.stringify(record, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: `${artifactDirectory}/record.json`,
|
||||
schema: runbookRecordArtifactSchema,
|
||||
value: record,
|
||||
});
|
||||
if (!passed) {
|
||||
process.stderr.write(`${runbookId} drill failed.\n`);
|
||||
process.exit(1);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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://clean-architecture-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://clean-architecture-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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, rm } from "node:fs/promises";
|
||||
import process from "node:process";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -9,14 +9,17 @@ import {
|
||||
} from "../src/features/installed-feature-contracts.ts";
|
||||
import {
|
||||
buildManifestArtifactSchema,
|
||||
jsonSchemaDocumentArtifactSchema,
|
||||
moduleInventoryArtifactSchema,
|
||||
releaseManifestV2ArtifactSchema,
|
||||
runtimeConfigV2ArtifactSchema,
|
||||
} from "../src/contracts/release-artifacts.ts";
|
||||
} from "./contracts/release-artifacts.ts";
|
||||
import { buildContractSet } from "./generate-contract-set.ts";
|
||||
import {
|
||||
assertCiBuildEnvironment,
|
||||
buildDate,
|
||||
} from "./lib/build-environment.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
assertCiBuildEnvironment(process.env);
|
||||
type ViteManifestEntry = Readonly<{
|
||||
@@ -41,12 +44,15 @@ const moduleInventory = await readFile(
|
||||
"dist/.vite/module-inventory.json",
|
||||
"utf8",
|
||||
);
|
||||
parseModuleInventory(JSON.parse(moduleInventory));
|
||||
const moduleInventoryDocument = moduleInventoryArtifactSchema.parse(
|
||||
JSON.parse(moduleInventory),
|
||||
);
|
||||
const moduleInventoryBytes = `${JSON.stringify(moduleInventoryDocument, null, 2)}\n`;
|
||||
const assetManifestHash = createHash("sha256")
|
||||
.update(viteManifest)
|
||||
.digest("hex");
|
||||
const moduleInventoryHash = createHash("sha256")
|
||||
.update(moduleInventory)
|
||||
.update(moduleInventoryBytes)
|
||||
.digest("hex");
|
||||
const runtimeConfig = runtimeConfigV2ArtifactSchema.parse(
|
||||
JSON.parse(await readFile("dist/config.json", "utf8")),
|
||||
@@ -108,28 +114,37 @@ const releaseManifest = releaseManifestV2ArtifactSchema.parse({
|
||||
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await mkdir("artifacts/quality", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/quality/vite-module-inventory.json",
|
||||
moduleInventory,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/quality/vite-module-inventory.json",
|
||||
schema: moduleInventoryArtifactSchema,
|
||||
value: moduleInventoryDocument,
|
||||
});
|
||||
await rm("dist/.vite/module-inventory.json");
|
||||
await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`);
|
||||
await writeFile(
|
||||
"dist/release-manifest.json",
|
||||
`${JSON.stringify(releaseManifest, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"dist/runtime-config.schema.json",
|
||||
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/release/runtime-config.schema.json",
|
||||
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/release/build-manifest.json",
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "dist/config.json",
|
||||
schema: runtimeConfigV2ArtifactSchema,
|
||||
value: runtimeConfig,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "dist/release-manifest.json",
|
||||
schema: releaseManifestV2ArtifactSchema,
|
||||
value: releaseManifest,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "dist/runtime-config.schema.json",
|
||||
schema: jsonSchemaDocumentArtifactSchema,
|
||||
value: runtimeConfigJsonSchema,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/runtime-config.schema.json",
|
||||
schema: jsonSchemaDocumentArtifactSchema,
|
||||
value: runtimeConfigJsonSchema,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/build-manifest.json",
|
||||
schema: buildManifestArtifactSchema,
|
||||
value: manifest,
|
||||
});
|
||||
|
||||
function parsePackageMetadata(value: unknown): Readonly<{
|
||||
version: string;
|
||||
@@ -165,23 +180,6 @@ function parseViteManifest(
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseModuleInventory(value: unknown): void {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
value.schemaVersion !== 1 ||
|
||||
!Array.isArray(value.chunks) ||
|
||||
value.chunks.some(
|
||||
(chunk) =>
|
||||
!isRecord(chunk) ||
|
||||
typeof chunk.fileName !== "string" ||
|
||||
!Array.isArray(chunk.modules) ||
|
||||
chunk.modules.some((moduleId) => typeof moduleId !== "string"),
|
||||
)
|
||||
) {
|
||||
throw new TypeError("Vite module inventory is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
bundlePerformanceArtifactSchema,
|
||||
dependencyDiffArtifactSchema,
|
||||
dependencyInventoryArtifactSchema,
|
||||
licenseReportArtifactSchema,
|
||||
provenanceArtifactSchema,
|
||||
sbomArtifactSchema,
|
||||
supplyChainVerificationArtifactSchema,
|
||||
vulnerabilityReportArtifactSchema,
|
||||
} from "./contracts/release-artifacts.ts";
|
||||
import {
|
||||
diffDependencyInventories,
|
||||
flattenPnpmDependencyTree,
|
||||
@@ -22,6 +32,7 @@ import {
|
||||
verifySupplyChainCoherence,
|
||||
type DependencyInventoryDiff,
|
||||
} from "./lib/supply-chain.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type Document = Record<string, unknown>;
|
||||
|
||||
@@ -428,81 +439,80 @@ const verification = {
|
||||
: "FAIL_UNVERIFIED",
|
||||
failures: localFailures,
|
||||
};
|
||||
const bundleReport = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
context: {
|
||||
nodeVersion: process.version,
|
||||
packageManager: String(packageJson.packageManager ?? ""),
|
||||
runnerImage:
|
||||
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
const dependencyDiffReport = {
|
||||
schemaVersion: 2,
|
||||
baselineDigest: baseline ? supplyChainDigest(baseline) : null,
|
||||
currentDigest: supplyChainDigest(inventory),
|
||||
...dependencyDiff,
|
||||
highRisk: reviewResult.highRisk,
|
||||
reviewFailures: reviewResult.failures,
|
||||
};
|
||||
const licenseReport = {
|
||||
schemaVersion: 1,
|
||||
status: licenseResult.passed ? "PASS" : "FAIL",
|
||||
dependencyCount: inventory.dependencyCount,
|
||||
results: licenseResult.results,
|
||||
failures: licenseResult.failures,
|
||||
};
|
||||
|
||||
await mkdir("artifacts/performance", { recursive: true });
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await mkdir("artifacts/security", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/performance/bundle.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
context: {
|
||||
nodeVersion: process.version,
|
||||
packageManager: String(packageJson.packageManager ?? ""),
|
||||
runnerImage:
|
||||
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
|
||||
},
|
||||
outputs,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/release/dependency-inventory.json",
|
||||
`${JSON.stringify(inventory, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/release/sbom.cdx.json",
|
||||
`${JSON.stringify(sbom, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/release/provenance.json",
|
||||
`${JSON.stringify(provenance, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/performance/bundle.json",
|
||||
schema: bundlePerformanceArtifactSchema,
|
||||
value: bundleReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/dependency-inventory.json",
|
||||
schema: dependencyInventoryArtifactSchema,
|
||||
value: inventory,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/sbom.cdx.json",
|
||||
schema: sbomArtifactSchema,
|
||||
value: sbom,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/provenance.json",
|
||||
schema: provenanceArtifactSchema,
|
||||
value: provenance,
|
||||
});
|
||||
await writeFile(
|
||||
"artifacts/release/checksums.txt",
|
||||
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/security/dependency-diff.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 2,
|
||||
baselineDigest: baseline ? supplyChainDigest(baseline) : null,
|
||||
currentDigest: supplyChainDigest(inventory),
|
||||
...dependencyDiff,
|
||||
highRisk: reviewResult.highRisk,
|
||||
reviewFailures: reviewResult.failures,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/security/license-report.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
status: licenseResult.passed ? "PASS" : "FAIL",
|
||||
dependencyCount: inventory.dependencyCount,
|
||||
results: licenseResult.results,
|
||||
failures: licenseResult.failures,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/security/vulnerability-report.json",
|
||||
`${JSON.stringify(vulnerabilityReport, null, 2)}\n`,
|
||||
);
|
||||
await writeFile(
|
||||
"artifacts/security/supply-chain-verification.json",
|
||||
`${JSON.stringify(verification, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/dependency-diff.json",
|
||||
schema: dependencyDiffArtifactSchema,
|
||||
value: dependencyDiffReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/license-report.json",
|
||||
schema: licenseReportArtifactSchema,
|
||||
value: licenseReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/vulnerability-report.json",
|
||||
schema: vulnerabilityReportArtifactSchema,
|
||||
value: vulnerabilityReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/supply-chain-verification.json",
|
||||
schema: supplyChainVerificationArtifactSchema,
|
||||
value: verification,
|
||||
});
|
||||
|
||||
if (!localPassed) {
|
||||
process.stderr.write(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
open as openFile,
|
||||
rename as renameFile,
|
||||
rm as removeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import type { z } from "zod";
|
||||
|
||||
export type ValidatedJsonArtifactInput = Readonly<{
|
||||
path: string;
|
||||
schema: z.ZodType;
|
||||
value: unknown;
|
||||
}>;
|
||||
|
||||
export type ValidatedJsonArtifactFileSystem = Readonly<{
|
||||
open: (path: string, flags: "wx") => Promise<{
|
||||
writeFile(data: string, encoding: "utf8"): Promise<unknown>;
|
||||
close(): Promise<unknown>;
|
||||
}>;
|
||||
rename: (source: string, destination: string) => Promise<unknown>;
|
||||
rm: (path: string, options: Readonly<{ force: true }>) => Promise<unknown>;
|
||||
}>;
|
||||
|
||||
type ValidatedJsonArtifactWriterDependencies = Readonly<{
|
||||
createNonce?: () => string;
|
||||
fileSystem?: ValidatedJsonArtifactFileSystem;
|
||||
}>;
|
||||
|
||||
const defaultFileSystem: ValidatedJsonArtifactFileSystem = Object.freeze({
|
||||
open: async (target, flags) => openFile(target, flags),
|
||||
rename: async (source, destination) => renameFile(source, destination),
|
||||
rm: async (target, options) => removeFile(target, options),
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds a writer whose only publish operation is an atomic sibling rename.
|
||||
* Dependency injection is limited to the file-system boundary so failure
|
||||
* ownership can be verified without exposing a caller-selected cleanup path.
|
||||
*/
|
||||
export function createValidatedJsonArtifactWriter(
|
||||
dependencies: ValidatedJsonArtifactWriterDependencies = {},
|
||||
) {
|
||||
const createNonce = dependencies.createNonce ?? randomUUID;
|
||||
const fileSystem = dependencies.fileSystem ?? defaultFileSystem;
|
||||
|
||||
return async function writeArtifact(
|
||||
input: ValidatedJsonArtifactInput,
|
||||
): Promise<void> {
|
||||
const parsed = input.schema.parse(input.value);
|
||||
const serialized = JSON.stringify(parsed, null, 2);
|
||||
if (serialized === undefined) {
|
||||
throw new TypeError("Validated JSON artifact is not serializable");
|
||||
}
|
||||
|
||||
const temporaryPath = path.join(
|
||||
path.dirname(input.path),
|
||||
`.${path.basename(input.path)}.${createNonce()}.tmp`,
|
||||
);
|
||||
let ownsTemporaryFile = false;
|
||||
try {
|
||||
const handle = await fileSystem.open(temporaryPath, "wx");
|
||||
ownsTemporaryFile = true;
|
||||
try {
|
||||
await handle.writeFile(`${serialized}\n`, "utf8");
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await fileSystem.rename(temporaryPath, input.path);
|
||||
} catch (error) {
|
||||
if (ownsTemporaryFile) {
|
||||
try {
|
||||
await fileSystem.rm(temporaryPath, { force: true });
|
||||
} catch {
|
||||
// Preserve the publishing failure; cleanup is confined to our nonce.
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const writeValidatedJsonArtifact =
|
||||
createValidatedJsonArtifactWriter();
|
||||
+16
-17
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import process from "node:process";
|
||||
|
||||
@@ -7,6 +7,8 @@ import { chromium } from "@playwright/test";
|
||||
|
||||
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.ts";
|
||||
import { ROUTE_REGISTRY } from "../src/features/installed-feature-contracts.ts";
|
||||
import { labPerformanceArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type ContractPerformanceEvidence = {
|
||||
lcpMs: number;
|
||||
@@ -142,22 +144,19 @@ try {
|
||||
];
|
||||
const passed = result.passed && fixtures.every((fixture) => fixture.passed);
|
||||
await mkdir("artifacts/performance", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/performance/lab.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
context: contextMetadata,
|
||||
metrics,
|
||||
thresholds,
|
||||
fixtures,
|
||||
passed,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/performance/lab.json",
|
||||
schema: labPerformanceArtifactSchema,
|
||||
value: {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
context: contextMetadata,
|
||||
metrics,
|
||||
thresholds,
|
||||
fixtures,
|
||||
passed,
|
||||
},
|
||||
});
|
||||
if (!passed) {
|
||||
throw new Error(`Lab performance failed: ${JSON.stringify(metrics)}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import {
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} 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;
|
||||
@@ -212,10 +214,11 @@ const report = {
|
||||
};
|
||||
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/release/verification.json",
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/verification.json",
|
||||
schema: releaseVerificationArtifactSchema,
|
||||
value: report,
|
||||
});
|
||||
|
||||
if (!passed) {
|
||||
process.stderr.write(
|
||||
|
||||
Reference in New Issue
Block a user