refactor: validate generated evidence artifacts
This commit is contained in:
+3
-1
@@ -70,6 +70,8 @@
|
||||
"test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes",
|
||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
||||
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
||||
"generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts",
|
||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
||||
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
||||
"verify:supply-chain": "node scripts/verify-supply-chain-artifacts.ts",
|
||||
"update:dependency-baseline": "node scripts/update-dependency-baseline.ts",
|
||||
@@ -101,7 +103,7 @@
|
||||
"drill:runbook": "node scripts/drill-runbook.ts",
|
||||
"drill:runbooks": "corepack pnpm drill:runbook -- FE-RB-001 && corepack pnpm drill:runbook -- FE-RB-002 && corepack pnpm drill:runbook -- FE-RB-003 && corepack pnpm drill:runbook -- FE-RB-004 && corepack pnpm drill:runbook -- FE-RB-005",
|
||||
"ci:gate": "node scripts/run-ci-gate.ts",
|
||||
"check:ci": "node scripts/check-ci-contract.ts",
|
||||
"check:ci": "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts",
|
||||
"verify:documentation": "node scripts/verify-documentation-readiness.ts",
|
||||
"build:app-only": "vite build && node scripts/generate-build-manifest.ts",
|
||||
"generate:contract-set": "node scripts/generate-contract-set.ts",
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "number",
|
||||
"const": 1
|
||||
},
|
||||
"buildId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"commitSha": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"releaseId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"moduleInventoryHash": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"buildContext": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nodeVersion": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"packageManagerVersion": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"runnerImage": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"sourceDateEpoch": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -59,37 +35,53 @@
|
||||
"runnerImage",
|
||||
"sourceDateEpoch"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
},
|
||||
"buildId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"commitSha": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"generatedAt": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"moduleInventoryHash": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"viteManifest": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"moduleInventory": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"routeChunks": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"propertyNames": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"runtimeConfigSchema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"viteManifest": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -99,7 +91,15 @@
|
||||
"routeChunks",
|
||||
"runtimeConfigSchema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
},
|
||||
"releaseId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -112,5 +112,5 @@
|
||||
"buildContext",
|
||||
"outputs"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://clean-architecture-frontend.local/schemas/dependency-inventory.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"packageManager",
|
||||
"lockfileSha256",
|
||||
"dependencyCount",
|
||||
"directDependencyCount",
|
||||
"dependencies"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 2 },
|
||||
"packageManager": { "type": "string", "minLength": 1 },
|
||||
"lockfileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"dependencyCount": { "type": "integer", "minimum": 1 },
|
||||
"directDependencyCount": { "type": "integer", "minimum": 1 },
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"direct": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"integrity": {
|
||||
"pattern": "^sha512-",
|
||||
"type": "string"
|
||||
},
|
||||
"license": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scope": {
|
||||
"enum": [
|
||||
"production",
|
||||
"development"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
@@ -36,25 +54,40 @@
|
||||
"integrity",
|
||||
"dependencies"
|
||||
],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"version": { "type": "string", "minLength": 1 },
|
||||
"direct": { "type": "boolean" },
|
||||
"scope": {
|
||||
"enum": ["production", "development"]
|
||||
},
|
||||
"optional": { "type": "boolean" },
|
||||
"license": { "type": "string", "minLength": 1 },
|
||||
"integrity": {
|
||||
"type": "string",
|
||||
"pattern": "^sha512-"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dependencyCount": {
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"directDependencyCount": {
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"packageManager": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"packageManager",
|
||||
"lockfileSha256",
|
||||
"dependencyCount",
|
||||
"directDependencyCount",
|
||||
"dependencies"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,257 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"__schema0": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"__schema1": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"__schema2": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"baselineDigest": {
|
||||
"anyOf": [
|
||||
{
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"changes": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"after": {
|
||||
"$ref": "#/$defs/__schema1"
|
||||
},
|
||||
"before": {
|
||||
"$ref": "#/$defs/__schema0"
|
||||
},
|
||||
"changeId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowName": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"changeId",
|
||||
"registryId",
|
||||
"rowName",
|
||||
"field",
|
||||
"kind",
|
||||
"impact"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"impact": {
|
||||
"enum": [
|
||||
"not-evaluated",
|
||||
"none",
|
||||
"additive",
|
||||
"behavior-change",
|
||||
"breaking"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"impact",
|
||||
"changes"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"currentDigest": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"failures": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"generatedAt": {
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"registries": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"contract": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"owner": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"registryId": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"rowCount": {
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"rows": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/__schema2"
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"source": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryId",
|
||||
"owner",
|
||||
"source",
|
||||
"rowCount",
|
||||
"contract",
|
||||
"rows"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"generatedAt",
|
||||
@@ -10,45 +261,5 @@
|
||||
"failures",
|
||||
"registries"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 2 },
|
||||
"generatedAt": { "type": "string", "format": "date-time" },
|
||||
"baselineDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"currentDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"compatibility": {
|
||||
"type": "object",
|
||||
"required": ["impact", "changes"],
|
||||
"properties": {
|
||||
"impact": {
|
||||
"enum": ["none", "additive", "behavior-change", "breaking"]
|
||||
},
|
||||
"changes": { "type": "array" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"failures": { "type": "array", "maxItems": 0 },
|
||||
"registries": {
|
||||
"type": "array",
|
||||
"minItems": 11,
|
||||
"maxItems": 11,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"registryId",
|
||||
"owner",
|
||||
"source",
|
||||
"rowCount",
|
||||
"contract",
|
||||
"rows"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,130 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://clean-architecture-frontend.local/schemas/supply-chain-verification.schema.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dependencyDiff": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"added": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"changed": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"removed": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"upgrades": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"from",
|
||||
"to"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"added",
|
||||
"removed",
|
||||
"changed",
|
||||
"upgrades"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"distSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"failures": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"highRiskReview": {
|
||||
"items": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"localStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"promotionStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"provenanceAttestationStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"sbomSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"sourceSetSha256": {
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"vulnerabilityStatus": {
|
||||
"enum": [
|
||||
"PASS",
|
||||
"FAIL",
|
||||
"FAIL_UNVERIFIED"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"localStatus",
|
||||
@@ -17,39 +139,5 @@
|
||||
"provenanceAttestationStatus",
|
||||
"failures"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"localStatus": { "enum": ["PASS", "FAIL"] },
|
||||
"promotionStatus": {
|
||||
"enum": ["PASS", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"lockfileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"sourceSetSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"distSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"sbomSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"dependencyDiff": { "type": "object" },
|
||||
"highRiskReview": { "type": "array" },
|
||||
"vulnerabilityStatus": {
|
||||
"enum": ["PASS", "FAIL", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"provenanceAttestationStatus": {
|
||||
"enum": ["PASS", "FAIL_UNVERIFIED"]
|
||||
},
|
||||
"failures": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { generateArtifactSchemas } from "../../scripts/generate-artifact-schemas.ts";
|
||||
import { assertMatchesJsonSchema } from "../../scripts/lib/json-schema.ts";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
async function temporaryDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "artifact-schemas-"));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
rm(directory, { force: true, recursive: true }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
async function json(path: string): Promise<unknown> {
|
||||
return JSON.parse(await readFile(path, "utf8")) as unknown;
|
||||
}
|
||||
@@ -55,4 +74,130 @@ describe("checked-in JSON Schema execution", () => {
|
||||
assertMatchesJsonSchema(schema, catalog, "optional recipe catalog"),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("generates deterministic draft 2020-12 artifact schemas and detects drift", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
await generateArtifactSchemas({ root });
|
||||
const generatedPath = path.join(
|
||||
root,
|
||||
"schemas/artifacts/dependency-inventory.schema.json",
|
||||
);
|
||||
const generated = await readFile(generatedPath, "utf8");
|
||||
expect(generated.endsWith("\n")).toBe(true);
|
||||
expect(JSON.parse(generated)).toMatchObject({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
await generateArtifactSchemas({ root, check: true });
|
||||
await writeFile(generatedPath, `${generated.trimEnd()} \n`, "utf8");
|
||||
|
||||
await expect(
|
||||
generateArtifactSchemas({ root, check: true }),
|
||||
).rejects.toThrow(/dependency-inventory\.schema\.json/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "dependency inventory",
|
||||
path: "schemas/artifacts/dependency-inventory.schema.json",
|
||||
value: {
|
||||
schemaVersion: 2,
|
||||
packageManager: "pnpm@11.17.0",
|
||||
lockfileSha256: "a".repeat(64),
|
||||
dependencyCount: 1,
|
||||
directDependencyCount: 1,
|
||||
dependencies: [
|
||||
{
|
||||
name: "zod",
|
||||
version: "4.4.3",
|
||||
direct: true,
|
||||
scope: "production",
|
||||
optional: false,
|
||||
license: "MIT",
|
||||
integrity: `sha512-${"a".repeat(86)}`,
|
||||
dependencies: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "registry snapshot",
|
||||
path: "schemas/artifacts/registry-snapshot.schema.json",
|
||||
value: {
|
||||
schemaVersion: 2,
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
baselineDigest: "b".repeat(64),
|
||||
currentDigest: "c".repeat(64),
|
||||
compatibility: { impact: "none", changes: [] },
|
||||
failures: [],
|
||||
registries: Array.from({ length: 11 }, (_, index) => ({
|
||||
registryId: `registry-${index}`,
|
||||
owner: "platform",
|
||||
source: `src/registry-${index}.ts`,
|
||||
rowCount: 0,
|
||||
contract: {},
|
||||
rows: {},
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "supply-chain verification",
|
||||
path: "schemas/artifacts/supply-chain-verification.schema.json",
|
||||
value: {
|
||||
schemaVersion: 1,
|
||||
localStatus: "PASS",
|
||||
promotionStatus: "FAIL_UNVERIFIED",
|
||||
lockfileSha256: "d".repeat(64),
|
||||
sourceSetSha256: "e".repeat(64),
|
||||
distSha256: "f".repeat(64),
|
||||
sbomSha256: "0".repeat(64),
|
||||
dependencyDiff: {
|
||||
added: [],
|
||||
removed: [],
|
||||
changed: [],
|
||||
upgrades: [],
|
||||
},
|
||||
highRiskReview: [],
|
||||
vulnerabilityStatus: "FAIL_UNVERIFIED",
|
||||
provenanceAttestationStatus: "FAIL_UNVERIFIED",
|
||||
failures: [],
|
||||
},
|
||||
},
|
||||
])("accepts $name and rejects undeclared top-level fields", async (fixture) => {
|
||||
const schema = await json(fixture.path);
|
||||
|
||||
expect(() =>
|
||||
assertMatchesJsonSchema(schema, fixture.value, fixture.name),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
assertMatchesJsonSchema(
|
||||
schema,
|
||||
{ ...fixture.value, undocumented: true },
|
||||
fixture.name,
|
||||
),
|
||||
).toThrow(/checked-in JSON Schema/u);
|
||||
if (fixture.name === "dependency inventory") {
|
||||
const dependency = fixture.value.dependencies?.[0];
|
||||
if (dependency === undefined) {
|
||||
throw new TypeError("dependency inventory fixture is incomplete");
|
||||
}
|
||||
expect(() =>
|
||||
assertMatchesJsonSchema(
|
||||
schema,
|
||||
{
|
||||
...fixture.value,
|
||||
dependencies: [
|
||||
{
|
||||
...dependency,
|
||||
undocumented: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
fixture.name,
|
||||
),
|
||||
).toThrow(/checked-in JSON Schema/u);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
dependencyInventoryArtifactSchema,
|
||||
fieldWebVitalsArtifactSchema,
|
||||
jsonSchemaDocumentArtifactSchema,
|
||||
labPerformanceArtifactSchema,
|
||||
parseBuildManifestArtifact,
|
||||
parseReleaseArtifact,
|
||||
parseRuntimeConfigArtifact,
|
||||
projectReleaseTokens,
|
||||
registrySnapshotArtifactSchema,
|
||||
supplyChainVerificationArtifactSchema,
|
||||
} from "../../scripts/contracts/release-artifacts.ts";
|
||||
|
||||
const EMPTY_CONTRACT_SET_DIGEST =
|
||||
@@ -126,4 +134,241 @@ describe("release artifact contracts", () => {
|
||||
}),
|
||||
).not.toHaveProperty("API_CONTRACT_VERSION");
|
||||
});
|
||||
|
||||
it("enforces dependency inventory counts at the executable writer boundary", () => {
|
||||
const inventory = {
|
||||
schemaVersion: 2,
|
||||
packageManager: "pnpm@11.17.0",
|
||||
lockfileSha256: "a".repeat(64),
|
||||
dependencyCount: 1,
|
||||
directDependencyCount: 1,
|
||||
dependencies: [
|
||||
{
|
||||
name: "zod",
|
||||
version: "4.4.3",
|
||||
direct: true,
|
||||
scope: "production",
|
||||
optional: false,
|
||||
license: "MIT",
|
||||
integrity: `sha512-${"a".repeat(86)}`,
|
||||
dependencies: [],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
expect(dependencyInventoryArtifactSchema.parse(inventory)).toEqual(
|
||||
inventory,
|
||||
);
|
||||
expect(() =>
|
||||
dependencyInventoryArtifactSchema.parse({
|
||||
...inventory,
|
||||
dependencyCount: 2,
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects undeclared supply-chain verification evidence", () => {
|
||||
const verification = {
|
||||
schemaVersion: 1,
|
||||
localStatus: "PASS",
|
||||
promotionStatus: "FAIL_UNVERIFIED",
|
||||
lockfileSha256: "a".repeat(64),
|
||||
sourceSetSha256: "b".repeat(64),
|
||||
distSha256: "c".repeat(64),
|
||||
sbomSha256: "d".repeat(64),
|
||||
dependencyDiff: {
|
||||
added: [],
|
||||
removed: [],
|
||||
changed: [],
|
||||
upgrades: [],
|
||||
},
|
||||
highRiskReview: [],
|
||||
vulnerabilityStatus: "FAIL_UNVERIFIED",
|
||||
provenanceAttestationStatus: "FAIL_UNVERIFIED",
|
||||
failures: [],
|
||||
} as const;
|
||||
|
||||
expect(supplyChainVerificationArtifactSchema.parse(verification)).toEqual(
|
||||
verification,
|
||||
);
|
||||
expect(() =>
|
||||
supplyChainVerificationArtifactSchema.parse({
|
||||
...verification,
|
||||
undocumented: true,
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("preserves no-baseline and failure registry evidence", () => {
|
||||
const failureEvidence = {
|
||||
schemaVersion: 2,
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
baselineDigest: null,
|
||||
currentDigest: "a".repeat(64),
|
||||
compatibility: { impact: "not-evaluated", changes: [] },
|
||||
failures: ["missing registry source"],
|
||||
registries: [],
|
||||
} as const;
|
||||
|
||||
expect(registrySnapshotArtifactSchema.parse(failureEvidence)).toEqual(
|
||||
failureEvidence,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves unverified field evidence when no samples are eligible", () => {
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
window: {
|
||||
days: 28,
|
||||
start: "2026-07-04T00:00:00.000Z",
|
||||
end: "2026-08-01T00:00:00.000Z",
|
||||
},
|
||||
context: {
|
||||
source: "config/performance/field-input.example.json",
|
||||
sourceSystem: null,
|
||||
exportId: null,
|
||||
network: "production-real-user",
|
||||
routeAggregation: "route-id-only",
|
||||
releaseId: null,
|
||||
privacyApprovalRef: null,
|
||||
thresholdDecisionRef: null,
|
||||
validationFailures: ["input: invalid evidence"],
|
||||
},
|
||||
metrics: { p75LcpMs: null, p75Cls: null, p75InpMs: null },
|
||||
thresholds: {
|
||||
p75LcpMs: 2_500,
|
||||
p75Cls: 0.1,
|
||||
p75InpMs: 200,
|
||||
minimumEligibleSamples: null,
|
||||
},
|
||||
eligibility: {
|
||||
consentRequired: true,
|
||||
totalSamples: 0,
|
||||
eligibleSamples: 0,
|
||||
minimumEligibleSamples: null,
|
||||
routeSamples: {},
|
||||
},
|
||||
status: "FAIL_UNVERIFIED",
|
||||
passed: false,
|
||||
} as const;
|
||||
|
||||
expect(fieldWebVitalsArtifactSchema.parse(report)).toEqual(report);
|
||||
});
|
||||
|
||||
it("preserves verified field evidence that fails an approved threshold", () => {
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
window: {
|
||||
days: 28,
|
||||
start: "2026-07-04T00:00:00.000Z",
|
||||
end: "2026-08-01T00:00:00.000Z",
|
||||
},
|
||||
context: {
|
||||
source: "provider.json",
|
||||
sourceSystem: "provider",
|
||||
exportId: "export-1",
|
||||
network: "production-real-user",
|
||||
routeAggregation: "route-id-only",
|
||||
releaseId: "release-1",
|
||||
privacyApprovalRef: "privacy-1",
|
||||
thresholdDecisionRef: "decision-1",
|
||||
validationFailures: [],
|
||||
},
|
||||
metrics: { p75LcpMs: 2_501, p75Cls: 0.1, p75InpMs: 200 },
|
||||
thresholds: {
|
||||
p75LcpMs: 2_500,
|
||||
p75Cls: 0.1,
|
||||
p75InpMs: 200,
|
||||
minimumEligibleSamples: 1,
|
||||
},
|
||||
eligibility: {
|
||||
consentRequired: true,
|
||||
totalSamples: 1,
|
||||
eligibleSamples: 1,
|
||||
minimumEligibleSamples: 1,
|
||||
routeSamples: { APP_HOME: 1 },
|
||||
},
|
||||
status: "FAIL_THRESHOLD",
|
||||
passed: false,
|
||||
} as const;
|
||||
|
||||
expect(fieldWebVitalsArtifactSchema.parse(report)).toEqual(report);
|
||||
});
|
||||
|
||||
it("accepts the lab writer's nested browser context as JSON evidence", () => {
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
context: {
|
||||
runner: { platform: "linux", architecture: "x64", nodeVersion: "v24" },
|
||||
browser: { name: "chromium", version: "140" },
|
||||
viewport: { width: 1280, height: 720 },
|
||||
network: {
|
||||
profile: "contract-fast-4g",
|
||||
latencyMs: 40,
|
||||
downloadBytesPerSecond: 200_000,
|
||||
uploadBytesPerSecond: 93_750,
|
||||
},
|
||||
cpu: { throttlingRate: 4 },
|
||||
cache: { state: "cold", isolation: "new-browser-context" },
|
||||
build: { buildId: "build-1", releaseId: "release-1" },
|
||||
},
|
||||
metrics: { lcpMs: 1_000, cls: 0.01, namedInteractionMs: 100 },
|
||||
thresholds: { lcpMs: 2_500, cls: 0.1, namedInteractionMs: 200 },
|
||||
fixtures: [
|
||||
{ name: "missing-context", passed: true },
|
||||
{ name: "lcp-over-threshold", passed: true },
|
||||
],
|
||||
passed: true,
|
||||
} as const;
|
||||
|
||||
expect(labPerformanceArtifactSchema.parse(report)).toEqual(report);
|
||||
});
|
||||
|
||||
it("rejects non-JSON values in generated schema documents", () => {
|
||||
expect(() =>
|
||||
jsonSchemaDocumentArtifactSchema.parse({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
invalid: () => undefined,
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("keeps machine-readable evidence publishers on the validated writer", async () => {
|
||||
const writerFiles = [
|
||||
"scripts/generate-build-manifest.ts",
|
||||
"scripts/generate-supply-chain.ts",
|
||||
"scripts/collect-web-vitals-evidence.ts",
|
||||
"scripts/test-performance.ts",
|
||||
"scripts/verify-release.ts",
|
||||
"scripts/drill-runbook.ts",
|
||||
"scripts/check-registries.ts",
|
||||
] as const;
|
||||
const sources = await Promise.all(
|
||||
writerFiles.map(async (file) => ({
|
||||
file,
|
||||
source: await readFile(file, "utf8"),
|
||||
})),
|
||||
);
|
||||
|
||||
for (const { file, source } of sources) {
|
||||
const directWrites = source.match(/\bwriteFile\s*\(/gu) ?? [];
|
||||
if (file === "scripts/generate-supply-chain.ts") {
|
||||
expect(directWrites, file).toHaveLength(1);
|
||||
expect(source, file).toMatch(
|
||||
/writeFile\(\s*["']artifacts\/release\/checksums\.txt["']/u,
|
||||
);
|
||||
} else {
|
||||
expect(directWrites, file).toHaveLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
sources.flatMap(({ source }) =>
|
||||
source.match(/\bwriteValidatedJsonArtifact\s*\(/gu) ?? [],
|
||||
),
|
||||
).toHaveLength(19);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
mkdtemp,
|
||||
open,
|
||||
readFile,
|
||||
readdir,
|
||||
rename,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createValidatedJsonArtifactWriter,
|
||||
writeValidatedJsonArtifact,
|
||||
} from "../../scripts/lib/validated-json-artifact.ts";
|
||||
|
||||
const artifactSchema = z
|
||||
.object({ schemaVersion: z.literal(1), name: z.string().min(1) })
|
||||
.strict();
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
async function temporaryDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(
|
||||
path.join(tmpdir(), "validated-json-artifact-"),
|
||||
);
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
rm(directory, { force: true, recursive: true }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("validated JSON artifact writer", () => {
|
||||
it.each(["existing", "missing"] as const)(
|
||||
"rejects invalid %s artifacts before changing destination state",
|
||||
async (destinationState) => {
|
||||
const directory = await temporaryDirectory();
|
||||
const destination = path.join(directory, "artifact.json");
|
||||
if (destinationState === "existing") {
|
||||
await writeFile(destination, "previous-bytes\n", "utf8");
|
||||
}
|
||||
const entriesBefore = await readdir(directory);
|
||||
|
||||
await expect(
|
||||
writeValidatedJsonArtifact({
|
||||
path: destination,
|
||||
schema: artifactSchema,
|
||||
value: { schemaVersion: 1, name: "" },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(await readdir(directory)).toEqual(entriesBefore);
|
||||
if (destinationState === "existing") {
|
||||
await expect(readFile(destination, "utf8")).resolves.toBe(
|
||||
"previous-bytes\n",
|
||||
);
|
||||
} else {
|
||||
await expect(readFile(destination, "utf8")).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("publishes complete formatted bytes through a sibling rename", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const destination = path.join(directory, "artifact.json");
|
||||
|
||||
await writeValidatedJsonArtifact({
|
||||
path: destination,
|
||||
schema: artifactSchema,
|
||||
value: { schemaVersion: 1, name: "valid" },
|
||||
});
|
||||
|
||||
await expect(readFile(destination, "utf8")).resolves.toBe(
|
||||
'{\n "schemaVersion": 1,\n "name": "valid"\n}\n',
|
||||
);
|
||||
expect(await readdir(directory)).toEqual(["artifact.json"]);
|
||||
});
|
||||
|
||||
it.each(["write", "rename"] as const)(
|
||||
"cleans only its owned sibling temp when %s fails",
|
||||
async (failurePoint) => {
|
||||
const directory = await temporaryDirectory();
|
||||
const destination = path.join(directory, "artifact.json");
|
||||
const unrelatedTemp = path.join(directory, ".artifact.json.unrelated.tmp");
|
||||
const ownedTemp = path.join(directory, ".artifact.json.owned.tmp");
|
||||
await writeFile(destination, "previous-bytes\n", "utf8");
|
||||
await writeFile(unrelatedTemp, "unrelated\n", "utf8");
|
||||
const writer = createValidatedJsonArtifactWriter({
|
||||
createNonce: () => "owned",
|
||||
fileSystem: {
|
||||
open: async (target, flags) => {
|
||||
const handle = await open(target, flags);
|
||||
return {
|
||||
writeFile: async (data, encoding) => {
|
||||
await handle.writeFile(data, encoding);
|
||||
if (failurePoint === "write") {
|
||||
throw new Error("injected write failure");
|
||||
}
|
||||
},
|
||||
close: async () => handle.close(),
|
||||
};
|
||||
},
|
||||
rename: async (source, target) => {
|
||||
if (failurePoint === "rename") {
|
||||
throw new Error("injected rename failure");
|
||||
}
|
||||
await rename(source, target);
|
||||
},
|
||||
rm,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
writer({
|
||||
path: destination,
|
||||
schema: artifactSchema,
|
||||
value: { schemaVersion: 1, name: "valid" },
|
||||
}),
|
||||
).rejects.toThrow(`injected ${failurePoint} failure`);
|
||||
|
||||
await expect(readFile(destination, "utf8")).resolves.toBe(
|
||||
"previous-bytes\n",
|
||||
);
|
||||
await expect(readFile(unrelatedTemp, "utf8")).resolves.toBe(
|
||||
"unrelated\n",
|
||||
);
|
||||
await expect(readFile(ownedTemp, "utf8")).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("does not delete a pre-existing colliding sibling temp", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const destination = path.join(directory, "artifact.json");
|
||||
const collidingTemp = path.join(directory, ".artifact.json.collision.tmp");
|
||||
await writeFile(collidingTemp, "another-writer\n", "utf8");
|
||||
const writer = createValidatedJsonArtifactWriter({
|
||||
createNonce: () => "collision",
|
||||
});
|
||||
|
||||
await expect(
|
||||
writer({
|
||||
path: destination,
|
||||
schema: artifactSchema,
|
||||
value: { schemaVersion: 1, name: "valid" },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "EEXIST" });
|
||||
|
||||
await expect(readFile(collidingTemp, "utf8")).resolves.toBe(
|
||||
"another-writer\n",
|
||||
);
|
||||
await expect(readFile(destination, "utf8")).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user