@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/actions/release-tail/README.md +8 -0
  2. package/architecture/ci-lane-change-budget.json +187 -0
  3. package/architecture/internal-capabilities.json +65 -5
  4. package/architecture/maintainability-policy.json +27 -21
  5. package/architecture/release-tail-contract-inventory.json +5 -2
  6. package/architecture/v4-adopter-delivery-parity.json +32 -5
  7. package/architecture/v4-floating-consumer-policy.json +6 -0
  8. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  9. package/bin/buildchain.mjs +7 -1
  10. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  11. package/bin/internal/command-registry.mjs +1 -0
  12. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  13. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  14. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  15. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  16. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  17. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  18. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  19. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  20. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  21. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  22. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  23. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  24. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  25. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  26. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  27. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  28. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  29. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  30. package/dist/site/agent-index.json +3 -0
  31. package/dist/site/artifact-schemas.json +6 -0
  32. package/dist/site/buildchain-contract.json +14 -14
  33. package/dist/site/buildchain-site.json +231 -24
  34. package/dist/site/capability-registry.json +11 -10
  35. package/dist/site/cli-registry.json +53 -1
  36. package/dist/site/kfd-claims.json +279 -14
  37. package/dist/site/kfd-upstream-aggregate.json +1 -1
  38. package/dist/site/manual-registry.json +20 -5
  39. package/dist/site/node-api-registry.json +11569 -9299
  40. package/dist/site/page-registry.json +214 -15
  41. package/dist/site/public-surface-audit.json +206 -11
  42. package/dist/site/publication-authority-registry.json +39 -8
  43. package/dist/site/publication-registry.json +4 -4
  44. package/dist/site/release-provenance.json +15 -0
  45. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  46. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  47. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  48. package/dist/site/site-manifest.json +17 -9
  49. package/dist/site/workflow-registry.json +110 -8
  50. package/docs/MAP.md +3 -1
  51. package/docs/cli-reference.md +68 -0
  52. package/docs/cli.md +8 -0
  53. package/docs/dev-delivery-warrant.md +22 -1
  54. package/docs/node-api-reference.md +332 -172
  55. package/docs/release-tail-provider-plane.md +30 -0
  56. package/docs/v4-adopter-delivery.md +133 -0
  57. package/docs/v4-publication-rehearsal.md +130 -0
  58. package/package.json +18 -3
  59. package/packages/core/adopter-delivery-passport.js +259 -0
  60. package/packages/core/adopter-delivery-vectors.js +158 -0
  61. package/packages/core/adopter-delivery-vectors.json +77 -0
  62. package/packages/core/buildchain-agent-manuals.js +1 -0
  63. package/packages/core/buildchain-config.js +84 -0
  64. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  65. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  66. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  67. package/packages/core/buildchain-publication-authority.js +1 -0
  68. package/packages/core/ci-lane-change-budget.js +247 -0
  69. package/packages/core/dev-delivery-native-proof.js +43 -27
  70. package/packages/core/dev-delivery-warrant-state.js +31 -8
  71. package/packages/core/dev-delivery-warrant.js +8 -3
  72. package/packages/core/github-governance-authority.js +1 -0
  73. package/packages/core/index.js +5 -0
  74. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  75. package/packages/core/published-delivery-authority.js +266 -0
  76. package/packages/core/release-candidate-recovery.js +66 -11
  77. package/packages/core/release-candidate.js +2 -2
  78. package/packages/core/release-passport.js +36 -9
  79. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  80. package/packages/core/v4-adopter-delivery.js +359 -0
  81. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  82. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  83. package/packages/core/v4-floating-consumer-policy.js +138 -35
  84. package/packages/core/v4-floating-consumer-release-passport.js +30 -3
  85. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  86. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  87. package/packages/core/v4-publication-rehearsal.js +481 -0
  88. package/packages/core/workflow-yaml-contract.js +24 -2
  89. package/scripts/buildchain-cli-help.mjs +10 -0
  90. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  91. package/scripts/check-inventory.mjs +3 -3
  92. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  93. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  94. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  95. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  96. package/scripts/dev-delivery-warrant.mjs +5 -1
  97. package/scripts/dev-pr-delivery-warrant.mjs +1 -0
  98. package/scripts/dev-qualification-patrol.mjs +9 -9
  99. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  100. package/scripts/generate-release-candidate-passport.mjs +152 -0
  101. package/scripts/generate-site-bundle.mjs +30 -0
  102. package/scripts/promotion-channel-router.mjs +4 -4
  103. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  104. package/scripts/release-tail.mjs +51 -3
  105. package/scripts/resume-from-candidate-run.mjs +556 -29
  106. package/scripts/site-capability-metadata.mjs +17 -0
  107. package/scripts/v4-consumer-policy.mjs +4 -0
  108. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
