@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,221 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const BUILDCHAIN_COMPILER_CACHE_PREPARATION_CONTRACT =
|
|
8
|
+
"kungfu-buildchain-compiler-cache-preparation";
|
|
9
|
+
|
|
10
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
11
|
+
|
|
12
|
+
function stableValue(value) {
|
|
13
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.keys(value)
|
|
17
|
+
.sort()
|
|
18
|
+
.map((key) => [key, stableValue(value[key])]),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function digest(value) {
|
|
25
|
+
return `sha256:${crypto
|
|
26
|
+
.createHash("sha256")
|
|
27
|
+
.update(JSON.stringify(stableValue(value)))
|
|
28
|
+
.digest("hex")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanText(value) {
|
|
32
|
+
return String(value || "").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function optionalDigest(value, label) {
|
|
36
|
+
const candidate = cleanText(value);
|
|
37
|
+
if (!candidate) return "";
|
|
38
|
+
if (!DIGEST_RE.test(candidate)) {
|
|
39
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
40
|
+
}
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function defaultRunCommand(command, args, options = {}) {
|
|
45
|
+
return spawnSync(command, args, {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
env: options.env,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertCommandSucceeded(result, label) {
|
|
55
|
+
if (result?.error) {
|
|
56
|
+
throw new Error(`${label} failed: ${result.error.code || result.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
if (result?.status !== 0) {
|
|
59
|
+
const detail = cleanText(result?.stderr || result?.stdout);
|
|
60
|
+
throw new Error(`${label} exited ${result?.status ?? "without status"}${detail ? `: ${detail}` : ""}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function appendGithubEnv(filePath, values) {
|
|
65
|
+
if (!filePath) return;
|
|
66
|
+
for (const [name, value] of Object.entries(values)) {
|
|
67
|
+
if (/[\r\n\0]/.test(String(value))) {
|
|
68
|
+
throw new Error(`${name} contains control characters`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fs.appendFileSync(
|
|
72
|
+
filePath,
|
|
73
|
+
`${Object.entries(values)
|
|
74
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
75
|
+
.join("\n")}\n`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readToolEvidence({ cwd, env, expectedRoot }) {
|
|
80
|
+
const configured = cleanText(
|
|
81
|
+
env.BUILDCHAIN_COMPILER_CACHE_TOOL_EVIDENCE_PATH,
|
|
82
|
+
);
|
|
83
|
+
if (!configured) return undefined;
|
|
84
|
+
const filePath = path.resolve(cwd, configured);
|
|
85
|
+
const relative = path.relative(path.resolve(cwd), filePath);
|
|
86
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"BUILDCHAIN_COMPILER_CACHE_TOOL_EVIDENCE_PATH must remain inside the workflow workspace",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const evidence = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
92
|
+
if (!DIGEST_RE.test(evidence?.root || "")) {
|
|
93
|
+
throw new Error("compiler-cache tool evidence root is invalid");
|
|
94
|
+
}
|
|
95
|
+
if (expectedRoot && evidence.root !== expectedRoot) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"compiler-cache tool evidence does not match BUILDCHAIN_COMPILER_CACHE_TOOL_ROOT",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return evidence;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function prepareCompilerCacheEvidence({
|
|
104
|
+
cwd = process.cwd(),
|
|
105
|
+
env = process.env,
|
|
106
|
+
runCommand = defaultRunCommand,
|
|
107
|
+
now = () => new Date(),
|
|
108
|
+
} = {}) {
|
|
109
|
+
const provider = cleanText(env.BUILDCHAIN_COMPILER_CACHE_PROVIDER || "none").toLowerCase();
|
|
110
|
+
const required = cleanText(env.BUILDCHAIN_COMPILER_CACHE_REQUIRED).toLowerCase() === "true";
|
|
111
|
+
if (provider !== "sccache") {
|
|
112
|
+
if (required) {
|
|
113
|
+
throw new Error(`required compiler cache provider must be sccache, received ${provider || "empty"}`);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outputPath = path.resolve(
|
|
119
|
+
cwd,
|
|
120
|
+
cleanText(env.BUILDCHAIN_COMPILER_CACHE_PREPARATION_PATH) ||
|
|
121
|
+
".buildchain/diagnostics/compiler-cache-preparation.json",
|
|
122
|
+
);
|
|
123
|
+
const versionResult = runCommand("sccache", ["--version"], { cwd, env });
|
|
124
|
+
assertCommandSucceeded(versionResult, "sccache version probe");
|
|
125
|
+
const version = cleanText(versionResult.stdout).split(/\r?\n/, 1)[0];
|
|
126
|
+
if (!version) {
|
|
127
|
+
throw new Error("sccache version probe returned empty output");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const resetResult = runCommand("sccache", ["--zero-stats"], { cwd, env });
|
|
131
|
+
assertCommandSucceeded(resetResult, "sccache current-run stats reset");
|
|
132
|
+
|
|
133
|
+
const compilerCacheToolRoot = optionalDigest(
|
|
134
|
+
env.BUILDCHAIN_COMPILER_CACHE_TOOL_ROOT,
|
|
135
|
+
"BUILDCHAIN_COMPILER_CACHE_TOOL_ROOT",
|
|
136
|
+
);
|
|
137
|
+
const toolEvidence = readToolEvidence({
|
|
138
|
+
cwd,
|
|
139
|
+
env,
|
|
140
|
+
expectedRoot: compilerCacheToolRoot,
|
|
141
|
+
});
|
|
142
|
+
const bindings = Object.fromEntries(
|
|
143
|
+
[
|
|
144
|
+
["sourceCommit", cleanText(env.BUILDCHAIN_SOURCE_SHA || env.GITHUB_SHA)],
|
|
145
|
+
["sourceTree", cleanText(env.BUILDCHAIN_SOURCE_TREE_SHA)],
|
|
146
|
+
["runtimeCommit", cleanText(env.BUILDCHAIN_RUNTIME_SHA)],
|
|
147
|
+
["dependencyLockRoot", cleanText(env.BUILDCHAIN_DEPENDENCY_LOCK_ROOT)],
|
|
148
|
+
["toolchainRoot", cleanText(env.BUILDCHAIN_TOOLCHAIN_ROOT)],
|
|
149
|
+
["policyRoot", cleanText(env.BUILDCHAIN_CACHE_POLICY_ROOT)],
|
|
150
|
+
["cacheProfileRoot", optionalDigest(env.SHIFU_CACHE_PROFILE_DIGEST, "SHIFU_CACHE_PROFILE_DIGEST")],
|
|
151
|
+
["compilerCacheToolRoot", compilerCacheToolRoot],
|
|
152
|
+
].filter(([, value]) => value),
|
|
153
|
+
);
|
|
154
|
+
if (!bindings.sourceCommit) {
|
|
155
|
+
throw new Error("BUILDCHAIN_SOURCE_SHA is required for compiler-cache preparation evidence");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const body = {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
contract: BUILDCHAIN_COMPILER_CACHE_PREPARATION_CONTRACT,
|
|
161
|
+
generatedAt: now().toISOString(),
|
|
162
|
+
provider,
|
|
163
|
+
required,
|
|
164
|
+
status: "prepared",
|
|
165
|
+
platform: {
|
|
166
|
+
id: cleanText(env.BUILDCHAIN_PLATFORM_ID) || "unknown",
|
|
167
|
+
os: cleanText(env.RUNNER_OS) || process.platform,
|
|
168
|
+
arch: cleanText(env.RUNNER_ARCH) || process.arch,
|
|
169
|
+
},
|
|
170
|
+
tool: {
|
|
171
|
+
command: "sccache",
|
|
172
|
+
version,
|
|
173
|
+
...(bindings.compilerCacheToolRoot
|
|
174
|
+
? { evidenceRoot: bindings.compilerCacheToolRoot }
|
|
175
|
+
: {}),
|
|
176
|
+
...(toolEvidence ? { evidence: toolEvidence } : {}),
|
|
177
|
+
},
|
|
178
|
+
action: {
|
|
179
|
+
statsReset: true,
|
|
180
|
+
command: ["sccache", "--zero-stats"],
|
|
181
|
+
},
|
|
182
|
+
bindings,
|
|
183
|
+
};
|
|
184
|
+
const receipt = {
|
|
185
|
+
...body,
|
|
186
|
+
root: digest(body),
|
|
187
|
+
};
|
|
188
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
189
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
190
|
+
appendGithubEnv(env.GITHUB_ENV, {
|
|
191
|
+
BUILDCHAIN_COMPILER_CACHE_ACTIVE_PROVIDER: provider,
|
|
192
|
+
BUILDCHAIN_COMPILER_CACHE_PREPARATION_PATH: outputPath,
|
|
193
|
+
BUILDCHAIN_COMPILER_CACHE_PREPARATION_ROOT: receipt.root,
|
|
194
|
+
});
|
|
195
|
+
return receipt;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function main() {
|
|
199
|
+
const command = process.argv[2] || "";
|
|
200
|
+
if (command !== "prepare") {
|
|
201
|
+
throw new Error("usage: node scripts/compiler-cache-evidence.mjs prepare");
|
|
202
|
+
}
|
|
203
|
+
const receipt = prepareCompilerCacheEvidence();
|
|
204
|
+
if (receipt) {
|
|
205
|
+
console.log(`compiler_cache_provider=${receipt.provider}`);
|
|
206
|
+
console.log(`compiler_cache_preparation_root=${receipt.root}`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log("compiler_cache_provider=none");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isMain = process.argv[1] &&
|
|
213
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
214
|
+
if (isMain) {
|
|
215
|
+
try {
|
|
216
|
+
main();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(error.message);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -12,6 +12,8 @@ const STATE_MARKER_START = "<!-- buildchain-dev-alpha-candidate-state";
|
|
|
12
12
|
const STATE_MARKER_END = "-->";
|
|
13
13
|
const LEGACY_BODY_MARKER = "Buildchain exact-source channel candidate.";
|
|
14
14
|
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
15
|
+
const EVIDENCE_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
16
|
+
const ABSENT_STATE_ROOT = "absent";
|
|
15
17
|
|
|
16
18
|
function text(value = "") {
|
|
17
19
|
return String(value ?? "").trim();
|
|
@@ -65,6 +67,25 @@ function pullRequestBodyPrefix(value) {
|
|
|
65
67
|
return normalized;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
function optionalExactSha(value, name) {
|
|
71
|
+
const normalized = text(value);
|
|
72
|
+
if (normalized && !EXACT_SHA.test(normalized))
|
|
73
|
+
throw new Error(`${name} must be an exact 40-character commit SHA`);
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function optionalStateRoot(value, name) {
|
|
78
|
+
const normalized = text(value);
|
|
79
|
+
if (
|
|
80
|
+
normalized &&
|
|
81
|
+
normalized !== ABSENT_STATE_ROOT &&
|
|
82
|
+
!EVIDENCE_ROOT.test(normalized)
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(`${name} must be absent or an exact sha256 root`);
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
function canonical(value) {
|
|
69
90
|
if (Array.isArray(value)) return value.map(canonical);
|
|
70
91
|
if (value && typeof value === "object") {
|
|
@@ -225,6 +246,36 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
225
246
|
options.pullRequestBodyPrefix ??
|
|
226
247
|
process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX,
|
|
227
248
|
),
|
|
249
|
+
expectedSelectedSha: optionalExactSha(
|
|
250
|
+
options.expectedSelectedSha ??
|
|
251
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_SELECTED_SHA,
|
|
252
|
+
"expectedSelectedSha",
|
|
253
|
+
),
|
|
254
|
+
expectedPriorStateRoot: optionalStateRoot(
|
|
255
|
+
options.expectedPriorStateRoot ??
|
|
256
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_PRIOR_STATE_ROOT,
|
|
257
|
+
"expectedPriorStateRoot",
|
|
258
|
+
),
|
|
259
|
+
reactivationAuthorized: bool(
|
|
260
|
+
options.reactivationAuthorized ??
|
|
261
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_REACTIVATION_AUTHORIZED,
|
|
262
|
+
false,
|
|
263
|
+
),
|
|
264
|
+
transitionAuthority: {
|
|
265
|
+
actor: text(
|
|
266
|
+
options.transitionAuthority?.actor ?? process.env.GITHUB_ACTOR,
|
|
267
|
+
),
|
|
268
|
+
workflow: text(
|
|
269
|
+
options.transitionAuthority?.workflow ?? process.env.GITHUB_WORKFLOW,
|
|
270
|
+
),
|
|
271
|
+
runId: text(
|
|
272
|
+
options.transitionAuthority?.runId ?? process.env.GITHUB_RUN_ID,
|
|
273
|
+
),
|
|
274
|
+
runAttempt: text(
|
|
275
|
+
options.transitionAuthority?.runAttempt ??
|
|
276
|
+
process.env.GITHUB_RUN_ATTEMPT,
|
|
277
|
+
),
|
|
278
|
+
},
|
|
228
279
|
createPullRequest,
|
|
229
280
|
settlementAuthorized: bool(
|
|
230
281
|
options.settlementAuthorized ??
|
|
@@ -384,6 +435,10 @@ function candidateFromDecision(decision) {
|
|
|
384
435
|
sourceLockRef: decision.sourceLockRef,
|
|
385
436
|
decisionRoot: decision.decisionRoot,
|
|
386
437
|
workflowEvidence: decision.workflowEvidence,
|
|
438
|
+
qualificationRoot: evidenceRoot({
|
|
439
|
+
sourceSha: decision.source.sha,
|
|
440
|
+
workflowEvidence: decision.workflowEvidence,
|
|
441
|
+
}),
|
|
387
442
|
};
|
|
388
443
|
}
|
|
389
444
|
|
|
@@ -394,6 +449,9 @@ function candidateStateBody({
|
|
|
394
449
|
activeCandidate,
|
|
395
450
|
nextCandidate,
|
|
396
451
|
supersededCandidate,
|
|
452
|
+
priorStateRoot = ABSENT_STATE_ROOT,
|
|
453
|
+
generation = 1,
|
|
454
|
+
tombstones = [],
|
|
397
455
|
}) {
|
|
398
456
|
const body = {
|
|
399
457
|
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
@@ -401,8 +459,11 @@ function candidateStateBody({
|
|
|
401
459
|
sourceBranch: options.sourceBranch,
|
|
402
460
|
targetBranch: options.targetBranch,
|
|
403
461
|
targetSha,
|
|
462
|
+
generation,
|
|
463
|
+
priorStateRoot,
|
|
404
464
|
activeCandidate,
|
|
405
465
|
nextCandidate,
|
|
466
|
+
tombstones,
|
|
406
467
|
observationDecisionRoot: decision.decisionRoot || null,
|
|
407
468
|
observedAt: options.now,
|
|
408
469
|
...(supersededCandidate ? { supersededCandidate } : {}),
|
|
@@ -410,6 +471,50 @@ function candidateStateBody({
|
|
|
410
471
|
return { ...body, stateRoot: evidenceRoot(body) };
|
|
411
472
|
}
|
|
412
473
|
|
|
474
|
+
function persistedStateRoot(candidate) {
|
|
475
|
+
return text(candidate?.state?.stateRoot) || ABSENT_STATE_ROOT;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function candidateTombstone({
|
|
479
|
+
options,
|
|
480
|
+
candidate,
|
|
481
|
+
generation,
|
|
482
|
+
priorStateRoot,
|
|
483
|
+
reason,
|
|
484
|
+
pullRequest,
|
|
485
|
+
}) {
|
|
486
|
+
const body = {
|
|
487
|
+
schema: "kungfu-buildchain-dev-alpha-candidate-tombstone/v1",
|
|
488
|
+
repository: options.repository,
|
|
489
|
+
targetBranch: options.targetBranch,
|
|
490
|
+
candidateSha: candidate.sourceSha,
|
|
491
|
+
qualificationRoot:
|
|
492
|
+
candidate.qualificationRoot ||
|
|
493
|
+
evidenceRoot({
|
|
494
|
+
sourceSha: candidate.sourceSha,
|
|
495
|
+
workflowEvidence: candidate.workflowEvidence || [],
|
|
496
|
+
}),
|
|
497
|
+
generation,
|
|
498
|
+
priorStateRoot,
|
|
499
|
+
reason,
|
|
500
|
+
transitionAuthority: options.transitionAuthority,
|
|
501
|
+
pullRequest: pullRequest
|
|
502
|
+
? {
|
|
503
|
+
number: Number(pullRequest.number),
|
|
504
|
+
url: text(pullRequest.url || pullRequest.html_url),
|
|
505
|
+
}
|
|
506
|
+
: null,
|
|
507
|
+
workflowRuns: (candidate.workflowEvidence || []).map((row) => ({
|
|
508
|
+
workflowPath: row.workflowPath,
|
|
509
|
+
runId: row.runId,
|
|
510
|
+
runAttempt: row.runAttempt,
|
|
511
|
+
url: row.url,
|
|
512
|
+
})),
|
|
513
|
+
recordedAt: options.now,
|
|
514
|
+
};
|
|
515
|
+
return { ...body, tombstoneRoot: evidenceRoot(body) };
|
|
516
|
+
}
|
|
517
|
+
|
|
413
518
|
function pullRequestBody({
|
|
414
519
|
options,
|
|
415
520
|
observedSourceHeadSha,
|
|
@@ -526,6 +631,14 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
526
631
|
maxAgeSeconds: options.maxAgeSeconds,
|
|
527
632
|
now: options.now,
|
|
528
633
|
});
|
|
634
|
+
if (
|
|
635
|
+
options.expectedSelectedSha &&
|
|
636
|
+
decision.source.sha !== options.expectedSelectedSha
|
|
637
|
+
) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
529
642
|
const openPullRequests = await client.listOpenPullRequests(
|
|
530
643
|
options.targetBranch,
|
|
531
644
|
);
|
|
@@ -553,9 +666,24 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
553
666
|
);
|
|
554
667
|
}
|
|
555
668
|
let activeCandidate = managedCandidates[0] || null;
|
|
669
|
+
const priorStateRoot = persistedStateRoot(activeCandidate);
|
|
670
|
+
if (
|
|
671
|
+
options.expectedPriorStateRoot &&
|
|
672
|
+
options.expectedPriorStateRoot !== priorStateRoot
|
|
673
|
+
) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const priorGeneration = Number(activeCandidate?.state?.generation || 0);
|
|
679
|
+
let generation = Math.max(priorGeneration, 1);
|
|
680
|
+
const priorTombstones = Array.isArray(activeCandidate?.state?.tombstones)
|
|
681
|
+
? activeCandidate.state.tombstones
|
|
682
|
+
: [];
|
|
556
683
|
const observedCandidate = candidateFromDecision(decision);
|
|
557
684
|
let nextCandidate = null;
|
|
558
685
|
let supersededCandidate = null;
|
|
686
|
+
let tombstones = [...priorTombstones];
|
|
559
687
|
let controllerState = decision.eligible
|
|
560
688
|
? "eligible-for-settlement"
|
|
561
689
|
: qualificationError
|
|
@@ -565,43 +693,100 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
565
693
|
: "observed";
|
|
566
694
|
if (activeCandidate) {
|
|
567
695
|
controllerState = "active";
|
|
696
|
+
nextCandidate = activeCandidate.nextCandidate || null;
|
|
568
697
|
if (
|
|
569
698
|
observedCandidate &&
|
|
570
699
|
observedCandidate.sourceSha !== activeCandidate.sourceSha
|
|
571
700
|
) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
701
|
+
const matchingTombstone = priorTombstones.find(
|
|
702
|
+
(row) => row.candidateSha === observedCandidate.sourceSha,
|
|
703
|
+
);
|
|
704
|
+
if (matchingTombstone && !options.reactivationAuthorized) {
|
|
705
|
+
controllerState = "tombstone-blocked";
|
|
706
|
+
} else {
|
|
707
|
+
if (
|
|
708
|
+
nextCandidate &&
|
|
709
|
+
nextCandidate.sourceSha !== observedCandidate.sourceSha
|
|
710
|
+
) {
|
|
711
|
+
supersededCandidate = nextCandidate;
|
|
712
|
+
generation = priorGeneration + 1;
|
|
713
|
+
tombstones.push(
|
|
714
|
+
candidateTombstone({
|
|
715
|
+
options,
|
|
716
|
+
candidate: supersededCandidate,
|
|
717
|
+
generation,
|
|
718
|
+
priorStateRoot,
|
|
719
|
+
reason: "newer-qualified-next-candidate",
|
|
720
|
+
pullRequest: activeCandidate,
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
nextCandidate = observedCandidate;
|
|
725
|
+
controllerState = "retained-next";
|
|
726
|
+
}
|
|
727
|
+
} else if (
|
|
728
|
+
!decision.eligible &&
|
|
729
|
+
nextCandidate &&
|
|
730
|
+
decision.source.sha === nextCandidate.sourceSha
|
|
578
731
|
) {
|
|
579
|
-
supersededCandidate =
|
|
732
|
+
supersededCandidate = nextCandidate;
|
|
733
|
+
nextCandidate = null;
|
|
734
|
+
generation = priorGeneration + 1;
|
|
735
|
+
tombstones.push(
|
|
736
|
+
candidateTombstone({
|
|
737
|
+
options,
|
|
738
|
+
candidate: supersededCandidate,
|
|
739
|
+
generation,
|
|
740
|
+
priorStateRoot,
|
|
741
|
+
reason: "qualification-evidence-rejected",
|
|
742
|
+
pullRequest: activeCandidate,
|
|
743
|
+
}),
|
|
744
|
+
);
|
|
745
|
+
controllerState = "rejected-next";
|
|
580
746
|
}
|
|
581
747
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
748
|
+
const persistedNextSha = text(activeCandidate?.nextCandidate?.sourceSha);
|
|
749
|
+
const desiredNextSha = text(nextCandidate?.sourceSha);
|
|
750
|
+
const transitionNeeded =
|
|
751
|
+
!activeCandidate?.state ||
|
|
752
|
+
persistedNextSha !== desiredNextSha ||
|
|
753
|
+
tombstones.length !== priorTombstones.length;
|
|
754
|
+
if (transitionNeeded && activeCandidate && generation === priorGeneration) {
|
|
755
|
+
generation = priorGeneration + 1;
|
|
756
|
+
}
|
|
757
|
+
let state =
|
|
758
|
+
activeCandidate?.state && !transitionNeeded
|
|
759
|
+
? activeCandidate.state
|
|
760
|
+
: candidateStateBody({
|
|
761
|
+
options,
|
|
762
|
+
targetSha,
|
|
763
|
+
decision,
|
|
764
|
+
activeCandidate: activeCandidate
|
|
765
|
+
? {
|
|
766
|
+
sourceSha: activeCandidate.sourceSha,
|
|
767
|
+
sourceLockRef: activeCandidate.sourceLockRef,
|
|
768
|
+
decisionRoot: activeCandidate.decisionRoot || null,
|
|
769
|
+
pullRequestNumber: activeCandidate.number,
|
|
770
|
+
pullRequestUrl: activeCandidate.url,
|
|
771
|
+
}
|
|
772
|
+
: null,
|
|
773
|
+
nextCandidate,
|
|
774
|
+
supersededCandidate,
|
|
775
|
+
priorStateRoot,
|
|
776
|
+
generation,
|
|
777
|
+
tombstones,
|
|
778
|
+
});
|
|
598
779
|
let pullRequest;
|
|
599
780
|
let settlementAction = "none";
|
|
600
781
|
if (options.settlementAuthorized && !options.dryRun) {
|
|
601
782
|
if (activeCandidate) {
|
|
602
783
|
const nextBody = replaceCandidateStateMarker(activeCandidate.body, state);
|
|
603
784
|
if (nextBody !== activeCandidate.body) {
|
|
604
|
-
await client.updatePullRequestBody(
|
|
785
|
+
await client.updatePullRequestBody(
|
|
786
|
+
activeCandidate.number,
|
|
787
|
+
nextBody,
|
|
788
|
+
priorStateRoot,
|
|
789
|
+
);
|
|
605
790
|
settlementAction = nextCandidate
|
|
606
791
|
? supersededCandidate
|
|
607
792
|
? "supersede-next-candidate"
|
|
@@ -636,15 +821,53 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
636
821
|
state,
|
|
637
822
|
}),
|
|
638
823
|
});
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
824
|
+
if (pullRequest.state && pullRequest.state !== "open") {
|
|
825
|
+
const tombstone = candidateTombstone({
|
|
826
|
+
options,
|
|
827
|
+
candidate: observedCandidate,
|
|
828
|
+
generation,
|
|
829
|
+
priorStateRoot,
|
|
830
|
+
reason: pullRequest.merged_at
|
|
831
|
+
? "candidate-pr-merged"
|
|
832
|
+
: "candidate-pr-closed",
|
|
833
|
+
pullRequest,
|
|
834
|
+
});
|
|
835
|
+
tombstones = [...priorTombstones, tombstone];
|
|
836
|
+
state = candidateStateBody({
|
|
837
|
+
options,
|
|
838
|
+
targetSha,
|
|
839
|
+
decision,
|
|
840
|
+
activeCandidate: null,
|
|
841
|
+
nextCandidate: null,
|
|
842
|
+
supersededCandidate: null,
|
|
843
|
+
priorStateRoot,
|
|
844
|
+
generation,
|
|
845
|
+
tombstones,
|
|
846
|
+
});
|
|
847
|
+
const nextBody = replaceCandidateStateMarker(pullRequest.body, state);
|
|
848
|
+
await client.updatePullRequestBody(
|
|
849
|
+
pullRequest.number,
|
|
850
|
+
nextBody,
|
|
851
|
+
persistedStateRoot({
|
|
852
|
+
state: parseCandidateStateMarker(pullRequest.body),
|
|
853
|
+
}),
|
|
854
|
+
);
|
|
855
|
+
settlementAction = "reuse-known-candidate-tombstone";
|
|
856
|
+
controllerState = "tombstoned";
|
|
857
|
+
activeCandidate = null;
|
|
858
|
+
} else {
|
|
859
|
+
settlementAction = pullRequest.reused
|
|
860
|
+
? "reuse-active-candidate"
|
|
861
|
+
: "create-active-candidate";
|
|
862
|
+
controllerState = "active";
|
|
863
|
+
activeCandidate = {
|
|
864
|
+
number: Number(pullRequest.number || 0),
|
|
865
|
+
url: text(pullRequest.html_url),
|
|
866
|
+
sourceSha,
|
|
867
|
+
sourceLockRef: decision.sourceLockRef,
|
|
868
|
+
decisionRoot: decision.decisionRoot,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
648
871
|
}
|
|
649
872
|
}
|
|
650
873
|
return {
|
|
@@ -661,6 +884,9 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
661
884
|
supersededCandidate,
|
|
662
885
|
settlementAction,
|
|
663
886
|
stateRoot: state.stateRoot,
|
|
887
|
+
priorStateRoot,
|
|
888
|
+
generation: state.generation,
|
|
889
|
+
tombstones: state.tombstones || [],
|
|
664
890
|
},
|
|
665
891
|
pullRequest: pullRequest || null,
|
|
666
892
|
};
|
|
@@ -789,18 +1015,36 @@ export function createGitHubChannelCandidateClient({
|
|
|
789
1015
|
});
|
|
790
1016
|
},
|
|
791
1017
|
async ensurePullRequest({ head, base, title, body }) {
|
|
792
|
-
const
|
|
793
|
-
`/repos/${owner}/${repo}/pulls?state=
|
|
794
|
-
);
|
|
795
|
-
return (
|
|
796
|
-
open[0] ||
|
|
797
|
-
api(`/repos/${owner}/${repo}/pulls`, {
|
|
798
|
-
method: "POST",
|
|
799
|
-
body: { head, base, title, body },
|
|
800
|
-
})
|
|
1018
|
+
const known = await api(
|
|
1019
|
+
`/repos/${owner}/${repo}/pulls?state=all&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
|
|
801
1020
|
);
|
|
1021
|
+
if (known.length > 1) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`multiple known candidate pull requests bind ${head}: ${known
|
|
1024
|
+
.map((row) => `#${row.number}`)
|
|
1025
|
+
.join(", ")}`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
if (known[0]) return { ...known[0], reused: true };
|
|
1029
|
+
return api(`/repos/${owner}/${repo}/pulls`, {
|
|
1030
|
+
method: "POST",
|
|
1031
|
+
body: { head, base, title, body },
|
|
1032
|
+
});
|
|
802
1033
|
},
|
|
803
|
-
async updatePullRequestBody(
|
|
1034
|
+
async updatePullRequestBody(
|
|
1035
|
+
number,
|
|
1036
|
+
body,
|
|
1037
|
+
expectedPriorStateRoot = ABSENT_STATE_ROOT,
|
|
1038
|
+
) {
|
|
1039
|
+
const current = await api(`/repos/${owner}/${repo}/pulls/${number}`);
|
|
1040
|
+
const observedPriorStateRoot = persistedStateRoot({
|
|
1041
|
+
state: parseCandidateStateMarker(current.body),
|
|
1042
|
+
});
|
|
1043
|
+
if (observedPriorStateRoot !== expectedPriorStateRoot) {
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`candidate controller compare-and-swap failed: expected prior state root ${expectedPriorStateRoot}, observed ${observedPriorStateRoot}`,
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
804
1048
|
return api(`/repos/${owner}/${repo}/pulls/${number}`, {
|
|
805
1049
|
method: "PATCH",
|
|
806
1050
|
body: { body },
|
|
@@ -846,6 +1090,7 @@ async function main() {
|
|
|
846
1090
|
"active-candidate-pr": result.controller.activeCandidate?.url || "",
|
|
847
1091
|
"next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
|
|
848
1092
|
"settlement-action": result.controller.settlementAction,
|
|
1093
|
+
"prior-state-root": result.controller.priorStateRoot,
|
|
849
1094
|
};
|
|
850
1095
|
fs.appendFileSync(
|
|
851
1096
|
process.env.GITHUB_OUTPUT,
|
|
@@ -17,6 +17,12 @@ function repository(value, label) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const TRANSIENT_GITHUB_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
20
|
+
const DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF = "authority/v3/v3.0/artifact-signing";
|
|
21
|
+
|
|
22
|
+
export function resolveAuthorityDispatchRef(value) {
|
|
23
|
+
const ref = required(value, "authority ref");
|
|
24
|
+
return /^[0-9a-f]{40}$/u.test(ref) ? DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF : ref;
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
function retryDelayMs(attempt) {
|
|
22
28
|
return Math.min(1_000 * (2 ** (attempt - 1)), 10_000);
|
|
@@ -98,7 +104,7 @@ export async function dispatchArtifactSigningAuthority({
|
|
|
98
104
|
const authToken = required(token, "Buildchain authority dispatch token");
|
|
99
105
|
const authorityRepo = repository(authorityRepository, "authority repository");
|
|
100
106
|
const sourceRepo = repository(sourceRepository, "source repository");
|
|
101
|
-
const ref =
|
|
107
|
+
const ref = resolveAuthorityDispatchRef(authorityRef);
|
|
102
108
|
const runtime = required(runtimeSha, "Buildchain runtime SHA");
|
|
103
109
|
if (!/^[0-9a-f]{40}$/u.test(runtime)) throw new Error("Buildchain runtime SHA must be exact");
|
|
104
110
|
const correlationId = `${required(sourceRunId, "source run ID")}-${required(sourceRunAttempt, "source run attempt")}-${runtime.slice(0, 12)}-${required(requestArtifact, "request artifact").replace(/[^A-Za-z0-9._-]+/gu, "-")}`;
|