@kungfu-tech/buildchain 3.0.2 → 3.0.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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { verifyPublicationReproducibility } from "../packages/core/publication-reproducibility.js";
5
+
6
+ function readFlag(args, name, fallback = "") {
7
+ const index = args.indexOf(`--${name}`);
8
+ return index === -1 ? fallback : args[index + 1] || "";
9
+ }
10
+
11
+ function hasFlag(args, name) {
12
+ return args.includes(`--${name}`);
13
+ }
14
+
15
+ export function runPublicationReproducibilityCli(args = process.argv.slice(2)) {
16
+ const result = verifyPublicationReproducibility({
17
+ cwd: readFlag(args, "cwd", process.cwd()),
18
+ sourceSha: readFlag(args, "source-sha", ""),
19
+ output: readFlag(
20
+ args,
21
+ "output",
22
+ ".buildchain/publication/reproducibility-receipt.json",
23
+ ),
24
+ promote: hasFlag(args, "promote"),
25
+ keepWorkspaces: hasFlag(args, "keep-workspaces"),
26
+ pullToolchain: !hasFlag(args, "no-toolchain-pull"),
27
+ packageName: readFlag(args, "package-name", ""),
28
+ allowUnpinnedToolchain: hasFlag(args, "allow-unpinned-toolchain"),
29
+ });
30
+ if (hasFlag(args, "json")) {
31
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
32
+ } else {
33
+ process.stdout.write(
34
+ [
35
+ `publication-reproducibility=${result.status}`,
36
+ `publication-reproducibility-qualifying=${result.qualifying}`,
37
+ `publication-reproducibility-receipt=${result.outputPath}`,
38
+ `publication-reproducibility-digest=${result.receiptDigest}`,
39
+ "",
40
+ ].join("\n"),
41
+ );
42
+ }
43
+ if (!result.qualifying && !hasFlag(args, "allow-unpinned-toolchain")) {
44
+ process.exitCode = 1;
45
+ } else if (result.status !== "passed") {
46
+ process.exitCode = 1;
47
+ }
48
+ return result;
49
+ }
50
+
51
+ if (
52
+ !process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT &&
53
+ process.argv[1] &&
54
+ import.meta.url === pathToFileURL(process.argv[1]).href
55
+ ) {
56
+ try {
57
+ runPublicationReproducibilityCli();
58
+ } catch (error) {
59
+ console.error(`publication-reproducibility: ${error.message}`);
60
+ process.exitCode = 1;
61
+ }
62
+ }
@@ -96,6 +96,11 @@ function writeJson(filePath, value) {
96
96
  fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
97
97
  }
98
98
 