@@ -3,9 +3,12 @@ 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
14
  githubDownload,
@@ -27,6 +30,15 @@ import {
27
30
  } from "../packages/core/publication-artifact-candidate.js";
28
31
  import { createPublicationSealedBundle } from "../packages/core/publication-sealed-bundle.js";
29
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";
30
42
 
31
43
  function env(name, fallback = "") {
32
44
  return process.env[name] || fallback;
@@ -99,8 +111,373 @@ function readOnlyJson(files, label) {
99
111
  return JSON.parse(fs.readFileSync(files[0].absolutePath, "utf8"));
100
112
  }
101
113
 
102
- async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version }) {
103
- 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(
316
+ ".buildchain/release-candidate/v4-runtime-authorization.json",
317
+ );
318
+ if (!fs.existsSync(authorizationPath)) {
319
+ throw new Error(
320
+ "cross-runtime recovery requires a fresh runtime authorization receipt",
321
+ );
322
+ }
323
+ const delegated = readOnlyJson([{ absolutePath: authorizationPath }], "runtime authorization");
324
+ const delegatedVerification = verifyV4RuntimeAuthorizationReceipt({
325
+ receipt: delegated.receipt,
326
+ receiptRoot: delegated.receiptRoot,
327
+ repository: repoInfo.fullName,
328
+ runtimeSha,
329
+ });
330
+ if (!delegatedVerification.ok) {
331
+ throw new Error(
332
+ `fresh recovery runtime authorization rejected: ${delegatedVerification.failures.join(", ")}`,
333
+ );
334
+ }
335
+ const policy = passport.consumerPolicy.receipt;
336
+ const authorization = authorizeV4RuntimeSelection({
337
+ repository: repoInfo.fullName,
338
+ eventName: "workflow_dispatch",
339
+ mode: "resume",
340
+ actor: delegated.receipt.actor.login,
341
+ actorPermission: delegated.receipt.actor.permission,
342
+ reason:
343
+ `resume sealed candidate run ${passport.workflow.runId} from a tree-equivalent protected source`,
344
+ authorizedAt: new Date().toISOString(),
345
+ sourceSha: sidecar.source.sha,
346
+ sourceTreeSha: sidecar.source.treeSha,
347
+ requestedRef: delegated.receipt.request.ref,
348
+ resolvedRuntimeSha: runtimeSha,
349
+ approvedRefReadbacks: delegated.receipt.runtime.reachableFrom,
350
+ stableContractLockRoot: policy.contractLocks.stable.root,
351
+ alphaContractLockRoot: policy.contractLocks.alpha.root,
352
+ consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
353
+ persistenceScan: trackedRuntimePersistenceScan(),
354
+ });
355
+ const planBody = {
356
+ schemaVersion: 1,
357
+ contract: "kungfu-buildchain-v4-runtime-resume-plan/v1",
358
+ originalRunId: String(passport.workflow.runId),
359
+ resumeRunId: String(env("GITHUB_RUN_ID")),
360
+ recoveryReceiptRoot: recovery.receipt.root,
361
+ requiredPlatforms: stageCapsules.map((entry) => entry.platform),
362
+ reusedCapsuleRoots: stageCapsules.map((entry) => entry.capsuleRoot),
363
+ rebuildPlatforms: [],
364
+ skippedBuildStages: recovery.receipt.skippedBuildStages,
365
+ };
366
+ const plan = { ...planBody, root: v4RuntimeResumeDocumentRoot(planBody) };
367
+ const materialBody = {
368
+ schemaVersion: 1,
369
+ contract: "kungfu-buildchain-v4-runtime-resume-material/v1",
370
+ repository: repoInfo.fullName,
371
+ targetRef,
372
+ runtimeSha,
373
+ version,
374
+ authorization: authorization.receipt,
375
+ authorizationRoot: authorization.receiptRoot,
376
+ buildAttempt: sidecar.buildAttempt,
377
+ resumeAttempt: {
378
+ id: `github-run:${env("GITHUB_RUN_ID")}:attempt:${env("GITHUB_RUN_ATTEMPT", "1")}`,
379
+ runtimeSha,
380
+ },
381
+ source: sidecar.source,
382
+ consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
383
+ requiredPlatforms: stageCapsules.map((entry) => entry.platform),
384
+ stageCapsules,
385
+ resumePlanRoot: plan.root,
386
+ floatingRefBefore: {
387
+ ref: "v4-alpha",
388
+ sha: sidecar.buildAttempt.runtimeSha,
389
+ },
390
+ };
391
+ const material = {
392
+ ...materialBody,
393
+ root: v4RuntimeResumeDocumentRoot(materialBody),
394
+ };
395
+ for (const [name, value] of [
396
+ ["v4-runtime-resume-plan.json", plan],
397
+ ["v4-runtime-resume-material.json", material],
398
+ ]) {
399
+ fs.writeFileSync(
400
+ path.join(outputDir, name),
401
+ `${JSON.stringify(value, null, 2)}\n`,
402
+ );
403
+ }
404
+ return path.join(outputDir, "v4-runtime-resume-evidence.json");
405
+ }
406
+
407
+ export async function finalizeRuntimeResumeEvidence({
408
+ materialPath,
409
+ transaction,
410
+ token = env("GITHUB_TOKEN", env("INPUT_TOKEN")),
411
+ apiUrl = env("GITHUB_API_URL", "https://api.github.com"),
412
+ fetchImpl = globalThis.fetch,
413
+ outputDir = path.dirname(materialPath),
414
+ }) {
415
+ const material = JSON.parse(fs.readFileSync(materialPath, "utf8"));
416
+ const body = { ...material };
417
+ delete body.root;
418
+ if (
419
+ material.contract !== "kungfu-buildchain-v4-runtime-resume-material/v1" ||
420
+ material.root !== v4RuntimeResumeDocumentRoot(body)
421
+ ) {
422
+ throw new Error("cross-runtime resume material root mismatch");
423
+ }
424
+ const repoInfo = splitRepository(material.repository);
425
+ const version = env("BUILDCHAIN_RELEASE_VERSION", material.version);
426
+ const targetRef = env("BUILDCHAIN_RELEASE_TARGET_REF", material.targetRef);
427
+ if (
428
+ version !== material.version ||
429
+ targetRef !== material.targetRef ||
430
+ transaction?.version !== version ||
431
+ transaction?.state !== "complete"
432
+ ) {
433
+ throw new Error(
434
+ "cross-runtime resume finalization does not match the completed publication transaction",
435
+ );
436
+ }
437
+ const readback = await readPublicResumeState({
438
+ repoInfo,
439
+ targetRef,
440
+ runtimeSha: material.runtimeSha,
441
+ version,
442
+ transaction,
443
+ token,
444
+ apiUrl,
445
+ fetchImpl,
446
+ });
447
+ const resumed = createV4RuntimeResumeLineage({
448
+ authorization: material.authorization,
449
+ authorizationRoot: material.authorizationRoot,
450
+ buildAttempt: material.buildAttempt,
451
+ resumeAttempt: material.resumeAttempt,
452
+ source: material.source,
453
+ consumerPolicyReceiptRoot: material.consumerPolicyReceiptRoot,
454
+ requiredPlatforms: material.requiredPlatforms,
455
+ stageCapsules: material.stageCapsules,
456
+ resumePlanRoot: material.resumePlanRoot,
457
+ finalPublicReadbackRoot: readback.root,
458
+ floatingRefBefore: material.floatingRefBefore,
459
+ floatingRefAfter: { ref: "v4-alpha", sha: material.runtimeSha },
460
+ });
461
+ const evidence = {
462
+ authorization: material.authorization,
463
+ authorizationRoot: material.authorizationRoot,
464
+ lineage: resumed.lineage,
465
+ lineageRoot: resumed.lineageRoot,
466
+ };
467
+ const evidencePath = path.join(outputDir, "v4-runtime-resume-evidence.json");
468
+ for (const [name, value] of [
469
+ ["v4-runtime-resume-public-readback.json", readback],
470
+ [path.basename(evidencePath), evidence],
471
+ ]) {
472
+ fs.writeFileSync(
473
+ path.join(outputDir, name),
474
+ `${JSON.stringify(value, null, 2)}\n`,
475
+ );
476
+ }
477
+ return { evidence, path: evidencePath, readback };
478
+ }
479
+
480
+ async function readTransactionAtRef({ repoInfo, apiUrl, token, fetchImpl, stateRef }) {
104
481
  const response = await githubJson({
105
482
  apiUrl,
106
483
  token,
@@ -109,10 +486,65 @@ async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, ver
109
486
  path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/state.json?ref=${encodeURIComponent(stateRef)}`,
110
487
  });
111
488
  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`);
489
+ if (
490
+ response.type !== "file" ||
491
+ response.encoding !== "base64" ||
492
+ !response.content
493
+ ) {
494
+ throw new Error(
495
+ `durable transaction ${stateRef} did not expose a base64 state.json file`,
496
+ );
497
+ }
498
+ return JSON.parse(
499
+ Buffer.from(
500
+ String(response.content).replace(/\s/g, ""),
501
+ "base64",
502
+ ).toString("utf8"),
503
+ );
504
+ }
505
+
506
+ async function readExistingTransaction({
507
+ repoInfo,
508
+ apiUrl,
509
+ token,
510
+ fetchImpl,
511
+ version,
512
+ }) {
513
+ return readTransactionAtRef({
514
+ repoInfo,
515
+ apiUrl,
516
+ token,
517
+ fetchImpl,
518
+ stateRef: releaseTransactionStateRef(version),
519
+ });
520
+ }
521
+
522
+ async function readExistingTransactionById({
523
+ repoInfo,
524
+ apiUrl,
525
+ token,
526
+ fetchImpl,
527
+ transactionId,
528
+ }) {
529
+ if (!transactionId) return undefined;
530
+ const statePrefix = "buildchain/release-state/";
531
+ const refs = await githubJson({
532
+ apiUrl,
533
+ token,
534
+ fetchImpl,
535
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/matching-refs/heads/${statePrefix}`,
536
+ });
537
+ for (const ref of refs) {
538
+ const transaction = await readTransactionAtRef({
539
+ repoInfo,
540
+ apiUrl,
541
+ token,
542
+ fetchImpl,
543
+ stateRef: String(ref.ref).replace(/^refs\/heads\//, ""),
544
+ });
545
+ if (transaction?.id === transactionId) return transaction;
114
546
  }
115
- return JSON.parse(Buffer.from(String(response.content).replace(/\s/g, ""), "base64").toString("utf8"));
547
+ throw new Error(`durable publication transaction ${transactionId} was not found`);
116
548
  }
117
549
 
118
550
  function outputPath(filePath) {
@@ -349,6 +781,14 @@ async function recoverCandidateEvidence({
349
781
  for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
350
782
  const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
351
783
  const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
784
+ const sidecarFiles = initialDownloads[0].files.filter(
785
+ (file) =>
786
+ path.basename(file.path) === "release-candidate-stage-capsules.json",
787
+ );
788
+ const stageCapsuleSidecar =
789
+ sidecarFiles.length === 1
790
+ ? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
791
+ : undefined;
352
792
  const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
353
793
  const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
354
794
  if (chosen.length !== requiredNames.size) {
@@ -360,7 +800,7 @@ async function recoverCandidateEvidence({
360
800
  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
801
  return {
362
802
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
363
- passport, buildSummary, chosen, downloads, publicationNames,
803
+ passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
364
804
  };
365
805
  }
366
806
 
@@ -370,6 +810,52 @@ async function resolveTargetAdvance({ observedTargetSha, targetSha, transactionI
370
810
  return { status: comparison.status, mergeIsAncestor: ["ahead", "identical"].includes(comparison.status) };
371
811
  }
372
812
 
813
+ async function resolveRecoveryTransaction({
814
+ repoInfo,
815
+ apiUrl,
816
+ token,
817
+ fetchImpl,
818
+ transactionId,
819
+ publicationVersion,
820
+ }) {
821
+ const byId = await readExistingTransactionById({
822
+ repoInfo,
823
+ apiUrl,
824
+ token,
825
+ fetchImpl,
826
+ transactionId,
827
+ });
828
+ const version = byId?.version || publicationVersion;
829
+ const transaction = byId || await readExistingTransaction({
830
+ repoInfo,
831
+ apiUrl,
832
+ token,
833
+ fetchImpl,
834
+ version,
835
+ });
836
+ return { version, transaction };
837
+ }
838
+
839
+ function resolveRecoveredStageCapsules({
840
+ candidateRuntimeSha,
841
+ runtimeSha,
842
+ sidecar,
843
+ passport,
844
+ downloads,
845
+ }) {
846
+ if (candidateRuntimeSha === runtimeSha) return [];
847
+ return verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads });
848
+ }
849
+
850
+ function validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot) {
851
+ const runId = String(candidateRunId || "").trim();
852
+ if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
853
+ if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
854
+ throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
855
+ }
856
+ return runId;
857
+ }
858
+
373
859
  export async function resumeFromCandidateRun({
374
860
  repository, targetRepository = repository, candidateRunId,
375
861
  expectedWorkflowFile, expectedWorkflowName,
@@ -384,21 +870,23 @@ export async function resumeFromCandidateRun({
384
870
  recoveryRunId = env("GITHUB_RUN_ID"), fetchImpl = globalThis.fetch,
385
871
  } = {}) {
386
872
  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
- }
873
+ const runId = validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot);
392
874
  const archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-resume-"));
393
875
  try {
394
876
  const {
395
877
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
396
- passport, buildSummary, chosen, downloads, publicationNames,
878
+ passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
397
879
  } = await recoverCandidateEvidence({
398
880
  repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
399
881
  outputDir, apiUrl, token, fetchImpl, archiveDir,
400
882
  });
401
- const prNumber = Number(passport.pullRequest?.number || 0);
883
+ const { anchorProvenance, provenancePassport } = await recoverCandidateProvenance({
884
+ passport, buildSummary, transactionId, repoInfo, artifactName,
885
+ expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
886
+ fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
887
+ downloadArtifact, readOnlyJson,
888
+ });
889
+ const prNumber = Number(provenancePassport.pullRequest?.number || 0);
402
890
  if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
403
891
  const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
404
892
  const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
@@ -419,8 +907,24 @@ export async function resumeFromCandidateRun({
419
907
  releasePatterns,
420
908
  platformManifests: platformManifestEvidence.manifests,
421
909
  });
422
- const candidateVersion = publication.version;
423
- const existingTransaction = await readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version: candidateVersion });
910
+ const stageCapsules = resolveRecoveredStageCapsules({
911
+ candidateRuntimeSha,
912
+ runtimeSha,
913
+ sidecar: stageCapsuleSidecar,
914
+ passport,
915
+ downloads,
916
+ });
917
+ const {
918
+ version: candidateVersion,
919
+ transaction: existingTransaction,
920
+ } = await resolveRecoveryTransaction({
921
+ repoInfo,
922
+ apiUrl,
923
+ token,
924
+ fetchImpl,
925
+ transactionId,
926
+ publicationVersion: publication.version,
927
+ });
424
928
  const targetAdvance = await resolveTargetAdvance({
425
929
  observedTargetSha, targetSha, transactionId, existingTransaction,
426
930
  repoInfo, apiUrl, token, fetchImpl,
@@ -448,20 +952,9 @@ export async function resumeFromCandidateRun({
448
952
  expectedRuntimeSha: candidateRuntimeSha,
449
953
  expectedTransactionId: transactionId,
450
954
  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
- },
955
+ run: normalizeCandidateRun(run, repoInfo.fullName),
464
956
  workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
957
+ anchorProvenance: normalizeAnchorProvenance(anchorProvenance, repoInfo.fullName),
465
958
  pullRequest: {
466
959
  number: pullRequest.number,
467
960
  merged: pullRequest.merged === true,
@@ -491,6 +984,19 @@ export async function resumeFromCandidateRun({
491
984
  if (publication.manifest) fs.writeFileSync(sealedManifestPath, `${JSON.stringify(publication.manifest, null, 2)}\n`);
492
985
  const publishRequiredArtifacts = publication.publishRequiredArtifacts;
493
986
  fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(publishRequiredArtifacts, null, 2)}\n`);
987
+ const runtimeResumeEvidencePath = stageCapsules.length
988
+ ? prepareRuntimeResumeEvidence({
989
+ repoInfo,
990
+ targetRef,
991
+ runtimeSha,
992
+ version: candidateVersion,
993
+ passport,
994
+ sidecar: stageCapsuleSidecar,
995
+ stageCapsules,
996
+ recovery,
997
+ outputDir: resolvedOutput,
998
+ })
999
+ : "";
494
1000
  const tarballs = publication.npmArtifacts.map((entry) => outputPath(entry.file.absolutePath));
495
1001
  return {
496
1002
  enabled: true,
@@ -514,6 +1020,9 @@ export async function resumeFromCandidateRun({
514
1020
  sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
515
1021
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
516
1022
  recoveryReceipt: outputPath(recoveryReceiptPath),
1023
+ runtimeResumeEvidence: runtimeResumeEvidencePath
1024
+ ? outputPath(runtimeResumeEvidencePath)
1025
+ : "",
517
1026
  },
518
1027
  };
519
1028
  } finally {
@@ -565,6 +1074,10 @@ export async function resumeFromCandidateRunCli() {
565
1074
  "release-candidate-run-url": result.run.url,
566
1075
  "release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
567
1076
  "release-candidate-recovery-root": result.receipt.root,
1077
+ "v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
1078
+ "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
1079
+ ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
1080
+ : "",
568
1081
  "release-candidate-root": result.candidateRoot,
569
1082
  "release-candidate-artifact-root": result.artifactRoot,
570
1083
  "publish-sealed-bundle-root": result.paths.sealedBundleRoot,
@@ -586,7 +1099,21 @@ export async function resumeFromCandidateRunCli() {
586
1099
  }
587
1100
 
588
1101
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
589
- resumeFromCandidateRunCli().catch((error) => {
1102
+ const operation = process.argv[2] === "finalize"
1103
+ ? finalizeRuntimeResumeEvidence({
1104
+ materialPath: requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL"),
1105
+ transaction: JSON.parse(
1106
+ requiredEnv("BUILDCHAIN_RELEASE_TRANSACTION_JSON"),
1107
+ ),
1108
+ outputDir: env(
1109
+ "BUILDCHAIN_RELEASE_PASSPORT_OUTPUT_DIR",
1110
+ path.dirname(requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL")),
1111
+ ),
1112
+ }).then((result) =>
1113
+ console.log(JSON.stringify({ files: [outputPath(result.path)] })),
1114
+ )
1115
+ : resumeFromCandidateRunCli();
1116
+ operation.catch((error) => {
590
1117
  const failure = error.recoveryFailure || recoveryFailure(error);
591
1118
  console.error(`candidate recovery rejected [${failure.code}]: ${failure.reason}`);
592
1119
  console.error(`next action: ${failure.nextAction}`);