fix: preserve artifact writer failures
This commit is contained in:
@@ -56,16 +56,17 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"dependencyCount": {
|
"dependencyCount": {
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
"maximum": 9007199254740991,
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"directDependencyCount": {
|
"directDependencyCount": {
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
"maximum": 9007199254740991,
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"lockfileSha256": {
|
"lockfileSha256": {
|
||||||
|
|||||||
@@ -95,50 +95,123 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"anyOf": [
|
||||||
"properties": {
|
{
|
||||||
"baselineDigest": {
|
"additionalProperties": false,
|
||||||
"anyOf": [
|
"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}$",
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
"failures": {
|
||||||
"type": "null"
|
"items": {
|
||||||
}
|
"type": "string"
|
||||||
]
|
},
|
||||||
},
|
"maxItems": 0,
|
||||||
"compatibility": {
|
"type": "array"
|
||||||
"additionalProperties": false,
|
},
|
||||||
"properties": {
|
"generatedAt": {
|
||||||
"changes": {
|
"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": {
|
"items": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"after": {
|
"contract": {
|
||||||
"$ref": "#/$defs/__schema1"
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
},
|
},
|
||||||
"before": {
|
"owner": {
|
||||||
"$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,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -146,120 +219,224 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"rowName": {
|
"rowCount": {
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"rows": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"changeId",
|
|
||||||
"registryId",
|
"registryId",
|
||||||
"rowName",
|
"owner",
|
||||||
"field",
|
"source",
|
||||||
"kind",
|
"rowCount",
|
||||||
"impact"
|
"contract",
|
||||||
|
"rows"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"maxItems": 11,
|
||||||
|
"minItems": 11,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"const": 2,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"schemaVersion",
|
||||||
|
"generatedAt",
|
||||||
|
"baselineDigest",
|
||||||
|
"currentDigest",
|
||||||
|
"compatibility",
|
||||||
|
"failures",
|
||||||
|
"registries"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"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": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"impact": {
|
"schemaVersion": {
|
||||||
"enum": [
|
"const": 2,
|
||||||
"not-evaluated",
|
"type": "number"
|
||||||
"none",
|
|
||||||
"additive",
|
|
||||||
"behavior-change",
|
|
||||||
"breaking"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"impact",
|
"schemaVersion",
|
||||||
"changes"
|
"generatedAt",
|
||||||
|
"baselineDigest",
|
||||||
|
"currentDigest",
|
||||||
|
"compatibility",
|
||||||
|
"failures",
|
||||||
|
"registries"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"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",
|
|
||||||
"baselineDigest",
|
|
||||||
"currentDigest",
|
|
||||||
"compatibility",
|
|
||||||
"failures",
|
|
||||||
"registries"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ 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 {
|
||||||
|
registryGovernanceRunArtifactSchema,
|
||||||
|
registrySnapshotArtifactSchema,
|
||||||
|
} from "./contracts/release-artifacts.ts";
|
||||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||||
|
|
||||||
type RegistryRow = Record<string, unknown>;
|
type RegistryRow = Record<string, unknown>;
|
||||||
@@ -79,8 +82,9 @@ const governancePath =
|
|||||||
const artifactPath =
|
const artifactPath =
|
||||||
argumentValue("--artifact", "artifacts/quality/registries.json") ??
|
argumentValue("--artifact", "artifacts/quality/registries.json") ??
|
||||||
"artifacts/quality/registries.json";
|
"artifacts/quality/registries.json";
|
||||||
|
const usesRepositoryGovernance = governancePath === defaultGovernancePath;
|
||||||
const usesRepositoryBaseline =
|
const usesRepositoryBaseline =
|
||||||
governancePath === defaultGovernancePath &&
|
usesRepositoryGovernance &&
|
||||||
!process.argv.includes("--no-baseline");
|
!process.argv.includes("--no-baseline");
|
||||||
const baselinePath = argumentValue(
|
const baselinePath = argumentValue(
|
||||||
"--baseline",
|
"--baseline",
|
||||||
@@ -678,7 +682,9 @@ if (usesRepositoryBaseline && failures.length === 0) {
|
|||||||
await mkdir(path.dirname(artifactPath), { recursive: true });
|
await mkdir(path.dirname(artifactPath), { recursive: true });
|
||||||
await writeValidatedJsonArtifact({
|
await writeValidatedJsonArtifact({
|
||||||
path: artifactPath,
|
path: artifactPath,
|
||||||
schema: registrySnapshotArtifactSchema,
|
schema: usesRepositoryGovernance
|
||||||
|
? registrySnapshotArtifactSchema
|
||||||
|
: registryGovernanceRunArtifactSchema,
|
||||||
value: report,
|
value: report,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ export const dependencyInventoryArtifactSchema = z
|
|||||||
schemaVersion: z.literal(2),
|
schemaVersion: z.literal(2),
|
||||||
packageManager: nonEmptyString,
|
packageManager: nonEmptyString,
|
||||||
lockfileSha256: sha256,
|
lockfileSha256: sha256,
|
||||||
dependencyCount: z.int().nonnegative(),
|
dependencyCount: z.int().positive(),
|
||||||
directDependencyCount: z.int().nonnegative(),
|
directDependencyCount: z.int().positive(),
|
||||||
dependencies: z.array(dependencyInventoryRowSchema),
|
dependencies: z.array(dependencyInventoryRowSchema).min(1),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.superRefine((inventory, context) => {
|
.superRefine((inventory, context) => {
|
||||||
@@ -128,7 +128,7 @@ const registryArtifactRowSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export const registrySnapshotArtifactSchema = z
|
const registrySnapshotBaseArtifactSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(2),
|
schemaVersion: z.literal(2),
|
||||||
generatedAt: timestamp,
|
generatedAt: timestamp,
|
||||||
@@ -146,11 +146,34 @@ export const registrySnapshotArtifactSchema = z
|
|||||||
changes: z.array(registryChangeSchema),
|
changes: z.array(registryChangeSchema),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
failures: z.array(z.string()),
|
|
||||||
registries: z.array(registryArtifactRowSchema),
|
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const successfulRegistrySnapshotArtifactSchema =
|
||||||
|
registrySnapshotBaseArtifactSchema.extend({
|
||||||
|
failures: z.array(z.string()).max(0),
|
||||||
|
registries: z.array(registryArtifactRowSchema).length(11),
|
||||||
|
});
|
||||||
|
|
||||||
|
const failedRegistrySnapshotArtifactSchema =
|
||||||
|
registrySnapshotBaseArtifactSchema.extend({
|
||||||
|
failures: z.array(z.string()).min(1),
|
||||||
|
registries: z.array(registryArtifactRowSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registrySnapshotArtifactSchema = z.union([
|
||||||
|
successfulRegistrySnapshotArtifactSchema,
|
||||||
|
failedRegistrySnapshotArtifactSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registryGovernanceRunArtifactSchema = z.union([
|
||||||
|
registrySnapshotBaseArtifactSchema.extend({
|
||||||
|
failures: z.array(z.string()).max(0),
|
||||||
|
registries: z.array(registryArtifactRowSchema).min(1),
|
||||||
|
}),
|
||||||
|
failedRegistrySnapshotArtifactSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
const outputDigestSchema = z
|
const outputDigestSchema = z
|
||||||
.object({
|
.object({
|
||||||
path: nonEmptyString,
|
path: nonEmptyString,
|
||||||
|
|||||||
@@ -62,11 +62,24 @@ export function createValidatedJsonArtifactWriter(
|
|||||||
try {
|
try {
|
||||||
const handle = await fileSystem.open(temporaryPath, "wx");
|
const handle = await fileSystem.open(temporaryPath, "wx");
|
||||||
ownsTemporaryFile = true;
|
ownsTemporaryFile = true;
|
||||||
|
let writeFailed = false;
|
||||||
|
let writeFailure: unknown;
|
||||||
try {
|
try {
|
||||||
await handle.writeFile(`${serialized}\n`, "utf8");
|
await handle.writeFile(`${serialized}\n`, "utf8");
|
||||||
} finally {
|
} catch (error) {
|
||||||
await handle.close();
|
writeFailed = true;
|
||||||
|
writeFailure = error;
|
||||||
}
|
}
|
||||||
|
let closeFailed = false;
|
||||||
|
let closeFailure: unknown;
|
||||||
|
try {
|
||||||
|
await handle.close();
|
||||||
|
} catch (error) {
|
||||||
|
closeFailed = true;
|
||||||
|
closeFailure = error;
|
||||||
|
}
|
||||||
|
if (writeFailed) throw writeFailure;
|
||||||
|
if (closeFailed) throw closeFailure;
|
||||||
await fileSystem.rename(temporaryPath, input.path);
|
await fileSystem.rename(temporaryPath, input.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (ownsTemporaryFile) {
|
if (ownsTemporaryFile) {
|
||||||
|
|||||||
@@ -198,6 +198,40 @@ describe("checked-in JSON Schema execution", () => {
|
|||||||
fixture.name,
|
fixture.name,
|
||||||
),
|
),
|
||||||
).toThrow(/checked-in JSON Schema/u);
|
).toThrow(/checked-in JSON Schema/u);
|
||||||
|
expect(() =>
|
||||||
|
assertMatchesJsonSchema(
|
||||||
|
schema,
|
||||||
|
{
|
||||||
|
...fixture.value,
|
||||||
|
dependencyCount: 0,
|
||||||
|
directDependencyCount: 0,
|
||||||
|
dependencies: [],
|
||||||
|
},
|
||||||
|
fixture.name,
|
||||||
|
),
|
||||||
|
).toThrow(/checked-in JSON Schema/u);
|
||||||
|
}
|
||||||
|
if (fixture.name === "registry snapshot") {
|
||||||
|
expect(() =>
|
||||||
|
assertMatchesJsonSchema(
|
||||||
|
schema,
|
||||||
|
{ ...fixture.value, failures: [], registries: [] },
|
||||||
|
fixture.name,
|
||||||
|
),
|
||||||
|
).toThrow(/checked-in JSON Schema/u);
|
||||||
|
expect(() =>
|
||||||
|
assertMatchesJsonSchema(
|
||||||
|
schema,
|
||||||
|
{
|
||||||
|
...fixture.value,
|
||||||
|
baselineDigest: null,
|
||||||
|
compatibility: { impact: "not-evaluated", changes: [] },
|
||||||
|
failures: ["missing registry source"],
|
||||||
|
registries: [],
|
||||||
|
},
|
||||||
|
fixture.name,
|
||||||
|
),
|
||||||
|
).not.toThrow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ describe("release artifact contracts", () => {
|
|||||||
dependencyCount: 2,
|
dependencyCount: 2,
|
||||||
}),
|
}),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
dependencyInventoryArtifactSchema.parse({
|
||||||
|
...inventory,
|
||||||
|
dependencyCount: 0,
|
||||||
|
directDependencyCount: 0,
|
||||||
|
dependencies: [],
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects undeclared supply-chain verification evidence", () => {
|
it("rejects undeclared supply-chain verification evidence", () => {
|
||||||
@@ -215,6 +223,35 @@ describe("release artifact contracts", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires the complete repository registry set for success evidence", () => {
|
||||||
|
const successfulEvidence = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||||
|
baselineDigest: null,
|
||||||
|
currentDigest: "a".repeat(64),
|
||||||
|
compatibility: { impact: "not-evaluated", changes: [] },
|
||||||
|
failures: [],
|
||||||
|
registries: Array.from({ length: 11 }, (_, index) => ({
|
||||||
|
registryId: `registry-${index}`,
|
||||||
|
owner: "platform",
|
||||||
|
source: `src/registry-${index}.ts`,
|
||||||
|
rowCount: 0,
|
||||||
|
contract: {},
|
||||||
|
rows: {},
|
||||||
|
})),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
expect(registrySnapshotArtifactSchema.parse(successfulEvidence)).toEqual(
|
||||||
|
successfulEvidence,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
registrySnapshotArtifactSchema.parse({
|
||||||
|
...successfulEvidence,
|
||||||
|
registries: [],
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves unverified field evidence when no samples are eligible", () => {
|
it("preserves unverified field evidence when no samples are eligible", () => {
|
||||||
const report = {
|
const report = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|||||||
@@ -88,6 +88,141 @@ describe("validated JSON artifact writer", () => {
|
|||||||
expect(await readdir(directory)).toEqual(["artifact.json"]);
|
expect(await readdir(directory)).toEqual(["artifact.json"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes the schema's defaulted and transformed parsed output", async () => {
|
||||||
|
const directory = await temporaryDirectory();
|
||||||
|
const destination = path.join(directory, "artifact.json");
|
||||||
|
const parsedOutputSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().default("generated"),
|
||||||
|
count: z.string().transform((value) => Number(value)),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
await writeValidatedJsonArtifact({
|
||||||
|
path: destination,
|
||||||
|
schema: parsedOutputSchema,
|
||||||
|
value: { count: "3" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(JSON.parse(await readFile(destination, "utf8"))).toEqual({
|
||||||
|
name: "generated",
|
||||||
|
count: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a close-only failure and cleans its owned temp", async () => {
|
||||||
|
const directory = await temporaryDirectory();
|
||||||
|
const destination = path.join(directory, "artifact.json");
|
||||||
|
const ownedTemp = path.join(directory, ".artifact.json.close-only.tmp");
|
||||||
|
const closeError = new Error("injected close-only failure");
|
||||||
|
const writer = createValidatedJsonArtifactWriter({
|
||||||
|
createNonce: () => "close-only",
|
||||||
|
fileSystem: {
|
||||||
|
open: async (target, flags) => {
|
||||||
|
const handle = await open(target, flags);
|
||||||
|
return {
|
||||||
|
writeFile: async (data, encoding) =>
|
||||||
|
handle.writeFile(data, encoding),
|
||||||
|
close: async () => {
|
||||||
|
await handle.close();
|
||||||
|
throw closeError;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rename,
|
||||||
|
rm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
writer({
|
||||||
|
path: destination,
|
||||||
|
schema: artifactSchema,
|
||||||
|
value: { schemaVersion: 1, name: "valid" },
|
||||||
|
}),
|
||||||
|
).rejects.toBe(closeError);
|
||||||
|
await expect(readFile(destination, "utf8")).rejects.toMatchObject({
|
||||||
|
code: "ENOENT",
|
||||||
|
});
|
||||||
|
await expect(readFile(ownedTemp, "utf8")).rejects.toMatchObject({
|
||||||
|
code: "ENOENT",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the write error when write and close both fail", async () => {
|
||||||
|
const directory = await temporaryDirectory();
|
||||||
|
const destination = path.join(directory, "artifact.json");
|
||||||
|
const ownedTemp = path.join(directory, ".artifact.json.double-failure.tmp");
|
||||||
|
const writeError = new Error("injected primary write failure");
|
||||||
|
const closeError = new Error("injected secondary close failure");
|
||||||
|
const writer = createValidatedJsonArtifactWriter({
|
||||||
|
createNonce: () => "double-failure",
|
||||||
|
fileSystem: {
|
||||||
|
open: async (target, flags) => {
|
||||||
|
const handle = await open(target, flags);
|
||||||
|
return {
|
||||||
|
writeFile: async (data, encoding) => {
|
||||||
|
await handle.writeFile(data, encoding);
|
||||||
|
throw writeError;
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
await handle.close();
|
||||||
|
throw closeError;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rename,
|
||||||
|
rm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
writer({
|
||||||
|
path: destination,
|
||||||
|
schema: artifactSchema,
|
||||||
|
value: { schemaVersion: 1, name: "valid" },
|
||||||
|
}),
|
||||||
|
).rejects.toBe(writeError);
|
||||||
|
await expect(readFile(destination, "utf8")).rejects.toMatchObject({
|
||||||
|
code: "ENOENT",
|
||||||
|
});
|
||||||
|
await expect(readFile(ownedTemp, "utf8")).rejects.toMatchObject({
|
||||||
|
code: "ENOENT",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("publishes one complete document when two valid writers race", async () => {
|
||||||
|
const directory = await temporaryDirectory();
|
||||||
|
const destination = path.join(directory, "artifact.json");
|
||||||
|
const nonces = ["first", "second"];
|
||||||
|
const writer = createValidatedJsonArtifactWriter({
|
||||||
|
createNonce: () => {
|
||||||
|
const nonce = nonces.shift();
|
||||||
|
if (nonce === undefined) throw new Error("nonce fixture exhausted");
|
||||||
|
return nonce;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
writer({
|
||||||
|
path: destination,
|
||||||
|
schema: artifactSchema,
|
||||||
|
value: { schemaVersion: 1, name: "first" },
|
||||||
|
}),
|
||||||
|
writer({
|
||||||
|
path: destination,
|
||||||
|
schema: artifactSchema,
|
||||||
|
value: { schemaVersion: 1, name: "second" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect([
|
||||||
|
{ schemaVersion: 1, name: "first" },
|
||||||
|
{ schemaVersion: 1, name: "second" },
|
||||||
|
]).toContainEqual(JSON.parse(await readFile(destination, "utf8")));
|
||||||
|
expect(await readdir(directory)).toEqual(["artifact.json"]);
|
||||||
|
});
|
||||||
|
|
||||||
it.each(["write", "rename"] as const)(
|
it.each(["write", "rename"] as const)(
|
||||||
"cleans only its owned sibling temp when %s fails",
|
"cleans only its owned sibling temp when %s fails",
|
||||||
async (failurePoint) => {
|
async (failurePoint) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user