@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.11

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 (82) hide show
  1. package/architecture/agent-change-map.md +52 -5
  2. package/architecture/ci-lane-change-budget.json +408 -8
  3. package/architecture/internal-capabilities.json +44 -1
  4. package/architecture/maintainability-debt.json +65 -133
  5. package/architecture/maintainability-policy.json +20 -15
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +1 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  10. package/architecture/v4-release-invocation-fixtures.json +139 -0
  11. package/architecture/v4-release-topology.json +317 -0
  12. package/architecture/v4-runtime-semantic-closure.json +4 -1
  13. package/architecture/v4-universal-workflow-bootstrap.json +181 -0
  14. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  15. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  16. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  17. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  18. package/contracts/v4-release-invocation-v1.schema.json +134 -0
  19. package/dist/site/buildchain-contract.json +87 -42
  20. package/dist/site/buildchain-site.json +15 -10
  21. package/dist/site/capability-registry.json +1 -1
  22. package/dist/site/controller-registry.json +46 -10
  23. package/dist/site/kfd-claims.json +176 -69
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +2 -2
  26. package/dist/site/node-api-registry.json +97 -22
  27. package/dist/site/page-registry.json +9 -4
  28. package/dist/site/public-surface-audit.json +190 -71
  29. package/dist/site/publication-authority-registry.json +68 -3
  30. package/dist/site/publication-registry.json +4 -4
  31. package/dist/site/site-manifest.json +6 -6
  32. package/dist/site/workflow-registry.json +221 -72
  33. package/docs/node-api-reference.md +28 -25
  34. package/docs/reusable-build-surface.md +16 -0
  35. package/package.json +2 -2
  36. package/packages/core/buildchain-publication-authority.js +3 -0
  37. package/packages/core/ci-lane-change-budget.js +14 -1
  38. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  39. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  40. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  41. package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
  42. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  43. package/packages/core/dev-delivery-warrant-state.js +54 -54
  44. package/packages/core/dev-delivery-warrant.js +8 -0
  45. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  46. package/packages/core/release-tail-product-capabilities.js +29 -0
  47. package/packages/core/release-tail-provider-plane.js +3 -3
  48. package/packages/core/v4-canonical-contracts.js +14 -0
  49. package/packages/core/v4-floating-consumer-policy.js +4 -1
  50. package/packages/core/v4-product-publication.js +387 -0
  51. package/packages/core/v4-protected-publication-source.js +93 -0
  52. package/packages/core/v4-publication-qualification.js +1 -0
  53. package/packages/core/v4-release-invocation.js +425 -0
  54. package/packages/core/v4-universal-workflow-bootstrap.js +596 -0
  55. package/scripts/audit-publication-control-plane.mjs +0 -1
  56. package/scripts/check-inventory.mjs +27 -59
  57. package/scripts/check-maintainability.mjs +29 -118
  58. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  59. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  60. package/scripts/check-v4-release-topology.mjs +493 -0
  61. package/scripts/check-v4-universal-workflow-bootstrap.mjs +257 -0
  62. package/scripts/dev-delivery-warrant.mjs +26 -5
  63. package/scripts/generate-channel-build-workflow.mjs +5 -1
  64. package/scripts/generate-channel-promotion-workflow.mjs +17 -57
  65. package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
  66. package/scripts/maintainability-public-surface.mjs +115 -0
  67. package/scripts/release-candidate-resolver.mjs +23 -24
  68. package/scripts/resume-from-candidate-run.mjs +16 -17
  69. package/scripts/universal-facade-maintainability.mjs +82 -0
  70. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  71. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  72. package/scripts/v4-product-publication-intent.mjs +114 -0
  73. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  74. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  75. package/scripts/v4-universal-workflow-engine.mjs +586 -0
  76. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  77. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  78. package/templates/universal-buildchain-bootstrap.yml +28 -0
  79. package/scripts/capture-package-release-propagation.mjs +0 -263
  80. package/scripts/publication-commit-evidence.mjs +0 -444
  81. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  82. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -0,0 +1,257 @@
