@kungfu-tech/buildchain 4.0.8 → 4.0.9-alpha.0

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 (51) hide show
  1. package/AGENTS.md +12 -19
  2. package/CONTRIBUTING.md +6 -9
  3. package/README.md +11 -1
  4. package/architecture/agent-change-map.md +11 -4
  5. package/architecture/build-environments.json +71 -0
  6. package/architecture/build-orchestration.json +18 -0
  7. package/architecture/ci-lane-change-budget.json +34 -2
  8. package/architecture/internal-capabilities.json +17 -4
  9. package/architecture/maintainability-debt.json +34 -123
  10. package/architecture/maintainability-policy.json +19 -210
  11. package/architecture/release-topology.json +6 -5
  12. package/architecture/universal-workflow-bootstrap.json +5 -4
  13. package/architecture/universal-workflow-train-admission.json +2 -2
  14. package/bin/buildchain.mjs +1 -1
  15. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  16. package/dist/site/buildchain-contract.json +41 -1131
  17. package/dist/site/buildchain-site.json +39 -144
  18. package/dist/site/capability-registry.json +1 -1
  19. package/dist/site/cli-registry.json +1 -1
  20. package/dist/site/controller-registry.json +30 -868
  21. package/dist/site/kfd-claims.json +132 -394
  22. package/dist/site/kfd-upstream-aggregate.json +1 -1
  23. package/dist/site/manual-registry.json +6 -6
  24. package/dist/site/node-api-registry.json +34 -34
  25. package/dist/site/page-registry.json +30 -135
  26. package/dist/site/public-surface-audit.json +81 -401
  27. package/dist/site/publication-registry.json +2 -2
  28. package/dist/site/site-manifest.json +8 -8
  29. package/dist/site/workflow-registry.json +135 -391
  30. package/docs/MAP.md +5 -4
  31. package/docs/cli-reference.md +1 -1
  32. package/docs/getting-started.md +8 -7
  33. package/docs/node-api-reference.md +79 -79
  34. package/docs/reusable-build-surface.md +159 -1430
  35. package/docs/runtime-train-validation.md +45 -172
  36. package/package.json +1 -1
  37. package/packages/core/build-configuration.js +117 -0
  38. package/packages/core/buildchain-config.js +2 -0
  39. package/packages/core/buildchain-contract.js +14 -44
  40. package/packages/core/controller-evidence.js +2 -9
  41. package/scripts/buildchain-cli-help.mjs +1 -1
  42. package/scripts/check-floating-consumer-policy-contract.mjs +4 -7
  43. package/scripts/check-inventory.mjs +81 -54
  44. package/scripts/check-universal-workflow-bootstrap.mjs +12 -2
  45. package/scripts/generate-channel-build-workflow.mjs +50 -410
  46. package/scripts/generate-site-bundle.mjs +2 -1
  47. package/scripts/generate-universal-workflow-facades.mjs +1 -0
  48. package/scripts/init-repo.mjs +25 -26
  49. package/scripts/resolve-build-configuration.mjs +105 -0
  50. package/scripts/universal-workflow-self-dogfood.mjs +8 -1
  51. package/scripts/verify-golden-path.mjs +3 -3
@@ -1,429 +1,69 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import fs from "node:fs";
4
- import { writeWorkflowSource } from "./workflow-taxonomy.mjs";
5
3
  import path from "node:path";
6
4
  import { fileURLToPath } from "node:url";
