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",
|
"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",
|
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
||||||
"check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts",
|
"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",
|
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
||||||
"verify:supply-chain": "node scripts/verify-supply-chain-artifacts.ts",
|
"verify:supply-chain": "node scripts/verify-supply-chain-artifacts.ts",
|
||||||
"update:dependency-baseline": "node scripts/update-dependency-baseline.ts",
|
"update:dependency-baseline": "node scripts/update-dependency-baseline.ts",
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
"drill:runbook": "node scripts/drill-runbook.ts",
|
"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",
|
"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",
|
"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",
|
"verify:documentation": "node scripts/verify-documentation-readiness.ts",
|
||||||
"build:app-only": "vite build && node scripts/generate-build-manifest.ts",
|
"build:app-only": "vite build && node scripts/generate-build-manifest.ts",
|
||||||
"generate:contract-set": "node scripts/generate-contract-set.ts",
|
"generate:contract-set": "node scripts/generate-contract-set.ts",
|
||||||
|
|||||||
@@ -1,51 +1,27 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"type": "object",
|
"additionalProperties": false,
|
||||||
"properties": {
|
"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": {
|
"buildContext": {
|
||||||
"type": "object",
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"nodeVersion": {
|
"nodeVersion": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
},
|
||||||
"packageManagerVersion": {
|
"packageManagerVersion": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
},
|
||||||
"runnerImage": {
|
"runnerImage": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
},
|
||||||
"sourceDateEpoch": {
|
"sourceDateEpoch": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "null"
|
"type": "null"
|
||||||
@@ -59,37 +35,53 @@
|
|||||||
"runnerImage",
|
"runnerImage",
|
||||||
"sourceDateEpoch"
|
"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": {
|
"outputs": {
|
||||||
"type": "object",
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
|
||||||
"viteManifest": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
},
|
||||||
"moduleInventory": {
|
"moduleInventory": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
},
|
},
|
||||||
"routeChunks": {
|
"routeChunks": {
|
||||||
"type": "object",
|
|
||||||
"propertyNames": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"propertyNames": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
},
|
},
|
||||||
"runtimeConfigSchema": {
|
"runtimeConfigSchema": {
|
||||||
"type": "string",
|
"minLength": 1,
|
||||||
"minLength": 1
|
"type": "string"
|
||||||
|
},
|
||||||
|
"viteManifest": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -99,7 +91,15 @@
|
|||||||
"routeChunks",
|
"routeChunks",
|
||||||
"runtimeConfigSchema"
|
"runtimeConfigSchema"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"type": "object"
|
||||||
|
},
|
||||||
|
"releaseId": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"const": 1,
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -112,5 +112,5 @@
|
|||||||
"buildContext",
|
"buildContext",
|
||||||
"outputs"
|
"outputs"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"type": "object"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://clean-architecture-frontend.local/schemas/dependency-inventory.schema.json",
|
"$id": "https://clean-architecture-frontend.local/schemas/dependency-inventory.schema.json",
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
|
||||||
"schemaVersion",
|
|
||||||
"packageManager",
|
|
||||||
"lockfileSha256",
|
|
||||||
"dependencyCount",
|
|
||||||
"directDependencyCount",
|
|
||||||
"dependencies"
|
|
||||||
],
|
|
||||||
"properties": {
|
"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": {
|
"dependencies": {
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
"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": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"version",
|
"version",
|
||||||
@@ -36,25 +54,40 @@
|
|||||||
"integrity",
|
"integrity",
|
||||||
"dependencies"
|
"dependencies"
|
||||||
],
|
],
|
||||||
"properties": {
|
"type": "object"
|
||||||
"name": { "type": "string", "minLength": 1 },
|
},
|
||||||
"version": { "type": "string", "minLength": 1 },
|
"type": "array"
|
||||||
"direct": { "type": "boolean" },
|
},
|
||||||
"scope": {
|
"dependencyCount": {
|
||||||
"enum": ["production", "development"]
|
"maximum": 9007199254740991,
|
||||||
},
|
"minimum": 0,
|
||||||
"optional": { "type": "boolean" },
|
"type": "integer"
|
||||||
"license": { "type": "string", "minLength": 1 },
|
},
|
||||||
"integrity": {
|
"directDependencyCount": {
|
||||||
"type": "string",
|
"maximum": 9007199254740991,
|
||||||
"pattern": "^sha512-"
|
"minimum": 0,
|
||||||
},
|
"type": "integer"
|
||||||
"dependencies": {
|
},
|
||||||
"type": "array",
|
"lockfileSha256": {
|
||||||
"items": { "type": "string", "minLength": 1 }
|
"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",
|
"$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": [
|
"required": [
|
||||||
"schemaVersion",
|
"schemaVersion",
|
||||||
"generatedAt",
|
"generatedAt",
|
||||||
@@ -10,45 +261,5 @@
|
|||||||
"failures",
|
"failures",
|
||||||
"registries"
|
"registries"
|
||||||
],
|
],
|
||||||
"properties": {
|
"type": "object"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,130 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://clean-architecture-frontend.local/schemas/supply-chain-verification.schema.json",
|
"$id": "https://clean-architecture-frontend.local/schemas/supply-chain-verification.schema.json",
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
"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": [
|
"required": [
|
||||||
"schemaVersion",
|
"schemaVersion",
|
||||||
"localStatus",
|
"localStatus",
|
||||||
@@ -17,39 +139,5 @@
|
|||||||
"provenanceAttestationStatus",
|
"provenanceAttestationStatus",
|
||||||
"failures"
|
"failures"
|
||||||
],
|
],
|
||||||
"properties": {
|
"type": "object"
|
||||||
"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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
mkdir,
|
mkdir,
|
||||||
readFile,
|
readFile,
|
||||||
readdir,
|
readdir,
|
||||||
writeFile,
|
|
||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
@@ -16,6 +15,8 @@ import {
|
|||||||
verifyRegistryBaselineApproval,
|
verifyRegistryBaselineApproval,
|
||||||
} from "./lib/registry-compatibility.ts";
|
} from "./lib/registry-compatibility.ts";
|
||||||
import { assertMatchesJsonSchema } from "./lib/json-schema.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 RegistryRow = Record<string, unknown>;
|
||||||
type RegistryRows = Record<string, RegistryRow>;
|
type RegistryRows = Record<string, RegistryRow>;
|
||||||
@@ -675,7 +676,11 @@ if (usesRepositoryBaseline && failures.length === 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await mkdir(path.dirname(artifactPath), { recursive: true });
|
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) {
|
if (failures.length > 0) {
|
||||||
process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`);
|
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 {
|
import {
|
||||||
evaluateFieldBudget,
|
evaluateFieldBudget,
|
||||||
percentile75,
|
percentile75,
|
||||||
} from "../src/application/policies/performance-budgets.ts";
|
} from "../src/application/policies/performance-budgets.ts";
|
||||||
|
import { fieldWebVitalsArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||||
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.ts";
|
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.ts";
|
||||||
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
|
||||||
const inputPath =
|
const inputPath =
|
||||||
process.env.FIELD_WEB_VITALS_INPUT ||
|
process.env.FIELD_WEB_VITALS_INPUT ||
|
||||||
@@ -94,10 +96,11 @@ const report = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await mkdir("artifacts/performance", { recursive: true });
|
await mkdir("artifacts/performance", { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/performance/field-web-vitals.json",
|
path: "artifacts/performance/field-web-vitals.json",
|
||||||
`${JSON.stringify(report, null, 2)}\n`,
|
schema: fieldWebVitalsArtifactSchema,
|
||||||
);
|
value: report,
|
||||||
|
});
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`Field Web Vitals: ${status} (approved threshold decision and valid 28-day production evidence are required)\n`,
|
`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";
|
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 { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
|
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
|
||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
} from "../src/contracts/release-artifacts.ts";
|
} from "../src/contracts/release-artifacts.ts";
|
||||||
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
|
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
|
||||||
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.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 { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts";
|
||||||
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
|
||||||
type RecoveryAssertion = Readonly<{
|
type RecoveryAssertion = Readonly<{
|
||||||
assertion: string;
|
assertion: string;
|
||||||
@@ -354,10 +356,11 @@ async function main(): Promise<void> {
|
|||||||
};
|
};
|
||||||
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
|
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
|
||||||
await mkdir(artifactDirectory, { recursive: true });
|
await mkdir(artifactDirectory, { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
`${artifactDirectory}/record.json`,
|
path: `${artifactDirectory}/record.json`,
|
||||||
`${JSON.stringify(record, null, 2)}\n`,
|
schema: runbookRecordArtifactSchema,
|
||||||
);
|
value: record,
|
||||||
|
});
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
process.stderr.write(`${runbookId} drill failed.\n`);
|
process.stderr.write(`${runbookId} drill failed.\n`);
|
||||||
process.exit(1);
|
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 { 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 process from "node:process";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -9,14 +9,17 @@ import {
|
|||||||
} from "../src/features/installed-feature-contracts.ts";
|
} from "../src/features/installed-feature-contracts.ts";
|
||||||
import {
|
import {
|
||||||
buildManifestArtifactSchema,
|
buildManifestArtifactSchema,
|
||||||
|
jsonSchemaDocumentArtifactSchema,
|
||||||
|
moduleInventoryArtifactSchema,
|
||||||
releaseManifestV2ArtifactSchema,
|
releaseManifestV2ArtifactSchema,
|
||||||
runtimeConfigV2ArtifactSchema,
|
runtimeConfigV2ArtifactSchema,
|
||||||
} from "../src/contracts/release-artifacts.ts";
|
} from "./contracts/release-artifacts.ts";
|
||||||
import { buildContractSet } from "./generate-contract-set.ts";
|
import { buildContractSet } from "./generate-contract-set.ts";
|
||||||
import {
|
import {
|
||||||
assertCiBuildEnvironment,
|
assertCiBuildEnvironment,
|
||||||
buildDate,
|
buildDate,
|
||||||
} from "./lib/build-environment.ts";
|
} from "./lib/build-environment.ts";
|
||||||
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
|
||||||
assertCiBuildEnvironment(process.env);
|
assertCiBuildEnvironment(process.env);
|
||||||
type ViteManifestEntry = Readonly<{
|
type ViteManifestEntry = Readonly<{
|
||||||
@@ -41,12 +44,15 @@ const moduleInventory = await readFile(
|
|||||||
"dist/.vite/module-inventory.json",
|
"dist/.vite/module-inventory.json",
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
parseModuleInventory(JSON.parse(moduleInventory));
|
const moduleInventoryDocument = moduleInventoryArtifactSchema.parse(
|
||||||
|
JSON.parse(moduleInventory),
|
||||||
|
);
|
||||||
|
const moduleInventoryBytes = `${JSON.stringify(moduleInventoryDocument, null, 2)}\n`;
|
||||||
const assetManifestHash = createHash("sha256")
|
const assetManifestHash = createHash("sha256")
|
||||||
.update(viteManifest)
|
.update(viteManifest)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
const moduleInventoryHash = createHash("sha256")
|
const moduleInventoryHash = createHash("sha256")
|
||||||
.update(moduleInventory)
|
.update(moduleInventoryBytes)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
const runtimeConfig = runtimeConfigV2ArtifactSchema.parse(
|
const runtimeConfig = runtimeConfigV2ArtifactSchema.parse(
|
||||||
JSON.parse(await readFile("dist/config.json", "utf8")),
|
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/release", { recursive: true });
|
||||||
await mkdir("artifacts/quality", { recursive: true });
|
await mkdir("artifacts/quality", { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/quality/vite-module-inventory.json",
|
path: "artifacts/quality/vite-module-inventory.json",
|
||||||
moduleInventory,
|
schema: moduleInventoryArtifactSchema,
|
||||||
);
|
value: moduleInventoryDocument,
|
||||||
|
});
|
||||||
await rm("dist/.vite/module-inventory.json");
|
await rm("dist/.vite/module-inventory.json");
|
||||||
await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`);
|
await writeValidatedJsonArtifact({
|
||||||
await writeFile(
|
path: "dist/config.json",
|
||||||
"dist/release-manifest.json",
|
schema: runtimeConfigV2ArtifactSchema,
|
||||||
`${JSON.stringify(releaseManifest, null, 2)}\n`,
|
value: runtimeConfig,
|
||||||
);
|
});
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"dist/runtime-config.schema.json",
|
path: "dist/release-manifest.json",
|
||||||
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
|
schema: releaseManifestV2ArtifactSchema,
|
||||||
);
|
value: releaseManifest,
|
||||||
await writeFile(
|
});
|
||||||
"artifacts/release/runtime-config.schema.json",
|
await writeValidatedJsonArtifact({
|
||||||
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
|
path: "dist/runtime-config.schema.json",
|
||||||
);
|
schema: jsonSchemaDocumentArtifactSchema,
|
||||||
await writeFile(
|
value: runtimeConfigJsonSchema,
|
||||||
"artifacts/release/build-manifest.json",
|
});
|
||||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
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<{
|
function parsePackageMetadata(value: unknown): Readonly<{
|
||||||
version: string;
|
version: string;
|
||||||
@@ -165,23 +180,6 @@ function parseViteManifest(
|
|||||||
return entries;
|
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> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ import {
|
|||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
import {
|
||||||
|
bundlePerformanceArtifactSchema,
|
||||||
|
dependencyDiffArtifactSchema,
|
||||||
|
dependencyInventoryArtifactSchema,
|
||||||
|
licenseReportArtifactSchema,
|
||||||
|
provenanceArtifactSchema,
|
||||||
|
sbomArtifactSchema,
|
||||||
|
supplyChainVerificationArtifactSchema,
|
||||||
|
vulnerabilityReportArtifactSchema,
|
||||||
|
} from "./contracts/release-artifacts.ts";
|
||||||
import {
|
import {
|
||||||
diffDependencyInventories,
|
diffDependencyInventories,
|
||||||
flattenPnpmDependencyTree,
|
flattenPnpmDependencyTree,
|
||||||
@@ -22,6 +32,7 @@ import {
|
|||||||
verifySupplyChainCoherence,
|
verifySupplyChainCoherence,
|
||||||
type DependencyInventoryDiff,
|
type DependencyInventoryDiff,
|
||||||
} from "./lib/supply-chain.ts";
|
} from "./lib/supply-chain.ts";
|
||||||
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
|
||||||
type Document = Record<string, unknown>;
|
type Document = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -428,81 +439,80 @@ const verification = {
|
|||||||
: "FAIL_UNVERIFIED",
|
: "FAIL_UNVERIFIED",
|
||||||
failures: localFailures,
|
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/performance", { recursive: true });
|
||||||
await mkdir("artifacts/release", { recursive: true });
|
await mkdir("artifacts/release", { recursive: true });
|
||||||
await mkdir("artifacts/security", { recursive: true });
|
await mkdir("artifacts/security", { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/performance/bundle.json",
|
path: "artifacts/performance/bundle.json",
|
||||||
`${JSON.stringify(
|
schema: bundlePerformanceArtifactSchema,
|
||||||
{
|
value: bundleReport,
|
||||||
schemaVersion: 1,
|
});
|
||||||
generatedAt: new Date().toISOString(),
|
await writeValidatedJsonArtifact({
|
||||||
context: {
|
path: "artifacts/release/dependency-inventory.json",
|
||||||
nodeVersion: process.version,
|
schema: dependencyInventoryArtifactSchema,
|
||||||
packageManager: String(packageJson.packageManager ?? ""),
|
value: inventory,
|
||||||
runnerImage:
|
});
|
||||||
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
|
await writeValidatedJsonArtifact({
|
||||||
},
|
path: "artifacts/release/sbom.cdx.json",
|
||||||
outputs,
|
schema: sbomArtifactSchema,
|
||||||
},
|
value: sbom,
|
||||||
null,
|
});
|
||||||
2,
|
await writeValidatedJsonArtifact({
|
||||||
)}\n`,
|
path: "artifacts/release/provenance.json",
|
||||||
);
|
schema: provenanceArtifactSchema,
|
||||||
await writeFile(
|
value: provenance,
|
||||||
"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 writeFile(
|
await writeFile(
|
||||||
"artifacts/release/checksums.txt",
|
"artifacts/release/checksums.txt",
|
||||||
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
|
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
|
||||||
);
|
);
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/security/dependency-diff.json",
|
path: "artifacts/security/dependency-diff.json",
|
||||||
`${JSON.stringify(
|
schema: dependencyDiffArtifactSchema,
|
||||||
{
|
value: dependencyDiffReport,
|
||||||
schemaVersion: 2,
|
});
|
||||||
baselineDigest: baseline ? supplyChainDigest(baseline) : null,
|
await writeValidatedJsonArtifact({
|
||||||
currentDigest: supplyChainDigest(inventory),
|
path: "artifacts/security/license-report.json",
|
||||||
...dependencyDiff,
|
schema: licenseReportArtifactSchema,
|
||||||
highRisk: reviewResult.highRisk,
|
value: licenseReport,
|
||||||
reviewFailures: reviewResult.failures,
|
});
|
||||||
},
|
await writeValidatedJsonArtifact({
|
||||||
null,
|
path: "artifacts/security/vulnerability-report.json",
|
||||||
2,
|
schema: vulnerabilityReportArtifactSchema,
|
||||||
)}\n`,
|
value: vulnerabilityReport,
|
||||||
);
|
});
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/security/license-report.json",
|
path: "artifacts/security/supply-chain-verification.json",
|
||||||
`${JSON.stringify(
|
schema: supplyChainVerificationArtifactSchema,
|
||||||
{
|
value: verification,
|
||||||
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`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!localPassed) {
|
if (!localPassed) {
|
||||||
process.stderr.write(
|
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 { 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 { performance } from "node:perf_hooks";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ import { chromium } from "@playwright/test";
|
|||||||
|
|
||||||
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.ts";
|
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.ts";
|
||||||
import { ROUTE_REGISTRY } from "../src/features/installed-feature-contracts.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 = {
|
type ContractPerformanceEvidence = {
|
||||||
lcpMs: number;
|
lcpMs: number;
|
||||||
@@ -142,22 +144,19 @@ try {
|
|||||||
];
|
];
|
||||||
const passed = result.passed && fixtures.every((fixture) => fixture.passed);
|
const passed = result.passed && fixtures.every((fixture) => fixture.passed);
|
||||||
await mkdir("artifacts/performance", { recursive: true });
|
await mkdir("artifacts/performance", { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/performance/lab.json",
|
path: "artifacts/performance/lab.json",
|
||||||
`${JSON.stringify(
|
schema: labPerformanceArtifactSchema,
|
||||||
{
|
value: {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
context: contextMetadata,
|
context: contextMetadata,
|
||||||
metrics,
|
metrics,
|
||||||
thresholds,
|
thresholds,
|
||||||
fixtures,
|
fixtures,
|
||||||
passed,
|
passed,
|
||||||
},
|
},
|
||||||
null,
|
});
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
throw new Error(`Lab performance failed: ${JSON.stringify(metrics)}`);
|
throw new Error(`Lab performance failed: ${JSON.stringify(metrics)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createHash } from "node:crypto";
|
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 { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
} from "../src/features/installed-feature-contracts.ts";
|
} from "../src/features/installed-feature-contracts.ts";
|
||||||
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
|
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
|
||||||
import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.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<{
|
type CoherenceFixture = Readonly<{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -212,10 +214,11 @@ const report = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await mkdir("artifacts/release", { recursive: true });
|
await mkdir("artifacts/release", { recursive: true });
|
||||||
await writeFile(
|
await writeValidatedJsonArtifact({
|
||||||
"artifacts/release/verification.json",
|
path: "artifacts/release/verification.json",
|
||||||
`${JSON.stringify(report, null, 2)}\n`,
|
schema: releaseVerificationArtifactSchema,
|
||||||
);
|
value: report,
|
||||||
|
});
|
||||||
|
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
process.stderr.write(
|
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";
|
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> {
|
async function json(path: string): Promise<unknown> {
|
||||||
return JSON.parse(await readFile(path, "utf8")) as 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"),
|
assertMatchesJsonSchema(schema, catalog, "optional recipe catalog"),
|
||||||
).not.toThrow();
|
).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 { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
dependencyInventoryArtifactSchema,
|
||||||
|
fieldWebVitalsArtifactSchema,
|
||||||
|
jsonSchemaDocumentArtifactSchema,
|
||||||
|
labPerformanceArtifactSchema,
|
||||||
parseBuildManifestArtifact,
|
parseBuildManifestArtifact,
|
||||||
parseReleaseArtifact,
|
parseReleaseArtifact,
|
||||||
parseRuntimeConfigArtifact,
|
parseRuntimeConfigArtifact,
|
||||||
projectReleaseTokens,
|
projectReleaseTokens,
|
||||||
|
registrySnapshotArtifactSchema,
|
||||||
|
supplyChainVerificationArtifactSchema,
|
||||||
} from "../../scripts/contracts/release-artifacts.ts";
|
} from "../../scripts/contracts/release-artifacts.ts";
|
||||||
|
|
||||||
const EMPTY_CONTRACT_SET_DIGEST =
|
const EMPTY_CONTRACT_SET_DIGEST =
|
||||||
@@ -126,4 +134,241 @@ describe("release artifact contracts", () => {
|
|||||||
}),
|
}),
|
||||||
).not.toHaveProperty("API_CONTRACT_VERSION");
|
).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