1
+ import assert from "node:assert/strict";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const contract = JSON.parse(
10
+ fs.readFileSync(
11
+ path.join(root, "architecture/v4-universal-workflow-bootstrap.json"),
12
+ "utf8",
13
+ ),
14
+ );
15
+ const admissionPolicy = JSON.parse(
16
+ fs.readFileSync(
17
+ path.join(root, "architecture/v4-universal-workflow-train-admission.json"),
18
+ "utf8",
19
+ ),
20
+ );
21
+ const fileRoot = (relative) =>
22
+ `sha256:${crypto
23
+ .createHash("sha256")
24
+ .update(fs.readFileSync(path.join(root, relative)))
25
+ .digest("hex")}`;
26
+
27
+ const workflowRoot = path.join(root, ".github/workflows");
28
+ const discovered = fs
29
+ .readdirSync(workflowRoot)
30
+ .filter((name) => /\.ya?ml$/u.test(name))
31
+ .map((name) => `.github/workflows/${name}`)
32
+ .filter((relative) =>
33
+ /(?:^|\n)\s*workflow_call:\s*(?:\n|$)/u.test(
34
+ fs.readFileSync(path.join(root, relative), "utf8"),
35
+ ),
36
+ )
37
+ .sort();
38
+
39
+ assert.equal(
40
+ contract.schema,
41
+ "kungfu-buildchain-v4-universal-workflow-bootstrap/v1",
42
+ );
43
+ assert.deepEqual(
44
+ contract.inventoryWorkflows,
45
+ [...new Set(contract.inventoryWorkflows)].sort(),
46
+ "inventoried public workflows must be sorted and duplicate-free",
47
+ );
48
+ assert.deepEqual(
49
+ contract.inventoryWorkflows,
50
+ discovered,
51
+ "every active public workflow_call surface must be inventoried",
52
+ );
53
+ for (const relative of contract.inventoryWorkflows)
54
+ assert.ok(
55
+ fs.statSync(path.join(root, relative)).isFile(),
56
+ `inventoried workflow is unavailable: ${relative}`,
57
+ );
58
+ assert.deepEqual(
59
+ contract.bootstrapGovernedWorkflows,
60
+ [...new Set(contract.bootstrapGovernedWorkflows)].sort(),
61
+ "Bootstrap-governed workflows must be sorted and duplicate-free",
62
+ );
63
+ assert.deepEqual(
64
+ contract.retiredWorkflowSurfaces,
65
+ [...new Set(contract.retiredWorkflowSurfaces)].sort(),
66
+ "retired workflow surfaces must be sorted and duplicate-free",
67
+ );
68
+ for (const relative of contract.retiredWorkflowSurfaces) {
69
+ assert.ok(
70
+ contract.inventoryWorkflows.includes(relative),
71
+ `retired workflow is outside the inventory: ${relative}`,
72
+ );
73
+ assert.match(
74
+ fs.readFileSync(path.join(root, relative), "utf8"),
75
+ /retired/u,
76
+ `retired workflow does not fail closed explicitly: ${relative}`,
77
+ );
78
+ }
79
+ const activeWorkflows = contract.inventoryWorkflows.filter(
80
+ (relative) =>
81
+ !contract.retiredWorkflowSurfaces.includes(relative) &&
82
+ relative !== contract.bootstrap.consumerRecoveryWorkflow,
83
+ );
84
+ assert.deepEqual(
85
+ contract.bootstrapGovernedWorkflows,
86
+ activeWorkflows,
87
+ "every active public reusable workflow must be Bootstrap-governed",
88
+ );
89
+ for (const relative of contract.bootstrapGovernedWorkflows) {
90
+ assert.ok(
91
+ contract.inventoryWorkflows.includes(relative),
92
+ `Bootstrap-governed workflow is outside the inventory: ${relative}`,
93
+ );
94
+ const source = fs.readFileSync(path.join(root, relative), "utf8");
95
+ assert.ok(
96
+ relative === contract.bootstrap.publicWorkflow ||
97
+ /uses:\s+(?:\.\/)?\.github\/workflows\/bootstrap\.yml/u.test(source) ||
98
+ /uses:\s+kungfu-systems\/buildchain\/\.github\/workflows\/bootstrap\.yml@/u.test(
99
+ source,
100
+ ),
101
+ `workflow is declared governed without a Bootstrap edge: ${relative}`,
102
+ );
103
+ }
104
+
105
+ assert.deepEqual(contract.complexityBudget, {
106
+ bootstrapAbis: 1,
107
+ requestEnvelopes: 1,
108
+ candidateResolvers: 1,
109
+ candidateEngines: 1,
110
+ terminalReceiptAuthorities: 1,
111
+ perWorkflowBootstrapImplementations: 0,
112
+ buildchainOnlySelfReleasePaths: 0,
113
+ perIncidentConsumerEdits: 0,
114
+ });
115
+ assert.equal(contract.bootstrap.discoveryRefExecutionAuthority, false);
116
+ assert.equal(contract.bootstrap.exactShaExecutionAuthority, true);
117
+ assert.equal(contract.bootstrap.consumerSourceMutationPerIncident, false);
118
+ assert.equal(contract.bootstrap.buildchainReleasePromotionPerIncident, false);
119
+ assert.equal(contract.bootstrap.selfDogfoodUsesPublicContract, true);
120
+ assert.equal(contract.bootstrap.recoveryDependsOnPublishedBuildchain, false);
121
+ assert.deepEqual(
122
+ contract.capabilityAdapters.map(({ id }) => id).sort(),
123
+ [...admissionPolicy.allowedCapabilities].sort(),
124
+ "every admitted capability must have one real candidate adapter",
125
+ );
126
+ for (const adapter of contract.capabilityAdapters) {
127
+ assert.match(adapter.execution, /^exact-candidate-/u);
128
+ assert.ok(adapter.payloadSchema.endsWith("/v1"));
129
+ }
130
+ assert.ok(!admissionPolicy.allowedCapabilities.includes("workflow-contract"));
131
+ assert.deepEqual(
132
+ admissionPolicy.contractRoots,
133
+ [
134
+ fileRoot("architecture/v4-universal-workflow-bootstrap.json"),
135
+ fileRoot(contract.bootstrap.faultCampaign),
136
+ fileRoot("packages/core/v4-universal-workflow-bootstrap.js"),
137
+ fileRoot("scripts/v4-universal-workflow-backflow.mjs"),
138
+ fileRoot("scripts/v4-universal-workflow-engine.mjs"),
139
+ fileRoot("scripts/v4-universal-workflow-self-dogfood.mjs"),
140
+ fileRoot(contract.bootstrap.publicWorkflow),
141
+ ].sort(),
142
+ );
143
+ assert.doesNotMatch(
144
+ fs.readFileSync(
145
+ path.join(root, contract.bootstrap.candidateEnginePath),
146
+ "utf8",
147
+ ),
148
+ /request\.capability\.id\s*===\s*["']workflow-contract["']/u,
149
+ "contract-only validation must not be admitted as successful execution",
150
+ );
151
+ const bootstrapSource = fs.readFileSync(
152
+ path.join(root, contract.bootstrap.publicWorkflow),
153
+ "utf8",
154
+ );
155
+ assert.doesNotMatch(
156
+ bootstrapSource,
157
+ /admission-policy-json:/u,
158
+ "callers must not supply their own admission authority",
159
+ );
160
+ execFileSync(
161
+ process.execPath,
162
+ [path.join(root, contract.migration.compatibilityFacadeGenerator), "--check"],
163
+ { stdio: "inherit" },
164
+ );
165
+ assert.doesNotMatch(bootstrapSource, /^ permissions:/mu);
166
+ assert.match(bootstrapSource, /settle:[\s\S]*needs: \[admit, execute\]/u);
167
+ assert.match(
168
+ bootstrapSource,
169
+ /ascii_downcase\) == "kungfu-origin"[\s\S]*\.name == "check"[\s\S]*map\(select\(\.status == "completed" and \(\.conclusion \/\/ ""\) != ""\)\)[\s\S]*candidate\/architecture\/v4-universal-workflow-train-admission\.json/u,
170
+ );
171
+ const consumerTemplate = fs.readFileSync(
172
+ path.join(root, contract.bootstrap.consumerTemplate),
173
+ "utf8",
174
+ );
175
+ const consumerRecoveryTemplate = fs.readFileSync(
176
+ path.join(root, contract.bootstrap.consumerRecoveryTemplate),
177
+ "utf8",
178
+ );
179
+ const consumerRecoveryWorkflow = fs.readFileSync(
180
+ path.join(root, contract.bootstrap.consumerRecoveryWorkflow),
181
+ "utf8",
182
+ );
183
+ const selfDogfood = fs.readFileSync(
184
+ path.join(root, contract.bootstrap.selfDogfoodWorkflow),
185
+ "utf8",
186
+ );
187
+ assert.match(
188
+ consumerTemplate,
189
+ /uses:\s+kungfu-systems\/buildchain\/\.github\/workflows\/bootstrap\.yml@v4/u,
190
+ );
191
+ assert.match(
192
+ consumerRecoveryTemplate,
193
+ /recovery-admit:[\s\S]*recovery-execute:/u,
194
+ );
195
+ assert.match(
196
+ consumerRecoveryTemplate,
197
+ /Parse exact recovery coordinates without Buildchain code[\s\S]*Prove exact independent review before candidate code runs[\s\S]*ascii_downcase\) == "kungfu-origin"[\s\S]*\.name == "check"[\s\S]*Admit candidate policy and exact-head checks[\s\S]*map\(select\(\.status == "completed" and \(\.conclusion \/\/ ""\) != ""\)\)/u,
198
+ );
199
+ assert.match(
200
+ consumerRecoveryTemplate,
201
+ /recovery-execute:[\s\S]*needs: recovery-admit[\s\S]*contents: write/u,
202
+ );
203
+ assert.doesNotMatch(
204
+ consumerRecoveryTemplate,
205
+ /uses:\s+kungfu-systems\/buildchain\/\.github\/workflows\//u,
206
+ "consumer recovery must not parse any published Buildchain workflow",
207
+ );
208
+ assert.equal(
209
+ consumerRecoveryWorkflow,
210
+ consumerRecoveryTemplate,
211
+ "the Buildchain self-consumer must exercise the exact distributable recovery shell",
212
+ );
213
+ assert.match(
214
+ selfDogfood,
215
+ /uses:\s+kungfu-systems\/buildchain\/\.github\/workflows\/bootstrap\.yml@train\/v4\/v4\.0\/universal-reusable-workflow-bootstrap/u,
216
+ );
217
+ assert.match(
218
+ selfDogfood,
219
+ /uses:\s+\.\/\.github\/workflows\/universal-bootstrap-recovery\.yml/u,
220
+ );
221
+ assert.doesNotMatch(
222
+ selfDogfood,
223
+ /\.github\/workflows\/bootstrap\.yml@(?:v4|v4-alpha)(?:\s|$)/u,
224
+ "Train self-dogfood must not depend on an alpha or stable Bootstrap tag",
225
+ );
226
+ for (const channel of ["conformance", "alpha", "stable"]) {
227
+ assert.match(selfDogfood, new RegExp(`primary-${channel}:`, "u"));
228
+ assert.match(selfDogfood, new RegExp(`recovery-${channel}:`, "u"));
229
+ }
230
+ assert.match(
231
+ selfDogfood,
232
+ /\.state == "APPROVED"[\s\S]*ascii_downcase\) == "kungfu-origin"[\s\S]*\.name == "check"[\s\S]*\.conclusion == "success"/u,
233
+ );
234
+ assert.match(
235
+ consumerRecoveryTemplate,
236
+ /result-json:[\s\S]*value:\s*\$\{\{ jobs\.recovery-execute\.outputs\.result-json \}\}/u,
237
+ );
238
+
239
+ console.log(
240
+ JSON.stringify({
241
+ ok: true,
242
+ schema: contract.schema,
243
+ publicWorkflowCount: discovered.length,
244
+ activePublicWorkflowCount: activeWorkflows.length,
245
+ retiredWorkflowCount: contract.retiredWorkflowSurfaces.length,
246
+ inventoriedWorkflowCount: contract.inventoryWorkflows.length,
247
+ inventoryCoveragePercent: 100,
248
+ governedWorkflowCount: contract.bootstrapGovernedWorkflows.length,
249
+ governedCoveragePercent: Number(
250
+ (
251
+ (contract.bootstrapGovernedWorkflows.length / activeWorkflows.length) *
252
+ 100
253
+ ).toFixed(2),
254
+ ),
255
+ migration: contract.migration,
256
+ }),
257
+ );
@@ -6,6 +6,7 @@ import {
6
6
  closeDevDeliveryWarrant,
7
7
  createDevDeliveryQueue,
8
8
  createNativeCommandContract,
9
+ fenceDevDeliveryWriterProtocol,
9
10
  heartbeatDevDeliveryWarrant,
10
11
  observeDevDeliveryQueue,
11
12
  qualifyDevDeliveryWarrant,
@@ -91,6 +92,13 @@ function requireTerminalEvidenceCas(options) {
91
92
  "terminal evidence reconciliation execute requires expected-old CAS",
92
93
  );
93
94
  }
