@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.
- package/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -1
- package/actions/promote-buildchain-ref/README.md +90 -19
- package/actions/report-buildchain-issue/README.md +20 -2
- package/actions/run-lifecycle/README.md +5 -0
- package/actions/validate-config/README.md +19 -1
- package/bin/buildchain.mjs +56 -2
- package/bin/internal/command-registry.mjs +82 -0
- package/bin/internal/trust-release-cli.mjs +1 -0
- package/contracts/auditable-demo-media-profiles-v1.json +116 -1
- package/contracts/buildchain-v2-residuals-v1.json +146 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +188 -46
- package/dist/site/buildchain-site.json +181 -90
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +118 -5
- package/dist/site/kfd-claims.json +233 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -20
- package/dist/site/node-api-registry.json +374 -59
- package/dist/site/page-registry.json +156 -65
- package/dist/site/public-surface-audit.json +231 -22
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +24 -24
- package/dist/site/workflow-registry.json +575 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +54 -13
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +20 -2
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +24 -31
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/ownership.md +19 -1
- package/docs/publication-artifacts.md +158 -18
- package/docs/publication-authority.md +4 -4
- package/docs/publish-transaction.md +78 -31
- package/docs/release-candidate.md +24 -0
- package/docs/release-flow.md +50 -32
- package/docs/release-governance.md +39 -32
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +218 -90
- package/docs/runtime-train-validation.md +20 -2
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +46 -24
- package/docs/web-surface-deployments.md +82 -1
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +9 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/diagnostics.js +105 -30
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/index.js +40 -0
- package/packages/core/paper.js +3226 -0
- package/packages/core/public-surface-audit.js +3 -0
- package/packages/core/publication-artifact.js +1 -6
- package/packages/core/publication-authority.js +1 -0
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/publication-reproducibility.js +947 -0
- package/packages/core/publication-sealed-bundle.js +190 -0
- package/packages/core/publish-transaction.js +147 -17
- package/packages/core/release-candidate.js +234 -0
- package/packages/core/release-passport.js +519 -228
- package/packages/core/release-propagation.js +175 -1
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +266 -7
- package/scripts/aws-codebuild-toolchain.mjs +285 -0
- package/scripts/aws-macos-jit-core.mjs +378 -0
- package/scripts/aws-macos-jit.mjs +123 -0
- package/scripts/aws-runner-burst-core.mjs +248 -0
- package/scripts/aws-runner-burst.mjs +79 -0
- package/scripts/aws-windows-jit-core.mjs +374 -0
- package/scripts/aws-windows-jit.mjs +121 -0
- package/scripts/build-contract-core.mjs +69 -6
- package/scripts/build-standalone-binary.mjs +23 -0
- package/scripts/buildchain-contract-lock.mjs +1 -1
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +70 -2
- package/scripts/check-javascript-syntax.mjs +31 -0
- package/scripts/check-maintainability.mjs +371 -0
- package/scripts/compiler-cache-evidence.mjs +221 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/generate-channel-promotion-workflow.mjs +3 -12
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +84 -18
- package/scripts/import-artifact-signing-results.mjs +64 -5
- package/scripts/inspect-artifact-signing-requests.mjs +52 -14
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +36 -4
- package/scripts/seal-artifact-signing-requests.mjs +64 -12
- package/scripts/stable-candidate-qualification.mjs +50 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-core.mjs +272 -86
- 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 (
|
|
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
|
+
}
|