@kungfu-tech/buildchain 4.0.2-alpha.10 → 4.0.2-alpha.12

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 (44) hide show
  1. package/architecture/agent-change-map.md +40 -0
  2. package/architecture/ci-lane-change-budget.json +332 -0
  3. package/architecture/internal-capabilities.json +39 -0
  4. package/architecture/maintainability-debt.json +11 -9
  5. package/architecture/maintainability-policy.json +7 -7
  6. package/architecture/v4-release-topology.json +2 -1
  7. package/architecture/v4-universal-workflow-bootstrap.json +181 -0
  8. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  9. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  10. package/dist/site/buildchain-contract.json +86 -41
  11. package/dist/site/buildchain-site.json +15 -10
  12. package/dist/site/capability-registry.json +1 -1
  13. package/dist/site/controller-registry.json +46 -10
  14. package/dist/site/kfd-claims.json +154 -68
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +2 -2
  17. package/dist/site/node-api-registry.json +7 -7
  18. package/dist/site/page-registry.json +9 -4
  19. package/dist/site/public-surface-audit.json +158 -66
  20. package/dist/site/publication-authority-registry.json +67 -1
  21. package/dist/site/publication-registry.json +4 -4
  22. package/dist/site/site-manifest.json +6 -6
  23. package/dist/site/workflow-registry.json +189 -67
  24. package/docs/node-api-reference.md +9 -9
  25. package/docs/reusable-build-surface.md +16 -0
  26. package/package.json +2 -2
  27. package/packages/core/buildchain-publication-authority.js +3 -0
  28. package/packages/core/ci-lane-change-budget.js +14 -1
  29. package/packages/core/release-tail-provider-plane.js +1 -1
  30. package/packages/core/v4-product-publication.js +1 -1
  31. package/packages/core/v4-universal-workflow-bootstrap.js +596 -0
  32. package/scripts/check-maintainability.mjs +16 -113
  33. package/scripts/check-v4-release-topology.mjs +5 -2
  34. package/scripts/check-v4-universal-workflow-bootstrap.mjs +257 -0
  35. package/scripts/generate-channel-build-workflow.mjs +5 -1
  36. package/scripts/generate-channel-promotion-workflow.mjs +3 -2
  37. package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
  38. package/scripts/maintainability-public-surface.mjs +115 -0
  39. package/scripts/universal-facade-maintainability.mjs +82 -0
  40. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  41. package/scripts/v4-universal-workflow-engine.mjs +586 -0
  42. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  43. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  44. package/templates/universal-buildchain-bootstrap.yml +28 -0
@@ -19,6 +19,8 @@ import {
19
19
  evaluateTestBudgets,
20
20
  evaluateWorkflowBudgets,
21
21
  } from "./maintainability-governance.mjs";
22
+ import { governUniversalFacadeWorkflowMetrics } from "./universal-facade-maintainability.mjs";
23
+ import { evaluatePublicSurface } from "./maintainability-public-surface.mjs";
22
24
 