95
+ if (
96
+ options.command === "fence-writer-protocol" &&
97
+ options.execute &&
98
+ !options.expectedOldStateRoot
99
+ ) {
100
+ throw new Error("writer protocol fence execute requires expected-old CAS");
101
+ }
94
102
  }
95
103
 
96
104
  function normalizeRepository(value) {
@@ -175,6 +183,9 @@ function jsonObject(value, label) {
175
183
  }
176
184
 
177
185
  function transitionFor(command, queue, options) {
186
+ if (command === "fence-writer-protocol") {
187
+ return fenceDevDeliveryWriterProtocol(queue, { now: options.now });
188
+ }
178
189
  if (command === "submit") {
179
190
  const nativeCommandContract = options.environmentRoot
180
191
  ? createNativeCommandContract(options.nativeCommand)
@@ -430,20 +441,29 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
430
441
  options.command,
431
442
  ),
432
443
  });
444
+ let concurrencyRecovery = null;
433
445
  if (
434
446
  options.expectedOldStateRoot &&
435
447
  loaded.queue.stateRoot !== options.expectedOldStateRoot
436
448
  ) {
437
- throw new Error(
438
- `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
439
- );
449
+ if (options.command !== "heartbeat") {
450
+ throw new Error(
451
+ `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
452
+ );
453
+ }
454
+ concurrencyRecovery = {
455
+ schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
456
+ action: "heartbeat-state-root-rebased",
457
+ requestedStateRoot: options.expectedOldStateRoot,
458
+ observedStateRoot: loaded.queue.stateRoot,
459
+ observedCommitSha: loaded.commitSha,
460
+ };
440
461
  }
