@kungfu-tech/buildchain 2.11.14-alpha.7 → 2.12.0-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/AGENTS.md +12 -12
- package/README.md +3 -1
- package/dist/site/buildchain-contract.json +45 -6
- package/dist/site/buildchain-site.json +26 -21
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/kfd-claims.json +74 -6
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/page-registry.json +17 -12
- package/dist/site/public-surface-audit.json +63 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +64 -1
- package/docs/MAP.md +1 -0
- package/docs/release-governance.md +12 -9
- package/docs/reusable-build-surface.md +79 -7
- package/docs/versioning.md +1 -0
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +43 -1
- package/scripts/buildchain-channel-router.mjs +223 -0
- package/scripts/check-inventory.mjs +52 -3
- package/scripts/generate-channel-build-workflow.mjs +101 -0
- package/scripts/generate-site-bundle.mjs +2 -1
- package/scripts/locked-source-checkout.mjs +2 -2
|
@@ -0,0 +1,223 @@
|
|
|
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 CHANNELS = new Set(["auto", "alpha", "stable"]);
|
|
8
|
+
const STABLE_PUBLISH_CHANNELS = new Set(["release", "major"]);
|
|
9
|
+
const OFFICIAL_REF = /^v(\d+)(?:\.\d+)?(?:-alpha)?$/;
|
|
10
|
+
const EXACT_SHA = /^[0-9a-f]{40}$/i;
|
|
11
|
+
const TRAIN_REF = /^(?:refs\/heads\/)?train\/v(\d+)\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
12
|
+
const SEMVER_TAG = /^refs\/tags\/v?\d+\.\d+\.\d+(?:-([0-9A-Za-z.-]+))?$/;
|
|
13
|
+
|
|
14
|
+
function normalized(value) {
|
|
15
|
+
return String(value ?? "").trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBoolean(value, name) {
|
|
19
|
+
const text = normalized(value).toLowerCase();
|
|
20
|
+
if (text === "true") return true;
|
|
21
|
+
if (text === "false") return false;
|
|
22
|
+
throw new Error(`${name} must be true or false`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readPackageVersion(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).version || "";
|
|
28
|
+
} catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function majorFrom(value) {
|
|
34
|
+
const text = normalized(value).replace(/^refs\/(?:heads|tags)\//, "");
|
|
35
|
+
const official = text.match(OFFICIAL_REF);
|
|
36
|
+
if (official) return Number(official[1]);
|
|
37
|
+
const train = text.match(TRAIN_REF);
|
|
38
|
+
if (train) return Number(train[1]);
|
|
39
|
+
const embedded = text.match(/(?:^|\/)v(\d+)(?:$|[./-])/);
|
|
40
|
+
if (embedded) return Number(embedded[1]);
|
|
41
|
+
const version = text.match(/^(\d+)\.\d+\.\d+/);
|
|
42
|
+
return version ? Number(version[1]) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveMajor({ requestedRef, routerRef, packageVersion }) {
|
|
46
|
+
for (const candidate of [requestedRef, routerRef, packageVersion]) {
|
|
47
|
+
const major = majorFrom(candidate);
|
|
48
|
+
if (Number.isInteger(major)) return major;
|
|
49
|
+
}
|
|
50
|
+
throw new Error("unable to derive the Buildchain major; call the router from a vN ref or provide a vN buildchain-ref");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function classifyRequestedRef(value) {
|
|
54
|
+
const ref = normalized(value).replace(/^refs\/(?:heads|tags)\//, "");
|
|
55
|
+
if (!ref) return { kind: "none", ref };
|
|
56
|
+
if (EXACT_SHA.test(ref)) return { kind: "override", ref };
|
|
57
|
+
if (TRAIN_REF.test(value) || TRAIN_REF.test(ref)) {
|
|
58
|
+
return { kind: "override", ref: normalized(value).replace(/^refs\/heads\//, "") };
|
|
59
|
+
}
|
|
60
|
+
const official = ref.match(OFFICIAL_REF);
|
|
61
|
+
if (official) {
|
|
62
|
+
return {
|
|
63
|
+
kind: ref.endsWith("-alpha") ? "alpha" : "stable",
|
|
64
|
+
ref,
|
|
65
|
+
major: Number(official[1]),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw new Error("buildchain-ref must be an official vN/vN.M channel, a train ref, or an exact 40-character SHA");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function selected(channel, major, source, reason) {
|
|
72
|
+
return {
|
|
73
|
+
channel,
|
|
74
|
+
major,
|
|
75
|
+
buildchainRef: channel === "alpha" ? `v${major}-alpha` : `v${major}`,
|
|
76
|
+
selectionSource: source,
|
|
77
|
+
reason,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveBuildchainChannel({
|
|
82
|
+
requestedChannel = "auto",
|
|
83
|
+
requestedRef = "",
|
|
84
|
+
publishChannel = "none",
|
|
85
|
+
eventName = "",
|
|
86
|
+
gitRef = "",
|
|
87
|
+
releasePrerelease = "",
|
|
88
|
+
routerRef = "",
|
|
89
|
+
packageVersion = "",
|
|
90
|
+
} = {}) {
|
|
91
|
+
const channel = normalized(requestedChannel || "auto").toLowerCase();
|
|
92
|
+
if (!CHANNELS.has(channel)) {
|
|
93
|
+
throw new Error("buildchain-channel must be auto, alpha, or stable");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const explicitRef = classifyRequestedRef(requestedRef);
|
|
97
|
+
const major = resolveMajor({ requestedRef: explicitRef.ref, routerRef, packageVersion });
|
|
98
|
+
if (explicitRef.kind !== "none") {
|
|
99
|
+
if (channel !== "auto" && explicitRef.kind !== "override" && channel !== explicitRef.kind) {
|
|
100
|
+
throw new Error(`buildchain-channel=${channel} conflicts with buildchain-ref=${explicitRef.ref}`);
|
|
101
|
+
}
|
|
102
|
+
if (channel !== "auto" && explicitRef.kind === "override") {
|
|
103
|
+
throw new Error("train and exact-SHA buildchain-ref overrides require buildchain-channel=auto");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
channel: explicitRef.kind,
|
|
107
|
+
major,
|
|
108
|
+
buildchainRef: explicitRef.ref,
|
|
109
|
+
selectionSource: "explicit-buildchain-ref",
|
|
110
|
+
reason: `explicit Buildchain runtime ref ${explicitRef.ref}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (channel !== "auto") {
|
|
115
|
+
return selected(channel, major, "explicit-channel", `explicit buildchain-channel=${channel}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const publishing = normalized(publishChannel || "none").toLowerCase();
|
|
119
|
+
if (publishing === "alpha") {
|
|
120
|
+
return selected("alpha", major, "publish-channel", "publish-channel=alpha");
|
|
121
|
+
}
|
|
122
|
+
if (STABLE_PUBLISH_CHANNELS.has(publishing)) {
|
|
123
|
+
return selected("stable", major, "publish-channel", `publish-channel=${publishing}`);
|
|
124
|
+
}
|
|
125
|
+
if (publishing !== "none") {
|
|
126
|
+
throw new Error(`custom publish-channel=${publishing} requires an explicit buildchain-channel`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (eventName === "release") {
|
|
130
|
+
if (normalized(releasePrerelease) === "") {
|
|
131
|
+
throw new Error("release events require github.event.release.prerelease to select a Buildchain channel");
|
|
132
|
+
}
|
|
133
|
+
const prerelease = parseBoolean(releasePrerelease, "releasePrerelease");
|
|
134
|
+
return selected(
|
|
135
|
+
prerelease ? "alpha" : "stable",
|
|
136
|
+
major,
|
|
137
|
+
"release-event",
|
|
138
|
+
prerelease ? "GitHub prerelease event" : "GitHub stable release event",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ref = normalized(gitRef);
|
|
143
|
+
if (ref.startsWith("refs/tags/")) {
|
|
144
|
+
const tag = ref.match(SEMVER_TAG);
|
|
145
|
+
if (!tag) {
|
|
146
|
+
throw new Error(`tag ${ref} is not a canonical semver release tag; set buildchain-channel explicitly`);
|
|
147
|
+
}
|
|
148
|
+
return selected(
|
|
149
|
+
tag[1] ? "alpha" : "stable",
|
|
150
|
+
major,
|
|
151
|
+
"semver-tag",
|
|
152
|
+
tag[1] ? `prerelease tag ${ref}` : `stable tag ${ref}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return selected("alpha", major, "development-default", `non-release ${eventName || "unknown"} event`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseArgs(argv) {
|
|
160
|
+
const values = {};
|
|
161
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
162
|
+
const token = argv[index];
|
|
163
|
+
if (!token.startsWith("--")) throw new Error(`unexpected argument: ${token}`);
|
|
164
|
+
const key = token.slice(2);
|
|
165
|
+
const value = argv[index + 1];
|
|
166
|
+
if (value === undefined || value.startsWith("--")) throw new Error(`missing value for --${key}`);
|
|
167
|
+
values[key] = value;
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
return values;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendOutputs(file, result) {
|
|
174
|
+
const entries = {
|
|
175
|
+
channel: result.channel,
|
|
176
|
+
major: String(result.major),
|
|
177
|
+
"buildchain-ref": result.buildchainRef,
|
|
178
|
+
"selection-source": result.selectionSource,
|
|
179
|
+
reason: result.reason,
|
|
180
|
+
};
|
|
181
|
+
fs.appendFileSync(file, Object.entries(entries).map(([key, value]) => `${key}=${value}\n`).join(""));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function appendSummary(file, result) {
|
|
185
|
+
fs.appendFileSync(
|
|
186
|
+
file,
|
|
187
|
+
[
|
|
188
|
+
"## Buildchain channel router",
|
|
189
|
+
"",
|
|
190
|
+
`- selected channel: \`${result.channel}\``,
|
|
191
|
+
`- selected runtime ref: \`${result.buildchainRef}\``,
|
|
192
|
+
`- selection source: \`${result.selectionSource}\``,
|
|
193
|
+
`- reason: ${result.reason}`,
|
|
194
|
+
"",
|
|
195
|
+
].join("\n"),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function main() {
|
|
200
|
+
const args = parseArgs(process.argv.slice(2));
|
|
201
|
+
const cwd = path.resolve(args.cwd || process.cwd());
|
|
202
|
+
const result = resolveBuildchainChannel({
|
|
203
|
+
requestedChannel: args.channel,
|
|
204
|
+
requestedRef: args["buildchain-ref"],
|
|
205
|
+
publishChannel: args["publish-channel"],
|
|
206
|
+
eventName: args["event-name"],
|
|
207
|
+
gitRef: args.ref,
|
|
208
|
+
releasePrerelease: args["release-prerelease"],
|
|
209
|
+
routerRef: args["router-ref"],
|
|
210
|
+
packageVersion: readPackageVersion(cwd),
|
|
211
|
+
});
|
|
212
|
+
if (process.env.GITHUB_OUTPUT) appendOutputs(process.env.GITHUB_OUTPUT, result);
|
|
213
|
+
if (process.env.GITHUB_STEP_SUMMARY) appendSummary(process.env.GITHUB_STEP_SUMMARY, result);
|
|
214
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
218
|
+
if (isMain) {
|
|
219
|
+
main().catch((error) => {
|
|
220
|
+
console.error(`buildchain-channel-router: ${error.message}`);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
assertPublicSurfaceReverseAudit,
|
|
5
5
|
collectPublicSurfaceReverseAudit,
|
|
6
6
|
} from "../packages/core/public-surface-audit.js";
|
|
7
|
+
import { evaluateBuildchainContractLock } from "../packages/core/buildchain-contract.js";
|
|
8
|
+
import { generateChannelBuildWorkflow } from "./generate-channel-build-workflow.mjs";
|
|
7
9
|
|
|
8
10
|
const root = process.cwd();
|
|
9
11
|
const sharedActionTsupConfig = fs.readFileSync(path.join(root, "scripts/tsup-action.config.mjs"), "utf8");
|
|
@@ -38,6 +40,8 @@ const requiredPaths = [
|
|
|
38
40
|
"docs/toolkit-observability.md",
|
|
39
41
|
"docs/versioning.md",
|
|
40
42
|
"scripts/release-line-dry-run.mjs",
|
|
43
|
+
"scripts/buildchain-channel-router.mjs",
|
|
44
|
+
"scripts/generate-channel-build-workflow.mjs",
|
|
41
45
|
"scripts/build-standalone-binary.mjs",
|
|
42
46
|
"scripts/create-release-bundle.mjs",
|
|
43
47
|
"scripts/ensure-github-release.mjs",
|
|
@@ -58,6 +62,7 @@ const requiredPaths = [
|
|
|
58
62
|
"tests/buildchain-inventory.json",
|
|
59
63
|
".buildchain/buildchain.toml",
|
|
60
64
|
".buildchain/contract-lock.json",
|
|
65
|
+
".buildchain/alpha-contract-lock.json",
|
|
61
66
|
".buildchain/release-impact.json",
|
|
62
67
|
".github/actionlint.yaml",
|
|
63
68
|
".github/workflows/self-hosted-runner-smoke.yml",
|
|
@@ -79,6 +84,7 @@ const requiredPaths = [
|
|
|
79
84
|
".github/workflows/binary-distribution.yml",
|
|
80
85
|
".github/workflows/verify.yml",
|
|
81
86
|
".github/workflows/.build.yml",
|
|
87
|
+
".github/workflows/build.yml",
|
|
82
88
|
".github/workflows/build-surface-fixture.yml",
|
|
83
89
|
".github/workflows/candidate-lab.yml",
|
|
84
90
|
"fixtures/libnode-shaped/buildchain.toml",
|
|
@@ -102,14 +108,39 @@ const selfDogfoodWorkflow = fs.readFileSync(
|
|
|
102
108
|
path.join(root, ".github/workflows/buildchain-alpha-self-dogfood.yml"),
|
|
103
109
|
"utf8",
|
|
104
110
|
);
|
|
111
|
+
const selfDogfoodAlphaLock = JSON.parse(
|
|
112
|
+
fs.readFileSync(path.join(root, ".buildchain/alpha-contract-lock.json"), "utf8"),
|
|
113
|
+
);
|
|
114
|
+
const currentBuildchainContract = JSON.parse(
|
|
115
|
+
fs.readFileSync(path.join(root, "dist/site/buildchain-contract.json"), "utf8"),
|
|
116
|
+
);
|
|
105
117
|
const selfDogfoodMajor = String(rootPackage.version || "").match(/^(\d+)\./)?.[1];
|
|
106
118
|
if (!selfDogfoodMajor) {
|
|
107
119
|
throw new Error("root package version must expose a numeric major for self-dogfood");
|
|
108
120
|
}
|
|
121
|
+
if (selfDogfoodAlphaLock.buildchain?.ref !== `v${selfDogfoodMajor}-alpha`) {
|
|
122
|
+
throw new Error("Buildchain self-dogfood alpha lock must target the current major alpha ref");
|
|
123
|
+
}
|
|
124
|
+
if (!/^[0-9a-f]{40}$/.test(selfDogfoodAlphaLock.buildchain?.resolvedSha || "")) {
|
|
125
|
+
throw new Error("Buildchain self-dogfood alpha lock must record an exact accepted SHA");
|
|
126
|
+
}
|
|
127
|
+
if (selfDogfoodAlphaLock.buildchain?.compatibilityPolicy !== "major-compatible") {
|
|
128
|
+
throw new Error("Buildchain self-dogfood alpha lock must enforce major-compatible policy");
|
|
129
|
+
}
|
|
130
|
+
const selfDogfoodAlphaEvaluation = evaluateBuildchainContractLock({
|
|
131
|
+
lock: selfDogfoodAlphaLock,
|
|
132
|
+
current: currentBuildchainContract,
|
|
133
|
+
runtimeRef: `v${selfDogfoodMajor}-alpha`,
|
|
134
|
+
runtimeSha: "current-development-contract",
|
|
135
|
+
runtimeClass: "alpha",
|
|
136
|
+
});
|
|
137
|
+
if (!selfDogfoodAlphaEvaluation.compatible) {
|
|
138
|
+
throw new Error("Buildchain self-dogfood alpha lock requires review after a breaking contract change");
|
|
139
|
+
}
|
|
109
140
|
for (const requiredSnippet of [
|
|
110
|
-
`/.github/workflows
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
`/.github/workflows/build.yml@v${selfDogfoodMajor}-alpha`,
|
|
142
|
+
"buildchain-channel: auto",
|
|
143
|
+
"buildchain-channel: stable",
|
|
113
144
|
`const alphaRef = "v${selfDogfoodMajor}-alpha"`,
|
|
114
145
|
`const stableRef = "v${selfDogfoodMajor}"`,
|
|
115
146
|
]) {
|
|
@@ -130,6 +161,23 @@ for (const requiredSnippet of [
|
|
|
130
161
|
throw new Error(`reusable build workflow missing called-workflow identity: ${requiredSnippet}`);
|
|
131
162
|
}
|
|
132
163
|
}
|
|
164
|
+
const channelBuildWorkflow = fs.readFileSync(
|
|
165
|
+
path.join(root, ".github/workflows/build.yml"),
|
|
166
|
+
"utf8",
|
|
167
|
+
);
|
|
168
|
+
if (channelBuildWorkflow !== generateChannelBuildWorkflow(reusableBuildWorkflow)) {
|
|
169
|
+
throw new Error("generated channel build workflow is stale");
|
|
170
|
+
}
|
|
171
|
+
for (const requiredSnippet of [
|
|
172
|
+
"buildchain-channel:",
|
|
173
|
+
"uses: ./.github/workflows/.build.yml",
|
|
174
|
+
"needs.resolve-channel.outputs.buildchain-ref",
|
|
175
|
+
"needs.resolve-channel.outputs.contract-lock-path",
|
|
176
|
+
]) {
|
|
177
|
+
if (!channelBuildWorkflow.includes(requiredSnippet)) {
|
|
178
|
+
throw new Error(`channel build workflow missing routing contract: ${requiredSnippet}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
133
181
|
for (const requiredSnippet of [
|
|
134
182
|
"group: buildchain-release-promotion-${{ github.repository }}",
|
|
135
183
|
"cancel-in-progress: false",
|
|
@@ -143,6 +191,7 @@ const actionlintConfig = fs.readFileSync(
|
|
|
143
191
|
"utf8",
|
|
144
192
|
);
|
|
145
193
|
for (const requiredSnippet of [
|
|
194
|
+
".github/workflows/build.yml:",
|
|
146
195
|
".github/workflows/.build.yml:",
|
|
147
196
|
'property "workflow_ref" is not defined in object type',
|
|
148
197
|
]) {
|
|
@@ -0,0 +1,101 @@
|
|
|
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/.build.yml");
|
|
9
|
+
const targetPath = path.join(root, ".github/workflows/build.yml");
|
|
10
|
+
|
|
11
|
+
function blockBetween(source, start, end) {
|
|
12
|
+
const startIndex = source.indexOf(start);
|
|
13
|
+
const endIndex = source.indexOf(end, startIndex + start.length);
|
|
14
|
+
if (startIndex < 0 || endIndex < 0) throw new Error(`unable to locate workflow block: ${start.trim()}`);
|
|
15
|
+
return source.slice(startIndex + start.length, endIndex);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function inputNames(inputBlock) {
|
|
19
|
+
return [...inputBlock.matchAll(/^ ([a-z0-9-]+):$/gm)].map((match) => match[1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function routerInputs(inputBlock) {
|
|
23
|
+
const adjusted = inputBlock.replace(
|
|
24
|
+
/( buildchain-contract-lock-path:\n(?: .*\n)*? default:) "buildchain\.contract-lock\.json"/,
|
|
25
|
+
'$1 ""',
|
|
26
|
+
);
|
|
27
|
+
return [
|
|
28
|
+
" buildchain-channel:",
|
|
29
|
+
' description: "Buildchain runtime channel: auto, alpha, or stable"',
|
|
30
|
+
' default: "auto"',
|
|
31
|
+
" type: string",
|
|
32
|
+
" required: false",
|
|
33
|
+
" buildchain-alpha-contract-lock-path:",
|
|
34
|
+
' description: "Consumer-owned contract lock selected for the alpha channel"',
|
|
35
|
+
' default: ".buildchain/alpha-contract-lock.json"',
|
|
36
|
+
" type: string",
|
|
37
|
+
" required: false",
|
|
38
|
+
" buildchain-stable-contract-lock-path:",
|
|
39
|
+
' description: "Consumer-owned contract lock selected for the stable channel"',
|
|
40
|
+
' default: ".buildchain/contract-lock.json"',
|
|
41
|
+
" type: string",
|
|
42
|
+
" required: false",
|
|
43
|
+
adjusted.trimEnd(),
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function routerOutputs(outputBlock) {
|
|
48
|
+
const forwarded = outputBlock.replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.build.outputs.$1 }}");
|
|
49
|
+
return [
|
|
50
|
+
" buildchain-channel:",
|
|
51
|
+
' description: "Resolved Buildchain channel: alpha, stable, or override"',
|
|
52
|
+
" value: ${{ jobs.resolve-channel.outputs.channel }}",
|
|
53
|
+
" buildchain-channel-selection-source:",
|
|
54
|
+
' description: "Evidence source used to select the Buildchain channel"',
|
|
55
|
+
" value: ${{ jobs.resolve-channel.outputs.selection-source }}",
|
|
56
|
+
" buildchain-channel-reason:",
|
|
57
|
+
' description: "Human-readable Buildchain channel selection reason"',
|
|
58
|
+
" value: ${{ jobs.resolve-channel.outputs.reason }}",
|
|
59
|
+
forwarded.trimEnd(),
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function forwardedInputs(names) {
|
|
64
|
+
return names
|
|
65
|
+
.map((name) => {
|
|
66
|
+
if (name === "buildchain-ref") return ` buildchain-ref: \${{ needs.resolve-channel.outputs.buildchain-ref }}`;
|
|
67
|
+
if (name === "buildchain-contract-lock-path") {
|
|
68
|
+
return " buildchain-contract-lock-path: ${{ needs.resolve-channel.outputs.contract-lock-path }}";
|
|
69
|
+
}
|
|
70
|
+
return ` ${name}: \${{ inputs.${name} }}`;
|
|
71
|
+
})
|
|
72
|
+
.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function generateChannelBuildWorkflow(source) {
|
|
76
|
+
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
77
|
+
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
78
|
+
const outputs = blockBetween(source, " outputs:\n", "\npermissions:\n");
|
|
79
|
+
const names = inputNames(inputs);
|
|
80
|
+
if (!names.includes("buildchain-ref") || !names.includes("publish-channel")) {
|
|
81
|
+
throw new Error("source build workflow is missing channel-router inputs");
|
|
82
|
+
}
|
|
83
|
+
return `# Generated by scripts/generate-channel-build-workflow.mjs. Do not edit directly.\nname: Buildchain Channel Build\n\non:\n workflow_call:\n inputs:\n${routerInputs(inputs)}\n secrets:\n${secrets.trimEnd()}\n outputs:\n${routerOutputs(outputs)}\n\npermissions:\n contents: read\n issues: write\n id-token: write\n\njobs:\n resolve-channel:\n name: Resolve Buildchain channel\n runs-on: ubuntu-24.04\n outputs:\n channel: \${{ steps.channel.outputs.channel }}\n buildchain-ref: \${{ steps.channel.outputs.buildchain-ref }}\n contract-lock-path: \${{ steps.lock.outputs.path }}\n selection-source: \${{ steps.channel.outputs.selection-source }}\n reason: \${{ steps.channel.outputs.reason }}\n steps:\n - name: Resolve router source\n id: router\n shell: bash\n env:\n BUILDCHAIN_ROUTER_WORKFLOW_REF: \${{ job.workflow_ref }}\n run: |\n set -euo pipefail\n workflow_ref=\"\${BUILDCHAIN_ROUTER_WORKFLOW_REF}\"\n repository=\"\${workflow_ref%%/.github/workflows/*}\"\n ref=\"\${workflow_ref##*@}\"\n ref=\"\${ref#refs/heads/}\"\n ref=\"\${ref#refs/tags/}\"\n if [[ -z \"\${repository}\" || \"\${repository}\" = \"\${workflow_ref}\" || -z \"\${ref}\" ]]; then\n echo \"Unable to resolve Buildchain router source from job.workflow_ref=\${workflow_ref}\" >&2\n exit 1\n fi\n {\n echo \"repository=\${repository}\"\n echo \"ref=\${ref}\"\n } >> \"\${GITHUB_OUTPUT}\"\n\n - name: Checkout Buildchain router\n uses: actions/checkout@v7.0.0\n with:\n repository: \${{ steps.router.outputs.repository }}\n ref: \${{ steps.router.outputs.ref }}\n path: .buildchain/router\n persist-credentials: false\n\n - name: Resolve channel\n id: channel\n shell: bash\n env:\n BUILDCHAIN_CHANNEL: \${{ inputs.buildchain-channel }}\n BUILDCHAIN_REQUESTED_REF: \${{ inputs.buildchain-ref }}\n BUILDCHAIN_PUBLISH_CHANNEL: \${{ inputs.publish-channel }}\n BUILDCHAIN_EVENT_NAME: \${{ github.event_name }}\n BUILDCHAIN_GIT_REF: \${{ github.ref }}\n BUILDCHAIN_RELEASE_PRERELEASE: \${{ github.event.release.prerelease }}\n BUILDCHAIN_ROUTER_REF: \${{ steps.router.outputs.ref }}\n run: |\n node .buildchain/router/scripts/buildchain-channel-router.mjs \\\n --cwd .buildchain/router \\\n --channel \"\${BUILDCHAIN_CHANNEL}\" \\\n --buildchain-ref \"\${BUILDCHAIN_REQUESTED_REF}\" \\\n --publish-channel \"\${BUILDCHAIN_PUBLISH_CHANNEL}\" \\\n --event-name \"\${BUILDCHAIN_EVENT_NAME}\" \\\n --ref \"\${BUILDCHAIN_GIT_REF}\" \\\n --release-prerelease \"\${BUILDCHAIN_RELEASE_PRERELEASE}\" \\\n --router-ref \"\${BUILDCHAIN_ROUTER_REF}\"\n\n - name: Select consumer contract lock\n id: lock\n shell: bash\n env:\n EXPLICIT_PATH: \${{ inputs.buildchain-contract-lock-path }}\n ALPHA_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}\n STABLE_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}\n CHANNEL: \${{ steps.channel.outputs.channel }}\n run: |\n set -euo pipefail\n path=\"\${EXPLICIT_PATH}\"\n if [[ -z \"\${path}\" ]]; then\n if [[ \"\${CHANNEL}\" = \"alpha\" ]]; then\n path=\"\${ALPHA_PATH}\"\n else\n path=\"\${STABLE_PATH}\"\n fi\n fi\n echo \"path=\${path}\" >> \"\${GITHUB_OUTPUT}\"\n\n build:\n name: Build with resolved channel\n needs: resolve-channel\n uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n issues: write\n id-token: write\n with:\n${forwardedInputs(names)}\n secrets: inherit\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function main() {
|
|
87
|
+
const source = fs.readFileSync(sourcePath, "utf8");
|
|
88
|
+
const generated = generateChannelBuildWorkflow(source);
|
|
89
|
+
if (process.argv.includes("--check")) {
|
|
90
|
+
const current = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
|
|
91
|
+
if (current !== generated) {
|
|
92
|
+
console.error(".github/workflows/build.yml is stale; run node scripts/generate-channel-build-workflow.mjs");
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
fs.writeFileSync(targetPath, generated);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
101
|
+
if (isMain) main();
|
|
@@ -813,7 +813,8 @@ function buildSiteBundle() {
|
|
|
813
813
|
workflowSource: ".github/workflows reverse input enumeration",
|
|
814
814
|
workflows: enumerateWorkflowInputs({ root }).map((entry) => {
|
|
815
815
|
const surfaceById = new Map([
|
|
816
|
-
["build", "
|
|
816
|
+
["build", "channel-build-router"],
|
|
817
|
+
[".build", "reusable-build"],
|
|
817
818
|
["web-surface", "site-app-deployment"],
|
|
818
819
|
["buildchain-ref-promotion", "release-governance"],
|
|
819
820
|
["release-line-bootstrap", "release-governance"],
|
|
@@ -290,8 +290,8 @@ function writeEvidence(filePath, evidence) {
|
|
|
290
290
|
|
|
291
291
|
export function lockedSourceCheckout({
|
|
292
292
|
workspace = process.cwd(),
|
|
293
|
-
checkoutPath = ".",
|
|
294
|
-
repository = readEnv("GITHUB_REPOSITORY"),
|
|
293
|
+
checkoutPath = readEnv("BUILDCHAIN_SOURCE_CHECKOUT_PATH", "."),
|
|
294
|
+
repository = readEnv("BUILDCHAIN_SOURCE_REPOSITORY", readEnv("GITHUB_REPOSITORY")),
|
|
295
295
|
sourceSha = readEnv("BUILDCHAIN_SOURCE_SHA"),
|
|
296
296
|
sourceTreeSha = readEnv("BUILDCHAIN_SOURCE_TREE_SHA"),
|
|
297
297
|
fetchRef = readEnv("BUILDCHAIN_SOURCE_REF", readEnv("GITHUB_REF")),
|