@kungfu-tech/buildchain 4.0.1-alpha.0 → 4.0.1-alpha.11

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 (228) hide show
  1. package/AGENTS.md +13 -5
  2. package/actions/promote-buildchain-ref/README.md +11 -6
  3. package/actions/release-tail/README.md +8 -0
  4. package/architecture/ci-lane-change-budget.json +187 -0
  5. package/architecture/decisions/0002-next-development-transition.md +101 -0
  6. package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
  7. package/architecture/internal-capabilities.json +219 -7
  8. package/architecture/maintainability-policy.json +246 -29
  9. package/architecture/release-tail-contract-inventory.json +5 -2
  10. package/architecture/v3-core-mechanism-inventory.json +91 -0
  11. package/architecture/v4-adopter-delivery-parity.json +32 -5
  12. package/architecture/v4-capability-state-machine-manifest.json +73 -0
  13. package/architecture/v4-compatibility-facts-parity.json +77 -0
  14. package/architecture/v4-delivery-authority-parity.json +250 -0
  15. package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
  16. package/architecture/v4-floating-consumer-policy.json +81 -0
  17. package/architecture/v4-floating-consumer-policy.md +32 -0
  18. package/architecture/v4-next-development-parity.json +192 -0
  19. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  20. package/architecture/v4-release-train-parity.json +1 -0
  21. package/architecture/v4-runtime-ref-resume-authority.json +64 -0
  22. package/architecture/v4-stage-capsule-qualification.json +2 -2
  23. package/architecture/v4-tail-reseal-parity.json +150 -0
  24. package/bin/buildchain.mjs +32 -3
  25. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  26. package/bin/internal/command-registry.mjs +3 -0
  27. package/bin/internal/compatibility-facts-cli.mjs +101 -0
  28. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  29. package/contracts/dev-delivery-authority-v2.schema.json +662 -0
  30. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  31. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  32. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  33. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  34. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  35. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  36. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  37. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  38. package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
  39. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +59 -0
  40. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  41. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  42. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  43. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  44. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  45. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  46. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  47. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  48. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  49. package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
  50. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
  51. package/contracts/next-development-request-v1.schema.json +66 -0
  52. package/contracts/next-development-transition-v1.schema.json +292 -0
  53. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  54. package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
  55. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +107 -0
  56. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  57. package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
  58. package/contracts/v4-tail-reseal-v1.schema.json +276 -0
  59. package/dist/site/agent-index.json +5 -0
  60. package/dist/site/artifact-schemas.json +10 -0
  61. package/dist/site/buildchain-contract.json +1562 -44
  62. package/dist/site/buildchain-site.json +684 -42
  63. package/dist/site/capability-registry.json +17 -14
  64. package/dist/site/cli-registry.json +205 -3
  65. package/dist/site/controller-registry.json +62 -6
  66. package/dist/site/kfd-claims.json +551 -22
  67. package/dist/site/kfd-upstream-aggregate.json +1 -1
  68. package/dist/site/manual-registry.json +55 -9
  69. package/dist/site/node-api-registry.json +15247 -8690
  70. package/dist/site/page-registry.json +647 -29
  71. package/dist/site/public-surface-audit.json +876 -55
  72. package/dist/site/publication-authority-registry.json +44 -8
  73. package/dist/site/publication-registry.json +4 -4
  74. package/dist/site/release-provenance.json +26 -0
  75. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
  76. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  77. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  78. package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
  79. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  80. package/dist/site/site-manifest.json +37 -13
  81. package/dist/site/workflow-registry.json +200 -22
  82. package/docs/MAP.md +10 -2
  83. package/docs/cli-reference.md +404 -13
  84. package/docs/cli.md +8 -0
  85. package/docs/dev-alpha-candidate-patrol.md +8 -0
  86. package/docs/dev-delivery-qualification-landing-adr.md +251 -0
  87. package/docs/dev-delivery-warrant.md +361 -30
  88. package/docs/lifecycle-protocol.md +41 -0
  89. package/docs/next-development-transition.md +118 -0
  90. package/docs/node-api-reference.md +748 -322
  91. package/docs/release-tail-provider-plane.md +30 -0
  92. package/docs/reusable-build-surface.md +41 -0
  93. package/docs/runtime-train-validation.md +10 -0
  94. package/docs/v4-adopter-delivery.md +133 -0
  95. package/docs/v4-compatibility-facts.md +91 -0
  96. package/docs/v4-publication-rehearsal.md +130 -0
  97. package/docs/v4-runtime-ref-resume-authority.md +69 -0
  98. package/docs/v4-tail-reseal.md +73 -0
  99. package/docs/versioning.md +4 -2
  100. package/package.json +36 -6
  101. package/packages/core/adopter-delivery-passport.js +259 -0
  102. package/packages/core/adopter-delivery-vectors.js +158 -0
  103. package/packages/core/adopter-delivery-vectors.json +77 -0
  104. package/packages/core/artifact-signing.js +61 -0
  105. package/packages/core/buildchain-agent-manuals.js +3 -0
  106. package/packages/core/buildchain-compatibility-authority.js +336 -0
  107. package/packages/core/buildchain-compatibility-fact.js +394 -0
  108. package/packages/core/buildchain-compatibility-facts.json +217 -0
  109. package/packages/core/buildchain-compatibility-proof.js +566 -0
  110. package/packages/core/buildchain-config.js +150 -1
  111. package/packages/core/buildchain-contract.js +96 -143
  112. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  113. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  114. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  115. package/packages/core/buildchain-publication-authority.js +1 -0
  116. package/packages/core/channel-candidate.js +8 -0
  117. package/packages/core/channel-promotion-baseline.js +55 -0
  118. package/packages/core/ci-lane-change-budget.js +247 -0
  119. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  120. package/packages/core/dev-delivery-authority-candidate.js +270 -0
  121. package/packages/core/dev-delivery-authority-evidence.js +146 -0
  122. package/packages/core/dev-delivery-authority-landing.js +461 -0
  123. package/packages/core/dev-delivery-authority-observation.js +48 -0
  124. package/packages/core/dev-delivery-authority-qualification.js +591 -0
  125. package/packages/core/dev-delivery-authority-settlement.js +213 -0
  126. package/packages/core/dev-delivery-authority-state.js +583 -0
  127. package/packages/core/dev-delivery-candidate-identity.js +13 -0
  128. package/packages/core/dev-delivery-contract-surface.js +76 -0
  129. package/packages/core/dev-delivery-execution-failure.js +133 -0
  130. package/packages/core/dev-delivery-execution-transfer.js +592 -0
  131. package/packages/core/dev-delivery-landing-admission-core.js +119 -0
  132. package/packages/core/dev-delivery-landing-readback.js +598 -0
  133. package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
  134. package/packages/core/dev-delivery-landing-testing-port.js +6 -0
  135. package/packages/core/dev-delivery-native-execution.js +110 -0
  136. package/packages/core/dev-delivery-native-proof.js +562 -0
  137. package/packages/core/dev-delivery-process-boundary.js +563 -0
  138. package/packages/core/dev-delivery-proof.js +37 -3
  139. package/packages/core/dev-delivery-provider-attempt.js +127 -0
  140. package/packages/core/dev-delivery-provider-heartbeat.js +382 -0
  141. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  142. package/packages/core/dev-delivery-warrant-qualification.js +145 -0
  143. package/packages/core/dev-delivery-warrant-settlement.js +237 -36
  144. package/packages/core/dev-delivery-warrant-state.js +587 -0
  145. package/packages/core/dev-delivery-warrant.js +328 -370
  146. package/packages/core/github-governance-authority.js +1 -0
  147. package/packages/core/index.js +7 -0
  148. package/packages/core/kungfu-temporal-fact.js +557 -0
  149. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  150. package/packages/core/next-development-candidate-reservation.js +186 -0
  151. package/packages/core/next-development-controller.js +728 -0
  152. package/packages/core/next-development-projection.js +287 -0
  153. package/packages/core/next-development-transition.js +738 -0
  154. package/packages/core/paper-agent-entry.js +11 -5
  155. package/packages/core/paper.js +16 -5
  156. package/packages/core/publication-authority.js +5 -5
  157. package/packages/core/published-delivery-authority.js +266 -0
  158. package/packages/core/release-candidate-recovery.js +66 -11
  159. package/packages/core/release-candidate.js +79 -19
  160. package/packages/core/release-passport.js +271 -38
  161. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  162. package/packages/core/v4-adopter-delivery.js +359 -0
  163. package/packages/core/v4-canonical-contracts.js +5 -0
  164. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  165. package/packages/core/v4-floating-consumer-evidence.js +338 -0
  166. package/packages/core/v4-floating-consumer-policy.js +569 -0
  167. package/packages/core/v4-floating-consumer-release-passport.js +153 -0
  168. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  169. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  170. package/packages/core/v4-publication-rehearsal.js +481 -0
  171. package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
  172. package/packages/core/v4-runtime-selector-persistence.js +228 -0
  173. package/packages/core/v4-tail-reseal-contract.js +26 -0
  174. package/packages/core/v4-tail-reseal-github.js +178 -0
  175. package/packages/core/v4-tail-reseal-receipt.js +225 -0
  176. package/packages/core/v4-tail-reseal.js +594 -0
  177. package/packages/core/workflow-yaml-contract.js +72 -2
  178. package/scripts/audit-publication-control-plane.mjs +31 -31
  179. package/scripts/buildchain-cli-help.mjs +41 -2
  180. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  181. package/scripts/check-inventory.mjs +14 -3
  182. package/scripts/check-v4-floating-consumer-policy-contract.mjs +217 -0
  183. package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
  184. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  185. package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
  186. package/scripts/dev-delivery-authority-provider.mjs +28 -0
  187. package/scripts/dev-delivery-authority.mjs +490 -0
  188. package/scripts/dev-delivery-native-run.mjs +177 -0
  189. package/scripts/dev-delivery-process-boundary.mjs +260 -0
  190. package/scripts/dev-delivery-proof.mjs +67 -2
  191. package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
  192. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  193. package/scripts/dev-delivery-source-proof-reuse.mjs +547 -0
  194. package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
  195. package/scripts/dev-delivery-two-phase.mjs +573 -0
  196. package/scripts/dev-delivery-warrant-options.mjs +266 -0
  197. package/scripts/dev-delivery-warrant-store.mjs +231 -0
  198. package/scripts/dev-delivery-warrant.mjs +232 -194
  199. package/scripts/dev-pr-auto-merge.mjs +37 -48
  200. package/scripts/dev-pr-delivery-warrant.mjs +216 -2
  201. package/scripts/dev-pr-prequeue-guard.mjs +399 -0
  202. package/scripts/dev-qualification-patrol.mjs +9 -9
  203. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  204. package/scripts/ensure-github-release.mjs +3 -3
  205. package/scripts/generate-channel-build-workflow.mjs +3 -0
  206. package/scripts/generate-channel-promotion-workflow.mjs +114 -14
  207. package/scripts/generate-next-development-guidance.mjs +49 -0
  208. package/scripts/generate-release-candidate-passport.mjs +192 -32
  209. package/scripts/generate-site-bundle.mjs +69 -4
  210. package/scripts/init-repo.mjs +26 -4
  211. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  212. package/scripts/next-development-self-dogfood-harness.mjs +460 -0
  213. package/scripts/next-development-self-dogfood.mjs +597 -0
  214. package/scripts/next-development-transition.mjs +47 -0
  215. package/scripts/npm-publish-transaction.mjs +103 -91
  216. package/scripts/promotion-channel-router.mjs +4 -4
  217. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  218. package/scripts/release-tail.mjs +51 -3
  219. package/scripts/release-train-self-dogfood.mjs +94 -92
  220. package/scripts/resume-from-candidate-run.mjs +563 -41
  221. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  222. package/scripts/site-capability-metadata.mjs +43 -0
  223. package/scripts/stable-candidate-qualification.mjs +9 -10
  224. package/scripts/v4-consumer-policy.mjs +231 -0
  225. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
  226. package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
  227. package/scripts/v4-tail-reseal.mjs +416 -0
  228. package/templates/native-dev-delivery.yml +143 -0
