@kungfu-tech/buildchain 3.0.9-alpha.4 → 3.0.9-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/buildchain.mjs +5 -1
  2. package/dist/site/buildchain-contract.json +9 -9
  3. package/dist/site/buildchain-site.json +56 -26
  4. package/dist/site/cli-registry.json +18 -2
  5. package/dist/site/kfd-claims.json +8 -5
  6. package/dist/site/kfd-upstream-aggregate.json +1 -1
  7. package/dist/site/manual-registry.json +6 -6
  8. package/dist/site/node-api-registry.json +290 -49
  9. package/dist/site/page-registry.json +46 -16
  10. package/dist/site/public-surface-audit.json +122 -6
  11. package/dist/site/publication-registry.json +4 -4
  12. package/dist/site/site-manifest.json +10 -10
  13. package/dist/site/workflow-registry.json +12 -6
  14. package/docs/cli-reference.md +79 -13
  15. package/docs/dev-delivery-warrant.md +53 -19
  16. package/docs/kfd-support.md +7 -2
  17. package/docs/node-api-reference.md +59 -51
  18. package/docs/release-governance.md +5 -4
  19. package/docs/release-passport.md +8 -2
  20. package/docs/versioning.md +1 -0
  21. package/package.json +3 -2
  22. package/packages/core/artifact-verification-envelope.js +5 -5
  23. package/packages/core/dev-delivery-native-proof.js +333 -0
  24. package/packages/core/dev-delivery-warrant-settlement.js +5 -1
  25. package/packages/core/dev-delivery-warrant.js +40 -6
  26. package/packages/core/kfd-adopter-manifest.js +46 -47
  27. package/packages/core/release-passport-contract.js +5 -5
  28. package/packages/core/release-passport.js +5 -4
  29. package/scripts/aws-macos-jit-controller-core.mjs +82 -2
  30. package/scripts/aws-macos-jit-controller.mjs +81 -1
  31. package/scripts/aws-macos-jit-source-rebind.mjs +147 -24
  32. package/scripts/buildchain-cli-help.mjs +6 -2
  33. package/scripts/check-internal-architecture.mjs +1 -0
  34. package/scripts/dev-delivery-native-run.mjs +187 -0
  35. package/scripts/dev-delivery-proof.mjs +54 -2
  36. package/scripts/dev-delivery-two-phase.mjs +598 -0
  37. package/scripts/dev-delivery-warrant.mjs +38 -3
  38. package/scripts/dev-pr-delivery-warrant.mjs +6 -1
  39. package/scripts/generate-channel-build-workflow.mjs +4 -0
  40. package/scripts/init-repo.mjs +16 -0
  41. package/scripts/release-candidate-tail-reseal.mjs +426 -0
  42. package/templates/native-dev-delivery.yml +82 -0