7
- import { migrateUniversalWorkflowFacade } from "./generate-universal-workflow-facades.mjs";
8
-
9
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
- const sourcePath = path.join(root, ".github/workflows/.build.yml");
11
- const targetPath = path.join(root, ".github/workflows/build.yml");
12
-
13
- function blockBetween(source, start, end) {
14
- const startIndex = source.indexOf(start);
15
- const endIndex = source.indexOf(end, startIndex + start.length);
16
- if (startIndex < 0 || endIndex < 0) throw new Error(`unable to locate workflow block: ${start.trim()}`);
17
- return source.slice(startIndex + start.length, endIndex);
18
- }
19
-
20
- function inputNames(inputBlock) {
21
- return [...inputBlock.matchAll(/^ ([a-z0-9-]+):$/gm)].map((match) => match[1]);
22
- }
23
-
24
- function routerInputs(inputBlock) {
25
- const adjusted = inputBlock.replace(
26
- /( buildchain-contract-lock-path:\n(?: .*\n)*? default:) "buildchain\.contract-lock\.json"/,
27
- '$1 ""',
28
- );
29
- return [
30
- " buildchain-channel:",
31
- ' description: "Buildchain runtime channel: auto, alpha, or stable"',
32
- ' default: "auto"',
33
- " type: string",
34
- " required: false",
35
- " buildchain-alpha-contract-lock-path:",
36
- ' description: "Consumer-owned contract lock selected for the alpha channel"',
37
- ' default: ".buildchain/alpha-contract-lock.json"',
38
- " type: string",
39
- " required: false",
40
- " buildchain-stable-contract-lock-path:",
41
- ' description: "Consumer-owned contract lock selected for the stable channel"',
42
- ' default: ".buildchain/contract-lock.json"',
43
- " type: string",
44
- " required: false",
45
- adjusted.trimEnd(),
46
- ].join("\n");
47
- }
48
-
49
- function routerOutputs(outputBlock) {
50
- const forwarded = outputBlock
51
- .replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.override.outputs.$1 || jobs.alpha.outputs.$1 || jobs.stable.outputs.$1 }}")
52
- .replaceAll("build-lifecycle", "build-channel-router")
53
- .replace("value: ${{ jobs.override.outputs.controller-plan-artifact || jobs.alpha.outputs.controller-plan-artifact || jobs.stable.outputs.controller-plan-artifact }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-artifact }}")
54
- .replace("value: ${{ jobs.override.outputs.controller-plan-json || jobs.alpha.outputs.controller-plan-json || jobs.stable.outputs.controller-plan-json }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-json }}")
55
- .replace("value: ${{ jobs.override.outputs.controller-plan-digest || jobs.alpha.outputs.controller-plan-digest || jobs.stable.outputs.controller-plan-digest }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-digest }}")
56
- .replace("value: ${{ jobs.override.outputs.controller-receipt-artifact || jobs.alpha.outputs.controller-receipt-artifact || jobs.stable.outputs.controller-receipt-artifact }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-artifact }}")
57
- .replace("value: ${{ jobs.override.outputs.controller-receipt-json || jobs.alpha.outputs.controller-receipt-json || jobs.stable.outputs.controller-receipt-json }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-json }}")
58
- .replace("value: ${{ jobs.override.outputs.controller-receipt-digest || jobs.alpha.outputs.controller-receipt-digest || jobs.stable.outputs.controller-receipt-digest }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-digest }}")
59
- .replace("value: ${{ jobs.override.outputs.controller-receipt-status || jobs.alpha.outputs.controller-receipt-status || jobs.stable.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}")
60
- .replace(
61
- /( credential-island-macos-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.override\.outputs\.artifact-name \|\| jobs\.alpha\.outputs\.artifact-name \|\| jobs\.stable\.outputs\.artifact-name \}\}/,
62
- "$1 ${{ jobs.override.outputs.credential-island-macos-artifact || jobs.alpha.outputs.credential-island-macos-artifact || jobs.stable.outputs.credential-island-macos-artifact }}",
63
- )
64
- .replace(
65
- /( credential-island-macos-manifest-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.override\.outputs\.manifest-artifact-name \|\| jobs\.alpha\.outputs\.manifest-artifact-name \|\| jobs\.stable\.outputs\.manifest-artifact-name \}\}/,
66
- "$1 ${{ jobs.override.outputs.credential-island-macos-manifest-artifact || jobs.alpha.outputs.credential-island-macos-manifest-artifact || jobs.stable.outputs.credential-island-macos-manifest-artifact }}",
67
- );
68
- return [
69
- " buildchain-channel:",
70
- ' description: "Resolved Buildchain channel: alpha or stable"',
71
- " value: ${{ jobs.resolve-channel.outputs.channel }}",
72
- " buildchain-channel-selection-source:",
73
- ' description: "Evidence source used to select the Buildchain channel"',
74
- " value: ${{ jobs.resolve-channel.outputs.selection-source }}",
75
- " buildchain-channel-reason:",
76
- ' description: "Human-readable Buildchain channel selection reason"',
77
- " value: ${{ jobs.resolve-channel.outputs.reason }}",
78
- forwarded.trimEnd(),
79
- ].join("\n");
80
- }
81
-
82
- function forwardedInputs(names) {
83
- return names
84
- .map((name) => {
85
- if (name === "buildchain-ref") return ` buildchain-ref: \${{ needs.resolve-channel.outputs.buildchain-ref }}`;
86
- if (name === "buildchain-expected-channel") {
87
- return " buildchain-expected-channel: ${{ needs.resolve-channel.outputs.channel }}";
88
- }
89
- if (name === "buildchain-expected-major") {
90
- return " buildchain-expected-major: ${{ needs.resolve-channel.outputs.major }}";
91
- }
92
- if (name === "buildchain-contract-lock-path") {
93
- return " buildchain-contract-lock-path: ${{ needs.resolve-channel.outputs.contract-lock-path }}";
94
- }
95
- if (name === "buildchain-visible-workflow") {
96
- return " buildchain-visible-workflow: .github/workflows/build.yml";
97
- }
98
- return ` ${name}: \${{ inputs.${name} }}`;
99
- })
100
- .join("\n");
101
- }
102
-
103
- function routerControllerPlanJob() {
104
- return ` controller-plan:
105
- name: Plan channel router controller evidence
106
- needs: resolve-channel
107
- runs-on: \${{ fromJSON(inputs.control-runner-json) }}
108
- outputs:
109
- controller-plan-artifact: \${{ steps.names.outputs.controller-plan-artifact }}
110
- controller-plan-json: \${{ steps.plan.outputs.controller-plan-json }}
111
- controller-plan-digest: \${{ steps.plan.outputs.controller-plan-digest }}
112
- runtime-sha: \${{ steps.identities.outputs.runtime-sha }}
113
- steps:
114
- - name: Checkout Buildchain controller workflow shell
115
- uses: actions/checkout@v7.0.0
116
- with:
117
- repository: \${{ needs.resolve-channel.outputs.router-repository }}
118
- ref: \${{ needs.resolve-channel.outputs.router-sha }}
119
- path: .buildchain/controller-runtime
120
- persist-credentials: false
121
-
122
- - name: Resolve controller workflow shell identity
123
- id: identities
124
- shell: bash
125
- run: |
126
- runtime_sha="$(git -C .buildchain/controller-runtime rev-parse HEAD)"
127
- contract_digest="$(node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(".buildchain/controller-runtime/dist/site/buildchain-contract.json","utf8")); process.stdout.write(value.contractDigest)')"
128
- {
129
- echo "runtime-sha=\${runtime_sha}"
130
- echo "contract-digest=\${contract_digest}"
131
- } >> "$GITHUB_OUTPUT"
132
-
133
- - name: Create channel router controller plan
134
- id: plan
135
- run: node .buildchain/controller-runtime/scripts/controller-evidence.mjs --mode plan
136
- env:
137
- BUILDCHAIN_CONTROLLER_ID: build-channel-router
138
- BUILDCHAIN_CONTROLLER_SOURCE_REPOSITORY: \${{ github.repository }}
139
- BUILDCHAIN_CONTROLLER_SOURCE_SHA: \${{ github.sha }}
140
- BUILDCHAIN_CONTROLLER_RUNTIME_REF: \${{ needs.resolve-channel.outputs.router-ref }}
141
- BUILDCHAIN_CONTROLLER_RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
142
- BUILDCHAIN_CONTROLLER_CONTRACT_DIGEST: \${{ steps.identities.outputs.contract-digest }}
143
- BUILDCHAIN_CONTROLLER_REGISTRY: .buildchain/controller-runtime/dist/site/controller-registry.json
144
- BUILDCHAIN_CONTROLLER_INPUTS_JSON: \${{ toJSON(inputs) }}
145
- BUILDCHAIN_CONTROLLER_INPUT_BOUNDARY: workflow-call
146
- BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
147
-
148
- - name: Resolve channel router controller plan artifact
149
- id: names
150
- run: echo "controller-plan-artifact=buildchain-channel-controller-plan-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
151
- env:
152
- SOURCE_SHA: \${{ github.sha }}
153
-
154
- - name: Upload channel router controller plan
155
- uses: actions/upload-artifact@v7.0.1
156
- with:
157
- name: \${{ steps.names.outputs.controller-plan-artifact }}
158
- path: .buildchain/controller/plan.json
159
- if-no-files-found: error`;
160
- }
161
-
162
- function routerControllerReceiptJob() {
163
- return ` controller-receipt:
164
- name: Finalize channel router controller evidence
165
- needs:
166
- - resolve-channel
167
- - controller-plan
168
- - override
169
- - alpha
170
- - stable
171
- if: \${{ always() && needs.controller-plan.result == 'success' }}
172
- runs-on: \${{ fromJSON(inputs.control-runner-json) }}
173
- outputs:
174
- controller-receipt-artifact: \${{ steps.names.outputs.controller-receipt-artifact }}
175
- controller-receipt-json: \${{ steps.receipt.outputs.controller-receipt-json }}
176
- controller-receipt-digest: \${{ steps.receipt.outputs.controller-receipt-digest }}
177
- controller-receipt-status: \${{ steps.receipt.outputs.controller-receipt-status }}
178
- steps:
179
- - name: Checkout Buildchain controller workflow shell
180
- uses: actions/checkout@v7.0.0
181
- with:
182
- repository: \${{ needs.resolve-channel.outputs.router-repository }}
183
- ref: \${{ needs.controller-plan.outputs.runtime-sha }}
184
- path: .buildchain/controller-runtime
185
- persist-credentials: false
186
-
187
- - name: Download channel router controller plan
188
- uses: actions/download-artifact@v7.0.0
189
- with:
190
- name: \${{ needs.controller-plan.outputs.controller-plan-artifact }}
191
- path: .buildchain/controller
192
-
193
- - name: Create channel router controller receipt
194
- id: receipt
195
- run: node .buildchain/controller-runtime/scripts/controller-evidence.mjs --mode receipt
196
- env:
197
- BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
198
- BUILDCHAIN_CONTROLLER_STAGES_JSON: >-
199
- [{"id":"resolve-channel","status":"\${{ needs.resolve-channel.result }}"},{"id":"override","status":"\${{ needs.override.result }}"},{"id":"alpha","status":"\${{ needs.alpha.result }}"},{"id":"stable","status":"\${{ needs.stable.result }}"},{"id":"aggregate","status":"\${{ needs.resolve-channel.outputs.runtime-override == 'true' && needs.override.result || needs.resolve-channel.outputs.channel == 'alpha' && needs.alpha.result || needs.stable.result }}"}]
200
- BUILDCHAIN_CONTROLLER_EVIDENCE_JSON: \${{ (needs.override.outputs.controller-receipt-digest != '' || needs.alpha.outputs.controller-receipt-digest != '' || needs.stable.outputs.controller-receipt-digest != '') && format('[{{"kind":"nested-controller-receipt","digest":"{0}"}}]', needs.override.outputs.controller-receipt-digest || needs.alpha.outputs.controller-receipt-digest || needs.stable.outputs.controller-receipt-digest) || '[]' }}
201
- BUILDCHAIN_CONTROLLER_REASON_CODE: \${{ ((needs.resolve-channel.outputs.runtime-override == 'true' && needs.override.result == 'success') || (needs.resolve-channel.outputs.runtime-override != 'true' && needs.resolve-channel.outputs.channel == 'alpha' && needs.alpha.result == 'success') || (needs.resolve-channel.outputs.runtime-override != 'true' && needs.resolve-channel.outputs.channel == 'stable' && needs.stable.result == 'success')) && '' || 'nested-build-incomplete' }}
202
- BUILDCHAIN_CONTROLLER_REASON_SUMMARY: Nested build controller did not complete successfully
203
- BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT: buildchain-channel-controller-receipt-\${{ github.sha }}
204
- BUILDCHAIN_CONTROLLER_RECEIPT_PATH: .buildchain/controller/receipt.json
205
-
206
- - name: Resolve channel router controller receipt artifact
207
- id: names
208
- run: echo "controller-receipt-artifact=buildchain-channel-controller-receipt-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
209
- env:
210
- SOURCE_SHA: \${{ github.sha }}
211
-
212
- - name: Upload channel router controller receipt
213
- if: \${{ always() && steps.receipt.outcome == 'success' }}
214
- uses: actions/upload-artifact@v7.0.1
215
- with:
216
- name: \${{ steps.names.outputs.controller-receipt-artifact }}
217
- path: .buildchain/controller/receipt.json
218
- if-no-files-found: error
219
-
220
- - name: Enforce qualifying channel router controller receipt
221
- if: \${{ steps.receipt.outputs.controller-receipt-qualifying != 'true' }}
222
- run: |
223
- echo "::error::channel router controller receipt is not qualifying: \${{ steps.receipt.outputs.controller-receipt-status }}"
224
- exit 1`;
225
- }
5
+ import { writeWorkflowSource } from "./workflow-taxonomy.mjs";
226
6
 
