@kungfu-tech/buildchain 4.0.1-alpha.4 → 4.0.1-alpha.41

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