@kungfu-tech/buildchain 2.14.2 → 2.14.3-alpha.1

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 (57) hide show
  1. package/dist/site/agent-index.json +2 -0
  2. package/dist/site/artifact-schemas.json +4 -0
  3. package/dist/site/badge-endpoint-registry.json +320 -0
  4. package/dist/site/badges/v1/kfd-5/aligned.json +14 -0
  5. package/dist/site/badges/v1/kfd-5/declared.json +14 -0
  6. package/dist/site/badges/v1/kfd-5/downgraded.json +14 -0
  7. package/dist/site/badges/v1/kfd-5/draft.json +14 -0
  8. package/dist/site/badges/v1/kfd-5/failed.json +14 -0
  9. package/dist/site/badges/v1/kfd-5/missing.json +14 -0
  10. package/dist/site/badges/v1/kfd-5/passed.json +14 -0
  11. package/dist/site/badges/v1/kfd-5/planned.json +14 -0
  12. package/dist/site/badges/v1/kfd-6/aligned.json +14 -0
  13. package/dist/site/badges/v1/kfd-6/declared.json +14 -0
  14. package/dist/site/badges/v1/kfd-6/downgraded.json +14 -0
  15. package/dist/site/badges/v1/kfd-6/draft.json +14 -0
  16. package/dist/site/badges/v1/kfd-6/failed.json +14 -0
  17. package/dist/site/badges/v1/kfd-6/missing.json +14 -0
  18. package/dist/site/badges/v1/kfd-6/passed.json +14 -0
  19. package/dist/site/badges/v1/kfd-6/planned.json +14 -0
  20. package/dist/site/buildchain-contract.json +149 -28
  21. package/dist/site/buildchain-site.json +25 -23
  22. package/dist/site/capability-registry.json +2 -2
  23. package/dist/site/controller-registry.json +61 -5
  24. package/dist/site/kfd-claims.json +129 -11
  25. package/dist/site/kfd-upstream-aggregate.json +12 -12
  26. package/dist/site/manual-registry.json +6 -6
  27. package/dist/site/node-api-registry.json +20 -7
  28. package/dist/site/page-registry.json +12 -12
  29. package/dist/site/public-surface-audit.json +134 -8
  30. package/dist/site/publication-authority-registry.json +26 -1
  31. package/dist/site/publication-registry.json +4 -4
  32. package/dist/site/release-model.json +3 -1
  33. package/dist/site/release-passport-check-manifest.json +167 -0
  34. package/dist/site/release-provenance.json +3 -0
  35. package/dist/site/schemas/release-passport-v1.schema.json +198 -0
  36. package/dist/site/site-manifest.json +10 -10
  37. package/dist/site/workflow-registry.json +128 -4
  38. package/docs/migration-inventory.md +8 -0
  39. package/docs/release-candidate.md +3 -0
  40. package/docs/release-governance.md +5 -4
  41. package/docs/release-passport.md +19 -0
  42. package/docs/release-propagation.md +19 -7
  43. package/docs/reusable-build-surface.md +26 -1
  44. package/package.json +5 -2
  45. package/packages/core/buildchain-contract.js +41 -0
  46. package/packages/core/buildchain-kfd-claims.js +4 -0
  47. package/packages/core/buildchain-publication-authority.js +1 -0
  48. package/packages/core/controller-evidence.js +1 -1
  49. package/packages/core/index.js +9 -0
  50. package/packages/core/package-manager.js +32 -0
  51. package/packages/core/release-passport-contract.js +249 -0
  52. package/packages/core/release-passport.js +39 -0
  53. package/scripts/check-inventory.mjs +37 -3
  54. package/scripts/generate-channel-promotion-workflow.mjs +331 -0
  55. package/scripts/generate-site-bundle.mjs +15 -0
  56. package/scripts/promotion-channel-router.mjs +132 -0
  57. package/scripts/validate-package-manager-contract.mjs +24 -0
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const sourcePath = path.join(root, ".github/workflows/.release-candidate-promote.yml");
9
+ const targetPath = path.join(root, ".github/workflows/release-candidate-promote.yml");
10
+ const internalInputs = new Set([
11
+ "promotion-router-ref",
12
+ "promotion-router-sha",
13
+ "promotion-shell-ref",
14
+ "promotion-shell-sha",
15
+ "promotion-runtime-ref",
16
+ "promotion-runtime-sha",
17
+ "promotion-contract-lock-path",
18
+ "promotion-contract-lock-digest",
19
+ "promotion-publication-channel",
20
+ "promotion-target-ref",
21
+ "promotion-override-used",
22
+ ]);
23
+
24
+ function blockBetween(source, start, end) {
25
+ const startIndex = source.indexOf(start);
26
+ const endIndex = source.indexOf(end, startIndex + start.length);
27
+ if (startIndex < 0 || endIndex < 0) throw new Error(`unable to locate workflow block: ${start.trim()}`);
28
+ return source.slice(startIndex + start.length, endIndex);
29
+ }
30
+
31
+ function entries(block, indent = 6) {
32
+ const pattern = new RegExp(`^ {${indent}}([a-z0-9-]+):$`, "gm");
33
+ const matches = [...block.matchAll(pattern)];
34
+ return matches.map((match, index) => ({
35
+ name: match[1],
36
+ source: block.slice(match.index, matches[index + 1]?.index ?? block.length).trimEnd(),
37
+ }));
38
+ }
39
+
40
+ function publicInputs(inputBlock) {
41
+ const retained = entries(inputBlock)
42
+ .filter((entry) => !internalInputs.has(entry.name))
43
+ .map((entry) => entry.name === "buildchain-contract-lock-path"
44
+ ? entry.source.replace(' default: ".buildchain/contract-lock.json"', ' default: ""')
45
+ : entry.source);
46
+ return [
47
+ " buildchain-channel:",
48
+ ' description: "Promotion workflow-shell/runtime channel: auto, alpha, or stable"',
49
+ ' default: "auto"',
50
+ " type: string",
51
+ " required: false",
52
+ " buildchain-alpha-contract-lock-path:",
53
+ ' description: "Consumer-owned contract lock selected for alpha promotion"',
54
+ ' default: ".buildchain/alpha-contract-lock.json"',
55
+ " type: string",
56
+ " required: false",
57
+ " buildchain-stable-contract-lock-path:",
58
+ ' description: "Consumer-owned contract lock selected for stable promotion"',
59
+ ' default: ".buildchain/contract-lock.json"',
60
+ " type: string",
61
+ " required: false",
62
+ ...retained,
63
+ ].join("\n");
64
+ }
65
+
66
+ function publicOutputs(outputBlock) {
67
+ const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
68
+ /^ value:.*$/m,
69
+ ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
70
+ ));
71
+ const routed = [
72
+ ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
73
+ ["promotion-router-ref", "Router workflow ref", "router-ref"],
74
+ ["promotion-router-sha", "Resolved router workflow SHA", "router-sha"],
75
+ ["promotion-shell-ref", "Selected advanced promotion workflow-shell ref", "shell-ref"],
76
+ ["promotion-shell-sha", "Resolved advanced promotion workflow-shell SHA", "shell-sha"],
77
+ ["promotion-runtime-ref", "Selected Buildchain runtime ref", "runtime-ref"],
78
+ ["promotion-runtime-sha", "Resolved Buildchain runtime SHA", "runtime-sha"],
79
+ ["promotion-contract-lock-path", "Selected consumer contract-lock path", "contract-lock-path"],
80
+ ["promotion-contract-lock-digest", "Selected consumer contract-lock sha256 digest", "contract-lock-digest"],
81
+ ["promotion-publication-channel", "Derived publication channel", "publication-channel"],
82
+ ["promotion-target-ref", "Derived promotion target ref", "target-ref"],
83
+ ["promotion-override-used", "Whether a trusted runtime override was used", "override-used"],
84
+ ].map(([name, description, output]) => [
85
+ ` ${name}:`,
86
+ ` description: "${description}"`,
87
+ ` value: \${{ jobs.resolve-promotion.outputs.${output} }}`,
88
+ ].join("\n"));
89
+ return [...routed, ...forwarded].join("\n");
90
+ }
91
+
92
+ function forwardedInputs(inputNames) {
93
+ return inputNames.map((name) => {
94
+ const routed = {
95
+ "buildchain-ref": "runtime-ref",
96
+ "buildchain-contract-lock-path": "contract-lock-path",
97
+ channel: "publication-channel",
98
+ "target-ref": "target-ref",
99
+ "promotion-router-ref": "router-ref",
100
+ "promotion-router-sha": "router-sha",
101
+ "promotion-shell-ref": "shell-ref",
102
+ "promotion-shell-sha": "shell-sha",
103
+ "promotion-runtime-ref": "runtime-ref",
104
+ "promotion-runtime-sha": "runtime-sha",
105
+ "promotion-contract-lock-path": "contract-lock-path",
106
+ "promotion-contract-lock-digest": "contract-lock-digest",
107
+ "promotion-publication-channel": "publication-channel",
108
+ "promotion-target-ref": "target-ref",
109
+ "promotion-override-used": "override-used",
110
+ }[name];
111
+ return routed
112
+ ? ` ${name}: \${{ needs.resolve-promotion.outputs.${routed} }}`
113
+ : ` ${name}: \${{ inputs.${name} }}`;
114
+ }).join("\n");
115
+ }
116
+
117
+ export function generateChannelPromotionWorkflow(source, { major = 2 } = {}) {
118
+ const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
119
+ const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
120
+ const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
121
+ const inputNames = entries(inputs).map((entry) => entry.name);
122
+ for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
123
+ if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
124
+ }
125
+ const forwarded = forwardedInputs(inputNames);
126
+ return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
127
+ name: Release Candidate Promote
128
+
129
+ on:
130
+ workflow_call:
131
+ inputs:
132
+ ${publicInputs(inputs)}
133
+ secrets:
134
+ ${secrets.trimEnd()}
135
+ outputs:
136
+ ${publicOutputs(outputs)}
137
+
138
+ permissions:
139
+ actions: read
140
+ checks: write
141
+ contents: write
142
+ id-token: write
143
+ issues: write
144
+
145
+ jobs:
146
+ resolve-promotion:
147
+ name: Resolve promotion workflow shell and runtime
148
+ runs-on: ubuntu-24.04
149
+ permissions:
150
+ contents: read
151
+ outputs:
152
+ channel: \${{ steps.route.outputs.channel }}
153
+ publication-channel: \${{ steps.route.outputs.publication-channel }}
154
+ target-ref: \${{ steps.route.outputs.target-ref }}
155
+ router-ref: \${{ steps.router.outputs.ref }}
156
+ router-sha: \${{ steps.identities.outputs.router-sha }}
157
+ shell-ref: \${{ steps.route.outputs.shell-ref }}
158
+ shell-sha: \${{ steps.identities.outputs.shell-sha }}
159
+ runtime-ref: \${{ steps.route.outputs.runtime-ref }}
160
+ runtime-sha: \${{ steps.identities.outputs.runtime-sha }}
161
+ contract-lock-path: \${{ steps.lock.outputs.path }}
162
+ contract-lock-digest: \${{ steps.lock.outputs.digest }}
163
+ override-used: \${{ steps.route.outputs.override-used }}
164
+ selection-source: \${{ steps.route.outputs.selection-source }}
165
+ reason: \${{ steps.route.outputs.reason }}
166
+ steps:
167
+ - name: Resolve promotion router source
168
+ id: router
169
+ shell: bash
170
+ env:
171
+ BUILDCHAIN_ROUTER_WORKFLOW_REF: \${{ job.workflow_ref }}
172
+ run: |
173
+ set -euo pipefail
174
+ workflow_ref="\${BUILDCHAIN_ROUTER_WORKFLOW_REF}"
175
+ repository="\${workflow_ref%%/.github/workflows/*}"
176
+ ref="\${workflow_ref##*@}"
177
+ ref="\${ref#refs/heads/}"
178
+ ref="\${ref#refs/tags/}"
179
+ if [[ -z "\${repository}" || "\${repository}" = "\${workflow_ref}" || -z "\${ref}" ]]; then
180
+ echo "::error::Unable to resolve promotion router source from job.workflow_ref=\${workflow_ref}"
181
+ exit 1
182
+ fi
183
+ echo "repository=\${repository}" >> "\${GITHUB_OUTPUT}"
184
+ echo "ref=\${ref}" >> "\${GITHUB_OUTPUT}"
185
+
186
+ - name: Checkout promotion router
187
+ uses: actions/checkout@v7.0.0
188
+ with:
189
+ repository: \${{ steps.router.outputs.repository }}
190
+ ref: \${{ steps.router.outputs.ref }}
191
+ path: .buildchain/router
192
+ persist-credentials: false
193
+
194
+ - name: Resolve promotion channel
195
+ id: route
196
+ shell: bash
197
+ run: >-
198
+ node .buildchain/router/scripts/promotion-channel-router.mjs
199
+ --cwd .buildchain/router
200
+ --channel "\${{ inputs.buildchain-channel }}"
201
+ --buildchain-ref "\${{ inputs.buildchain-ref }}"
202
+ --publication-channel "\${{ inputs.channel }}"
203
+ --target-ref "\${{ inputs.target-ref || github.ref_name }}"
204
+ --router-ref "\${{ steps.router.outputs.ref }}"
205
+
206
+ - name: Authorize trusted runtime override
207
+ if: \${{ steps.route.outputs.override-used == 'true' }}
208
+ uses: actions/github-script@v8
209
+ with:
210
+ script: |
211
+ if (context.eventName !== "workflow_dispatch") {
212
+ throw new Error("promotion runtime override is only allowed for trusted workflow_dispatch runs");
213
+ }
214
+ const permission = await github.rest.repos.getCollaboratorPermissionLevel({
215
+ owner: context.repo.owner,
216
+ repo: context.repo.repo,
217
+ username: context.actor,
218
+ });
219
+ const level = permission.data.user?.permissions || permission.data.permission || "none";
220
+ if (!["write", "maintain", "admin"].includes(level)) {
221
+ throw new Error(\`promotion runtime override requires write, maintain, or admin permission; actor has \${level}\`);
222
+ }
223
+
224
+ - name: Checkout selected promotion workflow shell
225
+ uses: actions/checkout@v7.0.0
226
+ with:
227
+ repository: \${{ steps.router.outputs.repository }}
228
+ ref: \${{ steps.route.outputs.shell-ref }}
229
+ path: .buildchain/shell
230
+ persist-credentials: false
231
+
232
+ - name: Checkout selected Buildchain runtime
233
+ uses: actions/checkout@v7.0.0
234
+ with:
235
+ repository: \${{ steps.router.outputs.repository }}
236
+ ref: \${{ steps.route.outputs.runtime-ref }}
237
+ path: .buildchain/runtime
238
+ persist-credentials: false
239
+
240
+ - name: Checkout promotion source
241
+ uses: actions/checkout@v7.0.0
242
+ with:
243
+ ref: \${{ inputs.target-sha || github.sha }}
244
+ path: .buildchain/source
245
+ persist-credentials: false
246
+
247
+ - name: Select and verify consumer contract lock
248
+ id: lock
249
+ shell: bash
250
+ env:
251
+ EXPLICIT_PATH: \${{ inputs.buildchain-contract-lock-path }}
252
+ ALPHA_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}
253
+ STABLE_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
254
+ CHANNEL: \${{ steps.route.outputs.channel }}
255
+ run: |
256
+ set -euo pipefail
257
+ path="\${EXPLICIT_PATH}"
258
+ if [[ -z "\${path}" ]]; then
259
+ if [[ "\${CHANNEL}" = "alpha" ]]; then path="\${ALPHA_PATH}"; else path="\${STABLE_PATH}"; fi
260
+ fi
261
+ if [[ -z "\${path}" || ! -f ".buildchain/source/\${path}" ]]; then
262
+ echo "::error::Selected promotion contract lock is missing: \${path:-<empty>}"
263
+ exit 1
264
+ fi
265
+ digest="sha256:$(shasum -a 256 ".buildchain/source/\${path}" | awk '{print $1}')"
266
+ echo "path=\${path}" >> "\${GITHUB_OUTPUT}"
267
+ echo "digest=\${digest}" >> "\${GITHUB_OUTPUT}"
268
+
269
+ - name: Resolve immutable promotion identities
270
+ id: identities
271
+ shell: bash
272
+ run: |
273
+ set -euo pipefail
274
+ test -f .buildchain/shell/.github/workflows/.release-candidate-promote.yml
275
+ {
276
+ echo "router-sha=$(git -C .buildchain/router rev-parse HEAD)"
277
+ echo "shell-sha=$(git -C .buildchain/shell rev-parse HEAD)"
278
+ echo "runtime-sha=$(git -C .buildchain/runtime rev-parse HEAD)"
279
+ } >> "\${GITHUB_OUTPUT}"
280
+
281
+ alpha:
282
+ name: Promote with alpha workflow shell
283
+ needs: resolve-promotion
284
+ if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
285
+ uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@v${major}-alpha
286
+ permissions:
287
+ actions: read
288
+ checks: write
289
+ contents: write
290
+ id-token: write
291
+ issues: write
292
+ with:
293
+ ${forwarded}
294
+ secrets: inherit
295
+
296
+ stable:
297
+ name: Promote with stable workflow shell
298
+ needs: resolve-promotion
299
+ if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
300
+ uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@v${major}
301
+ permissions:
302
+ actions: read
303
+ checks: write
304
+ contents: write
305
+ id-token: write
306
+ issues: write
307
+ with:
308
+ ${forwarded}
309
+ secrets: inherit
310
+ `;
311
+ }
312
+
313
+ function main() {
314
+ const source = fs.readFileSync(sourcePath, "utf8");
315
+ const version = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
316
+ const major = Number(String(version).match(/^(\d+)\./)?.[1]);
317
+ if (!Number.isInteger(major)) throw new Error("package version must expose a numeric major");
318
+ const generated = generateChannelPromotionWorkflow(source, { major });
319
+ if (process.argv.includes("--check")) {
320
+ const current = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
321
+ if (current !== generated) {
322
+ console.error(".github/workflows/release-candidate-promote.yml is stale; run node scripts/generate-channel-promotion-workflow.mjs");
323
+ process.exitCode = 1;
324
+ }
325
+ return;
326
+ }
327
+ fs.writeFileSync(targetPath, generated);
328
+ }
329
+
330
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
331
+ if (isMain) main();
@@ -17,6 +17,10 @@ import {
17
17
  import {
18
18
  createReadmeBadgeEndpointRegistry,
19
19
  } from "../packages/core/readme-badges.js";
20
+ import {
21
+ RELEASE_PASSPORT_SCHEMA,
22
+ createReleasePassportCheckManifest,
23
+ } from "../packages/core/release-passport-contract.js";
20
24
  import {
21
25
  collectPublicSurfaceReverseAudit,
22
26
  enumerateActionInputs,
@@ -496,6 +500,7 @@ function nodeApiMeta(exportName) {
496
500
  "./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
497
501
  "./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
498
502
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
503
+ "./release-passport-contract": { group: "release-passport-trust", summary: "Standalone release passport JSON Schema, ownership/check manifest, and structural validation APIs." },
499
504
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
500
505
  "./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
501
506
  "./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
@@ -915,6 +920,8 @@ function buildSiteBundle() {
915
920
  entrypoint: "buildchain.release.json",
916
921
  bundle: "buildchain-release-bundle.tar.gz",
917
922
  contract: "kungfu-buildchain-release-passport",
923
+ schema: "schemas/release-passport-v1.schema.json",
924
+ checkManifest: "release-passport-check-manifest.json",
918
925
  },
919
926
  releasePropagation: {
920
927
  graphContract: "kungfu-buildchain-release-propagation-graph",
@@ -956,6 +963,8 @@ function buildSiteBundle() {
956
963
  contract: "kungfu-buildchain-artifact-schema-index",
957
964
  releasePassport: [
958
965
  "buildchain.release.json",
966
+ "release-passport-check-manifest.json",
967
+ "schemas/release-passport-v1.schema.json",
959
968
  "artifact-evidence.json",
960
969
  "product-mechanism.json",
961
970
  "impact.json",
@@ -981,6 +990,8 @@ function buildSiteBundle() {
981
990
  "public-surface-audit.json",
982
991
  "release-model.json",
983
992
  "artifact-schemas.json",
993
+ "release-passport-check-manifest.json",
994
+ "schemas/release-passport-v1.schema.json",
984
995
  "buildchain-contract.json",
985
996
  "kfd-claims.json",
986
997
  "product-mechanism.json",
@@ -1090,6 +1101,8 @@ function buildSiteBundle() {
1090
1101
  "publication-authority-registry.json",
1091
1102
  "public-surface-audit.json",
1092
1103
  "artifact-schemas.json",
1104
+ "release-passport-check-manifest.json",
1105
+ "schemas/release-passport-v1.schema.json",
1093
1106
  "buildchain-contract.json",
1094
1107
  "kfd-upstream-aggregate.json",
1095
1108
  "kfd-claims.json",
@@ -1260,6 +1273,8 @@ function buildSiteBundle() {
1260
1273
  "public-surface-audit.json": publicSurfaceAudit,
1261
1274
  "release-model.json": releaseModel,
1262
1275
  "artifact-schemas.json": artifactSchemas,
1276
+ "release-passport-check-manifest.json": createReleasePassportCheckManifest(),
1277
+ "schemas/release-passport-v1.schema.json": RELEASE_PASSPORT_SCHEMA,
1263
1278
  "badge-endpoint-registry.json": badgeEndpointRegistry,
1264
1279
  "publication-registry.json": publicationRegistry,
1265
1280
  "buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { resolveBuildchainChannel } from "./buildchain-channel-router.mjs";
7
+
8
+ const TARGETS = [
9
+ { pattern: /^alpha\/v(\d+)\/v\d+\.\d+$/, publicationChannel: "alpha", shellChannel: "alpha" },
10
+ { pattern: /^release\/v(\d+)\/v\d+\.\d+$/, publicationChannel: "release", shellChannel: "stable" },
11
+ { pattern: /^(?:publish-gate\/major|major-gate)$/, publicationChannel: "major", shellChannel: "stable" },
12
+ ];
13
+
14
+ function normalized(value) {
15
+ return String(value ?? "").trim();
16
+ }
17
+
18
+ function targetIntent(targetRef, requestedPublicationChannel = "") {
19
+ const ref = normalized(targetRef).replace(/^refs\/heads\//, "");
20
+ const target = TARGETS.find((entry) => entry.pattern.test(ref));
21
+ if (!target) throw new Error(`unsupported promotion target ref: ${ref || "<empty>"}`);
22
+ const requested = normalized(requestedPublicationChannel).toLowerCase();
23
+ if (requested && requested !== target.publicationChannel) {
24
+ throw new Error(`promotion channel ${requested} does not match target ref ${ref} (${target.publicationChannel})`);
25
+ }
26
+ const match = ref.match(target.pattern);
27
+ return {
28
+ targetRef: ref,
29
+ publicationChannel: target.publicationChannel,
30
+ shellChannel: target.shellChannel,
31
+ targetMajor: match?.[1] ? Number(match[1]) : undefined,
32
+ };
33
+ }
34
+
35
+ export function resolvePromotionChannel({
36
+ requestedChannel = "auto",
37
+ requestedRef = "",
38
+ publicationChannel = "",
39
+ targetRef = "",
40
+ routerRef = "",
41
+ packageVersion = "",
42
+ } = {}) {
43
+ const intent = targetIntent(targetRef, publicationChannel);
44
+ const selected = resolveBuildchainChannel({
45
+ requestedChannel,
46
+ requestedRef,
47
+ publishChannel: intent.publicationChannel,
48
+ eventName: "workflow_call",
49
+ routerRef,
50
+ packageVersion,
51
+ });
52
+ const overrideUsed = selected.channel === "override";
53
+ if (!overrideUsed && selected.channel !== intent.shellChannel) {
54
+ throw new Error(
55
+ `promotion target ${intent.targetRef} requires ${intent.shellChannel} shell/runtime, got ${selected.channel}`,
56
+ );
57
+ }
58
+ if (Number.isInteger(intent.targetMajor) && selected.major !== intent.targetMajor) {
59
+ throw new Error(
60
+ `promotion target ${intent.targetRef} requires Buildchain v${intent.targetMajor}, got v${selected.major}`,
61
+ );
62
+ }
63
+ const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
64
+ return {
65
+ targetRef: intent.targetRef,
66
+ publicationChannel: intent.publicationChannel,
67
+ channel: intent.shellChannel,
68
+ major: selected.major,
69
+ shellRef,
70
+ runtimeRef: selected.buildchainRef,
71
+ overrideUsed,
72
+ selectionSource: selected.selectionSource,
73
+ reason: selected.reason,
74
+ };
75
+ }
76
+
77
+ function parseArgs(argv) {
78
+ const values = {};
79
+ for (let index = 0; index < argv.length; index += 1) {
80
+ const token = argv[index];
81
+ if (!token.startsWith("--")) throw new Error(`unexpected argument: ${token}`);
82
+ const value = argv[index + 1];
83
+ if (value === undefined || value.startsWith("--")) throw new Error(`missing value for ${token}`);
84
+ values[token.slice(2)] = value;
85
+ index += 1;
86
+ }
87
+ return values;
88
+ }
89
+
90
+ function readPackageVersion(cwd) {
91
+ return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).version;
92
+ }
93
+
94
+ function writeOutputs(file, result) {
95
+ const outputs = {
96
+ "target-ref": result.targetRef,
97
+ "publication-channel": result.publicationChannel,
98
+ channel: result.channel,
99
+ major: String(result.major),
100
+ "shell-ref": result.shellRef,
101
+ "runtime-ref": result.runtimeRef,
102
+ "override-used": String(result.overrideUsed),
103
+ "selection-source": result.selectionSource,
104
+ reason: result.reason,
105
+ };
106
+ fs.appendFileSync(file, Object.entries(outputs).map(([key, value]) => `${key}=${value}\n`).join(""));
107
+ }
108
+
109
+ function main() {
110
+ const args = parseArgs(process.argv.slice(2));
111
+ const cwd = path.resolve(args.cwd || process.cwd());
112
+ const result = resolvePromotionChannel({
113
+ requestedChannel: args.channel,
114
+ requestedRef: args["buildchain-ref"],
115
+ publicationChannel: args["publication-channel"],
116
+ targetRef: args["target-ref"],
117
+ routerRef: args["router-ref"],
118
+ packageVersion: readPackageVersion(cwd),
119
+ });
120
+ if (process.env.GITHUB_OUTPUT) writeOutputs(process.env.GITHUB_OUTPUT, result);
121
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
122
+ }
123
+
124
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
125
+ if (isMain) {
126
+ try {
127
+ main();
128
+ } catch (error) {
129
+ console.error(`promotion-channel-router: ${error.message}`);
130
+ process.exitCode = 1;
131
+ }
132
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { validatePackageManagerContract } from "../packages/core/package-manager.js";
6
+
7
+ const cwd = path.resolve(
8
+ process.env.BUILDCHAIN_PACKAGE_MANAGER_CWD || process.argv[2] || process.cwd(),
9
+ );
10
+ const expectedManager = process.env.BUILDCHAIN_EXPECTED_PACKAGE_MANAGER || "";
11
+ const contract = validatePackageManagerContract({ cwd, expectedManager });
12
+
13
+ if (process.env.GITHUB_OUTPUT) {
14
+ fs.appendFileSync(
15
+ process.env.GITHUB_OUTPUT,
16
+ `name=${contract.name}\ndeclared-spec=${contract.declaredSpec}\ndeclared-version=${contract.declaredVersion}\n`,
17
+ );
18
+ }
19
+
20
+ console.log(JSON.stringify({
21
+ schemaVersion: 1,
22
+ contract: "kungfu-buildchain-consumer-package-manager",
23
+ ...contract,
24
+ }, null, 2));