@kungfu-tech/buildchain 4.1.6 → 4.1.7

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 (101) hide show
  1. package/AGENTS.md +7 -5
  2. package/CONTRIBUTING.md +12 -6
  3. package/README.md +9 -5
  4. package/architecture/action-taxonomy.json +2 -0
  5. package/architecture/agent-change-map.md +6 -1
  6. package/architecture/build-orchestration.json +7 -1
  7. package/architecture/ci-lane-change-budget.json +1836 -0
  8. package/architecture/consumer-upgrade.json +6482 -0
  9. package/architecture/internal-capabilities.json +5 -2
  10. package/architecture/maintainability-debt.json +18 -10
  11. package/architecture/maintainability-policy.json +9 -9
  12. package/architecture/release-topology.json +10 -1
  13. package/architecture/universal-workflow-bootstrap.json +88 -18
  14. package/architecture/universal-workflow-capability-policy.json +2 -2
  15. package/architecture/workflow-taxonomy.json +2 -1
  16. package/contracts/dev-delivery-authority-v2.schema.json +29 -3
  17. package/contracts/fixtures/consumer-upgrade/build-v4.0.0.json +764 -0
  18. package/contracts/fixtures/consumer-upgrade/consumer-calls.json +447 -0
  19. package/contracts/fixtures/consumer-upgrade/dev-delivery-identity-v4.0.0.json +27 -0
  20. package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.0.json +256 -0
  21. package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.9.json +358 -0
  22. package/contracts/fixtures/consumer-upgrade/release-v4.0.0.json +815 -0
  23. package/contracts/promotion-invocation-v1.schema.json +5 -0
  24. package/dist/site/buildchain-contract.json +40 -23
  25. package/dist/site/buildchain-site.json +20 -20
  26. package/dist/site/capability-registry.json +5 -5
  27. package/dist/site/controller-registry.json +17 -5
  28. package/dist/site/kfd-claims.json +1376 -143
  29. package/dist/site/kfd-upstream-aggregate.json +1 -1
  30. package/dist/site/manual-registry.json +4 -4
  31. package/dist/site/node-api-registry.json +22 -22
  32. package/dist/site/page-registry.json +14 -14
  33. package/dist/site/public-surface-audit.json +1733 -101
  34. package/dist/site/publication-authority-registry.json +651 -32
  35. package/dist/site/publication-registry.json +1 -1
  36. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +29 -3
  37. package/dist/site/site-manifest.json +5 -5
  38. package/dist/site/workflow-registry.json +2501 -149
  39. package/docs/MAP.md +5 -4
  40. package/docs/code-organization.md +11 -8
  41. package/docs/getting-started.md +31 -2
  42. package/docs/migration-inventory.md +8 -6
  43. package/docs/node-api-reference.md +36 -36
  44. package/docs/workflow-path-migration.md +8 -7
  45. package/package.json +1 -1
  46. package/packages/core/build/artifact/contracts.js +10 -2
  47. package/packages/core/build/artifact/transport.js +1 -1
  48. package/packages/core/build/artifact/upload.js +2 -2
  49. package/packages/core/build/plan/action.js +2 -0
  50. package/packages/core/build/plan/admission.js +9 -3
  51. package/packages/core/build/plan/compatibility-artifacts.js +47 -0
  52. package/packages/core/build/plan/compatibility-plan.js +121 -0
  53. package/packages/core/build/plan/compatibility.js +162 -0
  54. package/packages/core/build/plan/configuration.js +10 -37
  55. package/packages/core/build/plan/identity.js +42 -0
  56. package/packages/core/build/plan/lifecycle.js +1 -0
  57. package/packages/core/build/plan/resolve.js +2 -0
  58. package/packages/core/build/summary/aggregation.js +4 -2
  59. package/packages/core/build/summary/compatibility.js +24 -0
  60. package/packages/core/build/summary/finalization.js +18 -0
  61. package/packages/core/build/summary/historical-outputs.js +88 -0
  62. package/packages/core/contracts/runtime-contract-inspection.js +8 -0
  63. package/packages/core/dev-delivery/candidate/submission.js +2 -1
  64. package/packages/core/dev-delivery/commands/dev-delivery-authority-command-adapters.mjs +2 -1
  65. package/packages/core/dev-delivery/commands/dev-delivery-authority.mjs +6 -5
  66. package/packages/core/dev-delivery/commands/dev-delivery-warrant-options.mjs +22 -3
  67. package/packages/core/dev-delivery/dev-delivery-candidate-identity.js +13 -4
  68. package/packages/core/dev-delivery/dev-delivery-warrant.js +4 -1
  69. package/packages/core/dev-delivery/warrant/handoff.js +2 -1
  70. package/packages/core/dev-delivery/warrant/transitions.js +2 -2
  71. package/packages/core/governance/buildchain-publication-authority.js +5 -9
  72. package/packages/core/release/github-release-metadata.js +58 -0
  73. package/packages/core/release/promote-candidate/action.js +4 -2
  74. package/packages/core/release/promote-candidate/preparation.js +6 -0
  75. package/packages/core/release/promote-candidate/publication.js +36 -0
  76. package/packages/core/release/promote-candidate/release-documents.js +1 -1
  77. package/packages/core/release/promotion/candidate.js +4 -2
  78. package/packages/core/release/promotion/compatibility-context.js +93 -0
  79. package/packages/core/release/promotion/compatibility-qualification.js +48 -0
  80. package/packages/core/release/promotion/compatibility.js +141 -0
  81. package/packages/core/release/promotion/historical-evidence-action.js +17 -0
  82. package/packages/core/release/promotion/historical-evidence-files.js +112 -0
  83. package/packages/core/release/promotion/historical-evidence.js +231 -0
  84. package/packages/core/release/promotion/qualification.js +12 -1
  85. package/packages/core/release/promotion/routing.js +3 -2
  86. package/packages/core/release/promotion-request.js +31 -2
  87. package/packages/core/release/release-tail-provider-adapters.js +18 -0
  88. package/packages/core/runtime/entry/actions.js +4 -1
  89. package/packages/core/runtime/entry/compatibility.js +53 -0
  90. package/packages/core/workflow/engine/conformance.js +7 -3
  91. package/packages/core/workflow/pipeline/controller.js +26 -26
  92. package/scripts/check-dev-delivery-actions.mjs +12 -0
  93. package/scripts/check-inventory.mjs +1 -1
  94. package/scripts/check-public-dogfood-contract.mjs +2 -0
  95. package/scripts/check-release-topology.mjs +5 -1
  96. package/scripts/check-self-consumer-contract.mjs +5 -1
  97. package/scripts/generate-bootstrap-admission.mjs +20 -0
  98. package/scripts/generate-site-bundle.mjs +5 -5
  99. package/scripts/generate-workflow-taxonomy.mjs +17 -0
  100. package/scripts/maintainability-metrics.mjs +30 -2
  101. package/scripts/maintainability-public-surface.mjs +5 -3
