@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.
Files changed (108) hide show
  1. package/actions/release-tail/README.md +8 -0
  2. package/architecture/ci-lane-change-budget.json +187 -0
  3. package/architecture/internal-capabilities.json +65 -5
  4. package/architecture/maintainability-policy.json +27 -21
  5. package/architecture/release-tail-contract-inventory.json +5 -2
  6. package/architecture/v4-adopter-delivery-parity.json +32 -5
  7. package/architecture/v4-floating-consumer-policy.json +6 -0
  8. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  9. package/bin/buildchain.mjs +7 -1
  10. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  11. package/bin/internal/command-registry.mjs +1 -0
  12. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  13. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  14. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  15. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  16. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  17. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  18. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  19. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  20. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  21. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  22. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  23. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  24. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  25. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  26. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  27. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  28. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  29. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  30. package/dist/site/agent-index.json +3 -0
  31. package/dist/site/artifact-schemas.json +6 -0
  32. package/dist/site/buildchain-contract.json +14 -14
  33. package/dist/site/buildchain-site.json +231 -24
  34. package/dist/site/capability-registry.json +11 -10
  35. package/dist/site/cli-registry.json +53 -1
  36. package/dist/site/kfd-claims.json +279 -14
  37. package/dist/site/kfd-upstream-aggregate.json +1 -1
  38. package/dist/site/manual-registry.json +20 -5
  39. package/dist/site/node-api-registry.json +11569 -9299
  40. package/dist/site/page-registry.json +214 -15
  41. package/dist/site/public-surface-audit.json +206 -11
  42. package/dist/site/publication-authority-registry.json +39 -8
  43. package/dist/site/publication-registry.json +4 -4
  44. package/dist/site/release-provenance.json +15 -0
  45. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  46. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  47. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  48. package/dist/site/site-manifest.json +17 -9
  49. package/dist/site/workflow-registry.json +110 -8
  50. package/docs/MAP.md +3 -1
  51. package/docs/cli-reference.md +68 -0
  52. package/docs/cli.md +8 -0
  53. package/docs/dev-delivery-warrant.md +22 -1
  54. package/docs/node-api-reference.md +332 -172
  55. package/docs/release-tail-provider-plane.md +30 -0
  56. package/docs/v4-adopter-delivery.md +133 -0
  57. package/docs/v4-publication-rehearsal.md +130 -0
  58. package/package.json +18 -3
  59. package/packages/core/adopter-delivery-passport.js +259 -0
  60. package/packages/core/adopter-delivery-vectors.js +158 -0
  61. package/packages/core/adopter-delivery-vectors.json +77 -0
  62. package/packages/core/buildchain-agent-manuals.js +1 -0
  63. package/packages/core/buildchain-config.js +84 -0
  64. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  65. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  66. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  67. package/packages/core/buildchain-publication-authority.js +1 -0
  68. package/packages/core/ci-lane-change-budget.js +247 -0
  69. package/packages/core/dev-delivery-native-proof.js +43 -27
  70. package/packages/core/dev-delivery-warrant-state.js +31 -8
  71. package/packages/core/dev-delivery-warrant.js +8 -3
  72. package/packages/core/github-governance-authority.js +1 -0
  73. package/packages/core/index.js +5 -0
  74. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  75. package/packages/core/published-delivery-authority.js +266 -0
  76. package/packages/core/release-candidate-recovery.js +66 -11
  77. package/packages/core/release-candidate.js +2 -2
  78. package/packages/core/release-passport.js +36 -9
  79. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  80. package/packages/core/v4-adopter-delivery.js +359 -0
  81. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  82. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  83. package/packages/core/v4-floating-consumer-policy.js +138 -35
  84. package/packages/core/v4-floating-consumer-release-passport.js +30 -3
  85. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  86. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  87. package/packages/core/v4-publication-rehearsal.js +481 -0
  88. package/packages/core/workflow-yaml-contract.js +24 -2
  89. package/scripts/buildchain-cli-help.mjs +10 -0
  90. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  91. package/scripts/check-inventory.mjs +3 -3
  92. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  93. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  94. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  95. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  96. package/scripts/dev-delivery-warrant.mjs +5 -1
  97. package/scripts/dev-pr-delivery-warrant.mjs +1 -0
  98. package/scripts/dev-qualification-patrol.mjs +9 -9
  99. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  100. package/scripts/generate-release-candidate-passport.mjs +152 -0
  101. package/scripts/generate-site-bundle.mjs +30 -0
  102. package/scripts/promotion-channel-router.mjs +4 -4
  103. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  104. package/scripts/release-tail.mjs +51 -3
  105. package/scripts/resume-from-candidate-run.mjs +556 -29
  106. package/scripts/site-capability-metadata.mjs +17 -0
  107. package/scripts/v4-consumer-policy.mjs +4 -0
  108. 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
- publicationChannel: intent.publicationChannel,
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
- "publication-channel": result.publicationChannel,
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
+ }
@@ -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;