@kungfu-tech/buildchain 3.0.6-alpha.0 → 3.0.6-alpha.10

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 (83) hide show
  1. package/README.md +4 -4
  2. package/actions/promote-buildchain-ref/README.md +13 -1
  3. package/bin/buildchain.mjs +13 -1
  4. package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
  5. package/contracts/release-candidate-recovery-v1.schema.json +104 -0
  6. package/dist/site/buildchain-contract.json +134 -34
  7. package/dist/site/buildchain-site.json +178 -47
  8. package/dist/site/capability-registry.json +5 -5
  9. package/dist/site/cli-registry.json +40 -4
  10. package/dist/site/controller-registry.json +60 -4
  11. package/dist/site/kfd-claims.json +190 -20
  12. package/dist/site/kfd-upstream-aggregate.json +9 -9
  13. package/dist/site/manual-registry.json +10 -10
  14. package/dist/site/node-api-registry.json +1295 -80
  15. package/dist/site/page-registry.json +164 -33
  16. package/dist/site/public-surface-audit.json +428 -21
  17. package/dist/site/publication-authority-registry.json +66 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/release-provenance.json +2 -0
  20. package/dist/site/site-manifest.json +14 -14
  21. package/dist/site/workflow-registry.json +203 -15
  22. package/docs/MAP.md +2 -0
  23. package/docs/auditable-demo.md +58 -11
  24. package/docs/aws-us-elastic-runner-burst-plane.md +114 -80
  25. package/docs/cli-reference.md +154 -0
  26. package/docs/dev-alpha-candidate-patrol.md +13 -5
  27. package/docs/dev-delivery-warrant.md +158 -0
  28. package/docs/node-api-reference.md +105 -44
  29. package/docs/publication-authority.md +11 -0
  30. package/docs/publish-transaction.md +10 -1
  31. package/docs/release-candidate.md +140 -12
  32. package/docs/release-governance.md +66 -1
  33. package/docs/reusable-build-surface.md +11 -1
  34. package/docs/shifu-gate-profiles.md +12 -1
  35. package/docs/versioning.md +2 -0
  36. package/package.json +5 -2
  37. package/packages/core/buildchain-contract.js +30 -2
  38. package/packages/core/buildchain-publication-authority.js +3 -1
  39. package/packages/core/channel-candidate.js +2 -21
  40. package/packages/core/channel-promotion-baseline.js +199 -0
  41. package/packages/core/dev-delivery-candidate-identity.js +94 -0
  42. package/packages/core/dev-delivery-common.js +73 -0
  43. package/packages/core/dev-delivery-proof.js +252 -0
  44. package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
  45. package/packages/core/dev-delivery-warrant-settlement.js +73 -0
  46. package/packages/core/dev-delivery-warrant.js +591 -0
  47. package/packages/core/index.js +7 -0
  48. package/packages/core/publication-sealed-bundle.js +10 -1
  49. package/packages/core/release-candidate-recovery.js +539 -0
  50. package/scripts/audit-publication-control-plane.mjs +12 -4
  51. package/scripts/auditable-demo-bundle-verification.mjs +148 -0
  52. package/scripts/auditable-demo-platform.mjs +86 -50
  53. package/scripts/auditable-demo-presentation.mjs +83 -0
  54. package/scripts/auditable-demo-renditions.mjs +264 -0
  55. package/scripts/auditable-demo.mjs +24 -30
  56. package/scripts/aws-windows-jit-campaign-core.mjs +7 -8
  57. package/scripts/aws-windows-jit-controller.mjs +1 -0
  58. package/scripts/aws-windows-jit-core.mjs +1 -1
  59. package/scripts/build-contract-core.mjs +58 -3
  60. package/scripts/buildchain-cli-help.mjs +8 -0
  61. package/scripts/buildchain-patrol.mjs +9 -0
  62. package/scripts/check-inventory.mjs +25 -3
  63. package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
  64. package/scripts/dev-delivery-proof.mjs +193 -0
  65. package/scripts/dev-delivery-warrant.mjs +426 -0
  66. package/scripts/dev-pr-auto-merge.mjs +497 -55
  67. package/scripts/dev-pr-delivery-warrant.mjs +209 -0
  68. package/scripts/dispatch-artifact-signing-authority.mjs +2 -4
  69. package/scripts/gate-profile-core.mjs +24 -0
  70. package/scripts/generate-channel-promotion-workflow.mjs +30 -14
  71. package/scripts/generate-site-bundle.mjs +2 -2
  72. package/scripts/git-fetch-process-tree.mjs +142 -0
  73. package/scripts/lifecycle-substage-evidence.mjs +274 -0
  74. package/scripts/locked-source-checkout.mjs +6 -3
  75. package/scripts/publication-candidate-sealer.mjs +104 -0
  76. package/scripts/release-candidate-resolver.mjs +91 -5
  77. package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
  78. package/scripts/resolve-build-contract.mjs +7 -0
  79. package/scripts/resume-from-candidate-run.mjs +598 -0
  80. package/scripts/route-offline-runners.mjs +1 -0
  81. package/scripts/run-lifecycle-core.mjs +9 -9
  82. package/scripts/shifu-gate-profile.mjs +10 -16
  83. package/scripts/site-capability-metadata.mjs +15 -0