@@ -0,0 +1,88 @@
1
+ import { readJson } from "../plan/values.js";
2
+
3
+ // Project provider-confirmed evidence; this never grants publication authority.
4
+ export function historicalBuildOutputs(
5
+ plan,
6
+ artifacts,
7
+ controller,
8
+ summary,
9
+ file,
10
+ ) {
11
+ const runtime = summary.runtime;
12
+ const source = summary.publishSource;
13
+ const gate = summary.publishGate;
14
+ const json = (value) => JSON.stringify(value);
15
+ const name = (reference) => reference?.name || "";
16
+ const outputs = {
17
+ "buildchain-channel": plan.identity.channel,
18
+ "buildchain-channel-selection-source": "called-workflow",
19
+ "buildchain-channel-reason": `Selected by ${plan.identity.ref}`,
20
+ "runner-preset": plan.environment.runners.preset,
21
+ "platforms-json": json(
22
+ plan.platforms.map((platform) => ({
23
+ ...platform,
24
+ runner: json(platform.runner),
25
+ })),
26
+ ),
27
+ "runner-routing-json": plan.routing ? json(plan.routing) : "",
28
+ "platform-count": plan.platforms.length,
29
+ "linux-container-enabled": plan.container.enabled,
30
+ "linux-container-image": plan.container.image,
31
+ "build-summary-artifact": name(artifacts.summary),
32
+ "credential-island-macos-artifact": name(artifacts.credential?.payload),
33
+ "credential-island-macos-manifest-artifact": name(
34
+ artifacts.credential?.manifest,
35
+ ),
36
+ "build-diagnostics-summary-artifact": name(artifacts.diagnostics),
37
+ "release-candidate-passport-artifact": name(artifacts.release_candidate),
38
+ "release-candidate-artifact": name(artifacts.release_candidate),
39
+ "release-candidate-passport-json": artifacts.release_candidate
40
+ ? json(
41
+ readJson(
42
+ file(".buildchain/artifacts/release-candidate-passport.json"),
43
+ ),
44
+ )
45
+ : "",
46
+ "build-summary-json": json(summary),
47
+ "artifact-coordinates-json": json(
48
+ readJson(file(".buildchain/artifacts/artifact-coordinates.json")),
49
+ ),
50
+ "build-diagnostics-summary-json": json(
51
+ readJson(file(".buildchain/artifacts/diagnostics-summary.json")),
52
+ ),
53
+ "trusted-event": gate.trustedEvent,
54
+ "buildchain-runtime-ref": runtime.ref,
55
+ "buildchain-runtime-sha": runtime.sha,
56
+ "buildchain-runtime-class": runtime.class,
57
+ "buildchain-runtime-override": runtime.override,
58
+ "buildchain-runtime-trust-decision": runtime.trustDecision,
59
+ "buildchain-contract-lock-status": plan.admission.lock_status,
60
+ "buildchain-contract-lock-drift": plan.admission.lock_drift,
61
+ "buildchain-contract-digest": plan.admission.contract_digest,
62
+ "publish-channel": gate.channel,
63
+ "publish-allowed": gate.allowed,
64
+ "publish-reason": gate.reason,
65
+ "publish-source-ref": source.ref,
66
+ "publish-source-sha": source.sha,
67
+ "publish-source-tree-sha": plan.source.tree_sha,
68
+ "publish-source-locked": source.locked,
69
+ "publish-source-channel": source.channel,
70
+ "publish-source-line": source.line,
71
+ "publish-source-consumer-version": source.consumerVersion,
72
+ "release-manifest-json": source.releaseManifest,
73
+ "controller-plan-artifact": name(artifacts.controller_plan),
74
+ "controller-plan-json": json(plan.controller),
75
+ "controller-plan-digest": plan.controller.digest,
76
+ "controller-receipt-artifact": name(artifacts.controller_receipt),
77
+ "controller-receipt-json": json(controller.receipt),
78
+ "controller-receipt-digest": controller.receipt.digest,
79
+ "controller-receipt-status": controller.receipt.status,
80
+ };
81
+ return Object.fromEntries(
82
+ Object.entries(outputs).map(([key, value]) => {
83
+ if (value === undefined)
84
+ throw new Error(`Missing historical build evidence: ${key}`);
85
+ return [key, String(value)];
86
+ }),
87
+ );
88
+ }
@@ -113,3 +113,11 @@ export function assertRuntimeContractAccepted(result) {
113
113
  `Buildchain contract lock rejected: ${(result.evaluation.reasons || []).join("; ")}`,
114
114
  );