99
+ export function githubApiFailureIsAbsence(method, output) {
100
+ return String(method || "GET").toUpperCase() === "GET" &&
101
+ /404|not found/i.test(String(output || ""));
102
+ }
103
+
99
104
  function githubApi(route, { method = "GET", body } = {}) {
100
105
  const args = [
101
106
  "api",
@@ -115,7 +120,9 @@ function githubApi(route, { method = "GET", body } = {}) {
115
120
  });
116
121
  const output = `${result.stdout || ""}\n${result.stderr || ""}`;
117
122
  if (result.status !== 0) {
118
- if (/404|not found/i.test(output)) return { exists: false, data: null };
123
+ if (githubApiFailureIsAbsence(method, output)) {
124
+ return { exists: false, data: null };
125
+ }
119
126
  throw new Error(`GitHub API ${method} ${route} failed closed`);
120
127
  }
121
128
  return {
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import {
6
+ createReleasePropagationReceipt,
6
7
  planReleasePropagation,
7
8
  readReleasePropagationJson,
8
9
  writeReleasePropagationLock,
@@ -16,6 +17,14 @@ function usage() {
16
17
  buildchain release-propagation write-lock --plan <json-or-path>
17
18
  [--target <id-or-repo>] [--cwd <dir>]
18
19
  [--output <file>] [--json]
20
+ buildchain release-propagation receipt --plan <json-or-path>
21
+ --lock-result <json-or-path>
22
+ --pr-outcome <json-or-path>
23
+ [--target <id-or-repo>]
24
+ [--staging-state <state>]
25
+ [--production-state <state>]
26
+ [--observed-at <iso-8601>]
27
+ [--output <file>] [--json]
19
28
  `;
20
29
  }
21
30
 
@@ -90,6 +99,36 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
90
99
  }
91
100
  return;
92
101
  }
102
+ if (mode === "receipt") {
103
+ const plan = readReleasePropagationJson(readFlag(args, "plan"), {
104
+ label: "--plan",
105
+ cwd: process.cwd(),
106
+ });
107
+ const lockResult = readReleasePropagationJson(readFlag(args, "lock-result"), {
108
+ label: "--lock-result",
109
+ cwd: process.cwd(),
110
+ });
111
+ const prOutcome = readReleasePropagationJson(readFlag(args, "pr-outcome"), {
112
+ label: "--pr-outcome",
113
+ cwd: process.cwd(),
114
+ });
115
+ const receipt = createReleasePropagationReceipt({
116
+ plan,
117
+ target: readFlag(args, "target", ""),
118
+ lockResult,
119
+ prOutcome,
120
+ stagingState: readFlag(args, "staging-state", "pending"),
121
+ productionState: readFlag(args, "production-state", "not-requested"),
122
+ observedAt: readFlag(args, "observed-at", ""),
123
+ });
124
+ writeOutput(readFlag(args, "output", ""), receipt);
125
+ if (hasFlag(args, "json")) {
126
+ printJson(receipt);
127
+ } else {
128
+ process.stdout.write(`release propagation receipt: ${receipt.receiptSha256}\n`);
129
+ }
130
+ return;
131
+ }
93
132
  throw new Error(`unsupported release-propagation command: ${mode}`);
94
133
  }
95
134
 
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ function normalize(value, name) {
7
+ const normalized = String(value ?? "").trim();
8
+ if (/\r|\n|\0/.test(normalized)) {
9
+ throw new Error(`${name} must be a single-line NO_PROXY value`);
10
+ }
11
+ return normalized;
12
+ }
13
+
14
+ export function resolveArtifactSigningUploadRoute({
15
+ requestedNoProxy = "",
16
+ noProxy = "",
17
+ lowerNoProxy = "",
18
+ } = {}) {
19
+ const requested = normalize(requestedNoProxy, "artifact signing request upload NO_PROXY");
20
+ const currentUpper = normalize(noProxy, "NO_PROXY");
21
+ const currentLower = normalize(lowerNoProxy, "no_proxy");
22
+
23
+ return {
24
+ noProxy: requested || currentUpper || currentLower,
25
+ overrideApplied: requested !== "",
26
+ };
27
+ }
28
+
29
+ export function writeGitHubOutputs(route, outputPath = process.env.GITHUB_OUTPUT) {
30
+ if (!outputPath) return;
31
+ fs.appendFileSync(
32
+ outputPath,
33
+ [
34
+ `no-proxy=${route.noProxy}`,
35
+ `override-applied=${route.overrideApplied}`,
36
+ "",
37
+ ].join("\n"),
38
+ );
39
+ }
40
+
41
+ function main() {
42
+ const route = resolveArtifactSigningUploadRoute({
43
+ requestedNoProxy: process.env.BUILDCHAIN_SIGNING_REQUEST_UPLOAD_NO_PROXY,
44
+ noProxy: process.env.NO_PROXY,
45
+ lowerNoProxy: process.env.no_proxy,
46
+ });
47
+ writeGitHubOutputs(route);
48
+ process.stdout.write(
49
+ `Resolved artifact signing request upload route (override=${route.overrideApplied ? "yes" : "no"}).\n`,
50
+ );
51
+ }
52
+
53
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
54
+ main();
55
+ }
@@ -23,6 +23,12 @@ export function resolveBuildContractCli() {
23
23
  const resolved = resolveRunnerMatrix({
24
24
  runnerPreset: process.env.BUILDCHAIN_RUNNER_PRESET || "github-hosted",
25
25
  platformsJson: process.env.BUILDCHAIN_PLATFORMS_JSON || "",
26
+ awsCodeBuildProject:
27
+ process.env.BUILDCHAIN_AWS_CODEBUILD_PROJECT || "",
28
+ awsEc2WindowsRunnerLabel:
29
+ process.env.BUILDCHAIN_AWS_EC2_WINDOWS_RUNNER_LABEL || "",
30
+ awsEc2MacosRunnerLabel:
31
+ process.env.BUILDCHAIN_AWS_EC2_MACOS_RUNNER_LABEL || "",
26
32
  linuxContainerPreset: process.env.BUILDCHAIN_LINUX_CONTAINER_PRESET || "",
27
33
  linuxContainerImage: process.env.BUILDCHAIN_LINUX_CONTAINER_IMAGE || "",
28
34
  });
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import {
4
+ resolveRunnerMatrix,
5
+ writeGitHubOutputs,
6
+ } from "./build-contract-core.mjs";
7
+
8
+ export const HOSTED_RUNNER_FALLBACKS = Object.freeze({
9
+ "linux-x64": '["ubuntu-24.04"]',
10
+ "macos-arm64": '["macos-15"]',
11
+ "windows-x64": '["windows-2022"]',
12
+ });
13
+
14
+ function runnerLabels(runner) {
15
+ const labels = JSON.parse(String(runner || "[]"));
16
+ if (
17
+ !Array.isArray(labels) ||
18
+ labels.some((label) => typeof label !== "string")
19
+ ) {
20
+ throw new Error("runner must be a JSON array of label strings");
21
+ }
22
+ return labels;
23
+ }
24
+
25
+ function runnerHasLabels(runner, requiredLabels) {
26
+ const labels = new Set(
27
+ (runner.labels || []).map((label) => String(label?.name || label)),
28
+ );
29
+ return requiredLabels.every((label) => labels.has(label));
30
+ }
31
+
32
+ export function routeOfflineRunners({
33
+ platforms,
34
+ runners,
35
+ fallbacks = HOSTED_RUNNER_FALLBACKS,
36
+ }) {
37
+ const routes = [];
38
+ const routedPlatforms = platforms.map((platform) => {
39
+ const requiredLabels = runnerLabels(platform.runner);
40
+ if (!requiredLabels.includes("self-hosted")) {
41
+ routes.push({
42
+ id: platform.id,
43
+ status: "hosted",
44
+ selected: "declared-runner",
45
+ });
46
+ return platform;
47
+ }
48
+ const matching = runners.filter((runner) =>
49
+ runnerHasLabels(runner, requiredLabels),
50
+ );
51
+ const online = matching.filter((runner) => runner.status === "online");
52
+ const busyOnline = online.filter((runner) => runner.busy);
53
+ const fallbackRunner = fallbacks[platform.id] || "";
54
+ const offline = online.length === 0;
55
+ const selected =
56
+ offline && fallbackRunner
57
+ ? "github-hosted-offline-fallback"
58
+ : "self-hosted";
59
+ routes.push({
60
+ id: platform.id,
61
+ status: offline ? "offline" : "online",
62
+ selected,
63
+ matchingRunnerCount: matching.length,
64
+ onlineRunnerCount: online.length,
65
+ busyOnlineRunnerCount: busyOnline.length,
66
+ fallbackAvailable: Boolean(fallbackRunner),
67
+ });
68
+ if (selected !== "github-hosted-offline-fallback") {
69
+ return platform;
70
+ }
71
+ return {
72
+ ...platform,
73
+ runner: fallbackRunner,
74
+ };
75
+ });
76
+ return {
77
+ platforms: routedPlatforms,
78
+ platformsJson: JSON.stringify(routedPlatforms),
79
+ fallbackCount: routes.filter(
80
+ (route) => route.selected === "github-hosted-offline-fallback",
81
+ ).length,
82
+ routing: {
83
+ schema: "buildchain.runner-offline-routing/v1",
84
+ status: "observed",
85
+ routes,
86
+ },
87
+ };
88
+ }
89
+
90
+ async function fetchGitHubJson({ url, token, fetchImpl }) {
91
+ const response = await fetchImpl(url, {
92
+ headers: {
93
+ accept: "application/vnd.github+json",
94
+ authorization: `Bearer ${token}`,
95
+ "x-github-api-version": "2022-11-28",
96
+ },
97
+ });
98
+ if (!response.ok) {
99
+ throw new Error(
100
+ `runner inventory request failed with status ${response.status}`,
101
+ );
102
+ }
103
+ return response.json();
104
+ }
105
+
106
+ export async function listRepositoryRunners({
107
+ repository,
108
+ token,
109
+ apiUrl = "https://api.github.com",
110
+ fetchImpl = fetch,
111
+ }) {
112
+ const repositoryPayload = await fetchGitHubJson({
113
+ url: `${apiUrl}/repos/${repository}`,
114
+ token,
115
+ fetchImpl,
116
+ });
117
+ const ownerLogin = String(repositoryPayload?.owner?.login || "").trim();
118
+ const ownerType = String(repositoryPayload?.owner?.type || "").trim();
119
+ const inventoryPath =
120
+ ownerType === "Organization" && ownerLogin
121
+ ? `/orgs/${ownerLogin}/actions/runners`
122
+ : `/repos/${repository}/actions/runners`;
123
+ const runners = [];
124
+ for (let page = 1; ; page += 1) {
125
+ const payload = await fetchGitHubJson({
126
+ url: `${apiUrl}${inventoryPath}?per_page=100&page=${page}`,
127
+ token,
128
+ fetchImpl,
129
+ });
130
+ const pageRunners = Array.isArray(payload.runners) ? payload.runners : [];
131
+ runners.push(...pageRunners);
132
+ if (pageRunners.length < 100) {
133
+ return {
134
+ runners,
135
+ inventoryScope:
136
+ ownerType === "Organization" && ownerLogin
137
+ ? "organization"
138
+ : "repository",
139
+ };
140
+ }
141
+ }
142
+ }
143
+
144
+ export async function resolveOfflineRunnerFallback({
145
+ runnerPreset,
146
+ platformsJson = "",
147
+ repository,
148
+ token,
149
+ apiUrl,
150
+ fetchImpl,
151
+ }) {
152
+ const base = resolveRunnerMatrix({ runnerPreset, platformsJson });
153
+ const selfHosted = base.platforms.filter((platform) =>
154
+ runnerLabels(platform.runner).includes("self-hosted"),
155
+ );
156
+ if (selfHosted.length === 0) {
157
+ return {
158
+ platformsJson: base.platformsJson,
159
+ fallbackCount: 0,
160
+ routing: {
161
+ schema: "buildchain.runner-offline-routing/v1",
162
+ status: "not-applicable",
163
+ routes: [],
164
+ },
165
+ };
166
+ }
167
+ if (!String(token || "").trim()) {
168
+ return {
169
+ platformsJson: base.platformsJson,
170
+ fallbackCount: 0,
171
+ routing: {
172
+ schema: "buildchain.runner-offline-routing/v1",
173
+ status: "unavailable",
174
+ reason: "runner-inventory-token-not-projected",
175
+ routes: [],
176
+ },
177
+ };
178
+ }
179
+ try {
180
+ const inventory = await listRepositoryRunners({
181
+ repository,
182
+ token,
183
+ apiUrl,
184
+ fetchImpl,
185
+ });
186
+ const routed = routeOfflineRunners({
187
+ platforms: base.platforms,
188
+ runners: inventory.runners,
189
+ });
190
+ return {
191
+ ...routed,
192
+ routing: {
193
+ ...routed.routing,
194
+ inventoryScope: inventory.inventoryScope,
195
+ },
196
+ };
197
+ } catch {
198
+ return {
199
+ platformsJson: base.platformsJson,
200
+ fallbackCount: 0,
201
+ routing: {
202
+ schema: "buildchain.runner-offline-routing/v1",
203
+ status: "unavailable",
204
+ reason: "runner-inventory-unavailable",
205
+ routes: [],
206
+ },
207
+ };
208
+ }
209
+ }
210
+
211
+ async function main() {
212
+ const resolved = await resolveOfflineRunnerFallback({
213
+ runnerPreset: process.env.BUILDCHAIN_RUNNER_PRESET || "github-hosted",
214
+ platformsJson: process.env.BUILDCHAIN_PLATFORMS_JSON || "",
215
+ repository: process.env.GITHUB_REPOSITORY || "",
216
+ token: process.env.BUILDCHAIN_RUNNER_INVENTORY_TOKEN || "",
217
+ apiUrl: process.env.GITHUB_API_URL || "https://api.github.com",
218
+ });
219
+ writeGitHubOutputs({
220
+ "platforms-json": resolved.platformsJson,
221
+ "fallback-count": String(resolved.fallbackCount),
222
+ "routing-json": JSON.stringify(resolved.routing),
223
+ });
224
+ }
225
+
226
+ if (
227
+ process.argv[1] &&
228
+ import.meta.url === pathToFileURL(process.argv[1]).href
229
+ ) {
230
+ main().catch((error) => {
231
+ console.error(
232
+ `::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`,
233
+ );
234
+ process.exitCode = 1;
235
+ });
236
+ }
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import childProcess from "node:child_process";
5
+ import crypto from "node:crypto";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { TextDecoder } from "node:util";
9
+
10
+ const EXACT_SHA = /^[0-9a-f]{40}$/u;
11
+ const MANAGED_MARKER = "<!-- buildchain-dev-alpha-candidate-state";
12
+ const MAX_PREFIX_CHARACTERS = 32768;
13
+
14
+ function required(value, name) {
15
+ const normalized = String(value ?? "").trim();
16
+ if (!normalized) throw new Error(`${name} is required`);
17
+ return normalized;
18
+ }
19
+
20
+ export function resolveRendererPath(consumerRootValue, rendererValue) {
21
+ const consumerRoot = fs.realpathSync(
22
+ path.resolve(required(consumerRootValue, "consumerRoot")),
23
+ );
24
+ const renderer = required(rendererValue, "renderer");
25
+ if (path.isAbsolute(renderer))
26
+ throw new Error("renderer must be repository-relative");
27
+ const candidate = path.resolve(consumerRoot, renderer);
28
+ const candidateRelative = path.relative(consumerRoot, candidate);
29
+ if (
30
+ !candidateRelative ||
31
+ candidateRelative.startsWith("..") ||
32
+ path.isAbsolute(candidateRelative)
33
+ )
34
+ throw new Error("renderer must resolve inside the consumer checkout");
35
+ const absolute = fs.realpathSync(candidate);
36
+ const relative = path.relative(consumerRoot, absolute);
37
+ if (relative.startsWith("..") || path.isAbsolute(relative))
38
+ throw new Error("renderer must resolve inside the consumer checkout");
39
+ const stat = fs.statSync(absolute);
40
+ if (!stat.isFile())
41
+ throw new Error("renderer must resolve to a regular file");
42
+ return { consumerRoot, renderer: absolute };
43
+ }
44
+
45
+ function safeEnvironment(values, source = process.env) {
46
+ const retained = [
47
+ "CI",
48
+ "HOME",
49
+ "LANG",
50
+ "LC_ALL",
51
+ "PATH",
52
+ "RUNNER_ARCH",
53
+ "RUNNER_OS",
54
+ "TEMP",
55
+ "TMP",
56
+ "TMPDIR",
57
+ "TZ",
58
+ ];
59
+ return {
60
+ ...Object.fromEntries(
61
+ retained
62
+ .filter((key) => source[key] !== undefined)
63
+ .map((key) => [key, source[key]]),
64
+ ),
65
+ BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_OUTPUT: values.outputPath,
66
+ BUILDCHAIN_CHANNEL_PATROL_SELECTED_SHA: values.selectedSha,
67
+ BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH: values.sourceBranch,
68
+ BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH: values.targetBranch,
69
+ GIT_TERMINAL_PROMPT: "0",
70
+ };
71
+ }
72
+
73
+ function checkedSpawn(command, args, options, label) {
74
+ const result = childProcess.spawnSync(command, args, {
75
+ ...options,
76
+ encoding: "utf8",
77
+ });
78
+ if (result.error) throw result.error;
79
+ if (result.status !== 0) {
80
+ const detail = String(result.stderr || result.stdout || "").trim();
81
+ throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function readPrefix(outputPath) {
87
+ const bytes = fs.readFileSync(outputPath);
88
+ let value;
89
+ try {
90
+ value = new TextDecoder("utf-8", { fatal: true }).decode(bytes).trim();
91
+ } catch {
92
+ throw new Error("renderer output must be valid UTF-8");
93
+ }
94
+ if (!value) throw new Error("renderer output must not be empty");
95
+ if (value.length > MAX_PREFIX_CHARACTERS)
96
+ throw new Error(
97
+ `renderer output exceeds ${MAX_PREFIX_CHARACTERS} characters`,
98
+ );
99
+ if (value.includes(MANAGED_MARKER))
100
+ throw new Error(
101
+ "renderer output must not contain the managed candidate state marker",
102
+ );
103
+ return value;
104
+ }
105
+
106
+ export function appendMultilineOutput(outputFile, name, value) {
107
+ let delimiter;
108
+ do {
109
+ delimiter = `buildchain_${crypto.randomUUID()}`;
110
+ } while (value.split(/\r?\n/u).includes(delimiter));
111
+ fs.appendFileSync(
112
+ outputFile,
113
+ `${name}<<${delimiter}\n${value}\n${delimiter}\n`,
114
+ );
115
+ }
116
+
117
+ export function runCandidateBodyPrefixRenderer(options = {}) {
118
+ const { consumerRoot, renderer } = resolveRendererPath(
119
+ options.consumerRoot,
120
+ options.renderer,
121
+ );
122
+ const selectedSha = required(options.selectedSha, "selectedSha");
123
+ if (!EXACT_SHA.test(selectedSha))
124
+ throw new Error("selectedSha must be an exact 40-character commit SHA");
125
+ const sourceBranch = required(options.sourceBranch, "sourceBranch");
126
+ const targetBranch = required(options.targetBranch, "targetBranch");
127
+ const outputPath = path.resolve(required(options.outputPath, "outputPath"));
128
+ const githubOutput = required(options.githubOutput, "githubOutput");
129
+
130
+ const head = checkedSpawn(
131
+ "git",
132
+ ["rev-parse", "HEAD"],
133
+ {
134
+ cwd: consumerRoot,
135
+ env: safeEnvironment({
136
+ outputPath,
137
+ selectedSha,
138
+ sourceBranch,
139
+ targetBranch,
140
+ }),
141
+ },
142
+ "consumer HEAD resolution",
143
+ ).stdout.trim();
144
+ if (head !== selectedSha)
145
+ throw new Error(
146
+ `consumer checkout HEAD ${head} does not match selected SHA ${selectedSha}`,
147
+ );
148
+
149
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
150
+ fs.rmSync(outputPath, { force: true });
151
+ const env = safeEnvironment({
152
+ outputPath,
153
+ selectedSha,
154
+ sourceBranch,
155
+ targetBranch,
156
+ });
157
+ checkedSpawn(
158
+ process.execPath,
159
+ [renderer],
160
+ { cwd: consumerRoot, env },
161
+ "PR body prefix renderer",
162
+ );
163
+ const value = readPrefix(outputPath);
164
+ appendMultilineOutput(githubOutput, "pull-request-body-prefix", value);
165
+ return value;
166
+ }
167
+
168
+ function main() {
169
+ runCandidateBodyPrefixRenderer({
170
+ consumerRoot: process.env.BUILDCHAIN_CHANNEL_PATROL_CONSUMER_ROOT,
171
+ renderer: process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_RENDERER,
172
+ outputPath: process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_OUTPUT,
173
+ selectedSha: process.env.BUILDCHAIN_CHANNEL_PATROL_SELECTED_SHA,
174
+ sourceBranch: process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH,
175
+ targetBranch: process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH,
176
+ githubOutput: process.env.GITHUB_OUTPUT,
177
+ });
178
+ }
179
+
180
+ if (import.meta.url === `file://${process.argv[1]}`) {
181
+ try {
182
+ main();
183
+ } catch (error) {
184
+ console.error(error instanceof Error ? error.message : String(error));
185
+ process.exit(1);
186
+ }
187
+ }