23
25
  function collectHotspots(
24
26
  root,
@@ -162,116 +164,6 @@ function selectedFunction(metrics, file, name) {
162
164
  return metrics.files[file]?.functions.find((entry) => entry.name === name);
163
165
  }
164
166
 
165
- function readJsonAtRevision(root, revision, file) {
166
- return JSON.parse(gitOutput(root, ["show", `${revision}:${file}`]));
167
- }
168
-
169
- function publicSurfaceContract(entry, kind) {
170
- if (kind === "cli") return { id: entry.id, usage: entry.usage };
171
- if (kind === "node")
172
- return {
173
- export: entry.export,
174
- specifier: entry.specifier,
175
- target: entry.target,
176
- };
177
- return {
178
- id: entry.id,
179
- path: entry.path,
180
- reusable: entry.reusable,
181
- inputs: entry.inputs || [],
182
- secrets: entry.secrets || [],
183
- outputs: entry.outputs || [],
184
- };
185
- }
186
-
187
- function evaluatePublicSurface({ root, revision, policy }) {
188
- const issues = [];
189
- const lifecycleFields = policy.publicSurfacePolicy.requiredLifecycleFields;
190
- const capabilityGroups = new Set(
191
- readJson(root, "dist/site/capability-registry.json").groups.map(
192
- (entry) => entry.id,
193
- ),
194
- );
195
- const definitions = [
196
- {
197
- file: "dist/site/cli-registry.json",
198
- collection: "commands",
199
- kind: "cli",
200
- key: "id",
201
- },
202
- {
203
- file: "dist/site/node-api-registry.json",
204
- collection: "exports",
205
- kind: "node",
206
- key: "export",
207
- },
208
- {
209
- file: "dist/site/workflow-registry.json",
210
- collection: "workflows",
211
- kind: "workflow",
212
- key: "id",
213
- },
214
- {
215
- file: "dist/site/workflow-registry.json",
216
- collection: "actions",
217
- kind: "action",
218
- key: "id",
219
- },
220
- ];
221
- for (const definition of definitions) {
222
- const current =
223
- readJson(root, definition.file)[definition.collection] || [];
224
- const baseline =
225
- readJsonAtRevision(root, revision, definition.file)[
226
- definition.collection
227
- ] || [];
228
- const baselineByKey = new Map(
229
- baseline.map((entry) => [entry[definition.key], entry]),
230
- );
231
- for (const entry of current) {
232
- const label = `${definition.kind}:${entry[definition.key]}`;
233
- for (const field of lifecycleFields) {
234
- if (
235
- !Object.prototype.hasOwnProperty.call(entry, field) ||
236
- typeof entry[field] !== "string"
237
- ) {
238
- issues.push(`${label}: lifecycle field ${field} is missing`);
239
- }
240
- }
241
- if (!capabilityGroups.has(entry.capabilityGroup)) {
242
- issues.push(
243
- `${label}: capability group ${entry.capabilityGroup || "<empty>"} is not registered`,
244
- );
245
- }
246
- const previous = baselineByKey.get(entry[definition.key]);
247
- const currentContract = publicSurfaceContract(entry, definition.kind);
248
- if (
249
- previous &&
250
- JSON.stringify(currentContract) !==
251
- JSON.stringify(publicSurfaceContract(previous, definition.kind))
252
- ) {
253
- const approval = policy.approvedPublicSurfaceTransitions?.[label];
254
- const approved =
255
- approval?.fromRevision === revision &&
256
- String(approval?.rationale || "").trim() &&
257
- JSON.stringify(approval?.contract) ===
258
- JSON.stringify(currentContract);
259
- if (!approved) {
260
- issues.push(
261
- `${label}: existing public contract drifted from ${revision}`,
262
- );
263
- }
264
- }
265
- if (!previous && !entry.nonDuplicationRationale) {
266
- issues.push(
267
- `${label}: new public surface requires a non-duplication rationale`,
268
- );
269
- }
270
- }
271
- }
272
- return issues;
273
- }
274
-
275
167
  function evaluateAddedFunctionBudgets({
276
168
  file,
277
169
  metrics,
@@ -497,12 +389,18 @@ function checkMaintainability({ root = process.cwd() } = {}) {
497
389
  root,
498
390
  extendedCoverageRevision,
499
391
  );
392
+ const { governed: governedCurrent, migration: facadeMigration } =
393
+ governUniversalFacadeWorkflowMetrics({
394
+ root,
395
+ current,
396
+ workflowMetricsAtRevision,
397
+ });
500
398
  const issues = evaluateExceptionGovernance({ policy });
501
399
  issues.push(...evaluateExceptionBudget({ policy }));
502
400
  const hotspots = collectHotspots(root, current, 20, debt.hotspots || []);
503
401
  issues.push(
504
402
  ...evaluateDebtAuthority({
505
- current,
403
+ current: governedCurrent,
506
404
  policy,
507
405
  debt,
508
406
  capabilityIds: new Set(
@@ -518,14 +416,19 @@ function checkMaintainability({ root = process.cwd() } = {}) {
518
416
  );
519
417
  issues.push(
520
418
  ...evaluateWorkflowBudgets({
521
- current,
419
+ current: governedCurrent,
522
420
  baselineFiles: baselineWorkflows,
523
421
  policy,
524
422
  }),
525
423
  );
526
424
  issues.push(...evaluateRepositoryBudgets({ current, policy }));
527
425
  issues.push(
528
- ...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
426
+ ...evaluatePublicSurface({
427
+ root,
428
+ revision: enforcementRevision,
429
+ policy,
430
+ migration: facadeMigration,
431
+ }),
529
432
  );
530
433
  if (issues.length > 0) {
531
434
  throw new Error(`maintainability check failed:\n- ${issues.join("\n- ")}`);
@@ -180,7 +180,9 @@ function permission(block, name) {
180
180
  function workflowSnapshot(relative) {
181
181
  const source = read(relative);
182
182
  const document = parseWorkflowDocument(source);
183
- const jobs = document.jobs.map((job) => {
183
+ const jobs = document.jobs
184
+ .filter((job) => job.id !== "universal-bootstrap")
185
+ .map((job) => {
184
186
  const block = jobBlock(source, job.id);
185
187
  const uses = job.uses || null;
186
188
  return {
@@ -206,7 +208,7 @@ function workflowSnapshot(relative) {
206
208
  "direct-publication-command",
207
209
  ].filter(Boolean),
208
210
  };
209
- });
211
+ });
210
212
  return {
211
213
  path: relative,
212
214
  triggers: document.triggers,
@@ -214,6 +216,7 @@ function workflowSnapshot(relative) {
214
216
  reusableEdges: parseYamlUses(source)
215
217
  .map(({ value }) => value)
216
218
  .filter((value) => /\.github\/workflows\//u.test(value))
219
+ .filter((value) => !/\/bootstrap\.yml(?:@v4)?$/u.test(value))
217
220
  .sort(),
218
221
  };
219
222
  }
@@ -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
+ );
@@ -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");
@@ -302,7 +303,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
302
303
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
303
304
  }
304
305
  const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
305
- return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
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:
@@ -513,7 +514,7 @@ ${consumerAdmissionJob()}
513
514
  with:
514
515
  ${invokeForwarded}
515
516
  secrets: inherit
516
- `;
517
+ `, ".github/workflows/release-candidate-promote.yml");
517
518
  }
518
519
 
519
520
  function main() {