@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/release-tail/README.md +8 -0
- package/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/internal-capabilities.json +65 -5
- package/architecture/maintainability-policy.json +27 -21
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +14 -14
- package/dist/site/buildchain-site.json +231 -24
- package/dist/site/capability-registry.json +11 -10
- package/dist/site/cli-registry.json +53 -1
- package/dist/site/kfd-claims.json +279 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -5
- package/dist/site/node-api-registry.json +11569 -9299
- package/dist/site/page-registry.json +214 -15
- package/dist/site/public-surface-audit.json +206 -11
- package/dist/site/publication-authority-registry.json +39 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +15 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +110 -8
- package/docs/MAP.md +3 -1
- package/docs/cli-reference.md +68 -0
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +22 -1
- package/docs/node-api-reference.md +332 -172
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +18 -3
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-state.js +31 -8
- package/packages/core/dev-delivery-warrant.js +8 -3
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +66 -11
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +138 -35
- package/packages/core/v4-floating-consumer-release-passport.js +30 -3
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/buildchain-cli-help.mjs +10 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +3 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +5 -1
- package/scripts/dev-pr-delivery-warrant.mjs +1 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +30 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +556 -29
- package/scripts/site-capability-metadata.mjs +17 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -159,8 +159,8 @@ function validateWorkflowRoute(name, route, expectedLogicalRef) {
|
|
|
159
159
|
if (!/^\.github\/workflows\/[.a-z0-9-]+\.ya?ml$/.test(route.workflowPath || "")) {
|
|
160
160
|
throw new Error(`promotion shell ${name} workflowPath must name a reusable workflow`);
|
|
161
161
|
}
|
|
162
|
-
if (!/^(?:v[0-9]+(?:-alpha)?|train\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
|
|
163
|
-
throw new Error(`promotion shell ${name} callRef must be an official channel ref, trusted train, or exact SHA`);
|
|
162
|
+
if (!/^(?:v[0-9]+(?:-alpha)?|(?:train|alpha|release)\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
|
|
163
|
+
throw new Error(`promotion shell ${name} callRef must be an official tag, protected channel ref, trusted train, or exact SHA`);
|
|
164
164
|
}
|
|
165
165
|
if (typeof route.forwardInternalInputs !== "boolean") {
|
|
166
166
|
throw new Error(`promotion shell ${name} forwardInternalInputs must be boolean`);
|
|
@@ -5,11 +5,160 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { createReleaseCandidatePassport, validateReleaseCandidatePassport } from "../packages/core/release-candidate.js";
|
|
7
7
|
import { scanV4FloatingConsumerPolicy, v4ConsumerPolicyScannerRoot } from "../packages/core/v4-floating-consumer-policy.js";
|
|
8
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
9
|
+
import { V4_STAGE_CAPSULE_CONTRACT, V4_STAGE_CAPSULE_IDENTITY_CONTRACT, v4StageCapsuleIdentityRoot, v4StageCapsuleRoot, validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
|
|
10
|
+
import { v4RuntimeResumeDocumentRoot } from "../packages/core/v4-runtime-ref-resume-authority.js";
|
|
8
11
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
9
12
|
|
|
10
13
|
const env = (name, fallback = "") => process.env[name] || fallback;
|
|
11
14
|
const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
12
15
|
|
|
16
|
+
export function createReleaseCandidateStageCapsules({
|
|
17
|
+
passport,
|
|
18
|
+
buildSummary,
|
|
19
|
+
coordinates,
|
|
20
|
+
}) {
|
|
21
|
+
if (!passport.consumerPolicy?.receiptRoot) return undefined;
|
|
22
|
+
if (
|
|
23
|
+
coordinates.repository !== passport.repository ||
|
|
24
|
+
String(coordinates.runId) !== String(passport.workflow.runId) ||
|
|
25
|
+
String(coordinates.runAttempt) !== String(passport.workflow.runAttempt) ||
|
|
26
|
+
coordinates.sourceSha !== passport.source.headSha
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Stage Capsule coordinate set does not bind the candidate run",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const coordinateByPlatform = new Map(
|
|
33
|
+
coordinates.artifacts.map((entry) => [entry.platformId, entry]),
|
|
34
|
+
);
|
|
35
|
+
const qualificationRoot = passport.controllerReceipts?.[0]?.receiptDigest;
|
|
36
|
+
const entries = passport.platformMatrix
|
|
37
|
+
.map((platform) => {
|
|
38
|
+
const coordinate = coordinateByPlatform.get(platform.platformId);
|
|
39
|
+
if (!coordinate || coordinate.name !== platform.artifactName) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Stage Capsule coordinate missing for ${platform.platformId}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const manifest = readJsonFile(platform.manifestPath);
|
|
45
|
+
const identity = {
|
|
46
|
+
schema: V4_STAGE_CAPSULE_IDENTITY_CONTRACT,
|
|
47
|
+
sourceRoot: v4ContentRoot("candidate-identity", passport.source),
|
|
48
|
+
platform: platform.platformId,
|
|
49
|
+
platformRoot: v4ContentRoot("candidate-identity", manifest.platform),
|
|
50
|
+
stage: "verify",
|
|
51
|
+
toolchainRoots: [],
|
|
52
|
+
runtimeRoot: v4ContentRoot("candidate-identity", {
|
|
53
|
+
sha: passport.buildchain.sha,
|
|
54
|
+
}),
|
|
55
|
+
policyRoot: passport.consumerPolicy.receiptRoot,
|
|
56
|
+
declaredInputs: [
|
|
57
|
+
{
|
|
58
|
+
name: "release-candidate",
|
|
59
|
+
root: `sha256:${passport.candidateHash}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
transformationRoot: v4ContentRoot("candidate-identity", {
|
|
63
|
+
lifecycle: manifest.lifecycle,
|
|
64
|
+
summary: platform.summary,
|
|
65
|
+
}),
|
|
66
|
+
outputManifestRoot: v4ContentRoot(
|
|
67
|
+
"stage-capsule-artifact-manifest",
|
|
68
|
+
{ artifact: coordinate, manifest },
|
|
69
|
+
),
|
|
70
|
+
qualificationRoot,
|
|
71
|
+
observationRoots: [
|
|
72
|
+
{
|
|
73
|
+
name: "provider-coordinate",
|
|
74
|
+
root: v4ContentRoot("provider-readback-sample", coordinate),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const capsule = {
|
|
79
|
+
schema: V4_STAGE_CAPSULE_CONTRACT,
|
|
80
|
+
writerAuthority: "typescript-v3",
|
|
81
|
+
rustAuthority: "validation-only",
|
|
82
|
+
identity,
|
|
83
|
+
identityRoot: v4StageCapsuleIdentityRoot(identity),
|
|
84
|
+
retentionPromise: {
|
|
85
|
+
class: "github-artifact",
|
|
86
|
+
retainUntil: coordinate.expiresAt,
|
|
87
|
+
},
|
|
88
|
+
capsuleRoot: `sha256:${"0".repeat(64)}`,
|
|
89
|
+
};
|
|
90
|
+
capsule.capsuleRoot = v4StageCapsuleRoot(capsule);
|
|
91
|
+
validateV4StageCapsule(capsule);
|
|
92
|
+
return {
|
|
93
|
+
platform: platform.platformId,
|
|
94
|
+
artifactName: coordinate.name,
|
|
95
|
+
artifactDigest: coordinate.digest,
|
|
96
|
+
artifact: coordinate,
|
|
97
|
+
capsule,
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.sort((left, right) => left.platform.localeCompare(right.platform));
|
|
101
|
+
const body = {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
contract: "kungfu-buildchain-v4-release-candidate-stage-capsules/v1",
|
|
104
|
+
status: "sealed",
|
|
105
|
+
repository: passport.repository,
|
|
106
|
+
buildAttempt: {
|
|
107
|
+
id: `github-run:${passport.workflow.runId}:attempt:${passport.workflow.runAttempt}`,
|
|
108
|
+
runtimeSha: passport.buildchain.sha,
|
|
109
|
+
},
|
|
110
|
+
source: {
|
|
111
|
+
sha: passport.source.headSha,
|
|
112
|
+
treeSha: passport.source.treeHash,
|
|
113
|
+
},
|
|
114
|
+
consumerPolicyReceiptRoot: passport.consumerPolicy.receiptRoot,
|
|
115
|
+
capsules: entries,
|
|
116
|
+
};
|
|
117
|
+
return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeReleaseCandidateStageCapsules({
|
|
121
|
+
passport,
|
|
122
|
+
buildSummary,
|
|
123
|
+
outputPath,
|
|
124
|
+
}) {
|
|
125
|
+
const coordinatesPath = path.resolve(
|
|
126
|
+
env(
|
|
127
|
+
"BUILDCHAIN_ARTIFACT_COORDINATES_PATH",
|
|
128
|
+
".buildchain/artifacts/artifact-coordinates.json",
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
if (!fs.existsSync(coordinatesPath)) {
|
|
132
|
+
if (passport.consumerPolicy?.receiptRoot) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"v4 release candidate Stage Capsules require exact artifact coordinates",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return { path: "", root: "" };
|
|
138
|
+
}
|
|
139
|
+
const stageCapsules = createReleaseCandidateStageCapsules({
|
|
140
|
+
passport,
|
|
141
|
+
buildSummary,
|
|
142
|
+
coordinates: readJsonFile(coordinatesPath),
|
|
143
|
+
});
|
|
144
|
+
if (!stageCapsules) return { path: "", root: "" };
|
|
145
|
+
const stageCapsulesPath = path.join(
|
|
146
|
+
path.dirname(outputPath),
|
|
147
|
+
"release-candidate-stage-capsules.json",
|
|
148
|
+
);
|
|
149
|
+
fs.writeFileSync(
|
|
150
|
+
stageCapsulesPath,
|
|
151
|
+
`${JSON.stringify(stageCapsules, null, 2)}\n`,
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
path: path
|
|
155
|
+
.relative(process.cwd(), stageCapsulesPath)
|
|
156
|
+
.split(path.sep)
|
|
157
|
+
.join("/"),
|
|
158
|
+
root: stageCapsules.root,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
13
162
|
export function resolveLegacyConsumerPolicyReceipt(options = {}) {
|
|
14
163
|
if (options.json) return { receipt: JSON.parse(options.json), receiptRoot: options.receiptRoot };
|
|
15
164
|
if (options.repository !== "kungfu-systems/buildchain" || options.targetChannel !== "alpha" || options.workflowShellRef !== "v4-alpha" || options.runtimeOverride !== true) return undefined;
|
|
@@ -80,6 +229,7 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
80
229
|
if (!validation.ok) throw new Error(`release candidate passport invalid: ${validation.errors.join("; ")}`);
|
|
81
230
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
82
231
|
fs.writeFileSync(outputPath, `${JSON.stringify(passport, null, 2)}\n`);
|
|
232
|
+
const stageCapsules = writeReleaseCandidateStageCapsules({ passport, buildSummary, outputPath });
|
|
83
233
|
writeGitHubOutputs({
|
|
84
234
|
"release-candidate-passport-path": path.relative(process.cwd(), outputPath).split(path.sep).join("/"),
|
|
85
235
|
"release-candidate-passport-json": JSON.stringify({
|
|
@@ -94,6 +244,8 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
94
244
|
consumerPolicy: passport.consumerPolicy,
|
|
95
245
|
controllerReceipts: passport.controllerReceipts || [],
|
|
96
246
|
}),
|
|
247
|
+
"release-candidate-stage-capsules-path": stageCapsules.path,
|
|
248
|
+
"release-candidate-stage-capsules-root": stageCapsules.root,
|
|
97
249
|
});
|
|
98
250
|
return passport;
|
|
99
251
|
}
|
|
@@ -54,6 +54,23 @@ const V4_COMPATIBILITY_FACTS_SCHEMA =
|
|
|
54
54
|
"schemas/v4-compatibility-facts-v1.schema.json";
|
|
55
55
|
const V4_COMPATIBILITY_FACTS_SCHEMA_SOURCE =
|
|
56
56
|
"contracts/v4-compatibility-facts-v1.schema.json";
|
|
57
|
+
const V4_PUBLICATION_REHEARSAL_SCHEMA =
|
|
58
|
+
"schemas/v4-publication-rehearsal-capsule-v1.schema.json";
|
|
59
|
+
const V4_PUBLICATION_REHEARSAL_SCHEMA_SOURCE =
|
|
60
|
+
"contracts/v4-publication-rehearsal-capsule-v1.schema.json";
|
|
61
|
+
const V4_ADOPTER_DELIVERY_SCHEMA =
|
|
62
|
+
"schemas/v4-adopter-delivery-v1.schema.json";
|
|
63
|
+
const V4_ADOPTER_DELIVERY_SCHEMA_SOURCE =
|
|
64
|
+
"contracts/v4-adopter-delivery-v1.schema.json";
|
|
65
|
+
const RELEASE_TAIL_CAPABILITIES_SCHEMA =
|
|
66
|
+
"schemas/release-tail-capabilities-v1.schema.json";
|
|
67
|
+
const RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE =
|
|
68
|
+
"contracts/release-tail-capabilities-v1.schema.json";
|
|
69
|
+
const PUBLICATION_REHEARSAL_SITE_SCHEMAS = [
|
|
70
|
+
V4_PUBLICATION_REHEARSAL_SCHEMA,
|
|
71
|
+
V4_ADOPTER_DELIVERY_SCHEMA,
|
|
72
|
+
RELEASE_TAIL_CAPABILITIES_SCHEMA,
|
|
73
|
+
];
|
|
57
74
|
const root = path.resolve(import.meta.dirname, "..");
|
|
58
75
|
const outputDir = path.join(root, "dist", "site");
|
|
59
76
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -84,6 +101,9 @@ function stableJson(value) {
|
|
|
84
101
|
function siteFileBytes(name, value) {
|
|
85
102
|
if (name === DEV_DELIVERY_AUTHORITY_SCHEMA) return readText(DEV_DELIVERY_AUTHORITY_SCHEMA_SOURCE);
|
|
86
103
|
if (name === V4_COMPATIBILITY_FACTS_SCHEMA) return readText(V4_COMPATIBILITY_FACTS_SCHEMA_SOURCE);
|
|
104
|
+
if (name === V4_PUBLICATION_REHEARSAL_SCHEMA) return readText(V4_PUBLICATION_REHEARSAL_SCHEMA_SOURCE);
|
|
105
|
+
if (name === V4_ADOPTER_DELIVERY_SCHEMA) return readText(V4_ADOPTER_DELIVERY_SCHEMA_SOURCE);
|
|
106
|
+
if (name === RELEASE_TAIL_CAPABILITIES_SCHEMA) return readText(RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE);
|
|
87
107
|
return stableJson(value);
|
|
88
108
|
}
|
|
89
109
|
|
|
@@ -396,6 +416,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
396
416
|
"observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
|
|
397
417
|
"engineering-housekeeper": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer", "agent"], maturity: "preview", order: 145 },
|
|
398
418
|
"reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
|
|
419
|
+
"v4-adopter-delivery": { capabilityGroup: "reusable-build", audience: ["consumer", "developer", "agent"], maturity: "preview", order: 205 },
|
|
399
420
|
"lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
|
|
400
421
|
"runtime-train-validation": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer"], maturity: "stable", order: 220 },
|
|
401
422
|
"kfd-support": { capabilityGroup: "kfd-trust", audience: ["agent", "maintainer"], maturity: "stable", order: 300 },
|
|
@@ -956,6 +977,7 @@ function buildSiteBundle() {
|
|
|
956
977
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
957
978
|
"schemas/dev-delivery-authority-v2.schema.json",
|
|
958
979
|
"schemas/v4-compatibility-facts-v1.schema.json",
|
|
980
|
+
...PUBLICATION_REHEARSAL_SITE_SCHEMAS,
|
|
959
981
|
"kfd-support.json",
|
|
960
982
|
"artifact-evidence.json",
|
|
961
983
|
"product-mechanism.json",
|
|
@@ -992,6 +1014,7 @@ function buildSiteBundle() {
|
|
|
992
1014
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
993
1015
|
"schemas/dev-delivery-authority-v2.schema.json",
|
|
994
1016
|
"schemas/v4-compatibility-facts-v1.schema.json",
|
|
1017
|
+
...PUBLICATION_REHEARSAL_SITE_SCHEMAS,
|
|
995
1018
|
"buildchain-contract.json",
|
|
996
1019
|
"kfd-claims.json",
|
|
997
1020
|
"product-mechanism.json",
|
|
@@ -1124,6 +1147,7 @@ function buildSiteBundle() {
|
|
|
1124
1147
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
1125
1148
|
"schemas/dev-delivery-authority-v2.schema.json",
|
|
1126
1149
|
"schemas/v4-compatibility-facts-v1.schema.json",
|
|
1150
|
+
...PUBLICATION_REHEARSAL_SITE_SCHEMAS,
|
|
1127
1151
|
"buildchain-contract.json",
|
|
1128
1152
|
"kfd-upstream-aggregate.json",
|
|
1129
1153
|
"kfd-claims.json",
|
|
@@ -1304,6 +1328,12 @@ function buildSiteBundle() {
|
|
|
1304
1328
|
[V4_COMPATIBILITY_FACTS_SCHEMA]: readJson(
|
|
1305
1329
|
V4_COMPATIBILITY_FACTS_SCHEMA_SOURCE,
|
|
1306
1330
|
),
|
|
1331
|
+
...Object.fromEntries(
|
|
1332
|
+
PUBLICATION_REHEARSAL_SITE_SCHEMAS.map((name) => [
|
|
1333
|
+
name,
|
|
1334
|
+
JSON.parse(siteFileBytes(name)),
|
|
1335
|
+
]),
|
|
1336
|
+
),
|
|
1307
1337
|
"badge-endpoint-registry.json": badgeEndpointRegistry,
|
|
1308
1338
|
"publication-registry.json": publicationRegistry,
|
|
1309
1339
|
"buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
|
|
@@ -66,8 +66,8 @@ export function resolvePromotionChannel({
|
|
|
66
66
|
}
|
|
67
67
|
const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
|
|
68
68
|
return {
|
|
69
|
-
targetRef: intent.targetRef,
|
|
70
|
-
|
|
69
|
+
targetRef: intent.targetRef, publicationChannel: intent.publicationChannel,
|
|
70
|
+
routerRef: normalized(routerRef), routerSha: normalized(routerSha).toLowerCase(),
|
|
71
71
|
channel: intent.shellChannel,
|
|
72
72
|
major: selected.major,
|
|
73
73
|
shellRef,
|
|
@@ -99,8 +99,8 @@ function readPackageVersion(cwd) {
|
|
|
99
99
|
|
|
100
100
|
function writeOutputs(file, result) {
|
|
101
101
|
const outputs = {
|
|
102
|
-
"target-ref": result.targetRef,
|
|
103
|
-
"
|
|
102
|
+
"target-ref": result.targetRef, "publication-channel": result.publicationChannel,
|
|
103
|
+
"router-ref": result.routerRef, "router-sha": result.routerSha,
|
|
104
104
|
channel: result.channel,
|
|
105
105
|
major: String(result.major),
|
|
106
106
|
"shell-ref": result.shellRef,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId = "" } = {}) {
|
|
4
|
+
if (Number(passport?.pullRequest?.number || 0)) return undefined;
|
|
5
|
+
if (passport?.target?.channel !== "anchor" || passport?.target?.ref !== "publish-gate/anchor") {
|
|
6
|
+
throw new Error("Release Candidate Passport has no PR identity");
|
|
7
|
+
}
|
|
8
|
+
let releaseManifest;
|
|
9
|
+
try {
|
|
10
|
+
releaseManifest = JSON.parse(buildSummary?.publishSource?.releaseManifest || "");
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error("anchored candidate release manifest is invalid");
|
|
13
|
+
}
|
|
14
|
+
const request = releaseManifest?.anchorRequest;
|
|
15
|
+
if (
|
|
16
|
+
releaseManifest?.sourceRef !== "publish-gate/anchor" ||
|
|
17
|
+
releaseManifest?.sourceSha !== passport.source?.headSha ||
|
|
18
|
+
request?.schema !== 1 ||
|
|
19
|
+
request?.contract !== "kungfu-buildchain-explicit-publish-anchor-request/v1" ||
|
|
20
|
+
!request.assignmentId ||
|
|
21
|
+
!/^native:[0-9a-f-]+$/u.test(String(request.attemptId || "")) ||
|
|
22
|
+
request.transactionId !== transactionId ||
|
|
23
|
+
request.source?.sha !== passport.source?.headSha ||
|
|
24
|
+
request.source?.tree !== passport.source?.treeHash ||
|
|
25
|
+
!/^\d+$/u.test(String(request.supersededCandidate?.workflowRunId || "")) ||
|
|
26
|
+
!/^[0-9a-f]{40}$/u.test(String(request.supersededCandidate?.sha || "")) ||
|
|
27
|
+
!/^sha256:[0-9a-f]{64}$/u.test(String(request.supersededCandidate?.root || "")) ||
|
|
28
|
+
request.runtime?.sha !== passport.buildchain?.sha
|
|
29
|
+
) {
|
|
30
|
+
throw new Error("anchored candidate request does not bind the sealed recovery identity");
|
|
31
|
+
}
|
|
32
|
+
return request;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRun(run, repository) {
|
|
36
|
+
return {
|
|
37
|
+
id: String(run.id), repository,
|
|
38
|
+
headRepository: run.head_repository?.full_name || "",
|
|
39
|
+
status: run.status, conclusion: run.conclusion,
|
|
40
|
+
event: run.event, path: run.path, name: run.name,
|
|
41
|
+
headSha: run.head_sha || "", headBranch: run.head_branch || "",
|
|
42
|
+
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCandidateRun(run, repository) {
|
|
47
|
+
return normalizeRun(run, repository);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeAnchorProvenance(anchorProvenance, repository) {
|
|
51
|
+
if (!anchorProvenance) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
request: anchorProvenance.request,
|
|
54
|
+
run: normalizeRun(anchorProvenance.run, repository),
|
|
55
|
+
workflow: {
|
|
56
|
+
path: anchorProvenance.workflow.path,
|
|
57
|
+
name: anchorProvenance.workflow.name,
|
|
58
|
+
state: anchorProvenance.workflow.state,
|
|
59
|
+
},
|
|
60
|
+
passport: anchorProvenance.passport,
|
|
61
|
+
buildSummary: anchorProvenance.buildSummary,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function recoverCandidateProvenance({
|
|
66
|
+
passport, buildSummary, transactionId, repoInfo, artifactName,
|
|
67
|
+
expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
|
|
68
|
+
fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
|
|
69
|
+
downloadArtifact, readOnlyJson,
|
|
70
|
+
}) {
|
|
71
|
+
const request = resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId });
|
|
72
|
+
if (!request) {
|
|
73
|
+
return { anchorProvenance: undefined, provenancePassport: passport };
|
|
74
|
+
}
|
|
75
|
+
const runId = String(request.supersededCandidate.workflowRunId);
|
|
76
|
+
const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
|
|
77
|
+
const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
|
|
78
|
+
const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
|
|
79
|
+
const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
|
|
80
|
+
if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("superseded candidate run has more than 100 artifacts; complete pagination is required before recovery");
|
|
81
|
+
const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
|
|
82
|
+
const provenanceRoot = path.join(bundleRoot, "anchored-provenance");
|
|
83
|
+
const passportDownload = await downloadArtifact({ artifact: selected.passport, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
84
|
+
const summaryDownload = await downloadArtifact({ artifact: selected.summary, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
85
|
+
const recoveredPassport = readOnlyJson(passportDownload.files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "superseded release-candidate-passport.json");
|
|
86
|
+
const recoveredBuildSummary = readOnlyJson(summaryDownload.files.filter((file) => path.basename(file.path) === "build-summary.json"), "superseded build-summary.json");
|
|
87
|
+
if (
|
|
88
|
+
String(run.id) !== runId ||
|
|
89
|
+
recoveredPassport.workflow?.runId !== runId ||
|
|
90
|
+
recoveredPassport.workflow?.name !== expectedWorkflowName ||
|
|
91
|
+
String(workflow.path || run.path || "").split("@")[0].replace(/^\.github\/workflows\//u, "") !== expectedWorkflowFile.replace(/^\.github\/workflows\//u, "") ||
|
|
92
|
+
recoveredPassport.target?.channel !== channel ||
|
|
93
|
+
recoveredPassport.source?.headSha !== request.supersededCandidate.sha ||
|
|
94
|
+
recoveredPassport.source?.treeHash !== request.source.tree ||
|
|
95
|
+
`sha256:${recoveredPassport.candidateHash}` !== request.supersededCandidate.root
|
|
96
|
+
) throw new Error("superseded candidate provenance does not match the anchored request");
|
|
97
|
+
const anchorProvenance = { request, run, workflow, passport: recoveredPassport, buildSummary: recoveredBuildSummary };
|
|
98
|
+
return { anchorProvenance, provenancePassport: recoveredPassport };
|
|
99
|
+
}
|
package/scripts/release-tail.mjs
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
writeReleaseTailTransaction,
|
|
12
12
|
} from "../packages/core/release-tail-provider-plane.js";
|
|
13
13
|
import { diagnoseLegacyReleaseTailHooks } from "../packages/core/release-tail-compatibility.js";
|
|
14
|
+
import {
|
|
15
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
16
|
+
executeV4PublicationRehearsal,
|
|
17
|
+
} from "../packages/core/v4-publication-rehearsal.js";
|
|
14
18
|
|
|
15
19
|
function flag(args, name, fallback = "") {
|
|
16
20
|
const index = args.indexOf(`--${name}`);
|
|
@@ -34,8 +38,52 @@ function output(value, filePath = "") {
|
|
|
34
38
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
export function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
41
|
+
export async function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
38
42
|
const [mode = "status", ...options] = args;
|
|
43
|
+
if (mode === "rehearse") {
|
|
44
|
+
const capsulePath = flag(options, "capsule");
|
|
45
|
+
const candidateRoot = flag(options, "candidate-root");
|
|
46
|
+
const statePath = flag(options, "state");
|
|
47
|
+
const evidencePath = flag(options, "evidence");
|
|
48
|
+
for (const [name, value] of Object.entries({
|
|
49
|
+
"--capsule": capsulePath,
|
|
50
|
+
"--candidate-root": candidateRoot,
|
|
51
|
+
"--state": statePath,
|
|
52
|
+
"--evidence": evidencePath,
|
|
53
|
+
}))
|
|
54
|
+
if (!value || !path.isAbsolute(value))
|
|
55
|
+
throw new Error(`${name} must be an explicit absolute path`);
|
|
56
|
+
const rehearsalMode = flag(options, "mode", "simulate");
|
|
57
|
+
if (!new Set(["simulate", "replay"]).has(rehearsalMode))
|
|
58
|
+
throw new Error(
|
|
59
|
+
"local rehearsal --mode must be simulate or replay; authorized provider rehearsal uses the reusable workflow",
|
|
60
|
+
);
|
|
61
|
+
try {
|
|
62
|
+
const result = await executeV4PublicationRehearsal({
|
|
63
|
+
capsule: readJson(capsulePath, "--capsule"),
|
|
64
|
+
candidateRoot,
|
|
65
|
+
mode: rehearsalMode,
|
|
66
|
+
checkpoint: (transaction) =>
|
|
67
|
+
writeReleaseTailTransaction(statePath, transaction),
|
|
68
|
+
});
|
|
69
|
+
writeReleaseTailTransaction(statePath, result.transaction);
|
|
70
|
+
output(result.evidence, evidencePath);
|
|
71
|
+
return result;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
output(
|
|
74
|
+
{
|
|
75
|
+
schema: V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
76
|
+
status: "rejected",
|
|
77
|
+
code: error.code || "publication-rehearsal-runtime-error",
|
|
78
|
+
message: error.message,
|
|
79
|
+
productionAuthority: false,
|
|
80
|
+
releasePassport: null,
|
|
81
|
+
},
|
|
82
|
+
evidencePath,
|
|
83
|
+
);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
39
87
|
if (mode === "plan") {
|
|
40
88
|
const plan = compileReleaseTailDeclaration(
|
|
41
89
|
readJson(flag(options, "declaration"), "--declaration"),
|
|
@@ -95,7 +143,7 @@ export function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
|
95
143
|
return report;
|
|
96
144
|
}
|
|
97
145
|
throw new Error(
|
|
98
|
-
"usage: buildchain release-tail <plan|init|status|verify|compat> [--declaration <json-or-path>] [--state <path>] [--hooks-json <json-or-path>] [--output <path>]",
|
|
146
|
+
"usage: buildchain release-tail <plan|init|status|verify|compat|rehearse> [--declaration <json-or-path>] [--capsule <absolute-path>] [--candidate-root <absolute-path>] [--mode <simulate|replay>] [--state <absolute-path>] [--evidence <absolute-path>] [--hooks-json <json-or-path>] [--output <path>]",
|
|
99
147
|
);
|
|
100
148
|
}
|
|
101
149
|
|
|
@@ -105,7 +153,7 @@ if (
|
|
|
105
153
|
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
106
154
|
) {
|
|
107
155
|
try {
|
|
108
|
-
runReleaseTailCli();
|
|
156
|
+
await runReleaseTailCli();
|
|
109
157
|
} catch (error) {
|
|
110
158
|
console.error(`release-tail: ${error.message}`);
|
|
111
159
|
process.exitCode = 1;
|