115
115
  }
116
+
117
+ // Compare lock metadata with the selected runtime, independently of checkout paths.
118
+ export function runtimeContractLockDrift(lock, runtimeSha, contractDigest) {
119
+ if (lock.schema === "buildchain.consumer-contract-lock/v2")
120
+ return lock.runtime.sha !== runtimeSha;
121
+ return lock.buildchain.resolvedSha !== runtimeSha ||
122
+ lock.buildchain.contractDigest !== contractDigest;
123
+ }
@@ -1,3 +1,4 @@
1
+ import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
1
2
  export function deliverySubmissionRequest(
2
3
  input,
3
4
  { repository, branch, sourceProofRoot, affectedPaths },
@@ -9,7 +10,7 @@ export function deliverySubmissionRequest(
9
10
  branch,
10
11
  pullRequestNumber: Number(input["expected-pr-number"]),
11
12
  sourceHead: input["expected-head-sha"],
12
- sourceRoot: input["source-root"],
13
+ ...devDeliverySourceBinding({ sourceRoot: input["source-root"], assignmentRoot: input["assignment-root"], initiativeRoot: input["initiative-root"] }),
13
14
  sourceIdentityRoot: input["source-identity-root"],
14
15
  sourcePatchRoot: input["source-patch-root"],
15
16
  sourceProofRoot,
@@ -1,3 +1,4 @@
1
+ import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import {
@@ -55,7 +56,7 @@ function submitCandidate(state, options) {
55
56
  {
56
57
  pullRequestNumber: options.pullRequestNumber,
57
58
  sourceHead: options.sourceHead,
58
- sourceRoot: options.sourceRoot,
59
+ ...devDeliverySourceBinding(options),
59
60
  sourceIdentityRoot: options.sourceIdentityRoot,
60
61
  sourcePatchRoot: options.sourcePatchRoot,
61
62
  sourceProofRoot: options.sourceProofRoot,
@@ -7,7 +7,11 @@ import {
7
7
  } from "../dev-delivery-authority-landing.js";
8
8
  import { defaultDevDeliveryStateRef } from "../warrant/values.js";
9
9
  import { GitHubDevDeliveryStore } from "../../providers/dev-delivery/store.js";
10
- import { flag, hasFlag } from "./dev-delivery-warrant-options.mjs";
10
+ import {
11
+ flag,
12
+ hasFlag,
13
+ historicalDeliveryRootOptions,
14
+ } from "./dev-delivery-warrant-options.mjs";
11
15
  import { runDevDeliveryAuthorityCommandAdapter } from "./dev-delivery-authority-command-adapters.mjs";
12
16
  function text(value = "") {
13
17
  return String(value ?? "").trim();
@@ -247,10 +251,7 @@ export function devDeliveryAuthorityCliOptions(
247
251
  environment.BUILDCHAIN_WORK_SOURCE_ROOT ||
248
252
  environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_ROOT,
249
253
  ),
250
- // Retired roots remain readable in persisted historical candidates.
251
- // New authority submissions accept only the Work sourceRoot above.
252
- // Retired CLI flags are intentionally ignored by this parser.
253
- // This prevents producers from reconstructing obsolete identities.
254
+ ...historicalDeliveryRootOptions(rest, environment),
254
255
  sourceIdentityRoot: flag(
255
256
  rest,
256
257
  "source-identity-root",
@@ -79,8 +79,9 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
79
79
  "source-root",
80
80
  environment.BUILDCHAIN_WORK_SOURCE_ROOT ||
81
81
  environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_ROOT ||
82
- sourceRootFromGitHubEvent(environment),
82
+ rootFromGitHubEvent(environment),
83
83
  ),
84
+ ...historicalDeliveryRootOptions(rest, environment),
84
85
  // prettier-ignore
85
86
  ...{ expectedSourceHead: flag(rest, "expected-source-head", environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_SOURCE_HEAD), observedSourceHead: flag(rest, "observed-source-head", environment.BUILDCHAIN_DEV_DELIVERY_OBSERVED_SOURCE_HEAD) },
86
87
 
@@ -278,6 +279,24 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
278
279
  }
279
280
 
280
281
  // prettier-ignore
281
- function sourceRootFromGitHubEvent(environment) {
282
- try { const event = JSON.parse(fs.readFileSync(environment.GITHUB_EVENT_PATH, "utf8")); return event.client_payload?.candidate?.sourceRoot || JSON.parse(event.inputs?.["native-roots-json"] || "{}").sourceRoot || ""; } catch { return ""; }
282
+ function rootFromGitHubEvent(environment, key = "sourceRoot") {
283
+ try { const event = JSON.parse(fs.readFileSync(environment.GITHUB_EVENT_PATH, "utf8")); return event.client_payload?.candidate?.[key] || JSON.parse(event.inputs?.["native-roots-json"] || "{}")[key] || ""; } catch { return ""; }
284
+ }
285
+
286
+ export function historicalDeliveryRootOptions(args, environment) {
287
+ const result = {};
288
+ for (const [key, name] of [
289
+ ["assignmentRoot", "assignment-root"],
290
+ ["initiativeRoot", "initiative-root"],
291
+ ]) {
292
+ const value = flag(
293
+ args,
294
+ name,
295
+ environment[
296
+ `BUILDCHAIN_DEV_DELIVERY_${name.replaceAll("-", "_").toUpperCase()}`
297
+ ] || rootFromGitHubEvent(environment, key),
298
+ );
299
+ if (value) result[key] = value;
300
+ }
301
+ return result;
283
302
  }
@@ -18,9 +18,18 @@ export const EXACT_DEV_DELIVERY_PROOF_FIELDS = Object.freeze([
18
18
  "sourceWorkflowRunId",
19
19
  ]);
20
20
  export function devDeliverySourceBinding(input = {}) {
21
- if (Object.hasOwn(input, "assignmentRoot") || Object.hasOwn(input, "initiativeRoot"))
22
- throw new Error("provide sourceRoot alone; obsolete producer roots are unsupported");
23
- return { sourceRoot: exactRoot(input.sourceRoot, "sourceRoot") };
21
+ const legacy = ["assignmentRoot", "initiativeRoot"].some(
22
+ (key) => Object.hasOwn(input, key) && input[key] !== undefined && input[key] !== "",
23
+ );
24
+ if (!legacy) return { sourceRoot: exactRoot(input.sourceRoot, "sourceRoot") };
25
+ if (input.sourceRoot)
26
+ throw new Error("provide sourceRoot alone; do not mix historical producer roots");
27
+ // Keep the original identity preimage so persisted queues and Warrants survive
28
+ // a runtime upgrade without changing candidate IDs or their receipt roots.
29
+ return {
30
+ assignmentRoot: exactRoot(input.assignmentRoot, "assignmentRoot"),
31
+ initiativeRoot: exactRoot(input.initiativeRoot, "initiativeRoot"),
32
+ };
24
33
  }
25
34
  export function matchesExactDevDeliveryCandidate(existing, attempted) {
26
35
  return (
@@ -44,7 +53,7 @@ export function reuseExactActiveDevDeliverySourceProof(active, input) {
44
53
  );
45
54
  const exact =
46
55
  admittedPhase &&
47
- ["sourceRoot"].every(
56
+ ["sourceRoot", "assignmentRoot", "initiativeRoot"].every(
48
57
  (field) => active[field] === input[field],
49
58
  ) &&
50
59
  [
@@ -1,3 +1,4 @@
1
+ import { devDeliverySourceBinding } from "./dev-delivery-candidate-identity.js";
1
2
  import {
2
3
  devDeliveryClone as clone,
3
4
  devDeliveryContentRoot,
@@ -489,6 +490,8 @@ function createDevDeliverySuccessorWake(queue, now) {
489
490
  "pullRequestNumber",
490
491
  "sourceHead",
491
492
  "sourceRoot",
493
+ "assignmentRoot",
494
+ "initiativeRoot",
492
495
  "sourceIdentityRoot",
493
496
  "sourcePatchRoot",
494
497
  "sourceProofRoot",
@@ -584,5 +587,5 @@ export function observeDevDeliveryQueue(
584
587
  }
585
588
 
586
589
  function candidateSourceBinding(candidate) {
587
- return { sourceRoot: candidate.sourceRoot };
590
+ return devDeliverySourceBinding(candidate);
588
591
  }
@@ -1,3 +1,4 @@
1
+ import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
1
2
  import path from "node:path";
2
3
  import { requireValue } from "../../runtime/action-process.mjs";
3
4
  import { writeJson } from "../native/files.js";
@@ -19,7 +20,7 @@ export function handoffInputs(
19
20
  "expected-pr-number": warrant.pullRequestNumber,
20
21
  "expected-head-sha": warrant.sourceHead,
21
22
  "source-workflow-run-id": warrant.sourceWorkflowRunId,
22
- "native-roots-json": JSON.stringify({ sourceRoot: warrant.sourceRoot }),
23
+ "native-roots-json": JSON.stringify(devDeliverySourceBinding(warrant)),
23
24
  "source-identity-root": warrant.sourceIdentityRoot,
24
25
  "source-patch-root": warrant.sourcePatchRoot,
25
26
  "plan-root": warrant.planRoot,
@@ -13,7 +13,7 @@ import {
13
13
  settleDevDeliveryTerminalEvent,
14
14
  submitDevDeliveryCandidate,
15
15
  } from "../dev-delivery-warrant.js";
16
- import { reuseExactActiveDevDeliverySourceProof } from "../dev-delivery-candidate-identity.js";
16
+ import { devDeliverySourceBinding, reuseExactActiveDevDeliverySourceProof } from "../dev-delivery-candidate-identity.js";
17
17
  import { runDeliveryWarrantReadCandidate } from "../delivery-warrant-read-candidate.js";
18
18
 
19
19
  import {
@@ -83,7 +83,7 @@ export function transitionFor(command, queue, options) {
83
83
  "pullRequestNumber",
84
84
  ),
85
85
  sourceHead: exactSha(options.sourceHead, "sourceHead"),
86
- sourceRoot: exactRoot(options.sourceRoot, "sourceRoot"),
86
+ ...devDeliverySourceBinding(options),
87
87
  sourceIdentityRoot: exactRoot(
88
88
  options.sourceIdentityRoot,
89
89
  "sourceIdentityRoot",
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { readConsumerUpgrade, renderCompatibilityWorkflow } from "../consumer/compatibility-workflows.js";
3
4
 
4
5
  import { createPublicationAuthorityRegistry } from "../publication/publication-authority.js";
5
6
 
@@ -70,16 +71,11 @@ export function buildchainPublicationAuthorityDescriptors({ root = process.cwd()
70
71
  environmentMode,
71
72
  runnerPolicy: publicationCapable ? "qualified-measured" : "unqualified",
72
73
  }));
73
- const taxonomyPath = path.join(root, "architecture/workflow-taxonomy.json");
74
- if (!fs.existsSync(taxonomyPath)) return descriptors;
75
- const taxonomy = JSON.parse(fs.readFileSync(taxonomyPath, "utf8"));
76
- for (const entry of taxonomy.entries) {
77
- if (!entry.compatibility) continue;
78
- const previous = descriptors.find((item) => item.workflowPath === entry.compatibility.path);
74
+ for (const entry of readConsumerUpgrade(root)?.entries || []) {
75
+ const previous = descriptors.find((item) => item.workflowPath === entry.target);
79
76
  if (!previous) continue;
80
- const prefix = entry.role === "component" ? "." : `${entry.role}-`;
81
- const workflowPath = `.github/workflows/${prefix}${entry.category}-${entry.purpose}.yml`;
82
- if (fs.readFileSync(path.join(root, workflowPath), "utf8") !== fs.readFileSync(path.join(root, entry.compatibility.path), "utf8")) {
77
+ const workflowPath = entry.path;
78
+ if (fs.readFileSync(path.join(root, workflowPath), "utf8") !== renderCompatibilityWorkflow(entry, fs.readFileSync(path.join(root, entry.target), "utf8"))) {
83
79
  throw new Error(`publication authority alias differs from canonical workflow: ${workflowPath}`);
84
80
  }
85
81
  descriptors.push({ ...previous, workflowPath, publisherWorkflowPath: previous.publisherWorkflowPath === previous.workflowPath ? workflowPath : previous.publisherWorkflowPath });
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { historicalPromotionContext } from "./promotion/compatibility-context.js";
4
+
5
+ export const RELEASE_METADATA_NAME = "buildchain.release-metadata.json";
6
+ const schema = "buildchain.github-release-metadata/v1";
7
+
8
+ // The metadata is an ordinary content-rooted release asset. The provider reads
9
+ // it only after validating every local asset against its qualified root.
10
+ export function historicalReleaseMetadata(context, outputDir) {
11
+ const inputs = historicalPromotionContext(context)?.inputs;
12
+ if (
13
+ !inputs ||
14
+ !(inputs["github-release-title"] || inputs["github-release-notes"])
15
+ )
16
+ return [];
17
+ const metadata = {
18
+ schema,
19
+ ...(inputs["github-release-title"]
20
+ ? { name: inputs["github-release-title"] }
21
+ : {}),
22
+ ...(inputs["github-release-notes"]
23
+ ? { body: inputs["github-release-notes"] }
24
+ : {}),
25
+ };
26
+ fs.mkdirSync(outputDir, { recursive: true });
27
+ const file = path.join(outputDir, RELEASE_METADATA_NAME);
28
+ fs.writeFileSync(file, `${JSON.stringify(metadata, null, 2)}\n`);
29
+ return [file];
30
+ }
31
+
32
+ export function releaseMetadata(artifacts) {
33
+ const files = artifacts.filter(({ name }) => name === RELEASE_METADATA_NAME);
34
+ if (!files.length) return null;
35
+ if (files.length !== 1) throw new Error("Ambiguous GitHub Release metadata");
36
+ const value = JSON.parse(fs.readFileSync(files[0].path, "utf8"));
37
+ if (
38
+ !value ||
39
+ value.schema !== schema ||
40
+ Object.keys(value).some(
41
+ (key) => !["schema", "name", "body"].includes(key),
42
+ ) ||
43
+ !["name", "body"].some((key) => typeof value[key] === "string") ||
44
+ ["name", "body"].some(
45
+ (key) => Object.hasOwn(value, key) && typeof value[key] !== "string",
46
+ )
47
+ )
48
+ throw new Error("Invalid GitHub Release metadata");
49
+ const { schema: _schema, ...fields } = value;
50
+ return fields;
51
+ }
52
+
53
+ export function releaseMetadataMatches(metadata, release) {
54
+ return (
55
+ !metadata ||
56
+ Object.entries(metadata).every(([key, value]) => release[key] === value)
57
+ );
58
+ }
@@ -1,5 +1,5 @@
1
1
  import * as github from "@actions/github";
2
- import { publishWithDiscussion } from "../discussion/publication.js";
2
+ import { publishCandidate } from "./publication.js";
3
3
  import { selectedRecordRuntime } from "../discussion/session.js";
4
4
  export async function candidatePublicationAction(core, env) {
5
5
  const required = new Set([
@@ -39,6 +39,8 @@ export async function candidatePublicationAction(core, env) {
39
39
  "publish-package-main",
40
40
  "publish-package-set-order",
41
41
  "publisher-workflow-sha",
42
+ "publisher-workflow-path",
43
+ "historical-inputs-json",
42
44
  "repository",
43
45
  "required-artifacts-path",
44
46
  "required-status-check",
@@ -70,7 +72,7 @@ export async function candidatePublicationAction(core, env) {
70
72
  request["artifact-paths"] = core
71
73
  .getMultilineInput("artifact-paths")
72
74
  .filter(Boolean);
73
- return publishWithDiscussion(request, {
75
+ return publishCandidate(request, {
74
76
  octokit: github.getOctokit(request.token),
75
77
  mutationOctokit: github.getOctokit(
76
78
  request["mutation-token"] || request.token,
@@ -1,3 +1,4 @@
1
+ import { verifyHistoricalEvidence } from "../promotion/historical-evidence.js";
1
2
  import {
2
3
  resolveCandidateBuildSummaryPath,
3
4
  resolveCandidateProviderInputs,
@@ -28,6 +29,11 @@ export async function prepareCandidatePublication(
28
29
  declaredPath: request["candidate-build-summary-path"],
29
30
  });
30
31
  const candidate = read(candidatePassportPath);
32
+ verifyHistoricalEvidence({
33
+ context: request["historical-inputs-json"],
34
+ candidateDirectory: path.resolve(".buildchain/release-candidate"),
35
+ sourceSha: candidate.source?.headSha,
36
+ });
31
37
  const stageCapsules = read(request["stage-capsules-path"]);
32
38
  const qualification = read(request["publication-qualification-path"]);
33
39
  const token = request["token"];
@@ -0,0 +1,36 @@
1
+ import { assertHistoricalPromotionEffects } from "../promotion/compatibility-qualification.js";
2
+ import { publishWithDiscussion } from "../discussion/publication.js";
3
+ import { promoteReleaseCandidate } from "./transaction.js";
4
+ import {
5
+ historicalPromotionContext,
6
+ HISTORICAL_PUBLISHER,
7
+ } from "../promotion/compatibility-context.js";
8
+
9
+ // Both transports execute the same qualified provider transaction. Historical
10
+ // callers did not grant Discussions access; their evidence stays in the existing
11
+ // Actions artifacts and provider receipts instead of creating a Discussion.
12
+ export function publishCandidate(
13
+ request,
14
+ context,
15
+ {
16
+ historical = promoteReleaseCandidate,
17
+ current = publishWithDiscussion,
18
+ } = {},
19
+ ) {
20
+ assertHistoricalPromotionEffects(request);
21
+ const retained = historicalPromotionContext(
22
+ request["historical-inputs-json"],
23
+ );
24
+ if (
25
+ Boolean(retained) !==
26
+ (request["publisher-workflow-path"] === HISTORICAL_PUBLISHER)
27
+ )
28
+ throw new Error(
29
+ "Historical publisher requires its admitted compatibility context",
30
+ );
31
+ if (retained && request["resume-discussion-id"])
32
+ throw new Error(
33
+ "Historical publication cannot resume a Discussion transaction",
34
+ );
35
+ return (retained ? historical : current)(request, context);
36
+ }
@@ -50,7 +50,7 @@ export async function createReleaseDocuments({
50
50
  schema: RELEASE_INVOCATION_CONTRACT,
51
51
  publisher: {
52
52
  repository: "kungfu-systems/buildchain",
53
- workflow: ".github/workflows/.release-promote.yml",
53
+ workflow: request["publisher-workflow-path"] || ".github/workflows/.release-promote.yml",
54
54
  workflowSha: request["publisher-workflow-sha"],
55
55
  job: "apply",
56
56
  },
@@ -1,3 +1,4 @@
1
+ import { historicalRecoveryPins, verifyHistoricalCandidateRuntime } from "./compatibility-qualification.js";
1
2
  import { recoverDiscussionCandidate } from "../discussion/recovery.js";
2
3
  import { resolveReleaseCandidateArtifacts } from "../candidate/resolve.js";
3
4
  import { resumeFromCandidateRun } from "../recovery/candidate.js";
@@ -20,6 +21,7 @@ export async function qualifyPromotionCandidate(
20
21
  recoverDiscussion = recoverDiscussionCandidate,
21
22
  } = {},
22
23
  ) {
24
+ const expectedRuntime = historicalRecoveryPins(request, runtimeSha);
23
25
  const shared = {
24
26
  token,
25
27
  apiUrl,
@@ -41,7 +43,7 @@ export async function qualifyPromotionCandidate(
41
43
  targetRef: intent["target-ref"],
42
44
  });
43
45
  if (request["resume-candidate-run-id"])
44
- return recover({
46
+ return verifyHistoricalCandidateRuntime(await recover({
45
47
  ...shared,
46
48
  repository: request["resume-candidate-repository"],
47
49
  targetRepository: repository,
@@ -63,7 +65,7 @@ export async function qualifyPromotionCandidate(
63
65
  runtimeRoot,
64
66
  recoveryRunId,
65
67
  recoveryRunAttempt,
66
- });
68
+ }), expectedRuntime);
67
69
  return fresh({
68
70
  ...shared,
69
71
  repository,
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { installationRoot } from "../../runtime/installation-root.js";
4
+
5
+ export const HISTORICAL_PROMOTION_SCHEMA = "buildchain.historical-promotion/v1";
6
+ export const HISTORICAL_PUBLISHER = ".github/workflows/.release-historical.yml";
7
+ let fields, current;
8
+ function loadHistoricalFields() {
9
+ if (fields) return;
10
+ const root = installationRoot(import.meta.url);
11
+ fields = JSON.parse(
12
+ fs.readFileSync(
13
+ path.join(
14
+ root,
15
+ "contracts/fixtures/consumer-upgrade/release-v4.0.0.json",
16
+ ),
17
+ "utf8",
18
+ ),
19
+ ).inputs;
20
+ current = JSON.parse(
21
+ fs.readFileSync(
22
+ path.join(root, "contracts/promotion-request-v1.schema.json"),
23
+ "utf8",
24
+ ),
25
+ ).properties;
26
+ }
27
+ export const historicalEvidenceFileKeys = [
28
+ "release-passport-kfd-1-witness-jsons",
29
+ "release-passport-kfd-2-claim-jsons",
30
+ "release-passport-kfd-3-prebuild-witness-jsons",
31
+ "release-passport-kfd-3-artifact-witness-jsons",
32
+ "release-passport-impact-json",
33
+ ];
34
+ const excluded = new Set([
35
+ "buildchain-channel",
36
+ "buildchain-repository",
37
+ "buildchain-ref",
38
+ "buildchain-contract-lock-path",
39
+ "buildchain-alpha-contract-lock-path",
40
+ "buildchain-stable-contract-lock-path",
41
+ "branch-protection-bypass-users",
42
+ "branch-protection-bypass-teams",
43
+ ]);
44
+
45
+ export function historicalPromotionContext(value) {
46
+ if (!value) return null;
47
+ loadHistoricalFields();
48
+ const context = typeof value === "string" ? JSON.parse(value) : value;
49
+ if (
50
+ !context ||
51
+ context.schema !== HISTORICAL_PROMOTION_SCHEMA ||
52
+ Object.keys(context).some((key) => !["schema", "inputs"].includes(key)) ||
53
+ !context.inputs ||
54
+ typeof context.inputs !== "object" ||
55
+ Array.isArray(context.inputs)
56
+ )
57
+ throw new Error("Invalid historical promotion context");
58
+ for (const [key, value] of Object.entries(context.inputs)) {
59
+ const field = fields[key];
60
+ if (
61
+ !field ||
62
+ excluded.has(key) ||
63
+ (current[key] &&
64
+ ![
65
+ "publish-artifact-kind",
66
+ "publish-mode",
67
+ ...historicalEvidenceFileKeys,
68
+ ].includes(key)) ||
69
+ typeof value !== field.type ||
70
+ (field.type === "number" && !Number.isFinite(value))
71
+ )
72
+ throw new Error(`Unsupported historical promotion context field: ${key}`);
73
+ if (
74
+ /^resume-.*runtime-sha$/u.test(key) &&
75
+ value &&
76
+ !/^[a-f0-9]{40}$/u.test(value)
77
+ )
78
+ throw new Error(`Historical ${key} requires an exact runtime SHA`);
79
+ if (key === "publish-mode" && value !== "publish-final-version")
80
+ throw new Error(
81
+ "Historical publish-mode translation requires publish-final-version",
82
+ );
83
+ if (key === "publish-artifact-kind" && value !== "binary")
84
+ throw new Error("Historical artifact-kind translation requires binary");
85
+ }
86
+ return context;
87
+ }
88
+
89
+ export function historicalPublisherPath(value) {
90
+ return historicalPromotionContext(value)
91
+ ? HISTORICAL_PUBLISHER
92
+ : ".github/workflows/.release-promote.yml";
93
+ }
@@ -0,0 +1,48 @@
1
+ import { historicalEvidenceKeys } from "./historical-evidence.js";
2
+ import fs from "node:fs";
3
+ import { historicalPromotionContext } from "./compatibility-context.js";
4
+
5
+ export function historicalRecoveryPins(request, runtimeSha) {
6
+ const inputs = historicalPromotionContext(
7
+ request["historical-inputs-json"],
8
+ )?.inputs;
9
+ if (!inputs) return "";
10
+ const selected = inputs["resume-buildchain-runtime-sha"];
11
+ if (selected && selected !== runtimeSha)
12
+ throw new Error("Historical recovery runtime does not match its exact pin");
13
+ const expected = inputs["resume-expected-candidate-runtime-sha"] || "";
14
+ if (expected && !request["resume-candidate-run-id"])
15
+ throw new Error(
16
+ "Historical candidate runtime pin requires a candidate recovery run",
17
+ );
18
+ return expected;
19
+ }
20
+
21
+ export function verifyHistoricalCandidateRuntime(candidate, expected) {
22
+ if (expected) {
23
+ const passport = JSON.parse(
24
+ fs.readFileSync(candidate.paths.passport, "utf8"),
25
+ );
26
+ if (passport.buildchain?.sha !== expected)
27
+ throw new Error(
28
+ "Historical recovered candidate does not match its runtime pin",
29
+ );
30
+ }
31
+ return candidate;
32
+ }
33
+
34
+ // A retained input must never be accepted and then silently ignored. Each
35
+ // command requires its original execution/evidence contract before enabling it.
36
+ export function assertHistoricalPromotionEffects(request) {
37
+ const inputs =
38
+ historicalPromotionContext(request["historical-inputs-json"])?.inputs || {};
39
+ for (const [key, value] of Object.entries(inputs))
40
+ if (
41
+ key.endsWith("-command") &&
42
+ value &&
43
+ !historicalEvidenceKeys.includes(key)
44
+ )
45
+ throw new Error(
46
+ `Historical promotion command adapter is not implemented: ${key}`,
47
+ );
48
+ }