@@ -3,17 +3,19 @@ import crypto from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
6
7
  import { pathToFileURL } from "node:url";
7
8
 
8
9
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
+ import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
11
+ export { resolveAnchorRecoveryRequest } from "./release-candidate-anchor-provenance.mjs";
9
12
  import {
10
13
  generatePublishRequiredArtifacts,
11
- readNpmPackageArtifact,
12
- selectReleaseCandidateArtifacts,
13
- } from "./release-candidate-resolver.mjs";
14
- import {
15
14
  githubDownload,
16
15
  githubJson,
16
+ readNpmPackageArtifact,
17
+ selectPayloadArtifacts,
18
+ selectReleaseCandidateArtifacts,
17
19
  unzip,
18
20
  verifyArtifactArchive,
19
21
  } from "./release-candidate-resolver.mjs";
@@ -28,6 +30,15 @@ import {
28
30
  } from "../packages/core/publication-artifact-candidate.js";
29
31
  import { createPublicationSealedBundle } from "../packages/core/publication-sealed-bundle.js";
30
32
  import { releaseTransactionStateRef } from "../packages/core/publish-transaction.js";
33
+ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
34
+ import { validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
35
+ import {
36
+ authorizeV4RuntimeSelection,
37
+ createV4RuntimeResumeLineage,
38
+ scanV4RuntimeSelectorPersistence,
39
+ v4RuntimeResumeDocumentRoot,
40
+ verifyV4RuntimeAuthorizationReceipt,
41
+ } from "../packages/core/v4-runtime-ref-resume-authority.js";
31
42
 
32
43
  function env(name, fallback = "") {
33
44
  return process.env[name] || fallback;
@@ -100,8 +111,371 @@ function readOnlyJson(files, label) {
100
111
  return JSON.parse(fs.readFileSync(files[0].absolutePath, "utf8"));
101
112
  }
102
113
 
103
- async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version }) {
104
- const stateRef = releaseTransactionStateRef(version);
114
+ function trackedRuntimePersistenceScan() {
115
+ const paths = execFileSync(
116
+ "git",
117
+ ["ls-files", ".github/workflows", ".github/actions", ".buildchain"],
118
+ { encoding: "utf8" },
119
+ )
120
+ .split(/\r?\n/)
121
+ .filter((entry) => /\.(?:json|toml|ya?ml)$/u.test(entry));
122
+ return scanV4RuntimeSelectorPersistence({ root: process.cwd(), paths });
123
+ }
124
+
125
+ export function verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads }) {
126
+ if (
127
+ !sidecar ||
128
+ sidecar.contract !==
129
+ "kungfu-buildchain-v4-release-candidate-stage-capsules/v1" ||
130
+ sidecar.status !== "sealed"
131
+ ) {
132
+ throw new Error(
133
+ "cross-runtime recovery requires original sealed release-candidate Stage Capsules",
134
+ );
135
+ }
136
+ const payload = { ...sidecar };
137
+ delete payload.root;
138
+ const expectedAttempt =
139
+ `github-run:${passport.workflow.runId}:attempt:${passport.workflow.runAttempt}`;
140
+ if (
141
+ sidecar.root !== v4RuntimeResumeDocumentRoot(payload) ||
142
+ sidecar.repository !== passport.repository ||
143
+ sidecar.source?.sha !== passport.source.headSha ||
144
+ sidecar.source?.treeSha !== passport.source.treeHash ||
145
+ sidecar.buildAttempt?.id !== expectedAttempt ||
146
+ sidecar.buildAttempt?.runtimeSha !== passport.buildchain.sha ||
147
+ sidecar.consumerPolicyReceiptRoot !== passport.consumerPolicy?.receiptRoot
148
+ ) {
149
+ throw new Error("release-candidate Stage Capsule sidecar identity mismatch");
150
+ }
151
+ const artifactByName = new Map(downloads.map((entry) => [entry.artifact.name, entry]));
152
+ const required = [...passport.platformMatrix.map((entry) => entry.platformId)].sort();
153
+ const entries = [...(sidecar.capsules || [])].sort((left, right) =>
154
+ left.platform.localeCompare(right.platform),
155
+ );
156
+ if (
157
+ entries.length !== required.length ||
158
+ entries.some((entry, index) => entry.platform !== required[index])
159
+ ) {
160
+ throw new Error("release-candidate Stage Capsule platform set is incomplete");
161
+ }
162
+ return entries.map((entry) => {
163
+ validateV4StageCapsule(entry.capsule);
164
+ const download = artifactByName.get(entry.artifactName);
165
+ const artifact = entry.artifact;
166
+ if (
167
+ !download ||
168
+ artifact?.platformId !== entry.platform ||
169
+ artifact?.id !== String(download.artifact.id) ||
170
+ artifact?.name !== download.artifact.name ||
171
+ artifact?.digest !== entry.artifactDigest ||
172
+ artifact?.digest !== download.artifact.digest ||
173
+ artifact?.digest !== download.record.digest ||
174
+ artifact?.digest !== download.record.downloadedDigest ||
175
+ artifact?.expiresAt !==
176
+ new Date(download.artifact.expires_at).toISOString() ||
177
+ entry.capsule.identity.policyRoot !== sidecar.consumerPolicyReceiptRoot ||
178
+ entry.capsule.identity.sourceRoot !==
179
+ v4ContentRoot("candidate-identity", passport.source) ||
180
+ entry.capsule.identity.runtimeRoot !==
181
+ v4ContentRoot("candidate-identity", { sha: passport.buildchain.sha })
182
+ ) {
183
+ throw new Error(
184
+ `Stage Capsule ${entry.platform} does not bind the verified provider artifact`,
185
+ );
186
+ }
187
+ return {
188
+ platform: entry.platform,
189
+ capsuleRoot: entry.capsule.capsuleRoot,
190
+ identityRoot: entry.capsule.identityRoot,
191
+ artifactDigest: entry.artifactDigest,
192
+ sourceSha: sidecar.source.sha,
193
+ sourceTreeSha: sidecar.source.treeSha,
194
+ policyRoot: sidecar.consumerPolicyReceiptRoot,
195
+ buildRuntimeSha: sidecar.buildAttempt.runtimeSha,
196
+ sealed: true,
197
+ };
198
+ });
199
+ }
200
+
201
+ export function validateV4RuntimeResumePublicReadback({
202
+ targetRef,
203
+ targetSha,
204
+ alphaSha,
205
+ exactTagSha,
206
+ tagLineage,
207
+ runtimeSha,
208
+ version,
209
+ transaction,
210
+ main,
211
+ npm,
212
+ }) {
213
+ if (
214
+ transaction?.target_ref !== targetRef ||
215
+ transaction?.version !== version ||
216
+ exactTagSha !== transaction?.source_sha ||
217
+ !["ahead", "identical"].includes(tagLineage?.status) ||
218
+ alphaSha !== runtimeSha ||
219
+ !targetSha ||
220
+ npm["dist-tags"]?.alpha !== version ||
221
+ npm.versions?.[version]?.dist?.integrity !== main.digest
222
+ ) {
223
+ throw new Error(
224
+ "cross-runtime final public readback does not match durable publication bytes and runtime",
225
+ );
226
+ }
227
+ }
228
+
229
+ export async function readPublicResumeState({
230
+ repoInfo,
231
+ targetRef,
232
+ runtimeSha,
233
+ version,
234
+ transaction,
235
+ token,
236
+ apiUrl,
237
+ fetchImpl,
238
+ }) {
239
+ const readRef = async (ref) =>
240
+ (await githubJson({
241
+ apiUrl,
242
+ token,
243
+ fetchImpl,
244
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/ref/${ref}`,
245
+ })).object?.sha || "";
246
+ const main = transaction?.artifacts?.find((entry) => entry.kind === "npm" && entry.required !== false);
247
+ if (!main || main.ref !== version || !main.digest) {
248
+ throw new Error(
249
+ "cross-runtime recovery requires exact durable npm publication evidence",
250
+ );
251
+ }
252
+ const packageUrl =
253
+ `https://registry.npmjs.org/${encodeURIComponent(main.name)}`;
254
+ const exactTag = transaction.exact_tag || `v${version}`;
255
+ const [targetSha, alphaSha, exactTagSha, npmResponse] = await Promise.all([
256
+ readRef(`heads/${targetRef}`),
257
+ readRef("tags/v4-alpha"),
258
+ readRef(`tags/${exactTag}`),
259
+ fetchImpl(packageUrl),
260
+ ]);
261
+ if (!npmResponse.ok) {
262
+ throw new Error(`npm public readback failed with HTTP ${npmResponse.status}`);
263
+ }
264
+ const npm = await npmResponse.json();
265
+ const tagLineage = await githubJson({
266
+ apiUrl,
267
+ token,
268
+ fetchImpl,
269
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${exactTagSha}...${targetSha}`,
270
+ });
271
+ validateV4RuntimeResumePublicReadback({
272
+ targetRef,
273
+ targetSha,
274
+ alphaSha,
275
+ exactTagSha,
276
+ tagLineage,
277
+ runtimeSha,
278
+ version,
279
+ transaction,
280
+ main,
281
+ npm,
282
+ });
283
+ const body = {
284
+ schemaVersion: 1,
285
+ contract: "kungfu-buildchain-v4-runtime-resume-public-readback/v1",
286
+ observedAt: new Date().toISOString(),
287
+ repository: repoInfo.fullName,
288
+ version,
289
+ refs: {
290
+ target: { ref: targetRef, sha: targetSha },
291
+ floating: { ref: "v4-alpha", sha: alphaSha },
292
+ exactTag: { ref: exactTag, sha: exactTagSha },
293
+ },
294
+ npm: {
295
+ package: main.name,
296
+ version,
297
+ distTag: "alpha",
298
+ integrity: npm.versions[version].dist.integrity,
299
+ },
300
+ };
301
+ return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
302
+ }
303
+
304
+ function prepareRuntimeResumeEvidence({
305
+ repoInfo,
306
+ targetRef,
307
+ runtimeSha,
308
+ version,
309
+ passport,
310
+ sidecar,
311
+ stageCapsules,
312
+ recovery,
313
+ outputDir,
314
+ }) {
315
+ const authorizationPath = path.resolve(".buildchain/release-candidate/v4-runtime-authorization.json");
316
+ const delegatedRaw = env("BUILDCHAIN_RUNTIME_AUTHORIZATION_JSON").trim();
317
+ const delegatedRoot = env("BUILDCHAIN_RUNTIME_AUTHORIZATION_ROOT").trim();
318
+ const authorizationFileExists = fs.existsSync(authorizationPath);
319
+ if (!authorizationFileExists && (!delegatedRaw || !delegatedRoot)) throw new Error("cross-runtime recovery requires a fresh runtime authorization receipt");
320
+ const delegated = authorizationFileExists ? readOnlyJson([{ absolutePath: authorizationPath }], "runtime authorization") : JSON.parse(delegatedRaw);
321
+ if (!authorizationFileExists && delegated.receiptRoot !== delegatedRoot) throw new Error("fresh recovery runtime authorization handoff root mismatch");
322
+ const delegatedVerification = verifyV4RuntimeAuthorizationReceipt({
323
+ receipt: delegated.receipt,
324
+ receiptRoot: delegated.receiptRoot,
325
+ repository: repoInfo.fullName,
326
+ runtimeSha,
327
+ });
328
+ if (!delegatedVerification.ok) {
329
+ throw new Error(
330
+ `fresh recovery runtime authorization rejected: ${delegatedVerification.failures.join(", ")}`,
331
+ );
332
+ }
333
+ const policy = passport.consumerPolicy.receipt;
334
+ const authorization = authorizeV4RuntimeSelection({
335
+ repository: repoInfo.fullName,
336
+ eventName: "workflow_dispatch",
337
+ mode: "resume",
338
+ actor: delegated.receipt.actor.login,
339
+ actorPermission: delegated.receipt.actor.permission,
340
+ reason:
341
+ `resume sealed candidate run ${passport.workflow.runId} from a tree-equivalent protected source`,
342
+ authorizedAt: new Date().toISOString(),
343
+ sourceSha: sidecar.source.sha,
344
+ sourceTreeSha: sidecar.source.treeSha,
345
+ requestedRef: delegated.receipt.request.ref,
346
+ resolvedRuntimeSha: runtimeSha,
347
+ approvedRefReadbacks: delegated.receipt.runtime.reachableFrom,
348
+ stableContractLockRoot: policy.contractLocks.stable.root,
349
+ alphaContractLockRoot: policy.contractLocks.alpha.root,
350
+ consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
351
+ persistenceScan: trackedRuntimePersistenceScan(),
352
+ });
353
+ const planBody = {
354
+ schemaVersion: 1,
355
+ contract: "kungfu-buildchain-v4-runtime-resume-plan/v1",
356
+ originalRunId: String(passport.workflow.runId),
357
+ resumeRunId: String(env("GITHUB_RUN_ID")),
358
+ recoveryReceiptRoot: recovery.receipt.root,
359
+ requiredPlatforms: stageCapsules.map((entry) => entry.platform),
360
+ reusedCapsuleRoots: stageCapsules.map((entry) => entry.capsuleRoot),
361
+ rebuildPlatforms: [],
362
+ skippedBuildStages: recovery.receipt.skippedBuildStages,
363
+ };
364
+ const plan = { ...planBody, root: v4RuntimeResumeDocumentRoot(planBody) };
365
+ const materialBody = {
366
+ schemaVersion: 1,
367
+ contract: "kungfu-buildchain-v4-runtime-resume-material/v1",
368
+ repository: repoInfo.fullName,
369
+ targetRef,
370
+ runtimeSha,
371
+ version,
372
+ authorization: authorization.receipt,
373
+ authorizationRoot: authorization.receiptRoot,
374
+ buildAttempt: sidecar.buildAttempt,
375
+ resumeAttempt: {
376
+ id: `github-run:${env("GITHUB_RUN_ID")}:attempt:${env("GITHUB_RUN_ATTEMPT", "1")}`,
377
+ runtimeSha,
378
+ },
379
+ source: sidecar.source,
380
+ consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
381
+ requiredPlatforms: stageCapsules.map((entry) => entry.platform),
382
+ stageCapsules,
383
+ resumePlanRoot: plan.root,
384
+ floatingRefBefore: {
385
+ ref: "v4-alpha",
386
+ sha: sidecar.buildAttempt.runtimeSha,
387
+ },
388
+ };
389
+ const material = {
390
+ ...materialBody,
391
+ root: v4RuntimeResumeDocumentRoot(materialBody),
392
+ };
393
+ for (const [name, value] of [
394
+ ["v4-runtime-resume-plan.json", plan],
395
+ ["v4-runtime-resume-material.json", material],
396
+ ]) {
397
+ fs.writeFileSync(
398
+ path.join(outputDir, name),
399
+ `${JSON.stringify(value, null, 2)}\n`,
400
+ );
401
+ }
402
+ return path.join(outputDir, "v4-runtime-resume-evidence.json");
403
+ }
404
+
405
+ export async function finalizeRuntimeResumeEvidence({
406
+ materialPath,
407
+ transaction,
408
+ token = env("GITHUB_TOKEN", env("INPUT_TOKEN")),
409
+ apiUrl = env("GITHUB_API_URL", "https://api.github.com"),
410
+ fetchImpl = globalThis.fetch,
411
+ outputDir = path.dirname(materialPath),
412
+ }) {
413
+ const material = JSON.parse(fs.readFileSync(materialPath, "utf8"));
414
+ const body = { ...material };
415
+ delete body.root;
416
+ if (
417
+ material.contract !== "kungfu-buildchain-v4-runtime-resume-material/v1" ||
418
+ material.root !== v4RuntimeResumeDocumentRoot(body)
419
+ ) {
420
+ throw new Error("cross-runtime resume material root mismatch");
421
+ }
422
+ const repoInfo = splitRepository(material.repository);
423
+ const version = env("BUILDCHAIN_RELEASE_VERSION", material.version);
424
+ const targetRef = env("BUILDCHAIN_RELEASE_TARGET_REF", material.targetRef);
425
+ if (
426
+ version !== material.version ||
427
+ targetRef !== material.targetRef ||
428
+ transaction?.version !== version ||
429
+ transaction?.state !== "complete"
430
+ ) {
431
+ throw new Error(
432
+ "cross-runtime resume finalization does not match the completed publication transaction",
433
+ );
434
+ }
435
+ const readback = await readPublicResumeState({
436
+ repoInfo,
437
+ targetRef,
438
+ runtimeSha: material.runtimeSha,
439
+ version,
440
+ transaction,
441
+ token,
442
+ apiUrl,
443
+ fetchImpl,
444
+ });
445
+ const resumed = createV4RuntimeResumeLineage({
446
+ authorization: material.authorization,
447
+ authorizationRoot: material.authorizationRoot,
448
+ buildAttempt: material.buildAttempt,
449
+ resumeAttempt: material.resumeAttempt,
450
+ source: material.source,
451
+ consumerPolicyReceiptRoot: material.consumerPolicyReceiptRoot,
452
+ requiredPlatforms: material.requiredPlatforms,
453
+ stageCapsules: material.stageCapsules,
454
+ resumePlanRoot: material.resumePlanRoot,
455
+ finalPublicReadbackRoot: readback.root,
456
+ floatingRefBefore: material.floatingRefBefore,
457
+ floatingRefAfter: { ref: "v4-alpha", sha: material.runtimeSha },
458
+ });
459
+ const evidence = {
460
+ authorization: material.authorization,
461
+ authorizationRoot: material.authorizationRoot,
462
+ lineage: resumed.lineage,
463
+ lineageRoot: resumed.lineageRoot,
464
+ };
465
+ const evidencePath = path.join(outputDir, "v4-runtime-resume-evidence.json");
466
+ for (const [name, value] of [
467
+ ["v4-runtime-resume-public-readback.json", readback],
468
+ [path.basename(evidencePath), evidence],
469
+ ]) {
470
+ fs.writeFileSync(
471
+ path.join(outputDir, name),
472
+ `${JSON.stringify(value, null, 2)}\n`,
473
+ );
474
+ }
475
+ return { evidence, path: evidencePath, readback };
476
+ }
477
+
478
+ async function readTransactionAtRef({ repoInfo, apiUrl, token, fetchImpl, stateRef }) {
105
479
  const response = await githubJson({
106
480
  apiUrl,
107
481
  token,
@@ -110,10 +484,65 @@ async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, ver
110
484
  path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/state.json?ref=${encodeURIComponent(stateRef)}`,
111
485
  });
112
486
  if (!response) return undefined;
113
- if (response.type !== "file" || response.encoding !== "base64" || !response.content) {
114
- throw new Error(`durable transaction ${stateRef} did not expose a base64 state.json file`);
487
+ if (
488
+ response.type !== "file" ||
489
+ response.encoding !== "base64" ||
490
+ !response.content
491
+ ) {
492
+ throw new Error(
493
+ `durable transaction ${stateRef} did not expose a base64 state.json file`,
494
+ );
495
+ }
496
+ return JSON.parse(
497
+ Buffer.from(
498
+ String(response.content).replace(/\s/g, ""),
499
+ "base64",
500
+ ).toString("utf8"),
501
+ );
502
+ }
503
+
504
+ async function readExistingTransaction({
505
+ repoInfo,
506
+ apiUrl,
507
+ token,
508
+ fetchImpl,
509
+ version,
510
+ }) {
511
+ return readTransactionAtRef({
512
+ repoInfo,
513
+ apiUrl,
514
+ token,
515
+ fetchImpl,
516
+ stateRef: releaseTransactionStateRef(version),
517
+ });
518
+ }
519
+
520
+ async function readExistingTransactionById({
521
+ repoInfo,
522
+ apiUrl,
523
+ token,
524
+ fetchImpl,
525
+ transactionId,
526
+ }) {
527
+ if (!transactionId) return undefined;
528
+ const statePrefix = "buildchain/release-state/";
529
+ const refs = await githubJson({
530
+ apiUrl,
531
+ token,
532
+ fetchImpl,
533
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/matching-refs/heads/${statePrefix}`,
534
+ });
535
+ for (const ref of refs) {
536
+ const transaction = await readTransactionAtRef({
537
+ repoInfo,
538
+ apiUrl,
539
+ token,
540
+ fetchImpl,
541
+ stateRef: String(ref.ref).replace(/^refs\/heads\//, ""),
542
+ });
543
+ if (transaction?.id === transactionId) return transaction;
115
544
  }
116
- return JSON.parse(Buffer.from(String(response.content).replace(/\s/g, ""), "base64").toString("utf8"));
545
+ throw new Error(`durable publication transaction ${transactionId} was not found`);
117
546
  }
118
547
 
119
548
  function outputPath(filePath) {
@@ -156,7 +585,7 @@ async function downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir,
156
585
  };
157
586
  }
158
587
 
159
- function candidateArtifactNames({ passport, selected, artifacts, artifactPatterns }) {
588
+ export function candidateArtifactNames({ passport, selected, artifacts, artifactPatterns }) {
160
589
  const names = new Set([selected.passport.name, selected.summary.name]);
161
590
  for (const reference of passport.controllerReceipts || []) {
162
591
  if (!reference.artifact) throw new Error(`Passport controller receipt ${reference.controllerId} has no artifact identity`);
@@ -171,11 +600,9 @@ function candidateArtifactNames({ passport, selected, artifacts, artifactPattern
171
600
  for (const artifact of artifacts) {
172
601
  if (manifestPattern.test(String(artifact.name || ""))) names.add(artifact.name);
173
602
  }
174
- const matchers = splitPatterns(artifactPatterns).map(patternMatcher);
175
- for (const artifact of artifacts) {
176
- if (matchers.some((matcher) => matcher.test(String(artifact.name || "")))) names.add(artifact.name);
177
- }
178
- return names;
603
+ const publicationNames = new Set(selectPayloadArtifacts({ artifacts, artifactName: selected.prefix, sourceSha: selected.sourceSha, patterns: artifactPatterns }).map((artifact) => artifact.name));
604
+ for (const name of publicationNames) names.add(name);
605
+ return { names, publicationNames };
179
606
  }
180
607
 
181
608
  export function normalizePlatformManifests(downloads, passport) {
@@ -352,7 +779,15 @@ async function recoverCandidateEvidence({
352
779
  for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
353
780
  const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
354
781
  const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
355
- const requiredNames = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
782
+ const sidecarFiles = initialDownloads[0].files.filter(
783
+ (file) =>
784
+ path.basename(file.path) === "release-candidate-stage-capsules.json",
785
+ );
786
+ const stageCapsuleSidecar =
787
+ sidecarFiles.length === 1
788
+ ? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
789
+ : undefined;
790
+ const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
356
791
  const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
357
792
  if (chosen.length !== requiredNames.size) {
358
793
  const found = new Set(chosen.map((artifact) => artifact.name));
@@ -363,7 +798,7 @@ async function recoverCandidateEvidence({
363
798
  for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
364
799
  return {
365
800
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
366
- passport, buildSummary, chosen, downloads,
801
+ passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
367
802
  };
368
803
  }
369
804
 
@@ -373,6 +808,52 @@ async function resolveTargetAdvance({ observedTargetSha, targetSha, transactionI
373
808
  return { status: comparison.status, mergeIsAncestor: ["ahead", "identical"].includes(comparison.status) };
374
809
  }
375
810
 
811
+ async function resolveRecoveryTransaction({
812
+ repoInfo,
813
+ apiUrl,
814
+ token,
815
+ fetchImpl,
816
+ transactionId,
817
+ publicationVersion,
818
+ }) {
819
+ const byId = await readExistingTransactionById({
820
+ repoInfo,
821
+ apiUrl,
822
+ token,
823
+ fetchImpl,
824
+ transactionId,
825
+ });
826
+ const version = byId?.version || publicationVersion;
827
+ const transaction = byId || await readExistingTransaction({
828
+ repoInfo,
829
+ apiUrl,
830
+ token,
831
+ fetchImpl,
832
+ version,
833
+ });
834
+ return { version, transaction };
835
+ }
836
+
837
+ function resolveRecoveredStageCapsules({
838
+ candidateRuntimeSha,
839
+ runtimeSha,
840
+ sidecar,
841
+ passport,
842
+ downloads,
843
+ }) {
844
+ if (candidateRuntimeSha === runtimeSha) return [];
845
+ return verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads });
846
+ }
847
+
848
+ function validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot) {
849
+ const runId = String(candidateRunId || "").trim();
850
+ if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
851
+ if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
852
+ throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
853
+ }
854
+ return runId;
855
+ }
856
+
376
857
  export async function resumeFromCandidateRun({
377
858
  repository, targetRepository = repository, candidateRunId,
378
859
  expectedWorkflowFile, expectedWorkflowName,
@@ -387,21 +868,23 @@ export async function resumeFromCandidateRun({
387
868
  recoveryRunId = env("GITHUB_RUN_ID"), fetchImpl = globalThis.fetch,
388
869
  } = {}) {
389
870
  const repoInfo = splitRepository(repository);
390
- const runId = String(candidateRunId || "").trim();
391
- if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
392
- if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
393
- throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
394
- }
871
+ const runId = validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot);
395
872
  const archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-resume-"));
396
873
  try {
397
874
  const {
398
875
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
399
- passport, buildSummary, chosen, downloads,
876
+ passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
400
877
  } = await recoverCandidateEvidence({
401
878
  repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
402
879
  outputDir, apiUrl, token, fetchImpl, archiveDir,
403
880
  });
404
- const prNumber = Number(passport.pullRequest?.number || 0);
881
+ const { anchorProvenance, provenancePassport } = await recoverCandidateProvenance({
882
+ passport, buildSummary, transactionId, repoInfo, artifactName,
883
+ expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
884
+ fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
885
+ downloadArtifact, readOnlyJson,
886
+ });
887
+ const prNumber = Number(provenancePassport.pullRequest?.number || 0);
405
888
  if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
406
889
  const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
407
890
  const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
@@ -412,7 +895,7 @@ export async function resumeFromCandidateRun({
412
895
  const controllerReceipts = normalizeControllerReceipts(downloads, passport);
413
896
  const productPayloadManifests = normalizeProductPayloadManifests(downloads);
414
897
  const publication = createRecoveredPublication({
415
- downloads,
898
+ downloads: downloads.filter(({ artifact }) => publicationNames.has(artifact.name)),
416
899
  bundleRoot,
417
900
  repository: repoInfo.fullName,
418
901
  passport,
@@ -422,8 +905,24 @@ export async function resumeFromCandidateRun({
422
905
  releasePatterns,
423
906
  platformManifests: platformManifestEvidence.manifests,
424
907
  });
425
- const candidateVersion = publication.version;
426
- const existingTransaction = await readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version: candidateVersion });
908
+ const stageCapsules = resolveRecoveredStageCapsules({
909
+ candidateRuntimeSha,
910
+ runtimeSha,
911
+ sidecar: stageCapsuleSidecar,
912
+ passport,
913
+ downloads,
914
+ });
915
+ const {
916
+ version: candidateVersion,
917
+ transaction: existingTransaction,
918
+ } = await resolveRecoveryTransaction({
919
+ repoInfo,
920
+ apiUrl,
921
+ token,
922
+ fetchImpl,
923
+ transactionId,
924
+ publicationVersion: publication.version,
925
+ });
427
926
  const targetAdvance = await resolveTargetAdvance({
428
927
  observedTargetSha, targetSha, transactionId, existingTransaction,
429
928
  repoInfo, apiUrl, token, fetchImpl,
@@ -451,20 +950,9 @@ export async function resumeFromCandidateRun({
451
950
  expectedRuntimeSha: candidateRuntimeSha,
452
951
  expectedTransactionId: transactionId,
453
952
  existingTransaction,
454
- run: {
455
- id: String(run.id),
456
- repository: repoInfo.fullName,
457
- headRepository: run.head_repository?.full_name || "",
458
- status: run.status,
459
- conclusion: run.conclusion,
460
- event: run.event,
461
- path: run.path,
462
- name: run.name,
463
- headSha: run.head_sha || "",
464
- headBranch: run.head_branch || "",
465
- pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
466
- },
953
+ run: normalizeCandidateRun(run, repoInfo.fullName),
467
954
  workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
955
+ anchorProvenance: normalizeAnchorProvenance(anchorProvenance, repoInfo.fullName),
468
956
  pullRequest: {
469
957
  number: pullRequest.number,
470
958
  merged: pullRequest.merged === true,
@@ -494,6 +982,19 @@ export async function resumeFromCandidateRun({
494
982
  if (publication.manifest) fs.writeFileSync(sealedManifestPath, `${JSON.stringify(publication.manifest, null, 2)}\n`);
495
983
  const publishRequiredArtifacts = publication.publishRequiredArtifacts;
496
984
  fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(publishRequiredArtifacts, null, 2)}\n`);
985
+ const runtimeResumeEvidencePath = stageCapsules.length
986
+ ? prepareRuntimeResumeEvidence({
987
+ repoInfo,
988
+ targetRef,
989
+ runtimeSha,
990
+ version: candidateVersion,
991
+ passport,
992
+ sidecar: stageCapsuleSidecar,
993
+ stageCapsules,
994
+ recovery,
995
+ outputDir: resolvedOutput,
996
+ })
997
+ : "";
497
998
  const tarballs = publication.npmArtifacts.map((entry) => outputPath(entry.file.absolutePath));
498
999
  return {
499
1000
  enabled: true,
@@ -517,6 +1018,9 @@ export async function resumeFromCandidateRun({
517
1018
  sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
518
1019
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
519
1020
  recoveryReceipt: outputPath(recoveryReceiptPath),
1021
+ runtimeResumeEvidence: runtimeResumeEvidencePath
1022
+ ? outputPath(runtimeResumeEvidencePath)
1023
+ : "",
520
1024
  },
521
1025
  };
522
1026
  } finally {
@@ -568,6 +1072,10 @@ export async function resumeFromCandidateRunCli() {
568
1072
  "release-candidate-run-url": result.run.url,
569
1073
  "release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
570
1074
  "release-candidate-recovery-root": result.receipt.root,
1075
+ "v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
1076
+ "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
1077
+ ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
1078
+ : "",
571
1079
  "release-candidate-root": result.candidateRoot,
572
1080
  "release-candidate-artifact-root": result.artifactRoot,
573
1081
  "publish-sealed-bundle-root": result.paths.sealedBundleRoot,
@@ -589,7 +1097,21 @@ export async function resumeFromCandidateRunCli() {
589
1097
  }
590
1098
 
591
1099
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
592
- resumeFromCandidateRunCli().catch((error) => {
1100
+ const operation = process.argv[2] === "finalize"
1101
+ ? finalizeRuntimeResumeEvidence({
1102
+ materialPath: requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL"),
1103
+ transaction: JSON.parse(
1104
+ requiredEnv("BUILDCHAIN_RELEASE_TRANSACTION_JSON"),
1105
+ ),
1106
+ outputDir: env(
1107
+ "BUILDCHAIN_RELEASE_PASSPORT_OUTPUT_DIR",
1108
+ path.dirname(requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL")),
1109
+ ),
1110
+ }).then((result) =>
1111
+ console.log(JSON.stringify({ files: [outputPath(result.path)] })),
1112
+ )
1113
+ : resumeFromCandidateRunCli();
1114
+ operation.catch((error) => {
593
1115
  const failure = error.recoveryFailure || recoveryFailure(error);
594
1116
  console.error(`candidate recovery rejected [${failure.code}]: ${failure.reason}`);
595
1117
  console.error(`next action: ${failure.nextAction}`);