441
462
  if (options.command === "observe") return observeQueue(loaded, options);
442
463
  const initialLoaded = loaded;
443
464
  let changed = transitionFor(options.command, loaded.queue, options);
444
465
  let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
445
466
  let write = null;
446
- let concurrencyRecovery = null;
447
467
  if (options.execute && mutates) {
448
468
  if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
449
469
  throw new Error(
@@ -508,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
508
528
  }
509
529
 
510
530
  function usage() {
511
- return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
531
+ return "Usage:\n buildchain dev warrant <fence-writer-protocol|submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nWriter protocol fence:\n fence-writer-protocol --expected-old sha256:... [--execute]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
512
532
  }
513
533
 
514
534
  async function main() {
@@ -520,6 +540,7 @@ async function main() {
520
540
  const options = devDeliveryCliOptions(args);
521
541
  if (
522
542
  ![
543
+ "fence-writer-protocol",
523
544
  "submit",
524
545
  "select",
525
546
  "heartbeat",
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { migrateV4UniversalWorkflowFacade } from "./generate-v4-universal-workflow-facades.mjs";
6
7
 
7
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const sourcePath = path.join(root, ".github/workflows/.build.yml");
@@ -403,7 +404,10 @@ export function generateChannelBuildWorkflow(source) {
403
404
  ].join("\n"),
404
405
  )
405
406
  .replace("\n alpha:\n", `\n${routerControllerPlanJob()}\n\n alpha:\n`);
406
- return `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n\n${routerAggregateJob()}\n`;
407
+ return migrateV4UniversalWorkflowFacade(
408
+ `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n\n${routerAggregateJob()}\n`,
409
+ ".github/workflows/build.yml",
410
+ );
407
411
  }
408
412
 
409
413
  function main() {
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { migrateV4UniversalWorkflowFacade } from "./generate-v4-universal-workflow-facades.mjs";
6
7
 
7
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const sourcePath = path.join(root, ".github/workflows/.release-candidate-promote.yml");
@@ -74,7 +75,7 @@ function publicInputs(inputBlock) {
74
75
  function publicOutputs(outputBlock) {
75
76
  const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
76
77
  /^ value:.*$/m,
77
- ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
78
+ ` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
78
79
  ));
79
80
  const routed = [
80
81
  ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
@@ -214,8 +215,9 @@ function consumerAdmissionJob() {
214
215
  id: policy
215
216
  env:
216
217
  BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
218
+ BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
217
219
  BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
218
- BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
220
+ BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
219
221
  BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
220
222
  BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
221
223
  BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
@@ -292,7 +294,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
292
294
  },
293
295
  };
294
296
  const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
295
- const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
297
+ validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
298
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
297
299
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
298
300
  const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
@@ -300,9 +302,8 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
300
302
  for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
301
303
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
302
304
  }
303
- const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
304
- const stableForwarded = routedInputs(inputNames, stableRoute, major);
305
- return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
305
+ const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
306
+ return migrateV4UniversalWorkflowFacade(`# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
306
307
  name: Release Candidate Promote
307
308
 
308
309
  on:
@@ -336,7 +337,7 @@ jobs:
336
337
  target-ref: \${{ steps.route.outputs.target-ref }}
337
338
  router-ref: \${{ steps.router.outputs.ref }}
338
339
  router-sha: \${{ steps.router.outputs.sha }}
339
- shell-ref: \${{ steps.route.outputs.shell-ref }}
340
+ shell-ref: \${{ steps.identities.outputs.shell-ref }}
340
341
  shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
341
342
  shell-sha: \${{ steps.identities.outputs.shell-sha }}
342
343
  runtime-ref: \${{ steps.route.outputs.runtime-ref }}
@@ -425,32 +426,17 @@ jobs:
425
426
  shell: bash
426
427
  env:
427
428
  GITHUB_TOKEN: \${{ github.token }}
428
- CHANNEL: \${{ steps.route.outputs.channel }}
429
- SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
430
- ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
431
- ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
432
- STABLE_SHELL_REF: ${stableRoute.logicalRef}
433
- STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
429
+ SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
434
430
  run: |
435
431
  set -euo pipefail
436
- if [[ "\${CHANNEL}" = "alpha" ]]; then
437
- expected_ref="\${ALPHA_SHELL_REF}"
438
- call_ref="\${ALPHA_SHELL_CALL_REF}"
439
- else
440
- expected_ref="\${STABLE_SHELL_REF}"
441
- call_ref="\${STABLE_SHELL_CALL_REF}"
442
- fi
443
- if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
444
- echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
445
- exit 1
446
- fi
447
432
  node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
448
433
  --repository "\${{ steps.router.outputs.repository }}" \\
449
434
  --router-ref "\${{ steps.router.outputs.ref }}" \\
450
435
  --router-sha "\${{ steps.router.outputs.sha }}" \\
451
436
  --shell-ref "\${SELECTED_SHELL_REF}" \\
452
- --shell-call-ref "\${call_ref}" \\
437
+ --shell-call-ref "\${SELECTED_SHELL_REF}" \\
453
438
  --runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
439
+ echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
454
440
 
455
441
  - name: Checkout selected promotion workflow shell
456
442
  uses: actions/checkout@v7.0.0
@@ -500,29 +486,21 @@ jobs:
500
486
  - name: Verify immutable promotion checkouts
501
487
  shell: bash
502
488
  env:
503
- CHANNEL: \${{ steps.route.outputs.channel }}
504
- ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
505
- STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
489
+ SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
506
490
  ROUTER_SHA: \${{ steps.router.outputs.sha }}
507
491
  SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
508
492
  RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
509
493
  run: |
510
494
  set -euo pipefail
511
- if [[ "\${CHANNEL}" = "alpha" ]]; then
512
- workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
513
- else
514
- workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
515
- fi
516
- test -f ".buildchain/shell/\${workflow_path}"
495
+ test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
517
496
  [[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
518
497
  [[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
519
498
  [[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
520
499
 
521
500
  ${consumerAdmissionJob()}
522
- alpha:
523
- name: Promote with alpha workflow shell
501
+ invoke:
502
+ name: Invoke the single v4 publisher adapter
524
503
  needs: [resolve-promotion, consumer-admission]
525
- if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
526
504
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
527
505
  permissions:
528
506
  actions: write
@@ -534,27 +512,9 @@ ${consumerAdmissionJob()}
534
512
  issues: write
535
513
  pull-requests: write
536
514
  with:
537
- ${alphaForwarded}
538
- secrets: inherit
539
-
540
- stable:
541
- name: Promote with stable workflow shell
542
- needs: [resolve-promotion, consumer-admission]
543
- if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
544
- uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
545
- permissions:
546
- actions: write
547
- artifact-metadata: write
548
- attestations: write
549
- checks: write
550
- contents: write
551
- id-token: write
552
- issues: write
553
- pull-requests: write
554
- with:
555
- ${stableForwarded}
515
+ ${invokeForwarded}
556
516
  secrets: inherit
557
- `;
517
+ `, ".github/workflows/release-candidate-promote.yml");
558
518
  }
559
519
 
560
520
  function main() {