@kungfu-tech/buildchain 3.0.3-alpha.0 → 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 +19 -1
- 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 +0 -45
- 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 +161 -42
- package/dist/site/buildchain-site.json +175 -84
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +97 -5
- package/dist/site/kfd-claims.json +228 -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 +370 -55
- package/dist/site/page-registry.json +150 -59
- package/dist/site/public-surface-audit.json +226 -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 +570 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +35 -12
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +19 -1
- package/docs/github-governance-authority.md +2 -2
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- 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 +31 -31
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +95 -27
- package/docs/runtime-train-validation.md +20 -2
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +6 -4
- package/docs/web-surface-deployments.md +62 -0
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/buildchain-contract.js +8 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/diagnostics.js +105 -30
- 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-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 +483 -201
- package/packages/core/release-propagation.js +175 -1
- package/scripts/auditable-demo.mjs +119 -5
- 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/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +69 -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 +268 -43
- package/scripts/finalize-native-artifact-signing-result.mjs +75 -4
- 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 +7 -0
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +23 -2
- package/scripts/seal-artifact-signing-requests.mjs +13 -6
- package/scripts/stable-candidate-qualification.mjs +42 -0
- package/scripts/web-surface-core.mjs +272 -86
|
@@ -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();
|
|
@@ -72,6 +74,18 @@ function optionalExactSha(value, name) {
|
|
|
72
74
|
return normalized;
|
|
73
75
|
}
|
|
74
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
|
+
|
|
75
89
|
function canonical(value) {
|
|
76
90
|
if (Array.isArray(value)) return value.map(canonical);
|
|
77
91
|
if (value && typeof value === "object") {
|
|
@@ -237,6 +251,31 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
237
251
|
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_SELECTED_SHA,
|
|
238
252
|
"expectedSelectedSha",
|
|
239
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
|
+
},
|
|
240
279
|
createPullRequest,
|
|
241
280
|
settlementAuthorized: bool(
|
|
242
281
|
options.settlementAuthorized ??
|
|
@@ -396,6 +435,10 @@ function candidateFromDecision(decision) {
|
|
|
396
435
|
sourceLockRef: decision.sourceLockRef,
|
|
397
436
|
decisionRoot: decision.decisionRoot,
|
|
398
437
|
workflowEvidence: decision.workflowEvidence,
|
|
438
|
+
qualificationRoot: evidenceRoot({
|
|
439
|
+
sourceSha: decision.source.sha,
|
|
440
|
+
workflowEvidence: decision.workflowEvidence,
|
|
441
|
+
}),
|
|
399
442
|
};
|
|
400
443
|
}
|
|
401
444
|
|
|
@@ -406,6 +449,9 @@ function candidateStateBody({
|
|
|
406
449
|
activeCandidate,
|
|
407
450
|
nextCandidate,
|
|
408
451
|
supersededCandidate,
|
|
452
|
+
priorStateRoot = ABSENT_STATE_ROOT,
|
|
453
|
+
generation = 1,
|
|
454
|
+
tombstones = [],
|
|
409
455
|
}) {
|
|
410
456
|
const body = {
|
|
411
457
|
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
@@ -413,8 +459,11 @@ function candidateStateBody({
|
|
|
413
459
|
sourceBranch: options.sourceBranch,
|
|
414
460
|
targetBranch: options.targetBranch,
|
|
415
461
|
targetSha,
|
|
462
|
+
generation,
|
|
463
|
+
priorStateRoot,
|
|
416
464
|
activeCandidate,
|
|
417
465
|
nextCandidate,
|
|
466
|
+
tombstones,
|
|
418
467
|
observationDecisionRoot: decision.decisionRoot || null,
|
|
419
468
|
observedAt: options.now,
|
|
420
469
|
...(supersededCandidate ? { supersededCandidate } : {}),
|
|
@@ -422,6 +471,50 @@ function candidateStateBody({
|
|
|
422
471
|
return { ...body, stateRoot: evidenceRoot(body) };
|
|
423
472
|
}
|
|
424
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
|
+
|
|
425
518
|
function pullRequestBody({
|
|
426
519
|
options,
|
|
427
520
|
observedSourceHeadSha,
|
|
@@ -573,9 +666,24 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
573
666
|
);
|
|
574
667
|
}
|
|
575
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
|
+
: [];
|
|
576
683
|
const observedCandidate = candidateFromDecision(decision);
|
|
577
684
|
let nextCandidate = null;
|
|
578
685
|
let supersededCandidate = null;
|
|
686
|
+
let tombstones = [...priorTombstones];
|
|
579
687
|
let controllerState = decision.eligible
|
|
580
688
|
? "eligible-for-settlement"
|
|
581
689
|
: qualificationError
|
|
@@ -585,43 +693,100 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
585
693
|
: "observed";
|
|
586
694
|
if (activeCandidate) {
|
|
587
695
|
controllerState = "active";
|
|
696
|
+
nextCandidate = activeCandidate.nextCandidate || null;
|
|
588
697
|
if (
|
|
589
698
|
observedCandidate &&
|
|
590
699
|
observedCandidate.sourceSha !== activeCandidate.sourceSha
|
|
591
700
|
) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
598
731
|
) {
|
|
599
|
-
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";
|
|
600
746
|
}
|
|
601
747
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
+
});
|
|
618
779
|
let pullRequest;
|
|
619
780
|
let settlementAction = "none";
|
|
620
781
|
if (options.settlementAuthorized && !options.dryRun) {
|
|
621
782
|
if (activeCandidate) {
|
|
622
783
|
const nextBody = replaceCandidateStateMarker(activeCandidate.body, state);
|
|
623
784
|
if (nextBody !== activeCandidate.body) {
|
|
624
|
-
await client.updatePullRequestBody(
|
|
785
|
+
await client.updatePullRequestBody(
|
|
786
|
+
activeCandidate.number,
|
|
787
|
+
nextBody,
|
|
788
|
+
priorStateRoot,
|
|
789
|
+
);
|
|
625
790
|
settlementAction = nextCandidate
|
|
626
791
|
? supersededCandidate
|
|
627
792
|
? "supersede-next-candidate"
|
|
@@ -656,15 +821,53 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
656
821
|
state,
|
|
657
822
|
}),
|
|
658
823
|
});
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
}
|
|
668
871
|
}
|
|
669
872
|
}
|
|
670
873
|
return {
|
|
@@ -681,6 +884,9 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
681
884
|
supersededCandidate,
|
|
682
885
|
settlementAction,
|
|
683
886
|
stateRoot: state.stateRoot,
|
|
887
|
+
priorStateRoot,
|
|
888
|
+
generation: state.generation,
|
|
889
|
+
tombstones: state.tombstones || [],
|
|
684
890
|
},
|
|
685
891
|
pullRequest: pullRequest || null,
|
|
686
892
|
};
|
|
@@ -809,18 +1015,36 @@ export function createGitHubChannelCandidateClient({
|
|
|
809
1015
|
});
|
|
810
1016
|
},
|
|
811
1017
|
async ensurePullRequest({ head, base, title, body }) {
|
|
812
|
-
const
|
|
813
|
-
`/repos/${owner}/${repo}/pulls?state=
|
|
814
|
-
);
|
|
815
|
-
return (
|
|
816
|
-
open[0] ||
|
|
817
|
-
api(`/repos/${owner}/${repo}/pulls`, {
|
|
818
|
-
method: "POST",
|
|
819
|
-
body: { head, base, title, body },
|
|
820
|
-
})
|
|
1018
|
+
const known = await api(
|
|
1019
|
+
`/repos/${owner}/${repo}/pulls?state=all&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
|
|
821
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
|
+
});
|
|
822
1033
|
},
|
|
823
|
-
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
|
+
}
|
|
824
1048
|
return api(`/repos/${owner}/${repo}/pulls/${number}`, {
|
|
825
1049
|
method: "PATCH",
|
|
826
1050
|
body: { body },
|
|
@@ -866,6 +1090,7 @@ async function main() {
|
|
|
866
1090
|
"active-candidate-pr": result.controller.activeCandidate?.url || "",
|
|
867
1091
|
"next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
|
|
868
1092
|
"settlement-action": result.controller.settlementAction,
|
|
1093
|
+
"prior-state-root": result.controller.priorStateRoot,
|
|
869
1094
|
};
|
|
870
1095
|
fs.appendFileSync(
|
|
871
1096
|
process.env.GITHUB_OUTPUT,
|