@kungfu-tech/buildchain 2.11.14-alpha.7 → 2.12.0-alpha.0

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.
@@ -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/.build.yml@v${selfDogfoodMajor}-alpha`,
111
- `ref: v${selfDogfoodMajor}`,
112
- `echo "ref=v${selfDogfoodMajor}"`,
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", "reusable-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"],