@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
@@ -57,7 +57,7 @@ export async function transferBuild(context, platform) {
57
57
  sourceSha: plan.source.sha,
58
58
  platformId: platform.id,
59
59
  platformName: platform.name,
60
- groups: [{ role: "payload", artifactName: names.payload, paths }],
60
+ groups: [{ role: "payload", artifactName: names.transfer, paths }],
61
61
  });
62
62
  artifacts.push({
63
63
  role: "relay",
@@ -71,7 +71,7 @@ export async function transferBuild(context, platform) {
71
71
  } else
72
72
  artifacts.push({
73
73
  role: "payload",
74
- ref: await upload(plan, names.payload, paths, sourceRoot),
74
+ ref: await upload(plan, names.transfer, paths, sourceRoot),
75
75
  });
76
76
  const requestRoot = path.join(
77
77
  sourceRoot,
@@ -23,6 +23,8 @@ export async function resolveBuildPlanAction(core, env) {
23
23
  runtimeRoot,
24
24
  runtime: JSON.parse(env.BUILDCHAIN_RUNTIME_SELECTION),
25
25
  configPath: core.getInput("config-path"),
26
+ compatibilityInputs: core.getInput("compatibility-inputs"),
27
+ compatibilityEnvironment: Object.fromEntries(Object.entries(env).filter(([key]) => key.startsWith("BUILDCHAIN_HISTORICAL_"))),
26
28
  workflow: {
27
29
  ref: core.getInput("workflow-ref", { required: true }),
28
30
  sha: workflowSha,
@@ -1,5 +1,7 @@
1
+ import { runtimeContractLockDrift } from "../../contracts/runtime-contract-inspection.js";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
4
+ import { containedBuildPath } from "../build-configuration.js";
3
5
  import { scanConsumerPolicy } from "../../consumer/policy-scan.js";
4
6
  import { validatePackageManagerContract } from "../package-manager.js";
5
7
 
@@ -16,11 +18,15 @@ export async function admitBuildSource({ plan, sourceRoot, runtimeRoot, workflow
16
18
  expectedInvocationChannel: plan.identity.channel,
17
19
  resolvedWorkflowSha: plan.entry.sha,
18
20
  resolvedRuntimeSha: plan.identity.sha,
19
- stableLockPath: ".buildchain/contract-lock.json",
20
- alphaLockPath: ".buildchain/alpha-contract-lock.json",
21
+ stableLockPath: plan.contract.stable_lock_path || ".buildchain/contract-lock.json",
22
+ alphaLockPath: plan.contract.alpha_lock_path || ".buildchain/alpha-contract-lock.json",
21
23
  });
22
24
  if (!policy.ok) throw new Error("Build source rejected by floating consumer policy");
23
25
  validatePackageManagerContract({ cwd: sourceRoot, expectedManager: "" });
24
26
  const contract = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "dist/site/buildchain-contract.json"), "utf8"));
25
- return { policy: policy.receipt, policy_root: policy.receiptRoot, contract_digest: contract.contractDigest, lock_status: plan.runtime?.origin || "contract-lock" };
27
+ const lock = JSON.parse(fs.readFileSync(containedBuildPath(sourceRoot, plan.contract.lock_path), "utf8"));
28
+ const lockDrift = runtimeContractLockDrift(lock, plan.identity.sha, contract.contractDigest);
29
+ return { policy: policy.receipt, policy_root: policy.receiptRoot, contract_digest: contract.contractDigest,
30
+ lock_status: plan.runtime?.origin || "contract-lock", lock_drift: lockDrift };
31
+
26
32
  }
@@ -0,0 +1,47 @@
1
+ import { containedBuildPath } from "../build-configuration.js";
2
+ import path from "node:path";
3
+
4
+ export function historicalBuildArtifacts(plan, inputs, root) {
5
+ const remaining = { ...inputs };
6
+ const fields = {
7
+ "artifact-name": "name",
8
+ "artifact-name-template": "name_template",
9
+ "expected-artifacts-json": "expected_json",
10
+ };
11
+ for (const [input, field] of Object.entries(fields)) {
12
+ if (!Object.hasOwn(remaining, input)) continue;
13
+ plan.artifacts[field] = remaining[input];
14
+ delete remaining[input];
15
+ }
16
+ plan.build.artifacts.name = plan.artifacts.name;
17
+ if (Object.hasOwn(remaining, "artifact-paths")) {
18
+ const paths = remaining["artifact-paths"]
19
+ .split(/\r?\n/u)
20
+ .map((item) => item.trim())
21
+ .filter(Boolean);
22
+ plan.build.artifacts.paths = paths;
23
+ plan.artifacts.paths = paths
24
+ .map((item) => {
25
+ const relative = path.posix.join(plan.project.cwd, item);
26
+ containedBuildPath(root, relative);
27
+ return relative;
28
+ })
29
+ .join("\n");
30
+ delete remaining["artifact-paths"];
31
+ }
32
+ const expected = JSON.parse(plan.artifacts.expected_json);
33
+ if (!expected || typeof expected !== "object" || Array.isArray(expected))
34
+ throw new Error("expected-artifacts-json must describe artifact checks");
35
+ for (const field of ["minFiles", "maxFiles", "minTotalBytes"])
36
+ if (
37
+ expected[field] !== undefined &&
38
+ (!Number.isSafeInteger(expected[field]) || expected[field] < 0)
39
+ )
40
+ throw new Error(`Invalid artifact check: ${field}`);
41
+ if (expected.requiredPaths !== undefined) {
42
+ if (!Array.isArray(expected.requiredPaths))
43
+ throw new Error("requiredPaths must be an array");
44
+ for (const item of expected.requiredPaths) containedBuildPath(root, item);
45
+ }
46
+ return remaining;
47
+ }
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs";
2
+ import { resolvePublishGate } from "../../release/promotion/publish-gate.js";
3
+ import path from "node:path";
4
+ import { historicalEntrySelection } from "../../runtime/entry/compatibility.js";
5
+ import {
6
+ containedBuildPath,
7
+ normalizeBuildConfiguration,
8
+ } from "../build-configuration.js";
9
+ import { applyHistoricalBuildInputs } from "./compatibility.js";
10
+ import { historicalBuildArtifacts } from "./compatibility-artifacts.js";
11
+ import { rootOf } from "./values.js";
12
+
13
+ export function historicalBuildLocator(root, locator, inputs) {
14
+ const directory = inputs["working-directory"];
15
+ if (!directory || directory === ".") return locator;
16
+ containedBuildPath(root, directory);
17
+ if (locator)
18
+ throw new Error(
19
+ "config-path cannot be combined with a historical working-directory",
20
+ );
21
+ const candidates = [".buildchain/buildchain.toml", "buildchain.toml"].map(
22
+ (item) => path.posix.join(directory, item),
23
+ );
24
+ const present = candidates.filter((item) =>
25
+ fs.existsSync(containedBuildPath(root, item)),
26
+ );
27
+ if (present.length !== 1)
28
+ throw new Error(
29
+ "Historical working-directory must contain one buildchain.toml",
30
+ );
31
+ return present[0];
32
+ }
33
+
34
+ function applySelectors(plan, remaining) {
35
+ const selection = historicalEntrySelection(
36
+ remaining,
37
+ `${plan.identity.repository}/${plan.identity.visible_workflow}@${plan.identity.ref}`,
38
+ );
39
+ plan.contract.lock_path = selection.lockPath;
40
+ plan.contract.stable_lock_path = selection.stableLock;
41
+ plan.contract.alpha_lock_path = selection.alphaLock;
42
+ for (const [input, expected] of [
43
+ ["buildchain-expected-channel", plan.identity.channel],
44
+ ["buildchain-expected-major", plan.identity.major],
45
+ ])
46
+ if (remaining[input] && remaining[input] !== expected)
47
+ throw new Error(`Historical ${input} does not match the called entry`);
48
+ for (const input of [
49
+ "buildchain-channel",
50
+ "buildchain-ref",
51
+ "buildchain-repository",
52
+ "buildchain-contract-lock-path",
53
+ "buildchain-stable-contract-lock-path",
54
+ "buildchain-alpha-contract-lock-path",
55
+ "buildchain-expected-channel",
56
+ "buildchain-expected-major",
57
+ "working-directory",
58
+ ])
59
+ delete remaining[input];
60
+ }
61
+
62
+ export function finishHistoricalBuildPlan(plan, inputs, root) {
63
+ if (!Object.keys(inputs).length) return;
64
+ const remaining = historicalBuildArtifacts(
65
+ plan,
66
+ applyHistoricalBuildInputs(plan, inputs),
67
+ root,
68
+ );
69
+ applySelectors(plan, remaining);
70
+ if (Object.hasOwn(remaining, "publish-channel")) {
71
+ const channel = remaining["publish-channel"];
72
+ plan.source.candidate_channel = channel === "major" ? "release"
73
+ : ["alpha", "release"].includes(channel) ? channel : "none";
74
+ delete remaining["publish-channel"];
75
+ }
76
+ delete remaining["publish-refs-json"];
77
+ if (remaining["artifact-relay-s3-role-arn"]) {
78
+ for (const kind of ["upload", "download"])
79
+ plan.environment.transfer[`${kind}_role_arn`] ||=
80
+ remaining["artifact-relay-s3-role-arn"];
81
+ delete remaining["artifact-relay-s3-role-arn"];
82
+ }
83
+ for (const [input, field] of [
84
+ ["gate-profile-aggregate-json", "gate_profile_json"],
85
+ ["release-candidate-family-evidence-json", "candidate_family_json"],
86
+ ]) {
87
+ if (!Object.hasOwn(remaining, input)) continue;
88
+ JSON.parse(remaining[input]);
89
+ plan.evidence[field] = remaining[input];
90
+ delete remaining[input];
91
+ }
92
+ if (Object.keys(remaining).length)
93
+ throw new Error(
94
+ `Unmapped historical build inputs: ${Object.keys(remaining).join(", ")}`,
95
+ );
96
+ plan.build = normalizeBuildConfiguration(plan.build);
97
+ plan.configuration_root = rootOf({ toml: plan.configuration_root, inputs });
98
+ plan.environment_root = rootOf(plan.environment);
99
+ plan.cache.toolchain_root = rootOf({
100
+ tools: plan.build.tools,
101
+ environment: plan.environment.tools,
102
+ });
103
+ plan.cache.policy_root = rootOf(plan.environment.cache);
104
+ }
105
+
106
+ export function historicalPublishGate(plan, inputs, eventName) {
107
+ if (plan.identity.visible_workflow !== ".github/workflows/build.yml") return;
108
+ const resolved = resolvePublishGate({
109
+ trusted: true,
110
+ publishChannel: inputs["publish-channel"] || "none",
111
+ publishRefsJson: inputs["publish-refs-json"] || "",
112
+ eventName,
113
+ ref: plan.source.ref,
114
+ });
115
+ plan.historical_publish_gate = {
116
+ trustedEvent: resolved.trusted,
117
+ channel: resolved.publishChannel,
118
+ allowed: resolved.publishAllowed,
119
+ reason: resolved.publishReason,
120
+ };
121
+ }
@@ -0,0 +1,162 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { installationRoot } from "../../runtime/installation-root.js";
4
+
5
+ const contract = JSON.parse(
6
+ fs.readFileSync(
7
+ path.join(
8
+ installationRoot(import.meta.url),
9
+ "contracts/fixtures/consumer-upgrade/build-v4.0.0.json",
10
+ ),
11
+ "utf8",
12
+ ),
13
+ );
14
+ const bindings =
15
+ JSON.parse(
16
+ fs.readFileSync(
17
+ path.join(
18
+ installationRoot(import.meta.url),
19
+ "architecture/consumer-upgrade.json",
20
+ ),
21
+ "utf8",
22
+ ),
23
+ ).entries.find((entry) => entry.adapter === "build-backbone")
24
+ ?.inputVariables || {};
25
+
26
+ // The old workflow boundary is typed before values enter the current planner.
27
+ // Empty modern calls preserve the TOML plan; historical defaults do not replace
28
+ // explicit TOML choices when GitHub expands all optional workflow inputs.
29
+ export function historicalBuildInputs(value = {}, environment = {}) {
30
+ if (typeof value === "string") value = JSON.parse(value || "{}");
31
+ if (!value || typeof value !== "object" || Array.isArray(value))
32
+ throw new Error("Historical build inputs must be an object");
33
+ const current = new Set([
34
+ "config-path",
35
+ "runtime-ref",
36
+ "contract-lock",
37
+ "resume-run-id",
38
+ "runtime-selection",
39
+ ]);
40
+ const result = {};
41
+ for (const [key, input] of Object.entries(value)) {
42
+ if (current.has(key)) continue;
43
+ const field = contract.inputs[key];
44
+ if (
45
+ !field ||
46
+ typeof input !== field.type ||
47
+ (typeof input === "number" && !Number.isFinite(input))
48
+ )
49
+ throw new Error(`Invalid historical build input: ${key}`);
50
+ if (input !== field.default) result[key] = input;
51
+ }
52
+ if (Object.keys(value).some((key) => Object.hasOwn(contract.inputs, key)))
53
+ for (const key of Object.keys(bindings)) {
54
+ const selected =
55
+ environment[
56
+ `BUILDCHAIN_HISTORICAL_${key.replaceAll("-", "_").toUpperCase()}`
57
+ ];
58
+ if (selected && !value[key]) result[key] = String(selected);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ const paths = {
64
+ "runner-preset": "environment.runners.preset",
65
+ "platforms-json": "environment.runners.platforms_json",
66
+ "control-runner-json": "environment.runners.control_json",
67
+ "aws-codebuild-project": "environment.runners.codebuild_project",
68
+ "aws-ec2-windows-runner-label": "environment.runners.windows_label",
69
+ "aws-ec2-macos-runner-label": "environment.runners.macos_label",
70
+ "self-hosted-offline-fallback": "environment.runners.offline_fallback",
71
+ "linux-container-preset": "environment.runners.container_preset",
72
+ "linux-container-image": "environment.runners.container_image",
73
+ "rustup-dist-server": "environment.tools.rustup_dist_server",
74
+ "rustup-update-root": "environment.tools.rustup_update_root",
75
+ "cargo-registry-index": "environment.tools.cargo_registry_index",
76
+ "checkout-cache-mode": "environment.checkout.mode",
77
+ "checkout-cache-mirror-url-template":
78
+ "environment.checkout.mirror_url_template",
79
+ "checkout-cache-reference-repository-template":
80
+ "environment.checkout.reference_repository_template",
81
+ "checkout-cache-fallback": "environment.checkout.fallback",
82
+ "checkout-cache-timeout-seconds": "environment.checkout.timeout_seconds",
83
+ "checkout-cache-github-timeout-seconds":
84
+ "environment.checkout.github_timeout_seconds",
85
+ "checkout-cache-fetch-attempts": "environment.checkout.fetch_attempts",
86
+ "checkout-history-mode": "environment.checkout.history_mode",
87
+ "artifact-transfer-mode": "environment.transfer.mode",
88
+ "artifact-relay-s3-bucket": "environment.transfer.bucket",
89
+ "artifact-relay-s3-region": "environment.transfer.region",
90
+ "artifact-relay-s3-prefix": "environment.transfer.prefix",
91
+ "artifact-relay-s3-upload-role-arn": "environment.transfer.upload_role_arn",
92
+ "artifact-relay-s3-download-role-arn":
93
+ "environment.transfer.download_role_arn",
94
+ "artifact-relay-s3-oidc-audience": "environment.transfer.oidc_audience",
95
+ "shifu-cache-profile-ref": "environment.cache.profile_ref",
96
+ "shifu-cache-profile-digest": "environment.cache.profile_digest",
97
+ "compiler-cache-provider": "environment.cache.provider",
98
+ "compiler-cache-platforms-json": "environment.cache.platforms_json",
99
+ "compiler-cache-required": "environment.cache.required",
100
+ "artifact-signing-request-upload-no-proxy":
101
+ "environment.signing.upload_no_proxy",
102
+ "credential-island-environment": "environment.signing.environment",
103
+ "fail-fast": "build.fail_fast",
104
+ "lifecycle-timeout-minutes": "build.timeout_minutes",
105
+ "artifact-retention-days": "build.artifacts.retention_days",
106
+ "artifact-compression-level": "build.artifacts.compression_level",
107
+ "release-candidate": "build.artifacts.release_candidate",
108
+ "artifact-finalization-command": "build.finalization.command",
109
+ "artifact-finalization-on-platform": "build.finalization.on_platform",
110
+ "verify-substage-evidence-path": "build.verification.substage_evidence_path",
111
+ "pre-upload-transport-smoke-scenario-path":
112
+ "build.transport_smoke.scenario_path",
113
+ "pre-upload-transport-smoke-artifact-root":
114
+ "build.transport_smoke.artifact_root",
115
+ "credential-island-macos-app-path": "build.macos_signing.app_path",
116
+ "credential-island-macos-platform-id": "build.macos_signing.platform",
117
+ "github-artifact-attestation-subject-path": "build.attestation.subject_path",
118
+ "github-artifact-attestation-platform-id": "build.attestation.platform",
119
+ "sample-process-tree": "build.diagnostics.sample_process_tree",
120
+ "process-sample-interval-ms": "build.diagnostics.sample_interval_ms",
121
+ "requested-parallelism": "build.diagnostics.requested_parallelism",
122
+ "buildchain-contract-compatibility-policy":
123
+ "build.contract.compatibility_policy",
124
+ "buildchain-contract-drift-issue-mode": "build.contract.drift_issue_mode",
125
+ };
126
+
127
+ export function applyHistoricalBuildInputs(plan, inputs) {
128
+ const remaining = { ...inputs };
129
+ for (const [input, field] of Object.entries(paths)) {
130
+ if (!Object.hasOwn(remaining, input)) continue;
131
+ const names = field.split(".");
132
+ const key = names.pop();
133
+ const target = names.reduce((value, name) => value[name], plan);
134
+ target[key] = remaining[input];
135
+ delete remaining[input];
136
+ }
137
+ for (const stage of ["install", "build", "verify"]) {
138
+ if (Object.hasOwn(remaining, `${stage}-command`)) {
139
+ plan.lifecycle[stage].command = remaining[`${stage}-command`];
140
+ plan.lifecycle[stage].configured = Boolean(remaining[`${stage}-command`]);
141
+ delete remaining[`${stage}-command`];
142
+ }
143
+ if (Object.hasOwn(remaining, `require-${stage}`)) {
144
+ plan.lifecycle[stage].required = remaining[`require-${stage}`];
145
+ delete remaining[`require-${stage}`];
146
+ }
147
+ }
148
+ for (const [input, tool] of [
149
+ ["node-version", "node"],
150
+ ["rust-toolchain", "rust"],
151
+ ]) {
152
+ if (Object.hasOwn(remaining, input)) {
153
+ plan.tools[tool] = plan.build.tools[tool] = remaining[input];
154
+ delete remaining[input];
155
+ }
156
+ if (Object.hasOwn(remaining, `setup-${tool}`)) {
157
+ plan.tools[`setup_${tool}`] = remaining[`setup-${tool}`];
158
+ delete remaining[`setup-${tool}`];
159
+ }
160
+ }
161
+ return remaining;
162
+ }
@@ -1,8 +1,11 @@
1
+ import { resolveBuildIdentity } from "./identity.js";
1
2
  import { installationRoot } from "../../runtime/installation-root.js";
2
3
  import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { loadBuildchainConfig } from "../../consumer/buildchain-config.js";
7
+ import { historicalBuildInputs } from "./compatibility.js";
8
+ import { historicalBuildLocator, finishHistoricalBuildPlan, historicalPublishGate } from "./compatibility-plan.js";
6
9
  import {
7
10
  containedBuildPath,
8
11
  discoverBuildConfiguration,
@@ -25,6 +28,8 @@ export function resolveBuildConfiguration({
25
28
  callerWorkflowRef = "",
26
29
  eventName = "",
27
30
  baseRef = "",
31
+ compatibilityInputs = {},
32
+ compatibilityEnvironment = {},
28
33
  environmentRegistry = JSON.parse(
29
34
  fs.readFileSync(
30
35
  path.join(runtimeRoot, "architecture/build-environments.json"),
@@ -36,43 +41,9 @@ export function resolveBuildConfiguration({
36
41
  if (!/^[a-f0-9]{40}$/u.test(value || ""))
37
42
  throw new Error(`${label} must be an exact SHA`);
38
43
  }
39
- const match = String(workflowRef).match(
40
- /^([^/]+\/[^/]+)\/(\.github\/workflows\/[^@]+)@(?:refs\/(?:heads|tags)\/)?(v([1-9]\d*)(-alpha)?)$/u,
41
- );
42
- if (!match || match[1] !== repository)
43
- throw new Error(
44
- "Ordinary builds require the exact called workflow identity on a floating channel",
45
- );
46
- const identity = {
47
- repository,
48
- ref: match[3],
49
- full_ref: `refs/tags/${match[3]}`,
50
- sha: workflowSha,
51
- channel: match[5] ? "alpha" : "stable",
52
- major: match[4],
53
- visible_workflow: match[2],
54
- };
55
- const callerPath = callerWorkflowRef
56
- .split("@")[0]
57
- .split("/.github/workflows/")[1];
58
- if (callerPath) {
59
- const caller = fs.readFileSync(
60
- containedBuildPath(root, `.github/workflows/${callerPath}`),
61
- "utf8",
62
- );
63
- const calls = [
64
- ...caller.matchAll(
65
- /uses:\s+kungfu-systems\/buildchain\/(\.github\/workflows\/(?:build|\.build)\.yml)@(v\d+(?:-alpha)?)\s*$/gmu,
66
- ),
67
- ].filter((call) => call[2] === identity.ref);
68
- const paths = [...new Set(calls.map((call) => call[1]))];
69
- if (paths.length !== 1)
70
- throw new Error(
71
- "Unable to derive one visible build workflow from the exact caller",
72
- );
73
- identity.visible_workflow = paths[0];
74
- }
75
- const project = discoverBuildConfiguration(root, locator);
44
+ const identity = resolveBuildIdentity({ root, workflowRef, workflowSha, repository, callerWorkflowRef });
45
+ const historical = historicalBuildInputs(compatibilityInputs, compatibilityEnvironment);
46
+ const project = discoverBuildConfiguration(root, historicalBuildLocator(root, locator, historical));
76
47
  const loaded = loadBuildchainConfig(path.resolve(root, project.cwd));
77
48
  const build = normalizeBuildConfiguration(loaded.config.build);
78
49
  if (
@@ -188,6 +159,8 @@ export function resolveBuildConfiguration({
188
159
  : ".buildchain/contract-lock.json",
189
160
  },
190
161
  };
162
+ historicalPublishGate(plan, historical, eventName);
163
+ finishHistoricalBuildPlan(plan, historical, root);
191
164
  for (const [stage, status] of Object.entries(plan.lifecycle)) {
192
165
  if (status.required && !status.configured)
193
166
  throw new Error(`buildchain.toml must declare lifecycle.${stage}`);
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import { containedBuildPath } from "../build-configuration.js";
1
3
  import { rootOf } from "./values.js";
2
4
  export function assertPlan(plan) {
3
5
  const { root, ...body } = plan;
@@ -43,3 +45,43 @@ export function bindBuildPlan({
43
45
  if (!workspace) throw new Error("Build workspace is required");
44
46
  return { plan, platform, workspace };
45
47
  }
48
+
49
+ export function resolveBuildIdentity({ root, workflowRef, workflowSha, repository, callerWorkflowRef }) {
50
+ const match = String(workflowRef).match(
51
+ /^([^/]+\/[^/]+)\/(\.github\/workflows\/[^@]+)@(?:refs\/(?:heads|tags)\/)?(v([1-9]\d*)(-alpha)?)$/u,
52
+ );
53
+ if (!match || match[1] !== repository)
54
+ throw new Error(
55
+ "Ordinary builds require the exact called workflow identity on a floating channel",
56
+ );
57
+ const identity = {
58
+ repository,
59
+ ref: match[3],
60
+ full_ref: `refs/tags/${match[3]}`,
61
+ sha: workflowSha,
62
+ channel: match[5] ? "alpha" : "stable",
63
+ major: match[4],
64
+ visible_workflow: match[2],
65
+ };
66
+ const callerPath = callerWorkflowRef
67
+ .split("@")[0]
68
+ .split("/.github/workflows/")[1];
69
+ if (callerPath) {
70
+ const caller = fs.readFileSync(
71
+ containedBuildPath(root, `.github/workflows/${callerPath}`),
72
+ "utf8",
73
+ );
74
+ const calls = [
75
+ ...caller.matchAll(
76
+ /uses:\s+kungfu-systems\/buildchain\/(\.github\/workflows\/(?:build|\.build)\.yml)@(v\d+(?:-alpha)?)\s*$/gmu,
77
+ ),
78
+ ].filter((call) => call[2] === identity.ref);
79
+ const paths = [...new Set(calls.map((call) => call[1]))];
80
+ if (paths.length !== 1)
81
+ throw new Error(
82
+ "Unable to derive one visible build workflow from the exact caller",
83
+ );
84
+ identity.visible_workflow = paths[0];
85
+ }
86
+ return identity;
87
+ }
@@ -9,6 +9,7 @@ export function lifecycleOptions(plan, platform, stage, root) {
9
9
  cwd: path.join(root, plan.project.cwd),
10
10
  workspace: root,
11
11
  stageName: stage,
12
+ command: plan.lifecycle[stage]?.command || "",
12
13
  required: plan.lifecycle[stage]?.required || false,
13
14
  timeoutMinutes: plan.build.timeout_minutes,
14
15
  artifactName: names.payload,
@@ -34,6 +34,8 @@ export async function resolveBuildPlan(request, providers) {
34
34
  callerWorkflowRef: workflow.callerRef,
35
35
  eventName: event.name,
36
36
  baseRef: source.baseRef,
37
+ compatibilityInputs: request.compatibilityInputs,
38
+ compatibilityEnvironment: request.compatibilityEnvironment,
37
39
  });
38
40
  const git = (...args) =>
39
41
  execFileSync("git", ["-C", sourceRoot, ...args], {
@@ -58,7 +58,7 @@ export async function aggregate(context, plan, jobs, executions) {
58
58
  runId: plan.run.id,
59
59
  runAttempt: plan.run.attempt,
60
60
  },
61
- publishGate: {
61
+ publishGate: plan.historical_publish_gate || {
62
62
  trustedEvent: true,
63
63
  channel: "none",
64
64
  allowed: false,
@@ -94,7 +94,9 @@ export async function aggregate(context, plan, jobs, executions) {
94
94
  artifacts: await readProviderArtifacts(plan.run),
95
95
  platforms: plan.platforms,
96
96
  artifactName: plan.artifacts.name,
97
- artifactNameTemplate: "{artifact}-final-{platform}-{sha}",
97
+ artifactNameTemplate: plan.identity.visible_workflow === ".github/workflows/build.yml"
98
+ ? plan.artifacts.name_template || "{artifact}-{platform}-{sha}"
99
+ : "{artifact}-final-{platform}-{sha}",
98
100
  sourceSha: plan.source.sha,
99
101
  sourceRef: plan.source.ref,
100
102
  repository: plan.run.repository,
@@ -0,0 +1,24 @@
1
+ import { rootOf } from "../plan/values.js";
2
+
3
+ export function qualifyHistoricalBuild(conclusion, value) {
4
+ if (conclusion !== "success")
5
+ throw new Error(`Build did not qualify: ${conclusion || "missing"}`);
6
+ const result = typeof value === "string" ? JSON.parse(value) : value;
7
+ if (!result || typeof result !== "object" || Array.isArray(result))
8
+ throw new Error("Missing rooted build result");
9
+ const { root, ...body } = result;
10
+ if (
11
+ result.schema !== "buildchain.build-result/v1" ||
12
+ result.status !== "success" ||
13
+ root !== rootOf(body)
14
+ )
15
+ throw new Error("Build result failed its contract or content root");
16
+ return result;
17
+ }
18
+
19
+ export function qualifyHistoricalBuildAction(core) {
20
+ qualifyHistoricalBuild(
21
+ core.getInput("conclusion", { required: true }),
22
+ core.getInput("result", { required: true }),
23
+ );
24
+ }
@@ -1,3 +1,4 @@
1
+ import { historicalBuildOutputs } from "./historical-outputs.js";
1
2
  import { aggregate } from "./aggregation.js";
2
3
  import { controllerReceipt } from "./controller-receipt.js";
3
4
  import path from "node:path";
@@ -97,6 +98,22 @@ async function finalizeBuild(context, jobs) {
97
98
  ["diagnostics-summary.json"],
98
99
  file(".buildchain/artifacts"),
99
100
  );
101
+ let historicalOutputs;
102
+ if (plan.identity.visible_workflow === ".github/workflows/build.yml") {
103
+ artifacts.controller_plan = await upload(
104
+ plan,
105
+ `${plan.artifacts.name}-controller-plan-${plan.source.sha}`,
106
+ ["plan.json"],
107
+ file(".buildchain/controller"),
108
+ );
109
+ historicalOutputs = historicalBuildOutputs(
110
+ plan,
111
+ artifacts,
112
+ controller,
113
+ aggregated.summary,
114
+ file,
115
+ );
116
+ }
100
117
  const result = {
101
118
  schema: "buildchain.build-result/v1",
102
119
  status: "success",
@@ -104,6 +121,7 @@ async function finalizeBuild(context, jobs) {
104
121
  source: plan.source,
105
122
  runtime: plan.identity,
106
123
  artifacts,
124
+ ...(historicalOutputs ? { historical_outputs: historicalOutputs } : {}),
107
125
  };
108
126
  const rooted = { ...result, root: rootOf(result) };
109
127
  await publishRecord(