@@ -0,0 +1,539 @@
1
+ import {
2
+ createControllerReceiptReference,
3
+ validateControllerReceipt,
4
+ } from "./controller-evidence.js";
5
+ import {
6
+ sha256Json,
7
+ validateReleaseCandidatePassport,
8
+ } from "./release-candidate.js";
9
+ import { releaseTransactionId } from "./publish-transaction.js";
10
+ import {
11
+ publicationAuthorityDigest,
12
+ publicationGateAggregateBindings,
13
+ } from "./publication-authority.js";
14
+
15
+ export const RELEASE_CANDIDATE_RECOVERY_CONTRACT =
16
+ "kungfu-buildchain-release-candidate-recovery/v1";
17
+
18
+ export function rebindPublicationGateAggregateForEquivalentTree(
19
+ gateAggregate,
20
+ { evidenceSourceSha = "", targetSourceSha = "", targetSourceTreeSha = "", expectedSourceTreeSha = "" } = {},
21
+ ) {
22
+ const bindings = publicationGateAggregateBindings(gateAggregate);
23
+ const evidenceSource = exactSha(evidenceSourceSha, "evidenceSourceSha");
24
+ const targetSource = exactSha(targetSourceSha, "targetSourceSha");
25
+ if (bindings.sourceSha !== evidenceSource) {
26
+ throw new Error("consumer Gate aggregate evidence source SHA mismatch");
27
+ }
28
+ if (evidenceSource === targetSource) return gateAggregate;
29
+ const targetTree = exactSha(targetSourceTreeSha, "targetSourceTreeSha");
30
+ const expectedTree = exactSha(expectedSourceTreeSha, "expectedSourceTreeSha");
31
+ if (targetTree !== expectedTree) throw new Error("consumer Gate aggregate source tree mismatch");
32
+ if (gateAggregate.candidateReuse !== undefined) {
33
+ throw new Error("consumer Gate aggregate already declares candidate reuse");
34
+ }
35
+ const { digest: _digest, ...payload } = gateAggregate;
36
+ const rebound = {
37
+ ...payload,
38
+ sourceSha: targetSource,
39
+ candidateReuse: { action: "reused", evidenceSourceSha: evidenceSource, sourceTreeSha: expectedTree },
40
+ };
41
+ return { ...rebound, digest: `sha256:${publicationAuthorityDigest(rebound)}` };
42
+ }
43
+
44
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
45
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
46
+ const TRUSTED_ASSOCIATIONS = new Set(["COLLABORATOR", "MEMBER", "OWNER"]);
47
+ const RESUMABLE_TRANSACTION_STATES = new Set([
48
+ "sealed",
49
+ "publishing",
50
+ "published",
51
+ "package-published",
52
+ "finalizing",
53
+ "complete",
54
+ ]);
55
+
56
+ export class ReleaseCandidateRecoveryError extends Error {
57
+ constructor(code, message, nextAction) {
58
+ super(message);
59
+ this.name = "ReleaseCandidateRecoveryError";
60
+ this.code = code;
61
+ this.nextAction = nextAction;
62
+ }
63
+ }
64
+
65
+ function fail(code, message, nextAction) {
66
+ throw new ReleaseCandidateRecoveryError(code, message, nextAction);
67
+ }
68
+
69
+ function required(value, label, code = "invalid-recovery-input") {
70
+ const normalized = String(value || "").trim();
71
+ if (!normalized) fail(code, `${label} is required`, "Correct the recovery dispatch inputs and run a fresh recovery event.");
72
+ return normalized;
73
+ }
74
+
75
+ function exactSha(value, label) {
76
+ const normalized = required(value, label).toLowerCase();
77
+ if (!SHA_PATTERN.test(normalized)) {
78
+ fail("invalid-recovery-input", `${label} must be a 40-character Git SHA`, "Supply the exact immutable Git SHA.");
79
+ }
80
+ return normalized;
81
+ }
82
+
83
+ function contentRoot(value, label) {
84
+ const normalized = required(value, label).toLowerCase();
85
+ if (!SHA256_PATTERN.test(normalized)) {
86
+ fail("invalid-recovery-input", `${label} must be a sha256 content root`, "Supply the exact sha256 root from the sealed candidate evidence.");
87
+ }
88
+ return normalized;
89
+ }
90
+
91
+ function normalizeArtifact(record, index) {
92
+ const name = required(record?.name, `artifacts[${index}].name`);
93
+ if (record?.expired === true) {
94
+ fail("artifact-expired", `candidate artifact is expired: ${name}`, "Create a new candidate explicitly; recovery never falls back to rebuilding.");
95
+ }
96
+ if (record?.missing === true) {
97
+ fail("artifact-missing", `candidate artifact is missing: ${name}`, "Restore the immutable artifact if possible or explicitly create a new candidate.");
98
+ }
99
+ const size = Number(record?.size);
100
+ const downloadedSize = Number(record?.downloadedSize);
101
+ if (!Number.isSafeInteger(size) || size < 0 || !Number.isSafeInteger(downloadedSize) || downloadedSize < 0) {
102
+ fail("artifact-metadata-invalid", `candidate artifact size is invalid: ${name}`, "Inspect the candidate run artifact metadata and retry with a complete artifact set.");
103
+ }
104
+ const digest = contentRoot(record?.digest, `artifacts[${index}].digest`);
105
+ const downloadedDigest = contentRoot(record?.downloadedDigest, `artifacts[${index}].downloadedDigest`);
106
+ if (size !== downloadedSize || digest !== downloadedDigest) {
107
+ fail("artifact-digest-mismatch", `candidate artifact archive differs from GitHub metadata: ${name}`, "Do not publish; preserve the run and investigate artifact corruption or replacement.");
108
+ }
109
+ const files = (record?.files || []).map((file, fileIndex) => {
110
+ const fileSize = Number(file?.size);
111
+ if (!Number.isSafeInteger(fileSize) || fileSize < 0) {
112
+ fail("artifact-manifest-mismatch", `${name} file ${fileIndex} has an invalid size`, "Inspect the platform manifest and product payload bytes.");
113
+ }
114
+ return {
115
+ path: required(file?.path, `${name}.files[${fileIndex}].path`),
116
+ size: fileSize,
117
+ sha256: contentRoot(file?.sha256, `${name}.files[${fileIndex}].sha256`),
118
+ };
119
+ }).sort((left, right) => left.path.localeCompare(right.path));
120
+ if (new Set(files.map((file) => file.path)).size !== files.length) {
121
+ fail("artifact-manifest-mismatch", `candidate artifact contains duplicate file paths: ${name}`, "Regenerate a valid candidate explicitly.");
122
+ }
123
+ return { name, size, digest, files, kind: String(record?.kind || "payload") };
124
+ }
125
+
126
+ function assertEqual(actual, expected, code, label, nextAction) {
127
+ if (actual !== expected) fail(code, `${label} mismatch: expected ${expected}, got ${actual || "<empty>"}`, nextAction);
128
+ }
129
+
130
+ function normalizedRef(value) {
131
+ return String(value || "").replace(/^refs\/heads\//, "").trim();
132
+ }
133
+
134
+ export function validateRecoveryTargetRef({
135
+ targetSha,
136
+ observedTargetSha,
137
+ expectedTransactionId = "",
138
+ existingTransaction = undefined,
139
+ ancestry = undefined,
140
+ } = {}) {
141
+ const expected = exactSha(targetSha, "targetSha");
142
+ const observed = exactSha(observedTargetSha, "observedTargetSha");
143
+ if (observed === expected) return { advanced: false, observedSha: observed };
144
+ if (!expectedTransactionId) {
145
+ fail(
146
+ "target-ref-moved",
147
+ `target ref advanced from ${expected} to ${observed} without an explicit durable transaction identity`,
148
+ "Resume with the exact existing transaction identity, or select a new candidate explicitly; never rebuild implicitly.",
149
+ );
150
+ }
151
+ if (!existingTransaction || existingTransaction.id !== expectedTransactionId) {
152
+ fail(
153
+ "transaction-identity-conflict",
154
+ `target ref advanced but durable transaction ${expectedTransactionId} was not found exactly`,
155
+ "Enter repair_required and inspect the durable transaction before any retry.",
156
+ );
157
+ }
158
+ const state = String(existingTransaction.state || existingTransaction.publication_state || "");
159
+ if (!RESUMABLE_TRANSACTION_STATES.has(state)) {
160
+ fail(
161
+ "transaction-state-conflict",
162
+ `target ref advanced while durable transaction ${expectedTransactionId} is in non-resumable state ${state || "<empty>"}`,
163
+ "Preserve the transaction evidence and enter repair_required; do not republish or rebuild automatically.",
164
+ );
165
+ }
166
+ if (ancestry?.mergeIsAncestor !== true || !["ahead", "identical"].includes(String(ancestry?.status || ""))) {
167
+ fail(
168
+ "target-ancestry-mismatch",
169
+ `observed target ${observed} is not an ancestry-bound continuation of transaction source ${expected}`,
170
+ "Preserve the candidate and transaction evidence; inspect the target ref for unrelated advancement before retrying.",
171
+ );
172
+ }
173
+ return { advanced: true, observedSha: observed };
174
+ }
175
+
176
+ export function validateReleaseCandidateRecoveryReceipt({
177
+ receipt,
178
+ passport,
179
+ repository = "",
180
+ targetChannel = "",
181
+ targetRef = "",
182
+ targetSha = "",
183
+ targetTree = "",
184
+ version = "",
185
+ } = {}) {
186
+ const receiptObject = Boolean(receipt && typeof receipt === "object" && !Array.isArray(receipt));
187
+ const passportObject = Boolean(passport && typeof passport === "object" && !Array.isArray(passport));
188
+ const rootedReceipt = Object.assign({}, receipt);
189
+ delete rootedReceipt.root;
190
+ const candidateHash = String(passport?.candidateHash || "");
191
+ const candidateRoot = `sha256:${candidateHash}`;
192
+ const checks = [
193
+ [receiptObject, "receipt must be an object"],
194
+ [passportObject, "passport must be an object"],
195
+ [receipt?.contract === RELEASE_CANDIDATE_RECOVERY_CONTRACT, `contract must be ${RELEASE_CANDIDATE_RECOVERY_CONTRACT}`],
196
+ [Number(receipt?.schemaVersion) === 1, "schemaVersion must be 1"],
197
+ [receipt?.action === "reused", "action must be reused"],
198
+ [receipt?.payloadBytes === "unchanged", "payloadBytes must be unchanged"],
199
+ [JSON.stringify(receipt?.skippedBuildStages) === JSON.stringify(["install", "build", "verify", "platform-matrix"]), "skippedBuildStages must record the complete skipped product build matrix"],
200
+ [receipt?.root === `sha256:${sha256Json(rootedReceipt)}`, "receipt root mismatch"],
201
+ [!repository || receipt?.repository === repository, `repository mismatch: expected ${repository}, got ${receipt?.repository || "<empty>"}`],
202
+ [!targetChannel || receipt?.target?.channel === targetChannel, `target channel mismatch: expected ${targetChannel}, got ${receipt?.target?.channel || "<empty>"}`],
203
+ [!targetRef || normalizedRef(receipt?.target?.ref) === normalizedRef(targetRef), `target ref mismatch: expected ${normalizedRef(targetRef)}, got ${normalizedRef(receipt?.target?.ref) || "<empty>"}`],
204
+ [!targetSha || receipt?.target?.sha === targetSha, `target SHA mismatch: expected ${targetSha}, got ${receipt?.target?.sha || "<empty>"}`],
205
+ [!targetTree || receipt?.target?.tree === targetTree, `target tree mismatch: expected ${targetTree}, got ${receipt?.target?.tree || "<empty>"}`],
206
+ [!version || receipt?.target?.version === version, `publication version mismatch: expected ${version}, got ${receipt?.target?.version || "<empty>"}`],
207
+ [passportObject && /^[0-9a-f]{64}$/.test(candidateHash), "passport candidateHash must be a sha256 digest"],
208
+ [passportObject && receipt?.recovered?.candidateRoot === candidateRoot, `candidate root mismatch: expected ${candidateRoot}, got ${receipt?.recovered?.candidateRoot || "<empty>"}`],
209
+ [passportObject && receipt?.originalCandidate?.sourceSha === passport?.source?.headSha, "original candidate source SHA mismatch"],
210
+ [passportObject && receipt?.originalCandidate?.tree === passport?.source?.treeHash, "original candidate tree mismatch"],
211
+ ];
212
+ const errors = checks.filter(([ok]) => !ok).map(([, message]) => message);
213
+ return {
214
+ ok: errors.length === 0,
215
+ errors,
216
+ publicationVersion: String(receipt?.target?.version || ""),
217
+ candidateRoot: String(receipt?.recovered?.candidateRoot || ""),
218
+ };
219
+ }
220
+
221
+ function sameArtifactFile(left, right) {
222
+ return Boolean(left && right && left.size === right.size && left.sha256 === right.sha256);
223
+ }
224
+
225
+ function validatePlatformPayloads(passport, artifacts, platformManifests, platformManifestEvidence) {
226
+ const byName = new Map(artifacts.map((artifact) => [artifact.name, artifact]));
227
+ const manifestsByArtifact = new Map((platformManifests || []).map((manifest) => [manifest.artifactName, manifest]));
228
+ const evidenceByArtifact = new Map((platformManifestEvidence || []).map((evidence) => [evidence.artifactName, evidence]));
229
+ if (manifestsByArtifact.size !== (passport.platformMatrix || []).length) {
230
+ fail("platform-matrix-mismatch", "platform manifest count differs from the Passport platform matrix", "Restore every exact platform manifest from the candidate run.");
231
+ }
232
+ for (const platform of passport.platformMatrix || []) {
233
+ const payload = byName.get(platform.artifactName);
234
+ if (!payload) {
235
+ fail("artifact-missing", `platform payload artifact is missing: ${platform.artifactName}`, "Restore the exact candidate artifact or explicitly create a new candidate.");
236
+ }
237
+ const passportFiles = (platform.artifacts || []).map((file) => ({
238
+ path: String(file.path || file.name || ""),
239
+ size: Number(file.size),
240
+ sha256: `sha256:${String(file.sha256 || "").replace(/^sha256:/, "")}`,
241
+ })).sort((left, right) => left.path.localeCompare(right.path));
242
+ const manifest = manifestsByArtifact.get(platform.artifactName);
243
+ if (!manifest) {
244
+ fail("artifact-missing", `platform manifest is missing for ${platform.artifactName}`, "Restore the exact platform manifest artifact.");
245
+ }
246
+ const manifestFiles = (manifest.files || []).map((file) => ({
247
+ path: String(file.path || file.name || ""),
248
+ size: Number(file.size ?? file.bytes),
249
+ sha256: `sha256:${String(file.sha256 || "").replace(/^sha256:/, "")}`,
250
+ })).sort((left, right) => left.path.localeCompare(right.path));
251
+ const platformId = required(platform.platformId, `${platform.artifactName}.platformId`);
252
+ const buildchainEvidencePrefix = `.buildchain/artifacts/${platformId}/`;
253
+ const manifestByPath = new Map(manifestFiles.map((file) => [file.path, file]));
254
+ const evidenceFiles = new Map((evidenceByArtifact.get(platform.artifactName)?.files || []).map((file) => [file.path, file]));
255
+ const payloadPaths = new Set(payload.files.map((file) => file.path));
256
+ for (const manifestFile of manifestFiles) {
257
+ if (!payloadPaths.has(manifestFile.path)) {
258
+ fail("artifact-manifest-mismatch", `platform payload omits a manifest-declared file: ${platform.artifactName}/${manifestFile.path}`, "Do not publish; preserve the mismatched manifest and payload evidence.");
259
+ }
260
+ }
261
+ for (const payloadFile of payload.files) {
262
+ const manifestFile = manifestByPath.get(payloadFile.path);
263
+ if (sameArtifactFile(payloadFile, manifestFile)) continue;
264
+ const evidencePath = payloadFile.path.startsWith(buildchainEvidencePrefix)
265
+ ? payloadFile.path.slice(buildchainEvidencePrefix.length)
266
+ : "";
267
+ const evidenceFile = evidencePath ? evidenceFiles.get(evidencePath) : undefined;
268
+ if (!sameArtifactFile(payloadFile, evidenceFile)) {
269
+ fail("artifact-manifest-mismatch", `platform payload contains unbound bytes or evidence drift: ${platform.artifactName}/${payloadFile.path}`, "Do not publish; preserve the platform payload, manifest, and independently uploaded evidence.");
270
+ }
271
+ }
272
+ if (passportFiles.length > 0 && JSON.stringify(manifestFiles) !== JSON.stringify(passportFiles)) {
273
+ fail("artifact-manifest-mismatch", `platform manifest differs from Passport inventory: ${platform.artifactName}`, "Do not publish; preserve the mismatched manifest and Passport evidence.");
274
+ }
275
+ if (
276
+ Number(platform.summary?.fileCount) !== manifestFiles.length ||
277
+ Number(platform.summary?.totalBytes) !== manifestFiles.reduce((total, file) => total + file.size, 0)
278
+ ) {
279
+ fail("platform-matrix-mismatch", `platform summary differs from manifest inventory: ${platform.artifactName}`, "Use the exact summary, manifest, and payload uploaded by the successful candidate run.");
280
+ }
281
+ }
282
+ }
283
+
284
+ function validateProductPayloads({ passport, artifacts, productPayloadManifests, candidateRoot, buildSummaryRoot, runtimeSha }) {
285
+ const byName = new Map(artifacts.map((artifact) => [artifact.name, artifact]));
286
+ for (const [index, manifest] of (productPayloadManifests || []).entries()) {
287
+ if (manifest?.contract !== "kungfu-buildchain-product-payload-manifest/v1") {
288
+ fail("artifact-manifest-mismatch", `product payload manifest ${index} has an invalid contract`, "Use the exact Buildchain-produced product payload manifest.");
289
+ }
290
+ const { root: _root, ...rootInput } = manifest;
291
+ assertEqual(manifest.root, `sha256:${sha256Json(rootInput)}`, "artifact-manifest-mismatch", "product payload manifest root", "Use an untampered product payload manifest from the candidate run.");
292
+ assertEqual(manifest.candidateRoot, candidateRoot, "candidate-root-mismatch", "product payload candidate root", "Use the product payload sealed for this exact candidate.");
293
+ assertEqual(manifest.buildSummaryRoot, buildSummaryRoot, "build-summary-root-mismatch", "product payload build summary root", "Use the payload produced from the exact candidate summary.");
294
+ assertEqual(manifest.source?.tree, passport.source?.treeHash, "source-tree-mismatch", "product payload source tree", "Use payload bytes produced from the exact candidate tree.");
295
+ assertEqual(manifest.runtimeSha, runtimeSha, "runtime-mismatch", "product payload candidate runtime", "Use payload evidence created by the candidate runtime.");
296
+ const artifact = byName.get(manifest.artifactName);
297
+ if (!artifact) fail("artifact-missing", `product payload artifact is missing: ${manifest.artifactName}`, "Restore the exact product payload artifact.");
298
+ const manifestName = String(manifest.manifestPath || "product-payload-manifest.json");
299
+ const payloadFiles = artifact.files.filter((file) => file.path !== manifestName);
300
+ const expectedFiles = (manifest.files || []).map((file) => ({
301
+ path: String(file.path || ""),
302
+ size: Number(file.size),
303
+ sha256: String(file.sha256 || ""),
304
+ })).sort((left, right) => left.path.localeCompare(right.path));
305
+ if (JSON.stringify(payloadFiles) !== JSON.stringify(expectedFiles)) {
306
+ fail("artifact-manifest-mismatch", `product payload bytes differ from their manifest: ${manifest.artifactName}`, "Do not publish; preserve the manifest and uploaded product bytes.");
307
+ }
308
+ }
309
+ }
310
+
311
+ function validateCandidateProvenance({
312
+ candidateRepository,
313
+ targetRepository,
314
+ expectedRunId,
315
+ expectedWorkflowFile,
316
+ expectedWorkflowName,
317
+ targetRef,
318
+ run,
319
+ workflow,
320
+ pullRequest,
321
+ ancestry,
322
+ }) {
323
+ const repository = required(candidateRepository, "candidateRepository");
324
+ assertEqual(repository, required(targetRepository, "targetRepository"), "repository-mismatch", "candidate repository", "Dispatch recovery in the repository that created the candidate.");
325
+ assertEqual(String(run?.id || ""), required(expectedRunId, "expectedRunId"), "run-mismatch", "candidate run ID", "Select the exact successful candidate run.");
326
+ assertEqual(run?.repository, repository, "repository-mismatch", "run repository", "Use a candidate run from the target repository.");
327
+ assertEqual(run?.headRepository, repository, "provenance-insufficient", "run head repository", "Fork candidates are not recoverable; create a same-repository candidate.");
328
+ if (run?.status !== "completed" || run?.conclusion !== "success" || run?.event !== "pull_request") {
329
+ fail("untrusted-build-run", "candidate run must be a successful completed pull_request build", "Choose a successful allowed Build workflow run.");
330
+ }
331
+ const workflowFile = required(expectedWorkflowFile, "expectedWorkflowFile").replace(/^\.github\/workflows\//, "");
332
+ const actualWorkflowPath = String(workflow?.path || run?.path || "").split("@")[0].replace(/^\.github\/workflows\//, "");
333
+ assertEqual(actualWorkflowPath, workflowFile, "workflow-mismatch", "candidate workflow file", "Select a run from the documented trusted Build workflow.");
334
+ assertEqual(String(workflow?.name || run?.name || ""), required(expectedWorkflowName, "expectedWorkflowName"), "workflow-mismatch", "candidate workflow name", "Select a run from the documented trusted Build workflow.");
335
+ if (workflow?.state && workflow.state !== "active") fail("untrusted-build-run", `candidate workflow is not active: ${workflow.state}`, "Restore or select an allowed active Build workflow.");
336
+ if (!TRUSTED_ASSOCIATIONS.has(String(pullRequest?.authorAssociation || ""))) fail("permission-evidence-insufficient", "candidate PR author lacks trusted repository association evidence", "Have a repository member create or approve a same-repository candidate PR.");
337
+ assertEqual(pullRequest?.headRepository, repository, "provenance-insufficient", "candidate PR head repository", "Fork candidates are outside the recovery permission boundary.");
338
+ if (!pullRequest?.merged) fail("pr-identity-invalid", "candidate PR is not merged", "Use a merged channel candidate PR.");
339
+ assertEqual(normalizedRef(pullRequest?.baseRef), normalizedRef(targetRef), "target-mismatch", "candidate PR target ref", "Use the original release channel target.");
340
+ const boundByNumber = Array.isArray(run?.pullRequestNumbers) && run.pullRequestNumbers.includes(Number(pullRequest?.number));
341
+ const boundByHead = run?.headSha === pullRequest?.headSha && normalizedRef(run?.headBranch) === normalizedRef(pullRequest?.headRef);
342
+ if (!boundByNumber && !boundByHead) fail("pr-identity-invalid", "candidate run is not bound to the merged candidate PR", "Select the exact PR-stage Build run.");
343
+ if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical") fail("ancestry-invalid", "candidate merge is not an ancestor of the promotion SHA", "Promote a descendant of the verified merged candidate identity.");
344
+ return { repository, workflowFile };
345
+ }
346
+
347
+ function validateCandidateIdentity({
348
+ repository,
349
+ channel,
350
+ targetSha,
351
+ targetTree,
352
+ expectedSourceTree,
353
+ expectedRuntimeSha,
354
+ currentToolingSha,
355
+ passport,
356
+ buildSummary,
357
+ }) {
358
+ const sha = exactSha(targetSha, "targetSha");
359
+ const tree = exactSha(targetTree, "targetTree");
360
+ const runtimeSha = exactSha(expectedRuntimeSha, "expectedRuntimeSha");
361
+ const toolingSha = exactSha(currentToolingSha, "currentToolingSha");
362
+ const validation = validateReleaseCandidatePassport({ passport, repository, targetChannel: channel, buildSummary });
363
+ if (!validation.ok) fail("passport-invalid", `Release Candidate Passport validation failed: ${validation.errors.join("; ")}`, "Preserve the candidate evidence and explicitly create a new candidate after fixing the producer.");
364
+ const expectedPassportHash = sha256Json({
365
+ repository: passport.repository,
366
+ target: passport.target,
367
+ source: passport.source,
368
+ platformMatrix: passport.platformMatrix,
369
+ buildchain: passport.buildchain,
370
+ ...(passport.gateProfileEvidence ? { gateProfileEvidence: passport.gateProfileEvidence } : {}),
371
+ ...(passport.familyEvidence ? { familyEvidence: passport.familyEvidence } : {}),
372
+ ...(passport.controllerReceipts ? { controllerReceipts: passport.controllerReceipts } : {}),
373
+ });
374
+ if (passport.candidateHash !== expectedPassportHash) fail("candidate-root-mismatch", "Release Candidate Passport candidate hash does not match its content", "Preserve the run evidence and explicitly create a new candidate after fixing the producer.");
375
+ assertEqual(passport.source?.treeHash, tree, "source-tree-mismatch", "promotion Git tree", "Select a promotion SHA with the exact candidate Git tree or create a new candidate explicitly.");
376
+ if (expectedSourceTree) assertEqual(passport.source?.treeHash, exactSha(expectedSourceTree, "expectedSourceTree"), "source-tree-mismatch", "expected source tree", "Correct the expected tree or create a new candidate.");
377
+ assertEqual(passport.buildchain?.sha, runtimeSha, "runtime-mismatch", "candidate Buildchain runtime SHA", "Run recovery with the exact trusted Buildchain runtime recorded by the candidate.");
378
+ return { sha, tree, runtimeSha, toolingSha };
379
+ }
380
+
381
+ function validateRecoveryTransaction({ existingTransaction, expectedTransactionId, repository, version, passport, sha, targetRef, candidateRoot }) {
382
+ const actualTransactionId = String(existingTransaction?.id || "");
383
+ if (expectedTransactionId && !actualTransactionId) fail("transaction-identity-conflict", `expected transaction ${expectedTransactionId} does not exist`, "Remove the stale transaction identity only if no durable transaction was ever sealed; otherwise preserve evidence and enter repair_required.");
384
+ if (expectedTransactionId && expectedTransactionId !== actualTransactionId) fail("transaction-identity-conflict", `existing transaction ${actualTransactionId} conflicts with expected ${expectedTransactionId}`, "Enter repair_required and inspect the durable transaction before any retry.");
385
+ if (existingTransaction) {
386
+ const expectedIdentity = releaseTransactionId({ repository, version, sourceSha: sha, targetRef: normalizedRef(targetRef) });
387
+ assertEqual(existingTransaction.id, expectedIdentity, "transaction-identity-conflict", "durable transaction identity", "Enter repair_required; the durable transaction does not belong to this exact publication target.");
388
+ assertEqual(existingTransaction.repository, repository, "transaction-identity-conflict", "durable transaction repository", "Enter repair_required; never cross repository transaction boundaries.");
389
+ assertEqual(normalizedRef(existingTransaction.target_ref), normalizedRef(targetRef), "transaction-identity-conflict", "durable transaction target ref", "Enter repair_required; never retarget a sealed transaction.");
390
+ assertEqual(existingTransaction.source_sha, sha, "transaction-identity-conflict", "durable transaction source SHA", "Resume with the transaction's exact promotion SHA or enter repair_required.");
391
+ assertEqual(existingTransaction.version, version, "transaction-identity-conflict", "durable transaction version", "Enter repair_required; never change a sealed publication version.");
392
+ assertEqual(existingTransaction.channel, passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
393
+ }
394
+ if (existingTransaction?.candidateRoot && existingTransaction.candidateRoot !== candidateRoot) fail("transaction-identity-conflict", "existing transaction is sealed to a different candidate root", "Enter repair_required; never reuse the conflicting transaction.");
395
+ return actualTransactionId;
396
+ }
397
+
398
+ export function verifyReleaseCandidateRecovery({
399
+ candidateRepository,
400
+ targetRepository,
401
+ expectedRunId,
402
+ expectedWorkflowFile,
403
+ expectedWorkflowName,
404
+ channel,
405
+ targetRef,
406
+ targetSha,
407
+ targetRefSha = targetSha,
408
+ targetTree,
409
+ expectedSourceTree = "",
410
+ expectedCandidateRoot = "",
411
+ expectedRuntimeSha,
412
+ expectedTransactionId = "",
413
+ existingTransaction = undefined,
414
+ run,
415
+ workflow,
416
+ pullRequest,
417
+ ancestry,
418
+ passport,
419
+ buildSummary,
420
+ controllerReceipts = [],
421
+ platformManifests = [],
422
+ platformManifestEvidence = [],
423
+ productPayloadManifests = [],
424
+ artifacts = [],
425
+ publicationVersion = "",
426
+ currentToolingSha,
427
+ recoveryRunId = "",
428
+ createdAt = new Date().toISOString(),
429
+ } = {}) {
430
+ const { repository, workflowFile } = validateCandidateProvenance({
431
+ candidateRepository, targetRepository, expectedRunId, expectedWorkflowFile,
432
+ expectedWorkflowName, targetRef, run, workflow, pullRequest, ancestry,
433
+ });
434
+ const { sha, tree, runtimeSha, toolingSha } = validateCandidateIdentity({
435
+ repository, channel, targetSha, targetTree, expectedSourceTree,
436
+ expectedRuntimeSha, currentToolingSha, passport, buildSummary,
437
+ });
438
+
439
+ const recoveredArtifacts = artifacts.map(normalizeArtifact).sort((left, right) => left.name.localeCompare(right.name));
440
+ if (recoveredArtifacts.length === 0) fail("artifact-missing", "candidate artifact set is empty", "Select a retained run with the complete candidate artifact set.");
441
+ if (new Set(recoveredArtifacts.map((artifact) => artifact.name)).size !== recoveredArtifacts.length) {
442
+ fail("artifact-count-mismatch", "candidate artifact names are not unique", "Inspect the candidate run and remove ambiguity by creating a new candidate.");
443
+ }
444
+ validatePlatformPayloads(passport, recoveredArtifacts, platformManifests, platformManifestEvidence);
445
+
446
+ const references = new Map((passport.controllerReceipts || []).map((reference) => [reference.controllerId, reference]));
447
+ for (const receipt of controllerReceipts) {
448
+ const receiptValidation = validateControllerReceipt(receipt, {
449
+ expectedSourceSha: passport.source?.headSha,
450
+ expectedRuntimeSha: runtimeSha,
451
+ });
452
+ if (!receiptValidation.ok || !receiptValidation.qualifying) {
453
+ fail("controller-receipt-invalid", `controller receipt is not qualifying: ${receiptValidation.issues.join("; ")}`, "Use a successful candidate with complete controller evidence.");
454
+ }
455
+ const actualReference = createControllerReceiptReference(receipt);
456
+ const expectedReference = references.get(actualReference.controllerId);
457
+ if (!expectedReference || sha256Json(actualReference) !== sha256Json(expectedReference)) {
458
+ fail("controller-receipt-mismatch", `controller receipt does not match Passport reference: ${actualReference.controllerId}`, "Use the exact controller receipt artifact uploaded by the candidate run.");
459
+ }
460
+ }
461
+ if (references.size !== controllerReceipts.length) {
462
+ fail("controller-receipt-missing", "not every Passport controller receipt was recovered", "Restore every referenced controller receipt artifact before recovery.");
463
+ }
464
+
465
+ const candidateRoot = `sha256:${passport.candidateHash}`;
466
+ if (expectedCandidateRoot) assertEqual(candidateRoot, contentRoot(expectedCandidateRoot, "expectedCandidateRoot"), "candidate-root-mismatch", "candidate root", "Use the exact candidate root recorded by the original run.");
467
+ const artifactInventory = recoveredArtifacts.map(({ name, size, digest, files }) => ({ name, size, digest, files }));
468
+ const artifactRoot = `sha256:${sha256Json(artifactInventory)}`;
469
+ const artifactArchiveRoot = `sha256:${sha256Json(recoveredArtifacts.map(({ name, size, digest }) => ({ name, size, digest })))}`;
470
+ validateProductPayloads({
471
+ passport,
472
+ artifacts: recoveredArtifacts,
473
+ productPayloadManifests,
474
+ candidateRoot,
475
+ buildSummaryRoot: `sha256:${passport.diagnostics.buildSummaryHash}`,
476
+ runtimeSha,
477
+ });
478
+
479
+ const version = required(publicationVersion || passport.target?.version, "publicationVersion");
480
+ const actualTransactionId = validateRecoveryTransaction({
481
+ existingTransaction, expectedTransactionId, repository, version, passport, sha, targetRef, candidateRoot,
482
+ });
483
+
484
+ const receipt = {
485
+ schemaVersion: 1,
486
+ contract: RELEASE_CANDIDATE_RECOVERY_CONTRACT,
487
+ action: "reused",
488
+ createdAt,
489
+ repository,
490
+ recoveryRunId: String(recoveryRunId || ""),
491
+ originalCandidate: {
492
+ runId: String(run.id),
493
+ workflowFile,
494
+ workflowName: String(workflow?.name || run?.name || ""),
495
+ pullRequest: Number(pullRequest.number),
496
+ sourceSha: passport.source.headSha,
497
+ mergeSha: String(pullRequest.mergeSha || ""),
498
+ tree: passport.source.treeHash,
499
+ },
500
+ target: {
501
+ channel: passport.target.channel,
502
+ ref: normalizedRef(targetRef),
503
+ sha,
504
+ observedRefSha: exactSha(targetRefSha, "targetRefSha"),
505
+ tree,
506
+ version,
507
+ },
508
+ recovered: {
509
+ candidateRoot,
510
+ buildSummaryRoot: `sha256:${passport.diagnostics.buildSummaryHash}`,
511
+ artifactRoot,
512
+ artifactArchiveRoot,
513
+ artifactCount: recoveredArtifacts.length,
514
+ artifacts: recoveredArtifacts.map(({ name, size, digest }) => ({ name, size, digest })),
515
+ },
516
+ skippedBuildStages: ["install", "build", "verify", "platform-matrix"],
517
+ payloadBytes: "unchanged",
518
+ buildchainToolingSha: toolingSha,
519
+ transaction: {
520
+ identity: expectedTransactionId || actualTransactionId || "",
521
+ state: String(existingTransaction?.state || "absent"),
522
+ publicationState: String(existingTransaction?.publication_state || existingTransaction?.state || "absent"),
523
+ },
524
+ };
525
+ receipt.root = `sha256:${sha256Json(receipt)}`;
526
+ return { receipt, artifacts: recoveredArtifacts };
527
+ }
528
+
529
+ export function recoveryFailure(error) {
530
+ if (error instanceof ReleaseCandidateRecoveryError) {
531
+ return { ok: false, code: error.code, reason: error.message, nextAction: error.nextAction };
532
+ }
533
+ return {
534
+ ok: false,
535
+ code: "recovery-internal-error",
536
+ reason: String(error?.message || error),
537
+ nextAction: "Preserve the run logs and candidate evidence; do not rebuild automatically.",
538
+ };
539
+ }
@@ -9,13 +9,19 @@ import {
9
9
  matchesGithubDeploymentPolicy,
10
10
  } from "../packages/core/publication-control-plane-audit.js";
11
11
 
12
+ const GITHUB_JSON_MAX_BUFFER = 16 * 1024 * 1024;
13
+
12
14
  function flag(name, fallback = "") {
13
15
  const index = process.argv.indexOf(`--${name}`);
14
16
  return index === -1 ? fallback : String(process.argv[index + 1] || "");
15
17
  }
16
18
 
17
19
  function commandJson(command, args, label) {
18
- const result = spawnSync(command, args, { encoding: "utf8", timeout: 60_000 });
20
+ const result = spawnSync(command, args, {
21
+ encoding: "utf8",
22
+ timeout: 60_000,
23
+ maxBuffer: GITHUB_JSON_MAX_BUFFER,
24
+ });
19
25
  if (result.status !== 0) {
20
26
  const category = /401|E401|unauthorized/i.test(result.stderr) ? "unauthorized" : "unavailable";
21
27
  throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
@@ -35,6 +41,7 @@ function githubJsonOptional(apiPath, label, fallback) {
35
41
  const result = spawnSync("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
36
42
  encoding: "utf8",
37
43
  timeout: 60_000,
44
+ maxBuffer: GITHUB_JSON_MAX_BUFFER,
38
45
  });
39
46
  if (result.status !== 0) {
40
47
  if (/404|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
@@ -52,6 +59,7 @@ function githubJsonReadLimited(apiPath, label, fallback) {
52
59
  const result = spawnSync("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
53
60
  encoding: "utf8",
54
61
  timeout: 60_000,
62
+ maxBuffer: GITHUB_JSON_MAX_BUFFER,
55
63
  });
56
64
  if (result.status !== 0) {
57
65
  if (/401|403|404|unauthorized|forbidden|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
@@ -355,9 +363,6 @@ function main() {
355
363
  review.user?.login !== mergedPullRequest?.user?.login &&
356
364
  String(review.commit_id || "").toLowerCase() === pullRequestHeadSha
357
365
  );
358
- const checkRuns = /^[0-9a-f]{40}$/.test(pullRequestHeadSha)
359
- ? githubJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?per_page=100`, "merged pull-request head check runs")
360
- : { check_runs: [] };
361
366
  const requiredStatusCheckPolicy = branchState.protection?.required_status_checks || {};
362
367
  const requiredStatusChecks = [...new Set([
363
368
  ...(requiredStatusCheckPolicy.contexts || []),
@@ -372,6 +377,9 @@ function main() {
372
377
  const resolvedRequiredStatusCheck = exactRequiredStatusCheck ||
373
378
  (prefixedRequiredStatusChecks.length === 1 ? prefixedRequiredStatusChecks[0] : requiredStatusCheck);
374
379
  const requiredStatusCheckMatchCount = exactRequiredStatusCheck ? 1 : prefixedRequiredStatusChecks.length;
380
+ const checkRuns = /^[0-9a-f]{40}$/.test(pullRequestHeadSha)
381
+ ? githubJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?check_name=${encodeURIComponent(resolvedRequiredStatusCheck)}&filter=latest&per_page=100`, "merged pull-request required check runs")
382
+ : { check_runs: [] };
375
383
  const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) =>
376
384
  entry.context === resolvedRequiredStatusCheck
377
385
  );