@kungfu-tech/buildchain 2.14.3-alpha.0 → 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.
- package/dist/site/buildchain-contract.json +130 -24
- package/dist/site/buildchain-site.json +16 -16
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +48 -4
- package/dist/site/kfd-claims.json +111 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +5 -5
- package/dist/site/page-registry.json +8 -8
- package/dist/site/public-surface-audit.json +124 -6
- package/dist/site/publication-authority-registry.json +26 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +124 -3
- package/docs/migration-inventory.md +8 -0
- package/docs/release-candidate.md +3 -0
- package/docs/release-governance.md +5 -4
- package/docs/reusable-build-surface.md +26 -1
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +41 -0
- package/packages/core/buildchain-kfd-claims.js +4 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/controller-evidence.js +1 -1
- package/packages/core/release-passport.js +39 -0
- package/scripts/check-inventory.mjs +30 -2
- package/scripts/generate-channel-promotion-workflow.mjs +331 -0
- package/scripts/promotion-channel-router.mjs +132 -0
|
@@ -89,6 +89,7 @@ export function createBuildchainContractWorld({
|
|
|
89
89
|
"publication-artifact",
|
|
90
90
|
"paper-release",
|
|
91
91
|
"release-candidate-promote",
|
|
92
|
+
".release-candidate-promote",
|
|
92
93
|
"release-propagation",
|
|
93
94
|
]);
|
|
94
95
|
const hasCompleteControllerSource = [...controllerWorkflowIds]
|
|
@@ -173,11 +174,17 @@ export function createBuildchainContractWorld({
|
|
|
173
174
|
requiredInputs: ["channel"],
|
|
174
175
|
requiredOutputs: ["promoted-sha", "built-source-sha", "release-candidate-artifact"],
|
|
175
176
|
breakingDefaults: {
|
|
177
|
+
channelDefault: "auto",
|
|
178
|
+
alphaShellDefault: `${majorLine}-alpha`,
|
|
179
|
+
stableShellDefault: majorLine,
|
|
176
180
|
promoteOnlyReleaseCandidate: true,
|
|
177
181
|
requiredStatusCheck: "check / check",
|
|
178
182
|
},
|
|
179
183
|
optionalInputs: [
|
|
184
|
+
"buildchain-channel",
|
|
180
185
|
"buildchain-ref",
|
|
186
|
+
"buildchain-alpha-contract-lock-path",
|
|
187
|
+
"buildchain-stable-contract-lock-path",
|
|
181
188
|
"release-candidate-workflow-file",
|
|
182
189
|
"release-candidate-workflow-name",
|
|
183
190
|
"publish-required-artifacts-json",
|
|
@@ -210,6 +217,9 @@ export function createBuildchainContractWorld({
|
|
|
210
217
|
],
|
|
211
218
|
},
|
|
212
219
|
guarantees: [
|
|
220
|
+
"one declarative promotion job selects matching alpha or stable workflow shell, runtime, and contract lock",
|
|
221
|
+
"router, shell, runtime, lock, publication channel, target ref, and override identities are bound before candidate download",
|
|
222
|
+
"train and exact-SHA runtime overrides remain limited to trusted workflow_dispatch actors",
|
|
213
223
|
"promotion reuses PR-stage release-candidate artifacts",
|
|
214
224
|
"promotion does not run the heavy native build matrix",
|
|
215
225
|
"built source and promotion channel SHA are recorded separately",
|
|
@@ -218,6 +228,37 @@ export function createBuildchainContractWorld({
|
|
|
218
228
|
"GitHub Release passport and evidence publication is delegated to promote-buildchain-ref after the semver release transaction completes",
|
|
219
229
|
],
|
|
220
230
|
}),
|
|
231
|
+
surface(root, {
|
|
232
|
+
id: "advanced-release-candidate-promote",
|
|
233
|
+
kind: "workflow",
|
|
234
|
+
path: ".github/workflows/.release-candidate-promote.yml",
|
|
235
|
+
publicRef: `${pkg.repository ? "kungfu-systems/buildchain" : "buildchain"}/.github/workflows/.release-candidate-promote.yml@${majorLine}`,
|
|
236
|
+
requiredInputs: ["channel"],
|
|
237
|
+
requiredOutputs: ["promoted-sha", "built-source-sha", "release-candidate-artifact"],
|
|
238
|
+
breakingDefaults: {
|
|
239
|
+
promoteOnlyReleaseCandidate: true,
|
|
240
|
+
requiredStatusCheck: "check / check",
|
|
241
|
+
},
|
|
242
|
+
optionalInputs: [
|
|
243
|
+
"buildchain-ref",
|
|
244
|
+
"buildchain-contract-lock-path",
|
|
245
|
+
"promotion-router-ref",
|
|
246
|
+
"promotion-router-sha",
|
|
247
|
+
"promotion-shell-ref",
|
|
248
|
+
"promotion-shell-sha",
|
|
249
|
+
"promotion-runtime-ref",
|
|
250
|
+
"promotion-runtime-sha",
|
|
251
|
+
"promotion-contract-lock-path",
|
|
252
|
+
"promotion-contract-lock-digest",
|
|
253
|
+
"promotion-publication-channel",
|
|
254
|
+
"promotion-target-ref",
|
|
255
|
+
"promotion-override-used",
|
|
256
|
+
],
|
|
257
|
+
guarantees: [
|
|
258
|
+
"advanced promotion verifies routed shell, runtime, lock, publication channel, and target bindings before candidate resolution",
|
|
259
|
+
"promotion reuses PR-stage release-candidate artifacts and does not run the heavy native build matrix",
|
|
260
|
+
],
|
|
261
|
+
}),
|
|
221
262
|
surface(root, {
|
|
222
263
|
id: "web-surface",
|
|
223
264
|
kind: "workflow",
|
|
@@ -82,6 +82,7 @@ const SCHEMA_AND_STANDARD_FILES = Object.freeze([
|
|
|
82
82
|
|
|
83
83
|
const WORKFLOW_AND_ACTION_FILES = Object.freeze([
|
|
84
84
|
".github/workflows/.build.yml",
|
|
85
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
85
86
|
".github/workflows/release-candidate-promote.yml",
|
|
86
87
|
".github/workflows/buildchain-ref-promotion.yml",
|
|
87
88
|
".github/workflows/release-propagation.yml",
|
|
@@ -372,6 +373,7 @@ export function createBuildchainPublicClaimDefinitions() {
|
|
|
372
373
|
"packages/core/buildchain-contract.js",
|
|
373
374
|
"scripts/buildchain-contract-lock.mjs",
|
|
374
375
|
".github/workflows/.build.yml",
|
|
376
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
375
377
|
".github/workflows/release-candidate-promote.yml",
|
|
376
378
|
"docs/reusable-build-surface.md",
|
|
377
379
|
],
|
|
@@ -387,6 +389,7 @@ export function createBuildchainPublicClaimDefinitions() {
|
|
|
387
389
|
"actions/promote-buildchain-ref/index.js",
|
|
388
390
|
"actions/promote-buildchain-ref/action.yml",
|
|
389
391
|
"scripts/ensure-github-release.mjs",
|
|
392
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
390
393
|
".github/workflows/release-candidate-promote.yml",
|
|
391
394
|
"docs/release-governance.md",
|
|
392
395
|
],
|
|
@@ -415,6 +418,7 @@ export function createBuildchainPublicClaimDefinitions() {
|
|
|
415
418
|
sourcePaths: [
|
|
416
419
|
"scripts/npm-publish-transaction.mjs",
|
|
417
420
|
"actions/promote-buildchain-ref/index.js",
|
|
421
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
418
422
|
".github/workflows/release-candidate-promote.yml",
|
|
419
423
|
"docs/publish-transaction.md",
|
|
420
424
|
],
|
|
@@ -5,6 +5,7 @@ import { createPublicationAuthorityRegistry } from "./publication-authority.js";
|
|
|
5
5
|
|
|
6
6
|
const DESCRIPTORS = Object.freeze([
|
|
7
7
|
[".github/workflows/.build.yml", "non-publication-oidc"],
|
|
8
|
+
[".github/workflows/.release-candidate-promote.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "caller-bound"],
|
|
8
9
|
[".github/workflows/.publication-authority.yml", "evidence-publication"],
|
|
9
10
|
[".github/workflows/.release-verify.yml", "governance-write"],
|
|
10
11
|
[".github/workflows/.sam-release.yml", "retired-deny"],
|
|
@@ -72,7 +72,7 @@ const CONTROLLER_SPECS = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
id: "release-candidate-promotion",
|
|
75
|
-
workflowId: "release-candidate-promote",
|
|
75
|
+
workflowId: ".release-candidate-promote",
|
|
76
76
|
version: 1,
|
|
77
77
|
capabilities: ["promotion-preflight", "artifact-admission", "publication-authority", "publish-transaction", "release-passport"],
|
|
78
78
|
stages: ["preflight", "admit-release-candidate", "publication-authority", "publish", "passport", "aggregate"],
|
|
@@ -926,6 +926,43 @@ function normalizeTransaction(value = undefined) {
|
|
|
926
926
|
};
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
+
function normalizePromotionRouting(value = undefined) {
|
|
930
|
+
if (value === undefined) return undefined;
|
|
931
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
932
|
+
throw new Error("release.promotionRouting must be a JSON object");
|
|
933
|
+
}
|
|
934
|
+
if (value.contract !== "buildchain.promotion-routing/v1") {
|
|
935
|
+
throw new Error("release.promotionRouting contract must be buildchain.promotion-routing/v1");
|
|
936
|
+
}
|
|
937
|
+
for (const [label, field] of [
|
|
938
|
+
["router.ref", value.router?.ref],
|
|
939
|
+
["router.sha", value.router?.sha],
|
|
940
|
+
["shell.ref", value.shell?.ref],
|
|
941
|
+
["shell.sha", value.shell?.sha],
|
|
942
|
+
["runtime.requestedRef", value.runtime?.requestedRef],
|
|
943
|
+
["runtime.resolvedSha", value.runtime?.resolvedSha],
|
|
944
|
+
["contractLock.path", value.contractLock?.path],
|
|
945
|
+
["contractLock.digest", value.contractLock?.digest],
|
|
946
|
+
["publication.channel", value.publication?.channel],
|
|
947
|
+
["publication.targetRef", value.publication?.targetRef],
|
|
948
|
+
]) {
|
|
949
|
+
if (!String(field || "").trim()) throw new Error(`release.promotionRouting.${label} is required`);
|
|
950
|
+
}
|
|
951
|
+
for (const [label, sha] of [
|
|
952
|
+
["router.sha", value.router.sha],
|
|
953
|
+
["shell.sha", value.shell.sha],
|
|
954
|
+
["runtime.resolvedSha", value.runtime.resolvedSha],
|
|
955
|
+
]) {
|
|
956
|
+
if (!/^[0-9a-f]{40}$/i.test(String(sha))) {
|
|
957
|
+
throw new Error(`release.promotionRouting.${label} must be a 40-character Git SHA`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(String(value.contractLock.digest))) {
|
|
961
|
+
throw new Error("release.promotionRouting.contractLock.digest must be a sha256 digest");
|
|
962
|
+
}
|
|
963
|
+
return JSON.parse(JSON.stringify(value));
|
|
964
|
+
}
|
|
965
|
+
|
|
929
966
|
export function createArtifactEvidence({ assets = [], repository = "", tag = "", sourceSha = "", workflow = {} } = {}) {
|
|
930
967
|
const normalizedAssets = assets.map((asset, index) => normalizeAsset(asset, index));
|
|
931
968
|
return {
|
|
@@ -1002,6 +1039,7 @@ export function createReleasePassport({
|
|
|
1002
1039
|
const normalizedPackageSet = normalizePackageSet(packageSet, { packageName, packageVersion, publish });
|
|
1003
1040
|
const normalizedTrustedPublishing = normalizeTrustedPublishing(trustedPublishing, { workflow, publish });
|
|
1004
1041
|
const normalizedTransaction = normalizeTransaction(transaction);
|
|
1042
|
+
const normalizedPromotionRouting = normalizePromotionRouting(release.promotionRouting);
|
|
1005
1043
|
const normalizedBuildSummary = buildSummary ? normalizeEvidenceDocument(buildSummary, "buildSummary") : undefined;
|
|
1006
1044
|
const normalizedBuildFacts = (buildFacts || [])
|
|
1007
1045
|
.map((fact, index) => normalizeEvidenceDocument(fact, `buildFacts[${index}]`))
|
|
@@ -1150,6 +1188,7 @@ export function createReleasePassport({
|
|
|
1150
1188
|
...(anchorManifest ? { anchorManifest } : {}),
|
|
1151
1189
|
...(normalizedTrustedPublishing ? { trustedPublishing: normalizedTrustedPublishing } : {}),
|
|
1152
1190
|
...(normalizedTransaction ? { transaction: normalizedTransaction } : {}),
|
|
1191
|
+
...(normalizedPromotionRouting ? { promotionRouting: normalizedPromotionRouting } : {}),
|
|
1153
1192
|
...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
|
|
1154
1193
|
...(normalizedBuildFacts.length > 0 ? { buildFacts: normalizedBuildFacts } : {}),
|
|
1155
1194
|
...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../packages/core/public-surface-audit.js";
|
|
7
7
|
import { evaluateBuildchainContractLock } from "../packages/core/buildchain-contract.js";
|
|
8
8
|
import { generateChannelBuildWorkflow } from "./generate-channel-build-workflow.mjs";
|
|
9
|
+
import { generateChannelPromotionWorkflow } from "./generate-channel-promotion-workflow.mjs";
|
|
9
10
|
|
|
10
11
|
const root = process.cwd();
|
|
11
12
|
const sharedActionTsupConfig = fs.readFileSync(path.join(root, "scripts/tsup-action.config.mjs"), "utf8");
|
|
@@ -44,6 +45,8 @@ const requiredPaths = [
|
|
|
44
45
|
"scripts/release-line-dry-run.mjs",
|
|
45
46
|
"scripts/buildchain-channel-router.mjs",
|
|
46
47
|
"scripts/generate-channel-build-workflow.mjs",
|
|
48
|
+
"scripts/promotion-channel-router.mjs",
|
|
49
|
+
"scripts/generate-channel-promotion-workflow.mjs",
|
|
47
50
|
"scripts/build-standalone-binary.mjs",
|
|
48
51
|
"scripts/create-release-bundle.mjs",
|
|
49
52
|
"scripts/ensure-github-release.mjs",
|
|
@@ -81,6 +84,7 @@ const requiredPaths = [
|
|
|
81
84
|
".github/workflows/buildchain-patrol-monthly.yml",
|
|
82
85
|
".github/workflows/buildchain-alpha-self-dogfood.yml",
|
|
83
86
|
".github/workflows/release-candidate-promote.yml",
|
|
87
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
84
88
|
".github/workflows/release-propagation.yml",
|
|
85
89
|
".github/workflows/npm-publish.yml",
|
|
86
90
|
".github/workflows/paper-release.yml",
|
|
@@ -174,6 +178,30 @@ const channelBuildWorkflow = fs.readFileSync(
|
|
|
174
178
|
if (channelBuildWorkflow !== generateChannelBuildWorkflow(reusableBuildWorkflow)) {
|
|
175
179
|
throw new Error("generated channel build workflow is stale");
|
|
176
180
|
}
|
|
181
|
+
const advancedPromotionWorkflow = fs.readFileSync(
|
|
182
|
+
path.join(root, ".github/workflows/.release-candidate-promote.yml"),
|
|
183
|
+
"utf8",
|
|
184
|
+
);
|
|
185
|
+
const channelPromotionWorkflow = fs.readFileSync(
|
|
186
|
+
path.join(root, ".github/workflows/release-candidate-promote.yml"),
|
|
187
|
+
"utf8",
|
|
188
|
+
);
|
|
189
|
+
if (channelPromotionWorkflow !== generateChannelPromotionWorkflow(advancedPromotionWorkflow, {
|
|
190
|
+
major: Number(selfDogfoodMajor),
|
|
191
|
+
})) {
|
|
192
|
+
throw new Error("generated channel promotion workflow is stale");
|
|
193
|
+
}
|
|
194
|
+
for (const requiredSnippet of [
|
|
195
|
+
"buildchain-channel:",
|
|
196
|
+
`/.github/workflows/.release-candidate-promote.yml@v${selfDogfoodMajor}-alpha`,
|
|
197
|
+
`/.github/workflows/.release-candidate-promote.yml@v${selfDogfoodMajor}`,
|
|
198
|
+
"promotion-contract-lock-digest:",
|
|
199
|
+
"promotion runtime override is only allowed for trusted workflow_dispatch runs",
|
|
200
|
+
]) {
|
|
201
|
+
if (!channelPromotionWorkflow.includes(requiredSnippet)) {
|
|
202
|
+
throw new Error(`channel promotion workflow missing routing contract: ${requiredSnippet}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
177
205
|
for (const requiredSnippet of [
|
|
178
206
|
"buildchain-channel:",
|
|
179
207
|
"uses: ./.github/workflows/.build.yml",
|
|
@@ -814,7 +842,7 @@ for (const forbiddenSnippet of [
|
|
|
814
842
|
for (const requiredSnippet of [
|
|
815
843
|
"id-token: write",
|
|
816
844
|
"actions: read",
|
|
817
|
-
"uses: ./.github/workflows
|
|
845
|
+
"uses: ./.github/workflows/.release-candidate-promote.yml",
|
|
818
846
|
"github.event.workflow_run.event == 'push'",
|
|
819
847
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
|
|
820
848
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
|
|
@@ -828,7 +856,7 @@ for (const requiredSnippet of [
|
|
|
828
856
|
throw new Error(`buildchain ref promotion workflow missing npm transaction snippet: ${requiredSnippet}`);
|
|
829
857
|
}
|
|
830
858
|
}
|
|
831
|
-
const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows
|
|
859
|
+
const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows/.release-candidate-promote.yml"), "utf8");
|
|
832
860
|
for (const requiredSnippet of [
|
|
833
861
|
"release-passport-kfd-1-witness-jsons:",
|
|
834
862
|
"release-passport-kfd-1-witness-jsons: ${{ inputs.release-passport-kfd-1-witness-jsons }}",
|
|
@@ -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();
|