227
- function routerAggregateJob() {
228
- return ` summarize:
229
- name: Summarize build contract
230
- needs:
231
- - resolve-channel
232
- - override
233
- - alpha
234
- - stable
235
- - controller-receipt
236
- if: \${{ always() }}
237
- runs-on: \${{ fromJSON(inputs.control-runner-json) }}
238
- steps:
239
- - name: Enforce public channel router aggregate
240
- shell: bash
241
- env:
242
- CHANNEL: \${{ needs.resolve-channel.outputs.channel }}
243
- RUNTIME_OVERRIDE: \${{ needs.resolve-channel.outputs.runtime-override }}
244
- OVERRIDE_RESULT: \${{ needs.override.result }}
245
- ALPHA_RESULT: \${{ needs.alpha.result }}
246
- STABLE_RESULT: \${{ needs.stable.result }}
247
- CONTROLLER_RECEIPT_RESULT: \${{ needs.controller-receipt.result }}
248
- run: |
249
- set -euo pipefail
250
- selected_result="\${STABLE_RESULT}"
251
- if [[ "\${RUNTIME_OVERRIDE}" = "true" ]]; then
252
- selected_result="\${OVERRIDE_RESULT}"
253
- elif [[ "\${CHANNEL}" = "alpha" ]]; then
254
- selected_result="\${ALPHA_RESULT}"
255
- fi
256
- if [[ "\${selected_result}" != "success" || "\${CONTROLLER_RECEIPT_RESULT}" != "success" ]]; then
257
- echo "::error::Buildchain channel router did not qualify: channel=\${CHANNEL} selected=\${selected_result} controller-receipt=\${CONTROLLER_RECEIPT_RESULT}"
258
- exit 1
259
- fi
260
- echo "Buildchain channel router aggregate passed."`;
7
+ const root = path.resolve(import.meta.dirname, "..");
8
+ function block(source, start, end) {
9
+ const first = source.indexOf(start);
10
+ const last = source.indexOf(end, first + start.length);
11
+ if (first < 0 || last < 0) throw new Error(`Missing build contract block: ${start.trim()}`);
12
+ return source.slice(first + start.length, last).trimEnd();
261
13
  }
262
14
 
263
- function generateChannelBuildWorkflowBase(source) {
264
- const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
265
- const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
266
- const outputs = blockBetween(source, " outputs:\n", "\npermissions:\n");
267
- const names = inputNames(inputs);
268
- if (!names.includes("buildchain-ref") || !names.includes("publish-channel")) {
269
- throw new Error("source build workflow is missing channel-router inputs");
15
+ export function generateChannelBuildWorkflow(source) {
16
+ const inputs = block(source, " inputs:\n", " secrets:\n");
17
+ if (JSON.stringify([...inputs.matchAll(/^ ([a-z0-9-]+):$/gmu)].map((match) => match[1])) !== '["config-path"]') {
18
+ throw new Error("Ordinary builds expose only the optional configuration locator");
270
19
  }
271
- return `# Generated by scripts/generate-channel-build-workflow.mjs. Do not edit directly.\nname: Buildchain Channel Build\n\non:\n workflow_call:\n inputs:\n${routerInputs(inputs)}\n secrets:\n${secrets.trimEnd()}\n outputs:\n${routerOutputs(outputs)}\n\npermissions:\n contents: read\n issues: write\n id-token: write\n\njobs:\n resolve-channel:\n name: Resolve Buildchain channel\n runs-on: ubuntu-24.04\n outputs:\n channel: \${{ steps.channel.outputs.channel }}\n buildchain-ref: \${{ steps.channel.outputs.buildchain-ref }}\n contract-lock-path: \${{ steps.lock.outputs.path }}\n selection-source: \${{ steps.channel.outputs.selection-source }}\n reason: \${{ steps.channel.outputs.reason }}\n steps:\n - name: Resolve router source\n id: router\n shell: bash\n env:\n BUILDCHAIN_ROUTER_WORKFLOW_REF: \${{ job.workflow_ref }}\n run: |\n set -euo pipefail\n workflow_ref=\"\${BUILDCHAIN_ROUTER_WORKFLOW_REF}\"\n repository=\"\${workflow_ref%%/.github/workflows/*}\"\n ref=\"\${workflow_ref##*@}\"\n ref=\"\${ref#refs/heads/}\"\n ref=\"\${ref#refs/tags/}\"\n if [[ -z \"\${repository}\" || \"\${repository}\" = \"\${workflow_ref}\" || -z \"\${ref}\" ]]; then\n echo \"Unable to resolve Buildchain router source from job.workflow_ref=\${workflow_ref}\" >&2\n exit 1\n fi\n {\n echo \"repository=\${repository}\"\n echo \"ref=\${ref}\"\n } >> \"\${GITHUB_OUTPUT}\"\n\n - name: Checkout Buildchain router\n uses: actions/checkout@v7.0.0\n with:\n repository: \${{ steps.router.outputs.repository }}\n ref: \${{ steps.router.outputs.ref }}\n path: .buildchain/router\n persist-credentials: false\n\n - name: Resolve channel\n id: channel\n shell: bash\n env:\n BUILDCHAIN_CHANNEL: \${{ inputs.buildchain-channel }}\n BUILDCHAIN_REQUESTED_REF: \${{ inputs.buildchain-ref }}\n BUILDCHAIN_PUBLISH_CHANNEL: \${{ inputs.publish-channel }}\n BUILDCHAIN_EVENT_NAME: \${{ github.event_name }}\n BUILDCHAIN_GIT_REF: \${{ github.ref }}\n BUILDCHAIN_RELEASE_PRERELEASE: \${{ github.event.release.prerelease }}\n BUILDCHAIN_ROUTER_REF: \${{ steps.router.outputs.ref }}\n run: |\n node .buildchain/router/scripts/buildchain-channel-router.mjs \\\n --cwd .buildchain/router \\\n --channel \"\${BUILDCHAIN_CHANNEL}\" \\\n --buildchain-ref \"\${BUILDCHAIN_REQUESTED_REF}\" \\\n --publish-channel \"\${BUILDCHAIN_PUBLISH_CHANNEL}\" \\\n --event-name \"\${BUILDCHAIN_EVENT_NAME}\" \\\n --ref \"\${BUILDCHAIN_GIT_REF}\" \\\n --release-prerelease \"\${BUILDCHAIN_RELEASE_PRERELEASE}\" \\\n --router-ref \"\${BUILDCHAIN_ROUTER_REF}\"\n\n - name: Select consumer contract lock\n id: lock\n shell: bash\n env:\n EXPLICIT_PATH: \${{ inputs.buildchain-contract-lock-path }}\n ALPHA_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}\n STABLE_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}\n CHANNEL: \${{ steps.channel.outputs.channel }}\n run: |\n set -euo pipefail\n path=\"\${EXPLICIT_PATH}\"\n if [[ -z \"\${path}\" ]]; then\n if [[ \"\${CHANNEL}\" = \"alpha\" ]]; then\n path=\"\${ALPHA_PATH}\"\n else\n path=\"\${STABLE_PATH}\"\n fi\n fi\n echo \"path=\${path}\" >> \"\${GITHUB_OUTPUT}\"\n\n build:\n name: Build with resolved channel\n needs: resolve-channel\n uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n issues: write\n id-token: write\n with:\n${forwardedInputs(names)}\n secrets: inherit\n`;
272
- }
273
-
274
- function splitBuildJobsByChannel(workflow, names, major) {
275
- const marker = "\n build:\n";
276
- const index = workflow.indexOf(marker);
277
- if (index < 0) throw new Error("generated workflow is missing the build job");
278
- const inputs = forwardedInputs(names);
279
- const jobs = `
280
- override:
281
- name: Build trusted runtime override
282
- needs:
283
- - resolve-channel
284
- - controller-plan
285
- if: \${{ needs.resolve-channel.outputs.runtime-override == 'true' }}
286
- uses: ./.github/workflows/.build.yml
287
- permissions:
288
- actions: read
289
- contents: read
290
- issues: write
291
- id-token: write
292
- with:
20
+ const secrets = block(source, " secrets:\n", " outputs:\n");
21
+ const outputs = block(source, " outputs:\n", "\npermissions:\n")
22
+ .replace(/(^ ([a-z0-9-]+):\n(?: description:.*\n)*) value:.*$/gmu, "$1 value: ${{ jobs.build.outputs.$2 }}");
23
+ return `# Generated by scripts/generate-channel-build-workflow.mjs. Do not edit directly.
24
+ name: Buildchain Build
25
+
26
+ on:
27
+ workflow_call:
28
+ inputs:
293
29
  ${inputs}
294
- secrets: inherit
295
-
296
- alpha:
297
- name: Build alpha channel
298
- needs:
299
- - resolve-channel
300
- - controller-plan
301
- if: \${{ needs.resolve-channel.outputs.runtime-override != 'true' && needs.resolve-channel.outputs.channel == 'alpha' }}
302
- uses: ./.github/workflows/.build.yml
303
- permissions:
304
- actions: read
305
- contents: read
306
- issues: write
307
- id-token: write
308
- with:
309
- ${inputs}
310
- secrets: inherit
311
-
312
- stable:
313
- name: Build stable channel
314
- needs:
315
- - resolve-channel
316
- - controller-plan
317
- if: \${{ needs.resolve-channel.outputs.runtime-override != 'true' && needs.resolve-channel.outputs.channel == 'stable' }}
30
+ secrets:
31
+ ${secrets}
32
+ outputs:
33
+ buildchain-channel:
34
+ description: "Channel bound to the called workflow ref"
35
+ value: \${{ jobs.build.outputs.buildchain-runtime-class }}
36
+ buildchain-channel-selection-source:
37
+ description: "Runtime identity authority"
38
+ value: "called-workflow"
39
+ buildchain-channel-reason:
40
+ description: "Buildchain runtime is the exact called workflow"
41
+ value: \${{ jobs.build.outputs.buildchain-runtime-ref }}
42
+ ${outputs}
43
+
44
+ permissions:
45
+ actions: read
46
+ contents: read
47
+ issues: write
48
+ id-token: write
49
+
50
+ jobs:
51
+ build:
52
+ name: Build configured project
318
53
  uses: ./.github/workflows/.build.yml
319
- permissions:
320
- actions: read
321
- contents: read
322
- issues: write
323
- id-token: write
324
54
  with:
325
- ${inputs}
55
+ config-path: \${{ inputs.config-path }}
326
56
  secrets: inherit
327
57
  `;
328
- return `${workflow.slice(0, index)}${jobs}`;
329
- }
330
-
331
- function bindRouterCheckoutToWorkflowSha(workflow) {
332
- return workflow
333
- .replace(
334
- " BUILDCHAIN_ROUTER_WORKFLOW_REF: ${{ job.workflow_ref }}\n",
335
- [
336
- " BUILDCHAIN_ROUTER_WORKFLOW_REF: ${{ job.workflow_ref }}",
337
- " BUILDCHAIN_ROUTER_WORKFLOW_REPOSITORY: ${{ job.workflow_repository }}",
338
- " BUILDCHAIN_ROUTER_WORKFLOW_SHA: ${{ job.workflow_sha }}",
339
- "",
340
- ].join("\n"),
341
- )
342
- .replace(
343
- [
344
- ' repository="${workflow_ref%%/.github/workflows/*}"',
345
- ' ref="${workflow_ref##*@}"',
346
- ' ref="${ref#refs/heads/}"',
347
- ' ref="${ref#refs/tags/}"',
348
- ' if [[ -z "${repository}" || "${repository}" = "${workflow_ref}" || -z "${ref}" ]]; then',
349
- ' echo "Unable to resolve Buildchain router source from job.workflow_ref=${workflow_ref}" >&2',
350
- " exit 1",
351
- " fi",
352
- ].join("\n"),
353
- [
354
- ' repository="${BUILDCHAIN_ROUTER_WORKFLOW_REPOSITORY}"',
355
- ' ref="${workflow_ref##*@}"',
356
- ' ref="${ref#refs/heads/}"',
357
- ' ref="${ref#refs/tags/}"',
358
- ' parsed_repository="${workflow_ref%%/.github/workflows/*}"',
359
- ' if [[ -z "${repository}" || "${repository}" != "${parsed_repository}" || -z "${ref}" ]]; then',
360
- ' echo "::error::Unable to resolve Buildchain router source from job.workflow_ref=${workflow_ref}"',
361
- " exit 1",
362
- " fi",
363
- ' if [[ ! "${BUILDCHAIN_ROUTER_WORKFLOW_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then',
364
- ' echo "::error::job.workflow_sha is not an exact commit SHA"',
365
- " exit 1",
366
- " fi",
367
- ].join("\n"),
368
- )
369
- .replace(
370
- ' echo "ref=${ref}"\n',
371
- ' echo "ref=${ref}"\n echo "sha=${BUILDCHAIN_ROUTER_WORKFLOW_SHA,,}"\n',
372
- )
373
- .replace(
374
- " ref: ${{ steps.router.outputs.ref }}\n path: .buildchain/router",
375
- " ref: ${{ steps.router.outputs.sha }}\n path: .buildchain/router",
376
- );
377
- }
378
-
379
- export function generateChannelBuildWorkflow(source) {
380
- const inputBlock = blockBetween(source, " inputs:\n", " secrets:\n");
381
- const names = inputNames(inputBlock);
382
- const packageVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || "";
383
- const major = Number(String(packageVersion).split(".")[0]);
384
- if (!Number.isInteger(major) || major < 1) throw new Error("package.json version must declare a positive major");
385
- const generated = bindRouterCheckoutToWorkflowSha(
386
- splitBuildJobsByChannel(generateChannelBuildWorkflowBase(source), names, major),
387
- )
388
- .replaceAll("runs-on: ubuntu-24.04", "runs-on: ${{ fromJSON(inputs.control-runner-json) }}")
389
- .replace(
390
- " buildchain-ref: ${{ steps.channel.outputs.buildchain-ref }}\n",
391
- " buildchain-ref: ${{ steps.channel.outputs.buildchain-ref }}\n major: ${{ steps.channel.outputs.major }}\n runtime-override: ${{ steps.channel.outputs.runtime-override }}\n",
392
- )
393
- .replace(
394
- "\npermissions:\n contents: read\n",
395
- "\npermissions:\n actions: read\n contents: read\n",
396
- )
397
- .replace(
398
- " contract-lock-path: ${{ steps.lock.outputs.path }}\n",
399
- [
400
- " contract-lock-path: ${{ steps.lock.outputs.path }}",
401
- " router-repository: ${{ steps.router.outputs.repository }}",
402
- " router-ref: ${{ steps.router.outputs.ref }}",
403
- " router-sha: ${{ steps.router.outputs.sha }}",
404
- "",
405
- ].join("\n"),
406
- )
407
- .replace("\n alpha:\n", `\n${routerControllerPlanJob()}\n\n alpha:\n`);
408
- return migrateUniversalWorkflowFacade(
409
- `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n\n${routerAggregateJob()}\n`,
410
- ".github/workflows/build.yml",
411
- );
412
58
  }
413
59
 
414
60
  function main() {
415
- const source = fs.readFileSync(sourcePath, "utf8");
416
- const generated = generateChannelBuildWorkflow(source);
61
+ const generated = generateChannelBuildWorkflow(fs.readFileSync(path.join(root, ".github/workflows/.build.yml"), "utf8"));
62
+ const target = ".github/workflows/build.yml";
417
63
  if (process.argv.includes("--check")) {
418
- const current = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
419
- if (current !== generated) {
420
- console.error(".github/workflows/build.yml is stale; run node scripts/generate-channel-build-workflow.mjs");
421
- process.exitCode = 1;
64
+ if (fs.readFileSync(path.join(root, target), "utf8") !== generated) {
65
+ throw new Error("build.yml is stale; run node scripts/generate-channel-build-workflow.mjs");
422
66
  }
423
- return;
424
- }
425
- writeWorkflowSource(root, path.relative(root, targetPath), generated);
67
+ } else writeWorkflowSource(root, target, generated);
426
68
  }
427
-
428
- const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
429
- if (isMain) main();
69
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
@@ -588,7 +588,7 @@ function actionCapabilityGroup(id) {
588
588
  if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
589
589
  if (id === "release-tail") return capabilityGroup("release-passport-trust");
590
590
  if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
591
- if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
591
+ if (id.startsWith("build-") || id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
592
592
  if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
593
593
  return capabilityGroup("api-cli-reference");
594
594
  }
@@ -905,6 +905,7 @@ function buildSiteBundle() {
905
905
  ...publicSurfaceLifecycle({
906
906
  owner: "buildchain-actions",
907
907
  maturity: "stable",
908
+ introducedVersion: entry.id.startsWith("build-") ? "4.0.9-alpha.0" : undefined,
908
909
  nonDuplicationRationale: "Existing action identity retained as the canonical composite or JavaScript action boundary.",
909
910
  }),
910
911
  })),
@@ -53,6 +53,7 @@ function activeFacadePaths() {
53
53
  (relative) =>
54
54
  relative !== contract.bootstrap.publicWorkflow &&
55
55
  relative !== contract.bootstrap.consumerRecoveryWorkflow &&
56
+ !contract.configurationGovernedWorkflows.includes(relative) &&
56
57
  !retired.has(relative),
57
58
  );
58
59
  }
@@ -10,7 +10,7 @@ import {
10
10
  nextDevelopmentWorkflowHeader,
11
11
  } from "../packages/core/next-development-projection.js";
12
12
 
13
- const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@v4";
13
+ const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/build.yml@v4";
14
14
  const DEFAULT_PUBLICATION_LATEX_IMAGE = "ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
15
15
  const DEFAULT_PUBLICATION_LATEX_DIGEST = "sha256:c20f3809e96836c1c78e97c76939d12f1de3fed0ea9b7c40c43332ec2ea480f8";
16
16
  const DEFAULT_PUBLICATION_LATEX_COMMAND = "latexmk -pdf -outdir=_build paper/main.tex";
@@ -365,41 +365,27 @@ jobs:
365
365
  `;
366
366
  }
367
367
 
368
- function workflowYaml({ type, runnerPreset, artifactName }) {
369
- return `${nextDevelopmentWorkflowHeader()}# Buildchain v4 lifecycle checkpoints are governed by architecture/platform-stage-checkpoints.json.
370
- # Do not add undeclared runner-only inputs, outputs, environment, or provider effects.
371
- # Commit both .buildchain/contract-lock.json and .buildchain/alpha-contract-lock.json.
372
- # Persist only @v4 or @v4-alpha; pass temporary train/SHA runtimes through workflow_dispatch.
368
+ function workflowYaml() {
369
+ return `${nextDevelopmentWorkflowHeader()}# Project build settings live only in .buildchain/buildchain.toml.
370
+ # Commit .buildchain/contract-lock.json and .buildchain/alpha-contract-lock.json.
371
+ # The called ref selects the runtime; architecture/platform-stage-checkpoints.json owns checkpoint projections.
373
372
  name: Build
374
373
 
375
374
  on:
376
375
  workflow_dispatch:
377
- inputs:
378
- buildchain-ref:
379
- description: "Temporary Buildchain runtime ref for trusted manual validation"
380
- required: false
381
- default: ""
382
376
  pull_request:
383
377
  push:
384
- branches:
385
- - "dev/**"
386
- - "alpha/**"
387
- - "release/**"
388
- - "publish-gate/**"
378
+ branches: ["dev/**", "alpha/**", "release/**"]
389
379
 
390
380
  permissions:
381
+ actions: read
391
382
  contents: read
383
+ issues: write
384
+ id-token: write
392
385
 
393
386
  jobs:
394
387
  build:
395
388
  uses: ${BUILDCHAIN_WORKFLOW_REF}
396
- with:
397
- working-directory: "."
398
- buildchain-ref: \${{ inputs.buildchain-ref || '' }}
399
- runner-preset: "${runnerPreset}"
400
- artifact-name-template: "${artifactName}"
401
- artifact-paths: |
402
- ${workflowArtifactPaths(type)}
403
389
  `;
404
390
  }
405
391
 
@@ -425,11 +411,11 @@ export function initBuildchainRepo({
425
411
  force = false,
426
412
  packageManager = "",
427
413
  runnerPreset = "github-hosted",
428
- artifactName = "{repo}-{version}-{platform}",
414
+ artifactName = "",
429
415
  } = {}) {
430
416
  const resolvedCwd = path.resolve(cwd);
431
417
  const manager = detectOrDefaultPackageManager(resolvedCwd, packageManager);
432
- const toml = appendNextDevelopmentToml((() => {
418
+ let toml = appendNextDevelopmentToml((() => {
433
419
  if (type === "package") {
434
420
  return packageToml(resolvedCwd, manager);
435
421
  }
@@ -451,6 +437,19 @@ export function initBuildchainRepo({
451
437
  throw new Error("init --type must be one of package, native, web-surface, infra-contract, publication-artifact, or anchored-package");
452
438
  })());
453
439
 
440
+
441
+ if (type !== "publication-artifact") {
442
+ const paths = workflowArtifactPaths(type).split("\n").map((entry) => entry.trim());
443
+ toml += `
444
+ [build]
445
+ environment = ${JSON.stringify(runnerPreset)}
446
+
447
+ [build.artifacts]
448
+ name = ${JSON.stringify(artifactName || repoName(resolvedCwd))}
449
+ paths = ${JSON.stringify(paths)}
450
+ `;
451
+ }
452
+
454
453
  const agentsPath = path.join(resolvedCwd, "AGENTS.md");
455
454
  const currentAgents = fs.existsSync(agentsPath)
456
455
  ? fs.readFileSync(agentsPath, "utf8")
@@ -509,7 +508,7 @@ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.met
509
508
  force: process.argv.includes("--force"),
510
509
  packageManager: readArg("package-manager", ""),
511
510
  runnerPreset: readArg("runner-preset", "github-hosted"),
512
- artifactName: readArg("artifact-name", "{repo}-{version}-{platform}"),
511
+ artifactName: readArg("artifact-name", ""),
513
512
  });
514
513
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
515
514
  } catch (error) {