@@ -0,0 +1,598 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ createNativeProofReuseDecision,
8
+ createNativeQualificationProof,
9
+ devDeliveryContentRoot,
10
+ } from "../packages/core/dev-delivery-warrant.js";
11
+ import { runNativeWithHeartbeat } from "./dev-delivery-native-run.mjs";
12
+ import { runDevDeliveryCommand } from "./dev-delivery-warrant.mjs";
13
+
14
+ function flag(args, name, fallback = "") {
15
+ const index = args.indexOf(`--${name}`);
16
+ return index === -1 ? fallback : args[index + 1] || "";
17
+ }
18
+
19
+ function positiveInteger(value, label, fallback = 0) {
20
+ const parsed = Number(value || fallback);
21
+ if (!Number.isInteger(parsed) || parsed < 1) {
22
+ throw new Error(`${label} must be a positive integer`);
23
+ }
24
+ return parsed;
25
+ }
26
+
27
+ function jsonList(value, label) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(value || "[]");
31
+ } catch (cause) {
32
+ throw new Error(`${label} must be a JSON array: ${cause.message}`);
33
+ }
34
+ if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array`);
35
+ return parsed;
36
+ }
37
+
38
+ function exactSha(value, label) {
39
+ const normalized = String(value || "")
40
+ .trim()
41
+ .toLowerCase();
42
+ if (!/^[0-9a-f]{40}$/u.test(normalized)) {
43
+ throw new Error(`${label} must be an exact lowercase Git SHA`);
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ function readJson(file, label) {
49
+ if (!file) throw new Error(`${label} is required`);
50
+ return JSON.parse(fs.readFileSync(file, "utf8"));
51
+ }
52
+
53
+ function writeJson(file, value) {
54
+ fs.mkdirSync(path.dirname(file), { recursive: true });
55
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
56
+ }
57
+
58
+ export class GitHubTwoPhaseClient {
59
+ constructor({
60
+ repository,
61
+ token,
62
+ apiUrl = "https://api.github.com",
63
+ fetchImpl = globalThis.fetch,
64
+ } = {}) {
65
+ if (!token) throw new Error("GITHUB_TOKEN is required");
66
+ this.repository = repository;
67
+ this.token = token;
68
+ this.apiUrl = apiUrl.replace(/\/+$/u, "");
69
+ this.fetch = fetchImpl;
70
+ }
71
+
72
+ async request(requestPath, { method = "GET", body } = {}) {
73
+ const response = await this.fetch(`${this.apiUrl}${requestPath}`, {
74
+ method,
75
+ headers: {
76
+ accept: "application/vnd.github+json",
77
+ authorization: `Bearer ${this.token}`,
78
+ "content-type": "application/json",
79
+ "x-github-api-version": "2022-11-28",
80
+ },
81
+ body: body === undefined ? undefined : JSON.stringify(body),
82
+ });
83
+ const raw = await response.text();
84
+ const data = raw ? JSON.parse(raw) : null;
85
+ if (!response.ok) throw new Error(data?.message || `${requestPath} failed`);
86
+ return data;
87
+ }
88
+
89
+ async baseSha(branch) {
90
+ const data = await this.request(
91
+ `/repos/${this.repository}/git/ref/heads/${branch
92
+ .split("/")
93
+ .map(encodeURIComponent)
94
+ .join("/")}`,
95
+ );
96
+ return exactSha(data?.object?.sha, "protected base SHA");
97
+ }
98
+
99
+ async exactPullRequestHead(pullRequestNumber, expectedHead) {
100
+ const data = await this.request(
101
+ `/repos/${this.repository}/pulls/${pullRequestNumber}`,
102
+ );
103
+ const observed = exactSha(data?.head?.sha, "observed PR head");
104
+ if (observed !== expectedHead) {
105
+ throw new Error(
106
+ `semantic source head changed: ${observed} != ${expectedHead}`,
107
+ );
108
+ }
109
+ return observed;
110
+ }
111
+
112
+ async baseDelta(previousBase, currentBase) {
113
+ if (previousBase === currentBase) {
114
+ return { graphKnown: true, changedPaths: [] };
115
+ }
116
+ const data = await this.request(
117
+ `/repos/${this.repository}/compare/${previousBase}...${currentBase}`,
118
+ );
119
+ const files = Array.isArray(data.files) ? data.files : [];
120
+ const graphKnown =
121
+ data.status === "ahead" &&
122
+ data.merge_base_commit?.sha === previousBase &&
123
+ files.length < 300;
124
+ return {
125
+ graphKnown,
126
+ changedPaths: graphKnown
127
+ ? [
128
+ ...new Set(
129
+ files
130
+ .flatMap((entry) => [
131
+ String(entry.filename || ""),
132
+ String(entry.previous_filename || ""),
133
+ ])
134
+ .filter(Boolean),
135
+ ),
136
+ ].sort()
137
+ : [],
138
+ };
139
+ }
140
+
141
+ async wake(eventType, candidate) {
142
+ await this.request(`/repos/${this.repository}/dispatches`, {
143
+ method: "POST",
144
+ body: {
145
+ event_type: eventType,
146
+ client_payload: candidate,
147
+ },
148
+ });
149
+ }
150
+ }
151
+
152
+ function semanticCurrent(options, currentBase, delta) {
153
+ return {
154
+ sourceIdentityRoot: options.sourceIdentityRoot,
155
+ sourcePatchRoot: options.sourcePatchRoot,
156
+ planRoot: options.planRoot,
157
+ closureRoot: options.closureRoot,
158
+ dependencyRoot: options.dependencyRoot,
159
+ toolchainRoot: options.toolchainRoot,
160
+ currentBase,
161
+ graphKnown: delta.graphKnown,
162
+ changedPaths: delta.changedPaths,
163
+ };
164
+ }
165
+
166
+ export function composeCandidate(candidateDirectory, expectedHead, baseSha) {
167
+ try {
168
+ execFileSync("git", ["-C", candidateDirectory, "merge", "--abort"], {
169
+ stdio: "ignore",
170
+ });
171
+ } catch {
172
+ // No merge was active.
173
+ }
174
+ execFileSync(
175
+ "git",
176
+ ["-C", candidateDirectory, "reset", "--hard", expectedHead],
177
+ {
178
+ stdio: "ignore",
179
+ },
180
+ );
181
+ execFileSync(
182
+ "git",
183
+ ["-C", candidateDirectory, "merge", "--no-commit", "--no-ff", baseSha],
184
+ {
185
+ stdio: "pipe",
186
+ env: {
187
+ ...process.env,
188
+ GIT_COMMITTER_NAME: "Buildchain Delivery Warrant",
189
+ GIT_COMMITTER_EMAIL:
190
+ "buildchain-delivery-warrant@users.noreply.github.com",
191
+ },
192
+ },
193
+ );
194
+ }
195
+
196
+ async function classifyAgainstCurrent(proof, options, client) {
197
+ const currentBase = await client.baseSha(options.branch);
198
+ const delta = await client.baseDelta(proof.qualifiedBase, currentBase);
199
+ const current = semanticCurrent(options, currentBase, delta);
200
+ return {
201
+ current,
202
+ decision: createNativeProofReuseDecision({ proof, current }),
203
+ };
204
+ }
205
+
206
+ async function releaseFailedAttempt({
207
+ error,
208
+ options,
209
+ warrant,
210
+ nativeAttempts,
211
+ evidenceDirectory,
212
+ runCommand,
213
+ client,
214
+ }) {
215
+ const failure = {
216
+ schema: "kungfu.buildchain.two-phase-delivery-failure/v1",
217
+ pullRequestNumber: options.pullRequestNumber,
218
+ expectedHead: options.expectedHead,
219
+ fencingToken: warrant.fencingToken,
220
+ leaseGeneration: warrant.generation,
221
+ nativeAttempts,
222
+ reason: error.message,
223
+ };
224
+ const evidenceRoot = devDeliveryContentRoot(failure);
225
+ writeJson(path.join(evidenceDirectory, "failure.json"), {
226
+ ...failure,
227
+ evidenceRoot,
228
+ });
229
+ try {
230
+ const closed = await runCommand({
231
+ command: "close",
232
+ repository: options.repository,
233
+ branch: options.branch,
234
+ fencingToken: warrant.fencingToken,
235
+ leaseGeneration: warrant.generation,
236
+ outcome: "terminal-failure",
237
+ evidenceRoot,
238
+ reason: error.message,
239
+ execute: true,
240
+ token: options.token,
241
+ apiUrl: options.apiUrl,
242
+ });
243
+ writeJson(path.join(evidenceDirectory, "failure-close.json"), closed);
244
+ const nextCandidate = closed.observation?.queued?.[0] || null;
245
+ if (nextCandidate && options.wakeEventType) {
246
+ try {
247
+ await client.wake(options.wakeEventType, {
248
+ schema: "kungfu.buildchain.dev-delivery-wake/v1",
249
+ targetBranch: options.branch,
250
+ ...nextCandidate,
251
+ });
252
+ writeJson(path.join(evidenceDirectory, "wake-next.json"), {
253
+ schema: "kungfu.buildchain.dev-delivery-wake-receipt/v1",
254
+ eventType: options.wakeEventType,
255
+ candidateId: nextCandidate.candidateId,
256
+ pullRequestNumber: nextCandidate.pullRequestNumber,
257
+ sourceHead: nextCandidate.sourceHead,
258
+ action: "repository-dispatch-sent",
259
+ });
260
+ } catch (wakeError) {
261
+ writeJson(path.join(evidenceDirectory, "wake-next-error.json"), {
262
+ schema: "kungfu.buildchain.dev-delivery-wake-error/v1",
263
+ reason: wakeError.message,
264
+ candidateId: nextCandidate.candidateId,
265
+ nextAction:
266
+ "A later candidate submission or lease recovery will retry deterministic selection.",
267
+ });
268
+ }
269
+ }
270
+ } catch (closeError) {
271
+ writeJson(path.join(evidenceDirectory, "failure-close-error.json"), {
272
+ schema: "kungfu.buildchain.two-phase-delivery-close-error/v1",
273
+ reason: closeError.message,
274
+ nextAction:
275
+ "Recover the expired lease or close the current fenced generation; no merge admission was attempted.",
276
+ });
277
+ }
278
+ }
279
+
280
+ export async function runTwoPhaseDelivery(options, dependencies = {}) {
281
+ const client =
282
+ dependencies.client ||
283
+ new GitHubTwoPhaseClient({
284
+ repository: options.repository,
285
+ token: options.token,
286
+ apiUrl: options.apiUrl,
287
+ });
288
+ const runCommand = dependencies.runCommand || runDevDeliveryCommand;
289
+ const runNative = dependencies.runNative || runNativeWithHeartbeat;
290
+ const warrantResult = readJson(options.warrantResultPath, "Warrant result");
291
+ const warrant =
292
+ warrantResult.observation?.activeWarrant || warrantResult.warrant;
293
+ if (!warrant || warrant.phase !== "provisional") {
294
+ throw new Error(
295
+ "two-phase delivery requires an active provisional Warrant",
296
+ );
297
+ }
298
+ if (
299
+ Number(warrant.pullRequestNumber) !== options.pullRequestNumber ||
300
+ warrant.sourceHead !== options.expectedHead
301
+ ) {
302
+ throw new Error("provisional Warrant does not match the exact PR head");
303
+ }
304
+ const evidenceDirectory = path.resolve(options.evidenceDirectory);
305
+ fs.mkdirSync(evidenceDirectory, { recursive: true });
306
+ let proof = options.nativeProofPath
307
+ ? readJson(options.nativeProofPath, "native proof")
308
+ : null;
309
+ let classified = proof
310
+ ? await classifyAgainstCurrent(proof, options, client)
311
+ : null;
312
+ let nativeAttempts = 0;
313
+
314
+ try {
315
+ while (true) {
316
+ if (classified?.decision.reusable) {
317
+ await client.exactPullRequestHead(
318
+ options.pullRequestNumber,
319
+ options.expectedHead,
320
+ );
321
+ classified = await classifyAgainstCurrent(proof, options, client);
322
+ if (classified.decision.reusable) break;
323
+ }
324
+ if (nativeAttempts >= 2) break;
325
+ if (!String(options.nativeCommand || "").trim()) {
326
+ throw new Error(
327
+ `native proof cannot be reused (${classified?.decision.reason || "not-supplied"}) and native-command is empty`,
328
+ );
329
+ }
330
+ await client.exactPullRequestHead(
331
+ options.pullRequestNumber,
332
+ options.expectedHead,
333
+ );
334
+ const qualifiedBase = await client.baseSha(options.branch);
335
+ composeCandidate(
336
+ options.candidateDirectory,
337
+ options.expectedHead,
338
+ qualifiedBase,
339
+ );
340
+ nativeAttempts += 1;
341
+ const heartbeatReceipt = await runNative({
342
+ command: options.nativeCommand,
343
+ cwd: options.candidateDirectory,
344
+ intervalMs: options.heartbeatSeconds * 1000,
345
+ executionBinding: {
346
+ repository: options.repository,
347
+ protectedBase: options.branch,
348
+ sourceHead: options.expectedHead,
349
+ qualifiedBase,
350
+ toolchainRoot: options.toolchainRoot,
351
+ },
352
+ heartbeat: async () => {
353
+ await runCommand({
354
+ command: "heartbeat",
355
+ repository: options.repository,
356
+ branch: options.branch,
357
+ fencingToken: warrant.fencingToken,
358
+ leaseGeneration: warrant.generation,
359
+ leaseSeconds: options.leaseSeconds,
360
+ execute: true,
361
+ token: options.token,
362
+ apiUrl: options.apiUrl,
363
+ });
364
+ },
365
+ });
366
+ writeJson(
367
+ path.join(
368
+ evidenceDirectory,
369
+ `native-heartbeat-attempt-${nativeAttempts}.json`,
370
+ ),
371
+ heartbeatReceipt,
372
+ );
373
+ await client.exactPullRequestHead(
374
+ options.pullRequestNumber,
375
+ options.expectedHead,
376
+ );
377
+ proof = createNativeQualificationProof({
378
+ repository: options.repository,
379
+ protectedBase: options.branch,
380
+ sourceIdentityRoot: options.sourceIdentityRoot,
381
+ sourcePatchRoot: options.sourcePatchRoot,
382
+ planRoot: options.planRoot,
383
+ closureRoot: options.closureRoot,
384
+ dependencyRoot: options.dependencyRoot,
385
+ toolchainRoot: options.toolchainRoot,
386
+ qualifiedBase,
387
+ affectedPaths: options.affectedPaths,
388
+ shardEvidenceRoots: [
389
+ ...options.shardEvidenceRoots,
390
+ heartbeatReceipt.receiptRoot,
391
+ ],
392
+ qualifiedAt: new Date().toISOString(),
393
+ });
394
+ classified = await classifyAgainstCurrent(proof, options, client);
395
+ }
396
+
397
+ if (!classified?.decision.reusable) {
398
+ throw new Error(
399
+ `native proof remains non-reusable after ${nativeAttempts} attempt(s): ${classified?.decision.reason || "unknown"}`,
400
+ );
401
+ }
402
+ writeJson(path.join(evidenceDirectory, "native-proof.json"), proof);
403
+ writeJson(
404
+ path.join(evidenceDirectory, "native-reuse-decision.json"),
405
+ classified.decision,
406
+ );
407
+ const qualified = await runCommand({
408
+ command: "qualify",
409
+ repository: options.repository,
410
+ branch: options.branch,
411
+ fencingToken: warrant.fencingToken,
412
+ leaseGeneration: warrant.generation,
413
+ nativeProofPath: path.join(evidenceDirectory, "native-proof.json"),
414
+ nativeReuseDecisionPath: path.join(
415
+ evidenceDirectory,
416
+ "native-reuse-decision.json",
417
+ ),
418
+ currentBase: classified.current.currentBase,
419
+ graphKnown: classified.current.graphKnown,
420
+ changedPaths: JSON.stringify(classified.current.changedPaths),
421
+ execute: true,
422
+ token: options.token,
423
+ apiUrl: options.apiUrl,
424
+ });
425
+ writeJson(
426
+ path.join(evidenceDirectory, "qualified-warrant.json"),
427
+ qualified,
428
+ );
429
+ return {
430
+ schema: "kungfu.buildchain.two-phase-delivery-result/v1",
431
+ ok: true,
432
+ outcome: "qualified-warrant",
433
+ nativeAttempts,
434
+ nativeProofRoot: proof.proofRoot,
435
+ nativeReuseDecisionRoot: classified.decision.decisionRoot,
436
+ qualificationReceiptRoot: qualified.receiptRoot,
437
+ qualifiedWarrant: qualified.observation.activeWarrant,
438
+ };
439
+ } catch (error) {
440
+ await releaseFailedAttempt({
441
+ error,
442
+ options,
443
+ warrant,
444
+ nativeAttempts,
445
+ evidenceDirectory,
446
+ runCommand,
447
+ client,
448
+ });
449
+ throw error;
450
+ }
451
+ }
452
+
453
+ function cliOptions(args, environment = process.env) {
454
+ const leaseSeconds = positiveInteger(
455
+ flag(
456
+ args,
457
+ "lease-seconds",
458
+ environment.BUILDCHAIN_DEV_DELIVERY_LEASE_SECONDS,
459
+ ),
460
+ "leaseSeconds",
461
+ 3600,
462
+ );
463
+ const heartbeatSeconds = positiveInteger(
464
+ flag(
465
+ args,
466
+ "heartbeat-seconds",
467
+ environment.BUILDCHAIN_DEV_DELIVERY_HEARTBEAT_SECONDS,
468
+ ),
469
+ "heartbeatSeconds",
470
+ Math.max(15, Math.floor(leaseSeconds / 3)),
471
+ );
472
+ if (heartbeatSeconds >= leaseSeconds) {
473
+ throw new Error("heartbeatSeconds must be less than leaseSeconds");
474
+ }
475
+ return {
476
+ repository: flag(args, "repository", environment.GITHUB_REPOSITORY),
477
+ branch: flag(
478
+ args,
479
+ "branch",
480
+ environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF,
481
+ ),
482
+ pullRequestNumber: positiveInteger(
483
+ flag(args, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
484
+ "pullRequestNumber",
485
+ ),
486
+ expectedHead: exactSha(
487
+ flag(
488
+ args,
489
+ "expected-head",
490
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD,
491
+ ),
492
+ "expectedHead",
493
+ ),
494
+ sourceIdentityRoot: flag(
495
+ args,
496
+ "source-identity-root",
497
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT,
498
+ ),
499
+ sourcePatchRoot: flag(
500
+ args,
501
+ "source-patch-root",
502
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT,
503
+ ),
504
+ planRoot: flag(
505
+ args,
506
+ "plan-root",
507
+ environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT,
508
+ ),
509
+ closureRoot: flag(
510
+ args,
511
+ "closure-root",
512
+ environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT,
513
+ ),
514
+ dependencyRoot: flag(
515
+ args,
516
+ "dependency-root",
517
+ environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT,
518
+ ),
519
+ toolchainRoot: flag(
520
+ args,
521
+ "toolchain-root",
522
+ environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT,
523
+ ),
524
+ affectedPaths: jsonList(
525
+ flag(
526
+ args,
527
+ "affected-paths-json",
528
+ environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS,
529
+ ),
530
+ "affectedPaths",
531
+ ),
532
+ shardEvidenceRoots: jsonList(
533
+ flag(
534
+ args,
535
+ "shard-evidence-roots-json",
536
+ environment.BUILDCHAIN_DEV_DELIVERY_SHARD_EVIDENCE_ROOTS,
537
+ ),
538
+ "shardEvidenceRoots",
539
+ ),
540
+ nativeCommand: flag(
541
+ args,
542
+ "native-command",
543
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_COMMAND,
544
+ ),
545
+ nativeProofPath: flag(
546
+ args,
547
+ "native-proof",
548
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_PROOF,
549
+ ),
550
+ warrantResultPath: flag(
551
+ args,
552
+ "warrant-result",
553
+ environment.BUILDCHAIN_DEV_DELIVERY_WARRANT_RESULT,
554
+ ),
555
+ candidateDirectory: path.resolve(
556
+ flag(args, "candidate-directory", ".buildchain/candidate"),
557
+ ),
558
+ evidenceDirectory: path.resolve(
559
+ flag(args, "evidence-directory", ".buildchain/dev-delivery"),
560
+ ),
561
+ leaseSeconds,
562
+ heartbeatSeconds,
563
+ token: environment.GITHUB_TOKEN,
564
+ apiUrl: environment.GITHUB_API_URL || "https://api.github.com",
565
+ wakeEventType: flag(
566
+ args,
567
+ "wake-event-type",
568
+ environment.BUILDCHAIN_DEV_DELIVERY_WAKE_EVENT_TYPE ||
569
+ "buildchain-dev-delivery-wake",
570
+ ),
571
+ };
572
+ }
573
+
574
+ async function main() {
575
+ const args = process.argv.slice(2);
576
+ if (args.length === 0 || args.includes("--help")) {
577
+ process.stdout.write(
578
+ "Usage: buildchain dev two-phase --repository owner/repo --branch dev/vN/vN.M --pull-request N --expected-head SHA --warrant-result FILE [--native-proof FILE] [--native-command COMMAND]\n",
579
+ );
580
+ return;
581
+ }
582
+ const options = cliOptions(args);
583
+ const result = await runTwoPhaseDelivery(options);
584
+ writeJson(
585
+ path.join(options.evidenceDirectory, "two-phase-result.json"),
586
+ result,
587
+ );
588
+ process.stdout.write(
589
+ `Two-phase Delivery Warrant: ${result.qualificationReceiptRoot}\n`,
590
+ );
591
+ }
592
+
593
+ if (import.meta.url === `file://${process.argv[1]}`) {
594
+ main().catch((error) => {
595
+ console.error(`buildchain dev two-phase: ${error.message}`);
596
+ process.exit(1);
597
+ });
598
+ }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
4
+ import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, qualifyDevDeliveryWarrant, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
5
5
  import { planDevDeliveryWarrantShadow, qualifyDevDeliveryWarrantShadow } from "../packages/core/dev-delivery-warrant-shadow.js";
