@kungfu-tech/buildchain 3.0.9-alpha.8 → 3.0.9-alpha.9
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/bin/buildchain.mjs +5 -1
- package/contracts/dev-delivery-authority-v2.schema.json +253 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +7 -7
- package/dist/site/buildchain-site.json +135 -19
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +23 -0
- package/dist/site/kfd-claims.json +33 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +907 -143
- package/dist/site/page-registry.json +127 -11
- package/dist/site/public-surface-audit.json +228 -7
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +8 -6
- package/docs/MAP.md +1 -0
- package/docs/cli-reference.md +121 -0
- package/docs/dev-delivery-qualification-landing-adr.md +137 -0
- package/docs/dev-delivery-warrant.md +106 -13
- package/docs/node-api-reference.md +51 -20
- package/docs/versioning.md +2 -0
- package/package.json +4 -2
- package/packages/core/buildchain-channel-identity.js +2 -3
- package/packages/core/dev-delivery-authority.js +996 -0
- package/packages/core/dev-delivery-native-proof.js +89 -4
- package/packages/core/dev-delivery-warrant-settlement.js +65 -22
- package/packages/core/dev-delivery-warrant.js +92 -44
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/dev-delivery-authority.mjs +368 -0
- package/scripts/dev-delivery-native-run.mjs +52 -4
- package/scripts/dev-delivery-proof.mjs +10 -0
- package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
- package/scripts/dev-delivery-two-phase.mjs +31 -52
- package/scripts/dev-delivery-warrant.mjs +9 -2
- package/scripts/generate-site-bundle.mjs +6 -3
- package/scripts/site-capability-metadata.mjs +13 -1
- package/templates/native-dev-delivery.yml +17 -13
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
acquireDevDeliveryLandingWarrant,
|
|
7
|
+
acquireDevDeliveryQualificationLease,
|
|
8
|
+
admitDevDeliveryMergeGroup,
|
|
9
|
+
completeDevDeliveryQualification,
|
|
10
|
+
createDevDeliveryAuthorityState,
|
|
11
|
+
heartbeatDevDeliveryLandingWarrant,
|
|
12
|
+
heartbeatDevDeliveryQualificationLease,
|
|
13
|
+
migrateDevDeliveryAuthorityState,
|
|
14
|
+
observeDevDeliveryAuthorityState,
|
|
15
|
+
recoverDevDeliveryAuthority,
|
|
16
|
+
settleDevDeliveryAuthorityCandidate,
|
|
17
|
+
submitDevDeliveryAuthorityCandidate,
|
|
18
|
+
} from "../packages/core/dev-delivery-authority.js";
|
|
19
|
+
import { GitHubDevDeliveryStore } from "./dev-delivery-warrant.mjs";
|
|
20
|
+
|
|
21
|
+
const STATE_REF_PREFIX = "buildchain/dev-delivery-authority/";
|
|
22
|
+
|
|
23
|
+
function text(value = "") {
|
|
24
|
+
return String(value ?? "").trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positiveInteger(value, label, fallback = 0) {
|
|
28
|
+
const parsed = Number(value || fallback);
|
|
29
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
30
|
+
throw new Error(`${label} must be a positive integer`);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nonNegativeInteger(value, label, fallback = 0) {
|
|
36
|
+
const parsed = Number(value === undefined || value === "" ? fallback : value);
|
|
37
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bool(value, fallback = false) {
|
|
42
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
43
|
+
return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function repository(value) {
|
|
47
|
+
const normalized = text(value);
|
|
48
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) {
|
|
49
|
+
throw new Error("repository must be owner/repo");
|
|
50
|
+
}
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function branch(value) {
|
|
55
|
+
const normalized = text(value).replace(/^refs\/heads\//, "");
|
|
56
|
+
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalized)) {
|
|
57
|
+
throw new Error("branch must be dev/vN/vN.M");
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultDevDeliveryAuthorityStateRef(value) {
|
|
63
|
+
return `${STATE_REF_PREFIX}${branch(value).replaceAll("/", "-")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stateRef(value, protectedBranch) {
|
|
67
|
+
const normalized = text(value || defaultDevDeliveryAuthorityStateRef(protectedBranch)).replace(/^refs\/heads\//, "");
|
|
68
|
+
if (!normalized.startsWith(STATE_REF_PREFIX) || normalized.includes("..")) {
|
|
69
|
+
throw new Error(`state ref must remain under ${STATE_REF_PREFIX}`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function identity(state, options) {
|
|
75
|
+
return {
|
|
76
|
+
candidateId: options.candidateId,
|
|
77
|
+
token: options.authorityToken,
|
|
78
|
+
generation: options.authorityGeneration,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rootedList(value) {
|
|
83
|
+
if (Array.isArray(value)) return value;
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(value || "[]");
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
throw new Error(`qualification domains must be a JSON array: ${cause.message}`);
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(parsed)) throw new Error("qualification domains must be a JSON array");
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function transitionFor(command, state, options) {
|
|
95
|
+
if (command === "migrate") {
|
|
96
|
+
if (!options.legacyState) {
|
|
97
|
+
throw new Error("migrate requires --legacy-state <v1-queue.json>");
|
|
98
|
+
}
|
|
99
|
+
return migrateDevDeliveryAuthorityState(options.legacyState, {
|
|
100
|
+
now: options.now,
|
|
101
|
+
policy: state.policy,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (command === "submit") {
|
|
105
|
+
return submitDevDeliveryAuthorityCandidate(
|
|
106
|
+
state,
|
|
107
|
+
{
|
|
108
|
+
pullRequestNumber: options.pullRequestNumber,
|
|
109
|
+
sourceHead: options.sourceHead,
|
|
110
|
+
assignmentRoot: options.assignmentRoot,
|
|
111
|
+
initiativeRoot: options.initiativeRoot,
|
|
112
|
+
sourceIdentityRoot: options.sourceIdentityRoot,
|
|
113
|
+
sourcePatchRoot: options.sourcePatchRoot,
|
|
114
|
+
sourceProofRoot: options.sourceProofRoot,
|
|
115
|
+
planRoot: options.planRoot,
|
|
116
|
+
closureRoot: options.closureRoot,
|
|
117
|
+
dependencyRoot: options.dependencyRoot,
|
|
118
|
+
toolchainRoot: options.toolchainRoot,
|
|
119
|
+
deliveryClass: options.deliveryClass,
|
|
120
|
+
qualificationDomains: rootedList(options.qualificationDomains),
|
|
121
|
+
},
|
|
122
|
+
{ now: options.now },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (command === "lease-qualification") {
|
|
126
|
+
return acquireDevDeliveryQualificationLease(state, {
|
|
127
|
+
now: options.now,
|
|
128
|
+
leaseSeconds: options.leaseSeconds,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (command === "complete-qualification") {
|
|
132
|
+
return completeDevDeliveryQualification(state, identity(state, options), {
|
|
133
|
+
evidenceRoot: options.evidenceRoot,
|
|
134
|
+
now: options.now,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (command === "heartbeat-qualification") {
|
|
138
|
+
return heartbeatDevDeliveryQualificationLease(state, identity(state, options), {
|
|
139
|
+
now: options.now,
|
|
140
|
+
leaseSeconds: options.leaseSeconds,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (command === "lease-landing") {
|
|
144
|
+
return acquireDevDeliveryLandingWarrant(state, {
|
|
145
|
+
now: options.now,
|
|
146
|
+
leaseSeconds: options.leaseSeconds,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (command === "heartbeat-landing") {
|
|
150
|
+
return heartbeatDevDeliveryLandingWarrant(state, identity(state, options), {
|
|
151
|
+
now: options.now,
|
|
152
|
+
leaseSeconds: options.leaseSeconds,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (command === "recover") return recoverDevDeliveryAuthority(state, { now: options.now });
|
|
156
|
+
if (command === "settle") {
|
|
157
|
+
return settleDevDeliveryAuthorityCandidate(
|
|
158
|
+
state,
|
|
159
|
+
{
|
|
160
|
+
pullRequestNumber: options.pullRequestNumber,
|
|
161
|
+
sourceHead: options.sourceHead,
|
|
162
|
+
outcome: options.outcome,
|
|
163
|
+
evidenceRoot: options.evidenceRoot,
|
|
164
|
+
reason: options.reason,
|
|
165
|
+
authorityToken: options.authorityToken,
|
|
166
|
+
authorityGeneration: options.authorityGeneration,
|
|
167
|
+
},
|
|
168
|
+
{ now: options.now },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`unsupported dev authority command ${command || "<empty>"}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function runDevDeliveryAuthorityCommand(optionsInput = {}, clientInput) {
|
|
175
|
+
const options = {
|
|
176
|
+
...optionsInput,
|
|
177
|
+
repository: repository(optionsInput.repository),
|
|
178
|
+
branch: branch(optionsInput.branch),
|
|
179
|
+
now: new Date(optionsInput.now || Date.now()).toISOString(),
|
|
180
|
+
execute: bool(optionsInput.execute),
|
|
181
|
+
};
|
|
182
|
+
options.stateRef = stateRef(optionsInput.stateRef, options.branch);
|
|
183
|
+
const store =
|
|
184
|
+
clientInput ||
|
|
185
|
+
new GitHubDevDeliveryStore({
|
|
186
|
+
repository: options.repository,
|
|
187
|
+
token: options.token || process.env.GITHUB_TOKEN,
|
|
188
|
+
apiUrl: options.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
189
|
+
createInitialState: ({ repository: repositoryInput, protectedBase, now }) =>
|
|
190
|
+
createDevDeliveryAuthorityState({
|
|
191
|
+
repository: repositoryInput,
|
|
192
|
+
protectedBase,
|
|
193
|
+
policy: {
|
|
194
|
+
maxQualificationLeases: positiveInteger(options.maxQualificationLeases, "maxQualificationLeases", 2),
|
|
195
|
+
qualificationLeaseSeconds: positiveInteger(options.qualificationLeaseSeconds, "qualificationLeaseSeconds", 3600),
|
|
196
|
+
landingLeaseSeconds: positiveInteger(options.landingLeaseSeconds, "landingLeaseSeconds", 900),
|
|
197
|
+
maxLandingOvertakes: nonNegativeInteger(options.maxLandingOvertakes, "maxLandingOvertakes", 2),
|
|
198
|
+
maxQualificationAttempts: positiveInteger(options.maxQualificationAttempts, "maxQualificationAttempts", 3),
|
|
199
|
+
},
|
|
200
|
+
now,
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
const loaded = await store.read({
|
|
204
|
+
stateRef: options.stateRef,
|
|
205
|
+
protectedBase: options.branch,
|
|
206
|
+
now: options.now,
|
|
207
|
+
});
|
|
208
|
+
if (options.command === "migrate" && loaded.exists) {
|
|
209
|
+
throw new Error("migration target state ref already exists");
|
|
210
|
+
}
|
|
211
|
+
if (options.expectedOldStateRoot && loaded.queue.stateRoot !== options.expectedOldStateRoot) {
|
|
212
|
+
throw new Error(`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`);
|
|
213
|
+
}
|
|
214
|
+
if (options.command === "observe") {
|
|
215
|
+
return {
|
|
216
|
+
schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
|
|
217
|
+
ok: true,
|
|
218
|
+
mode: "observe",
|
|
219
|
+
stateRef: options.stateRef,
|
|
220
|
+
stateCommit: loaded.commitSha,
|
|
221
|
+
observation: observeDevDeliveryAuthorityState(loaded.queue, {
|
|
222
|
+
now: options.now,
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (options.command === "admit-merge-group") {
|
|
227
|
+
const admitted = admitDevDeliveryMergeGroup(loaded.queue, identity(loaded.queue, options), { mergeGroupHead: options.mergeGroupHead, now: options.now });
|
|
228
|
+
return {
|
|
229
|
+
schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
|
|
230
|
+
ok: true,
|
|
231
|
+
mode: "admission-check",
|
|
232
|
+
command: options.command,
|
|
233
|
+
stateRef: options.stateRef,
|
|
234
|
+
before: {
|
|
235
|
+
commitSha: loaded.commitSha,
|
|
236
|
+
stateRoot: loaded.queue.stateRoot,
|
|
237
|
+
},
|
|
238
|
+
after: { commitSha: loaded.commitSha, stateRoot: loaded.queue.stateRoot },
|
|
239
|
+
mutationAuthorized: false,
|
|
240
|
+
mutationApplied: false,
|
|
241
|
+
...admitted,
|
|
242
|
+
observation: observeDevDeliveryAuthorityState(loaded.queue, {
|
|
243
|
+
now: options.now,
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (options.legacyStatePath) {
|
|
248
|
+
options.legacyState = JSON.parse(fs.readFileSync(path.resolve(options.legacyStatePath), "utf8"));
|
|
249
|
+
}
|
|
250
|
+
const changed = transitionFor(options.command, loaded.queue, options);
|
|
251
|
+
if (changed.state.repository !== options.repository || changed.state.protectedBase !== options.branch) {
|
|
252
|
+
throw new Error("authority transition repository or protected base drift");
|
|
253
|
+
}
|
|
254
|
+
const mutates = changed.state.stateRoot !== loaded.queue.stateRoot;
|
|
255
|
+
let write = null;
|
|
256
|
+
if (options.execute && mutates) {
|
|
257
|
+
if (options.command !== "migrate" && changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
|
|
258
|
+
throw new Error("transition expected-old root does not match loaded authority");
|
|
259
|
+
}
|
|
260
|
+
write = await store.write({
|
|
261
|
+
stateRef: options.stateRef,
|
|
262
|
+
queue: changed.state,
|
|
263
|
+
expectedCommitSha: loaded.commitSha,
|
|
264
|
+
expectedStateRoot: loaded.queue.stateRoot,
|
|
265
|
+
receiptRoot: changed.receiptRoot,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
|
|
270
|
+
ok: true,
|
|
271
|
+
mode: options.execute ? "execute" : "plan",
|
|
272
|
+
command: options.command,
|
|
273
|
+
stateRef: options.stateRef,
|
|
274
|
+
before: { commitSha: loaded.commitSha, stateRoot: loaded.queue.stateRoot },
|
|
275
|
+
after: {
|
|
276
|
+
commitSha: write?.commitSha || loaded.commitSha,
|
|
277
|
+
stateRoot: changed.state.stateRoot,
|
|
278
|
+
},
|
|
279
|
+
mutationAuthorized: options.execute,
|
|
280
|
+
mutationApplied: Boolean(write),
|
|
281
|
+
receipt: changed.receipt,
|
|
282
|
+
receiptRoot: changed.receiptRoot,
|
|
283
|
+
lease: changed.lease || null,
|
|
284
|
+
warrant: changed.warrant || changed.state.landingWarrant || null,
|
|
285
|
+
observation: observeDevDeliveryAuthorityState(changed.state, {
|
|
286
|
+
now: options.now,
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function flag(args, name, fallback = "") {
|
|
292
|
+
const index = args.indexOf(`--${name}`);
|
|
293
|
+
return index === -1 ? fallback : args[index + 1] || "";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasFlag(args, name) {
|
|
297
|
+
return args.includes(`--${name}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function devDeliveryAuthorityCliOptions(args = [], environment = process.env) {
|
|
301
|
+
const [command = "", ...rest] = args;
|
|
302
|
+
return {
|
|
303
|
+
command,
|
|
304
|
+
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
305
|
+
branch: flag(rest, "branch", environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF),
|
|
306
|
+
stateRef: flag(rest, "state-ref", environment.BUILDCHAIN_DEV_AUTHORITY_STATE_REF),
|
|
307
|
+
expectedOldStateRoot: flag(rest, "expected-old", environment.BUILDCHAIN_DEV_AUTHORITY_EXPECTED_OLD),
|
|
308
|
+
pullRequestNumber: flag(rest, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
|
|
309
|
+
candidateId: flag(rest, "candidate-id", environment.BUILDCHAIN_DEV_DELIVERY_CANDIDATE_ID),
|
|
310
|
+
sourceHead: flag(rest, "source-head", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD),
|
|
311
|
+
assignmentRoot: flag(rest, "assignment-root", environment.BUILDCHAIN_DEV_DELIVERY_ASSIGNMENT_ROOT),
|
|
312
|
+
initiativeRoot: flag(rest, "initiative-root", environment.BUILDCHAIN_DEV_DELIVERY_INITIATIVE_ROOT),
|
|
313
|
+
sourceIdentityRoot: flag(rest, "source-identity-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT),
|
|
314
|
+
sourcePatchRoot: flag(rest, "source-patch-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT),
|
|
315
|
+
sourceProofRoot: flag(rest, "source-proof-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT),
|
|
316
|
+
planRoot: flag(rest, "plan-root", environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT),
|
|
317
|
+
closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
|
|
318
|
+
dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
|
|
319
|
+
toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
|
|
320
|
+
legacyStatePath: flag(rest, "legacy-state", environment.BUILDCHAIN_DEV_AUTHORITY_LEGACY_STATE),
|
|
321
|
+
deliveryClass: flag(rest, "delivery-class", environment.BUILDCHAIN_DEV_DELIVERY_CLASS),
|
|
322
|
+
qualificationDomains: flag(rest, "qualification-domains", environment.BUILDCHAIN_DEV_QUALIFICATION_DOMAINS || "[]"),
|
|
323
|
+
authorityToken: flag(rest, "authority-token", environment.BUILDCHAIN_DEV_AUTHORITY_TOKEN),
|
|
324
|
+
authorityGeneration: flag(rest, "authority-generation", environment.BUILDCHAIN_DEV_AUTHORITY_GENERATION),
|
|
325
|
+
evidenceRoot: flag(rest, "evidence-root", environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT),
|
|
326
|
+
mergeGroupHead: flag(rest, "merge-group-head", environment.BUILDCHAIN_DEV_MERGE_GROUP_HEAD),
|
|
327
|
+
outcome: flag(rest, "outcome", environment.BUILDCHAIN_DEV_DELIVERY_OUTCOME),
|
|
328
|
+
reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
|
|
329
|
+
leaseSeconds: flag(rest, "lease-seconds", environment.BUILDCHAIN_DEV_AUTHORITY_LEASE_SECONDS),
|
|
330
|
+
maxQualificationLeases: flag(rest, "max-qualification-leases", environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_LEASES || "2"),
|
|
331
|
+
qualificationLeaseSeconds: flag(rest, "qualification-lease-seconds", environment.BUILDCHAIN_DEV_QUALIFICATION_LEASE_SECONDS || "3600"),
|
|
332
|
+
landingLeaseSeconds: flag(rest, "landing-lease-seconds", environment.BUILDCHAIN_DEV_LANDING_LEASE_SECONDS || "900"),
|
|
333
|
+
maxLandingOvertakes: flag(rest, "max-landing-overtakes", environment.BUILDCHAIN_DEV_MAX_LANDING_OVERTAKES || "2"),
|
|
334
|
+
maxQualificationAttempts: flag(rest, "max-qualification-attempts", environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_ATTEMPTS || "3"),
|
|
335
|
+
now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
|
|
336
|
+
outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT || ".buildchain/dev-delivery/authority-result.json"),
|
|
337
|
+
execute: hasFlag(rest, "execute"),
|
|
338
|
+
json: hasFlag(rest, "json"),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function usage() {
|
|
343
|
+
return "Usage: buildchain dev authority <migrate|submit|lease-qualification|heartbeat-qualification|complete-qualification|lease-landing|heartbeat-landing|recover|admit-merge-group|settle|observe> --repository <owner/repo> --branch <dev/vN/vN.M> [--qualification-domains <json>] [--legacy-state <v1-queue.json>] [--execute] [--output <file>] [--json]\n";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function main() {
|
|
347
|
+
const args = process.argv.slice(2);
|
|
348
|
+
if (args.length === 0 || hasFlag(args, "help")) {
|
|
349
|
+
process.stdout.write(usage());
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const options = devDeliveryAuthorityCliOptions(args);
|
|
353
|
+
if (!["submit", "migrate", "lease-qualification", "heartbeat-qualification", "complete-qualification", "lease-landing", "heartbeat-landing", "recover", "admit-merge-group", "settle", "observe"].includes(options.command)) {
|
|
354
|
+
throw new Error(usage().trim());
|
|
355
|
+
}
|
|
356
|
+
const result = await runDevDeliveryAuthorityCommand(options);
|
|
357
|
+
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
358
|
+
fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
359
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
360
|
+
else process.stdout.write(`Buildchain dev authority ${options.command}: ${result.mode}\nResult: ${options.outputPath}\n`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
364
|
+
main().catch((error) => {
|
|
365
|
+
console.error(`buildchain dev authority: ${error.message}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -35,6 +35,8 @@ export async function runNativeWithHeartbeat({
|
|
|
35
35
|
heartbeat,
|
|
36
36
|
executionBinding = {},
|
|
37
37
|
intervalMs = 30_000,
|
|
38
|
+
terminationGraceMs = 10_000,
|
|
39
|
+
terminationKillMs = 5_000,
|
|
38
40
|
spawnImpl = spawn,
|
|
39
41
|
now = () => new Date().toISOString(),
|
|
40
42
|
} = {}) {
|
|
@@ -53,12 +55,48 @@ export async function runNativeWithHeartbeat({
|
|
|
53
55
|
let heartbeatCount = 1;
|
|
54
56
|
let heartbeatError = null;
|
|
55
57
|
let heartbeatRunning = false;
|
|
58
|
+
let childExited = false;
|
|
59
|
+
let forceKillTimer = null;
|
|
60
|
+
let terminationTimer = null;
|
|
61
|
+
let rejectTermination;
|
|
62
|
+
const terminationFailure = new Promise((_, reject) => {
|
|
63
|
+
rejectTermination = reject;
|
|
64
|
+
});
|
|
56
65
|
const child = spawnImpl("bash", ["-lc", command], {
|
|
57
66
|
cwd,
|
|
58
67
|
env: process.env,
|
|
59
68
|
stdio: "inherit",
|
|
69
|
+
detached: process.platform !== "win32",
|
|
60
70
|
});
|
|
61
71
|
|
|
72
|
+
const signalChild = (signal) => {
|
|
73
|
+
if (childExited) return;
|
|
74
|
+
if (process.platform !== "win32" && Number.isInteger(child.pid)) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(-child.pid, signal);
|
|
77
|
+
return;
|
|
78
|
+
} catch {
|
|
79
|
+
// Fall through to the direct child handle for mocked or already-exiting processes.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
child.kill(signal);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const terminateForFenceLoss = () => {
|
|
86
|
+
signalChild("SIGTERM");
|
|
87
|
+
forceKillTimer = setTimeout(() => {
|
|
88
|
+
signalChild("SIGKILL");
|
|
89
|
+
terminationTimer = setTimeout(() => {
|
|
90
|
+
if (!childExited)
|
|
91
|
+
rejectTermination(
|
|
92
|
+
new Error("native worker did not stop after fencing loss"),
|
|
93
|
+
);
|
|
94
|
+
}, terminationKillMs);
|
|
95
|
+
terminationTimer.unref?.();
|
|
96
|
+
}, terminationGraceMs);
|
|
97
|
+
forceKillTimer.unref?.();
|
|
98
|
+
};
|
|
99
|
+
|
|
62
100
|
const beat = async () => {
|
|
63
101
|
if (heartbeatRunning || heartbeatError) return;
|
|
64
102
|
heartbeatRunning = true;
|
|
@@ -67,16 +105,26 @@ export async function runNativeWithHeartbeat({
|
|
|
67
105
|
heartbeatCount += 1;
|
|
68
106
|
} catch (error) {
|
|
69
107
|
heartbeatError = error;
|
|
70
|
-
|
|
108
|
+
terminateForFenceLoss();
|
|
71
109
|
} finally {
|
|
72
110
|
heartbeatRunning = false;
|
|
73
111
|
}
|
|
74
112
|
};
|
|
75
113
|
const timer = setInterval(beat, intervalMs);
|
|
76
|
-
const
|
|
114
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
77
115
|
child.once("error", reject);
|
|
78
|
-
child.once("exit", (code, signal) =>
|
|
79
|
-
|
|
116
|
+
child.once("exit", (code, signal) => {
|
|
117
|
+
childExited = true;
|
|
118
|
+
resolve({ code, signal });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const exit = await Promise.race([exitPromise, terminationFailure]).finally(
|
|
122
|
+
() => {
|
|
123
|
+
clearInterval(timer);
|
|
124
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
125
|
+
if (terminationTimer) clearTimeout(terminationTimer);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
80
128
|
while (heartbeatRunning) {
|
|
81
129
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
82
130
|
}
|
|
@@ -49,6 +49,7 @@ export function devDeliveryProofCliOptions(args = [], environment = process.env)
|
|
|
49
49
|
closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
|
|
50
50
|
dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
|
|
51
51
|
toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
|
|
52
|
+
environmentRoot: flag(rest, "environment-root", environment.BUILDCHAIN_DEV_DELIVERY_ENVIRONMENT_ROOT),
|
|
52
53
|
affectedPaths: flag(rest, "affected-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS || "[]"),
|
|
53
54
|
shardEvidenceRoots: flag(rest, "shard-evidence-roots-json", environment.BUILDCHAIN_DEV_DELIVERY_SHARD_EVIDENCE_ROOTS || "[]"),
|
|
54
55
|
qualifiedAt: flag(rest, "qualified-at", environment.BUILDCHAIN_DEV_DELIVERY_QUALIFIED_AT),
|
|
@@ -67,6 +68,8 @@ export function devDeliveryProofCliOptions(args = [], environment = process.env)
|
|
|
67
68
|
pullRequestNumber: flag(rest, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
|
|
68
69
|
changedPaths: flag(rest, "changed-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_CHANGED_PATHS || "[]"),
|
|
69
70
|
graphKnown: boolean(flag(rest, "graph-known", environment.BUILDCHAIN_DEV_DELIVERY_GRAPH_KNOWN), false),
|
|
71
|
+
attributionComplete: boolean(flag(rest, "attribution-complete", environment.BUILDCHAIN_DEV_DELIVERY_ATTRIBUTION_COMPLETE), false),
|
|
72
|
+
renames: flag(rest, "renames-json", environment.BUILDCHAIN_DEV_DELIVERY_RENAMES || "[]"),
|
|
70
73
|
outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_PROOF_OUTPUT || ".buildchain/dev-delivery/proof.json"),
|
|
71
74
|
json: hasFlag(rest, "json"),
|
|
72
75
|
};
|
|
@@ -128,6 +131,7 @@ export function runDevDeliveryProofCommand(options) {
|
|
|
128
131
|
closureRoot: options.closureRoot,
|
|
129
132
|
dependencyRoot: options.dependencyRoot,
|
|
130
133
|
toolchainRoot: options.toolchainRoot,
|
|
134
|
+
environmentRoot: options.environmentRoot,
|
|
131
135
|
qualifiedBase: options.qualifiedBase,
|
|
132
136
|
affectedPaths: jsonList(options.affectedPaths, "affected paths"),
|
|
133
137
|
shardEvidenceRoots: jsonList(options.shardEvidenceRoots, "shard evidence roots", { required: true }),
|
|
@@ -147,9 +151,12 @@ export function runDevDeliveryProofCommand(options) {
|
|
|
147
151
|
closureRoot: options.closureRoot,
|
|
148
152
|
dependencyRoot: options.dependencyRoot,
|
|
149
153
|
toolchainRoot: options.toolchainRoot,
|
|
154
|
+
environmentRoot: options.environmentRoot,
|
|
150
155
|
currentBase: options.currentBase,
|
|
151
156
|
graphKnown: options.graphKnown,
|
|
157
|
+
attributionComplete: options.attributionComplete,
|
|
152
158
|
changedPaths: jsonList(options.changedPaths, "changed paths"),
|
|
159
|
+
renames: jsonList(options.renames, "renames"),
|
|
153
160
|
},
|
|
154
161
|
});
|
|
155
162
|
}
|
|
@@ -163,9 +170,12 @@ export function runDevDeliveryProofCommand(options) {
|
|
|
163
170
|
closureRoot: options.closureRoot,
|
|
164
171
|
dependencyRoot: options.dependencyRoot,
|
|
165
172
|
toolchainRoot: options.toolchainRoot,
|
|
173
|
+
environmentRoot: options.environmentRoot,
|
|
166
174
|
currentBase: options.currentBase,
|
|
167
175
|
graphKnown: options.graphKnown,
|
|
176
|
+
attributionComplete: options.attributionComplete,
|
|
168
177
|
changedPaths: jsonList(options.changedPaths, "changed paths"),
|
|
178
|
+
renames: jsonList(options.renames, "renames"),
|
|
169
179
|
},
|
|
170
180
|
});
|
|
171
181
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createNativeProofReuseDecision } from "../packages/core/dev-delivery-warrant.js";
|
|
2
|
+
|
|
3
|
+
const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
4
|
+
|
|
5
|
+
export function attributedGitHubBaseDelta(data, previousBase) {
|
|
6
|
+
const files = Array.isArray(data?.files) ? data.files : [];
|
|
7
|
+
const graphKnown =
|
|
8
|
+
data?.status === "ahead" &&
|
|
9
|
+
data?.merge_base_commit?.sha === previousBase &&
|
|
10
|
+
files.length < 300;
|
|
11
|
+
const renames = files
|
|
12
|
+
.filter((entry) => entry.status === "renamed")
|
|
13
|
+
.map((entry) => ({
|
|
14
|
+
from: String(entry.previous_filename || ""),
|
|
15
|
+
to: String(entry.filename || ""),
|
|
16
|
+
}));
|
|
17
|
+
const attributionComplete =
|
|
18
|
+
graphKnown && renames.every((entry) => entry.from && entry.to);
|
|
19
|
+
return {
|
|
20
|
+
graphKnown,
|
|
21
|
+
attributionComplete,
|
|
22
|
+
changedPaths: attributionComplete
|
|
23
|
+
? [
|
|
24
|
+
...new Set(
|
|
25
|
+
files
|
|
26
|
+
.flatMap((entry) => [
|
|
27
|
+
String(entry.filename || ""),
|
|
28
|
+
String(entry.previous_filename || ""),
|
|
29
|
+
])
|
|
30
|
+
.filter(Boolean),
|
|
31
|
+
),
|
|
32
|
+
].sort()
|
|
33
|
+
: [],
|
|
34
|
+
renames: attributionComplete ? renames : [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function replayQualifiedNativeWarrant({
|
|
39
|
+
warrant,
|
|
40
|
+
pullRequestNumber,
|
|
41
|
+
expectedHead,
|
|
42
|
+
exactPullRequestHead,
|
|
43
|
+
}) {
|
|
44
|
+
if (warrant.phase !== "qualified") return null;
|
|
45
|
+
if (
|
|
46
|
+
!ROOT_PATTERN.test(warrant.nativeProofRoot || "") ||
|
|
47
|
+
!ROOT_PATTERN.test(warrant.nativeProofReuseRoot || "")
|
|
48
|
+
) {
|
|
49
|
+
throw new Error("qualified Warrant is missing rooted native evidence");
|
|
50
|
+
}
|
|
51
|
+
await exactPullRequestHead(pullRequestNumber, expectedHead);
|
|
52
|
+
return {
|
|
53
|
+
schema: "kungfu.buildchain.two-phase-delivery-result/v1",
|
|
54
|
+
ok: true,
|
|
55
|
+
outcome: "already-qualified-warrant",
|
|
56
|
+
nativeAttempts: 0,
|
|
57
|
+
nativeProofRoot: warrant.nativeProofRoot,
|
|
58
|
+
nativeReuseDecisionRoot: warrant.nativeProofReuseRoot,
|
|
59
|
+
qualificationReceiptRoot: null,
|
|
60
|
+
landingAuthority: false,
|
|
61
|
+
qualifiedWarrant: warrant,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function classifyNativeProofAgainstCurrent(
|
|
66
|
+
proof,
|
|
67
|
+
options,
|
|
68
|
+
client,
|
|
69
|
+
) {
|
|
70
|
+
const currentBase = await client.baseSha(options.branch);
|
|
71
|
+
const delta = await client.baseDelta(proof.qualifiedBase, currentBase);
|
|
72
|
+
const current = {
|
|
73
|
+
sourceIdentityRoot: options.sourceIdentityRoot,
|
|
74
|
+
sourcePatchRoot: options.sourcePatchRoot,
|
|
75
|
+
planRoot: options.planRoot,
|
|
76
|
+
closureRoot: options.closureRoot,
|
|
77
|
+
dependencyRoot: options.dependencyRoot,
|
|
78
|
+
toolchainRoot: options.toolchainRoot,
|
|
79
|
+
environmentRoot: options.environmentRoot,
|
|
80
|
+
currentBase,
|
|
81
|
+
graphKnown: delta.graphKnown,
|
|
82
|
+
attributionComplete: delta.attributionComplete,
|
|
83
|
+
changedPaths: delta.changedPaths,
|
|
84
|
+
renames: delta.renames,
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
current,
|
|
88
|
+
decision: createNativeProofReuseDecision({ proof, current }),
|
|
89
|
+
};
|
|
90
|
+
}
|