6
6
 
7
7
  const STATE_PATH = "queue.json";
@@ -174,6 +174,22 @@ function warrantIdentity(queue, options) {
174
174
  return { candidateId: active.candidateId, fencingToken, generation };
175
175
  }
176
176
 
177
+ function jsonFile(file, label) {
178
+ if (!file) throw new Error(`${label} is required`);
179
+ return JSON.parse(fs.readFileSync(file, "utf8"));
180
+ }
181
+
182
+ function jsonList(value, label) {
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(value || "[]");
186
+ } catch (cause) {
187
+ throw new Error(`${label} must be a JSON array: ${cause.message}`);
188
+ }
189
+ if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array`);
190
+ return parsed;
191
+ }
192
+
177
193
  function transitionFor(command, queue, options) {
178
194
  if (command === "submit") {
179
195
  return submitDevDeliveryCandidate(
@@ -190,6 +206,7 @@ function transitionFor(command, queue, options) {
190
206
  closureRoot: exactRoot(options.closureRoot, "closureRoot"),
191
207
  dependencyRoot: exactRoot(options.dependencyRoot, "dependencyRoot"),
192
208
  toolchainRoot: exactRoot(options.toolchainRoot, "toolchainRoot"),
209
+ affectedPaths: jsonList(options.affectedPaths, "affected paths"),
193
210
  sourceWorkflowRunId: options.sourceWorkflowRunId ? positiveInteger(options.sourceWorkflowRunId, "sourceWorkflowRunId") : 0,
194
211
  deliveryClass: options.deliveryClass,
195
212
  priority: options.priority || "ordinary",
@@ -209,6 +226,18 @@ function transitionFor(command, queue, options) {
209
226
  leaseSeconds: options.leaseSeconds,
210
227
  });
211
228
  }
229
+ if (command === "qualify") {
230
+ return qualifyDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
231
+ nativeProof: jsonFile(options.nativeProofPath, "native proof"),
232
+ reuseDecision: jsonFile(options.nativeReuseDecisionPath, "native reuse decision"),
233
+ current: {
234
+ currentBase: options.currentBase,
235
+ graphKnown: options.graphKnown,
236
+ changedPaths: jsonList(options.changedPaths, "changed paths"),
237
+ },
238
+ now: options.now,
239
+ });
240
+ }
212
241
  if (command === "recover") return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
213
242
  if (command === "close") {
214
243
  return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
@@ -391,6 +420,12 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
391
420
  sourceIdentityRoot: flag(rest, "source-identity-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT),
392
421
  sourcePatchRoot: flag(rest, "source-patch-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT),
393
422
  sourceProofRoot: flag(rest, "source-proof-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT),
423
+ affectedPaths: flag(rest, "affected-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS || "[]"),
424
+ nativeProofPath: flag(rest, "native-proof", environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_PROOF),
425
+ nativeReuseDecisionPath: flag(rest, "native-reuse-decision", environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_REUSE_DECISION),
426
+ currentBase: flag(rest, "current-base", environment.BUILDCHAIN_DEV_DELIVERY_CURRENT_BASE),
427
+ changedPaths: flag(rest, "changed-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_CHANGED_PATHS || "[]"),
428
+ graphKnown: ["1", "true", "yes", "on"].includes(flag(rest, "graph-known", environment.BUILDCHAIN_DEV_DELIVERY_GRAPH_KNOWN).trim().toLowerCase()),
394
429
  planRoot: flag(rest, "plan-root", environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT),
395
430
  closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
396
431
  dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
@@ -414,7 +449,7 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
414
449
  }
415
450
 
416
451
  function usage() {
417
- return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
452
+ return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
418
453
  }
419
454
 
420
455
  async function main() {
@@ -424,7 +459,7 @@ async function main() {
424
459
  return;
425
460
  }
426
461
  const options = devDeliveryCliOptions(args);
427
- if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
462
+ if (!["submit", "select", "heartbeat", "qualify", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
428
463
  throw new Error(usage().trim());
429
464
  }
430
465
  const shadow = options.command.startsWith("shadow-");