@kungfu-tech/buildchain 3.0.9-alpha.1 → 3.0.9-alpha.11

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 (105) hide show
  1. package/actions/promote-buildchain-ref/README.md +4 -0
  2. package/bin/buildchain.mjs +9 -1
  3. package/bin/internal/trust-release-release-handlers.mjs +1 -0
  4. package/contracts/dev-delivery-authority-v2.schema.json +253 -0
  5. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  6. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  7. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  8. package/contracts/next-development-request-v1.schema.json +66 -0
  9. package/contracts/next-development-transition-v1.schema.json +292 -0
  10. package/dist/site/agent-index.json +1 -0
  11. package/dist/site/artifact-schemas.json +2 -0
  12. package/dist/site/buildchain-contract.json +92 -27
  13. package/dist/site/buildchain-site.json +351 -38
  14. package/dist/site/capability-registry.json +7 -6
  15. package/dist/site/cli-registry.json +43 -3
  16. package/dist/site/controller-registry.json +6 -2
  17. package/dist/site/kfd-claims.json +223 -12
  18. package/dist/site/kfd-upstream-aggregate.json +9 -9
  19. package/dist/site/manual-registry.json +23 -7
  20. package/dist/site/node-api-registry.json +7926 -4533
  21. package/dist/site/page-registry.json +332 -27
  22. package/dist/site/public-surface-audit.json +359 -11
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-passport-check-manifest.json +1 -0
  25. package/dist/site/release-provenance.json +14 -0
  26. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
  27. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  28. package/dist/site/site-manifest.json +19 -11
  29. package/dist/site/workflow-registry.json +21 -10
  30. package/docs/MAP.md +5 -2
  31. package/docs/adopter-delivery-gate.md +106 -0
  32. package/docs/aws-us-elastic-runner-burst-plane.md +30 -3
  33. package/docs/cli-reference.md +202 -15
  34. package/docs/dev-alpha-candidate-patrol.md +8 -0
  35. package/docs/dev-delivery-qualification-landing-adr.md +137 -0
  36. package/docs/dev-delivery-warrant.md +149 -22
  37. package/docs/kfd-support.md +7 -2
  38. package/docs/next-development-transition.md +119 -0
  39. package/docs/node-api-reference.md +402 -204
  40. package/docs/release-governance.md +10 -7
  41. package/docs/release-passport.md +18 -2
  42. package/docs/versioning.md +4 -1
  43. package/package.json +23 -6
  44. package/packages/core/README.md +25 -0
  45. package/packages/core/adopter-delivery-gate.js +576 -0
  46. package/packages/core/adopter-delivery-json.js +66 -0
  47. package/packages/core/adopter-delivery-passport.js +259 -0
  48. package/packages/core/adopter-delivery-vectors.js +158 -0
  49. package/packages/core/adopter-delivery-vectors.json +77 -0
  50. package/packages/core/artifact-verification-envelope.js +5 -5
  51. package/packages/core/buildchain-agent-manuals.js +1 -0
  52. package/packages/core/buildchain-channel-identity.js +2 -3
  53. package/packages/core/buildchain-compatibility-proof.js +22 -0
  54. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  55. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  56. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  57. package/packages/core/channel-candidate.js +8 -0
  58. package/packages/core/channel-promotion-baseline.js +52 -0
  59. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  60. package/packages/core/dev-delivery-authority.js +996 -0
  61. package/packages/core/dev-delivery-native-proof.js +418 -0
  62. package/packages/core/dev-delivery-warrant-settlement.js +67 -20
  63. package/packages/core/dev-delivery-warrant.js +129 -46
  64. package/packages/core/kfd-adopter-category-driver.js +167 -0
  65. package/packages/core/kfd-adopter-manifest.js +49 -50
  66. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  67. package/packages/core/next-development-candidate-reservation.js +186 -0
  68. package/packages/core/next-development-controller.js +726 -0
  69. package/packages/core/next-development-projection.js +288 -0
  70. package/packages/core/next-development-transition.js +738 -0
  71. package/packages/core/paper-agent-entry.js +7 -4
  72. package/packages/core/paper.js +14 -7
  73. package/packages/core/published-delivery-authority.js +247 -0
  74. package/packages/core/release-passport-contract.js +10 -6
  75. package/packages/core/release-passport.js +37 -13
  76. package/scripts/aws-macos-jit-controller-core.mjs +239 -4
  77. package/scripts/aws-macos-jit-controller.mjs +198 -2
  78. package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
  79. package/scripts/aws-macos-jit-source-rebind.mjs +235 -14
  80. package/scripts/aws-macos-jit.mjs +12 -0
  81. package/scripts/buildchain-cli-help.mjs +10 -2
  82. package/scripts/check-internal-architecture.mjs +1 -0
  83. package/scripts/check-inventory.mjs +12 -1
  84. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  85. package/scripts/dev-delivery-authority.mjs +368 -0
  86. package/scripts/dev-delivery-native-run.mjs +235 -0
  87. package/scripts/dev-delivery-proof.mjs +64 -2
  88. package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
  89. package/scripts/dev-delivery-two-phase.mjs +577 -0
  90. package/scripts/dev-delivery-warrant.mjs +48 -6
  91. package/scripts/dev-pr-delivery-warrant.mjs +19 -14
  92. package/scripts/generate-buildchain-kfd-witnesses.mjs +2 -2
  93. package/scripts/generate-channel-build-workflow.mjs +4 -0
  94. package/scripts/generate-next-development-guidance.mjs +49 -0
  95. package/scripts/generate-site-bundle.mjs +8 -3
  96. package/scripts/init-repo.mjs +135 -62
  97. package/scripts/next-development-self-dogfood-harness.mjs +409 -0
  98. package/scripts/next-development-self-dogfood.mjs +532 -0
  99. package/scripts/next-development-transition.mjs +47 -0
  100. package/scripts/release-candidate-tail-reseal.mjs +426 -0
  101. package/scripts/seal-artifact-signing-requests.mjs +7 -4
  102. package/scripts/site-capability-metadata.mjs +25 -1
  103. package/scripts/stable-candidate-qualification.mjs +7 -7
  104. package/scripts/workflow-call-contract.mjs +1 -1
  105. package/templates/native-dev-delivery.yml +86 -0
@@ -20,6 +20,7 @@ import {
20
20
  candidateFromDecision,
21
21
  selectLatestQualifiedSource,
22
22
  } from "../packages/core/dev-alpha-candidate-selection.js";
23
+ import { readGitHubNextDevelopmentVersionReservation } from "../packages/core/next-development-candidate-reservation.js";
23
24
 
24
25
  export { reconcileActiveReleaseTrain, selectLatestQualifiedSource };
25
26
 
@@ -617,6 +618,7 @@ export async function runDevAlphaCandidatePatrol(
617
618
  comparison,
618
619
  workflowEvidence,
619
620
  skippedNewerCommitCount,
621
+ versionReservation,
620
622
  qualificationError,
621
623
  } = await resolvePatrolSourceInputs({
622
624
  client,
@@ -646,6 +648,7 @@ export async function runDevAlphaCandidatePatrol(
646
648
  observedSourceHeadSha,
647
649
  skippedNewerCommitCount,
648
650
  ...(targetBaseline ? { targetBaseline } : {}),
651
+ ...(versionReservation ? { versionReservation } : {}),
649
652
  },
650
653
  workflowEvidence,
651
654
  requiredWorkflowPaths,
@@ -992,6 +995,20 @@ export function createGitHubChannelCandidateClient({
992
995
  targetBranch,
993
996
  parseCandidate: managedCandidateFromPullRequest,
994
997
  }),
998
+ async readNextDevelopmentVersionReservation({
999
+ reservationSha,
1000
+ candidateSha,
1001
+ targetVersion,
1002
+ }) {
1003
+ return readGitHubNextDevelopmentVersionReservation({
1004
+ api,
1005
+ owner,
1006
+ repo,
1007
+ reservationSha,
1008
+ candidateSha,
1009
+ targetVersion,
1010
+ });
1011
+ },
995
1012
  async listCompletedWorkflowRuns(workflowPathValue, sourceBranch) {
996
1013
  const runs = [];
997
1014
  for (let page = 1; page <= 10; page += 1) {
@@ -1015,7 +1032,11 @@ export function createGitHubChannelCandidateClient({
1015
1032
  for (const commit of rows) {
1016
1033
  const commitSha = text(commit.sha);
1017
1034
  if (commitSha === targetSha) return commits;
1018
- commits.push(commitSha);
1035
+ commits.push({
1036
+ sha: commitSha,
1037
+ message: text(commit.commit?.message),
1038
+ parents: (commit.parents || []).map((parent) => text(parent.sha)),
1039
+ });
1019
1040
  }
1020
1041
  if (rows.length < 100) return commits;
1021
1042
  }
@@ -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
+ }
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { devDeliveryContentRoot } from "../packages/core/dev-delivery-warrant.js";
7
+ import { runDevDeliveryCommand } from "./dev-delivery-warrant.mjs";
8
+
9
+ function flag(args, name, fallback = "") {
10
+ const index = args.indexOf(`--${name}`);
11
+ return index === -1 ? fallback : args[index + 1] || "";
12
+ }
13
+
14
+ function positiveInteger(value, label, fallback) {
15
+ const parsed = Number(value || fallback);
16
+ if (!Number.isInteger(parsed) || parsed < 1) {
17
+ throw new Error(`${label} must be a positive integer`);
18
+ }
19
+ return parsed;
20
+ }
21
+
22
+ function exactRoot(value, label) {
23
+ const normalized = String(value || "")
24
+ .trim()
25
+ .toLowerCase();
26
+ if (!/^sha256:[0-9a-f]{64}$/u.test(normalized)) {
27
+ throw new Error(`${label} must be a sha256 content root`);
28
+ }
29
+ return normalized;
30
+ }
31
+
32
+ export async function runNativeWithHeartbeat({
33
+ command,
34
+ cwd = process.cwd(),
35
+ heartbeat,
36
+ executionBinding = {},
37
+ intervalMs = 30_000,
38
+ terminationGraceMs = 10_000,
39
+ terminationKillMs = 5_000,
40
+ spawnImpl = spawn,
41
+ now = () => new Date().toISOString(),
42
+ } = {}) {
43
+ if (!String(command || "").trim()) {
44
+ throw new Error("native command is required");
45
+ }
46
+ if (typeof heartbeat !== "function") {
47
+ throw new Error("heartbeat callback is required");
48
+ }
49
+ const startedAt = now();
50
+ try {
51
+ await heartbeat();
52
+ } catch (error) {
53
+ throw new Error(`native heartbeat failed: ${error.message}`);
54
+ }
55
+ let heartbeatCount = 1;
56
+ let heartbeatError = null;
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
+ });
65
+ const child = spawnImpl("bash", ["-lc", command], {
66
+ cwd,
67
+ env: process.env,
68
+ stdio: "inherit",
69
+ detached: process.platform !== "win32",
70
+ });
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
+
100
+ const beat = async () => {
101
+ if (heartbeatRunning || heartbeatError) return;
102
+ heartbeatRunning = true;
103
+ try {
104
+ await heartbeat();
105
+ heartbeatCount += 1;
106
+ } catch (error) {
107
+ heartbeatError = error;
108
+ terminateForFenceLoss();
109
+ } finally {
110
+ heartbeatRunning = false;
111
+ }
112
+ };
113
+ const timer = setInterval(beat, intervalMs);
114
+ const exitPromise = new Promise((resolve, reject) => {
115
+ child.once("error", reject);
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
+ );
128
+ while (heartbeatRunning) {
129
+ await new Promise((resolve) => setTimeout(resolve, 10));
130
+ }
131
+ if (heartbeatError) {
132
+ throw new Error(`native heartbeat failed: ${heartbeatError.message}`);
133
+ }
134
+ if (exit.code !== 0) {
135
+ throw new Error(
136
+ `native command failed with ${exit.signal || `exit ${exit.code}`}`,
137
+ );
138
+ }
139
+ await beat();
140
+ const completedAt = now();
141
+ const receipt = {
142
+ schema: "kungfu.buildchain.native-heartbeat-run-receipt/v1",
143
+ outcome: "succeeded",
144
+ commandRoot: devDeliveryContentRoot({ command }),
145
+ executionBindingRoot: devDeliveryContentRoot(executionBinding),
146
+ startedAt,
147
+ completedAt,
148
+ heartbeatCount,
149
+ };
150
+ return { ...receipt, receiptRoot: devDeliveryContentRoot(receipt) };
151
+ }
152
+
153
+ async function main() {
154
+ const args = process.argv.slice(2);
155
+ const repository = flag(args, "repository", process.env.GITHUB_REPOSITORY);
156
+ const branch = flag(
157
+ args,
158
+ "branch",
159
+ process.env.BUILDCHAIN_DEV_DELIVERY_BRANCH || process.env.GITHUB_BASE_REF,
160
+ );
161
+ const command = flag(
162
+ args,
163
+ "command",
164
+ process.env.BUILDCHAIN_DEV_DELIVERY_NATIVE_COMMAND,
165
+ );
166
+ const cwd = path.resolve(
167
+ flag(args, "working-directory", process.env.GITHUB_WORKSPACE),
168
+ );
169
+ const fencingToken = exactRoot(
170
+ flag(
171
+ args,
172
+ "fencing-token",
173
+ process.env.BUILDCHAIN_DEV_DELIVERY_FENCING_TOKEN,
174
+ ),
175
+ "fencingToken",
176
+ );
177
+ const leaseGeneration = positiveInteger(
178
+ flag(
179
+ args,
180
+ "lease-generation",
181
+ process.env.BUILDCHAIN_DEV_DELIVERY_LEASE_GENERATION,
182
+ ),
183
+ "leaseGeneration",
184
+ );
185
+ const leaseSeconds = positiveInteger(
186
+ flag(
187
+ args,
188
+ "lease-seconds",
189
+ process.env.BUILDCHAIN_DEV_DELIVERY_LEASE_SECONDS,
190
+ ),
191
+ "leaseSeconds",
192
+ 3600,
193
+ );
194
+ const heartbeatSeconds = positiveInteger(
195
+ flag(
196
+ args,
197
+ "heartbeat-seconds",
198
+ process.env.BUILDCHAIN_DEV_DELIVERY_HEARTBEAT_SECONDS,
199
+ ),
200
+ "heartbeatSeconds",
201
+ Math.max(15, Math.floor(leaseSeconds / 3)),
202
+ );
203
+ if (heartbeatSeconds >= leaseSeconds) {
204
+ throw new Error("heartbeatSeconds must be less than leaseSeconds");
205
+ }
206
+ const output = path.resolve(
207
+ flag(args, "output", ".buildchain/dev-delivery/native-heartbeat-run.json"),
208
+ );
209
+ const result = await runNativeWithHeartbeat({
210
+ command,
211
+ cwd,
212
+ intervalMs: heartbeatSeconds * 1000,
213
+ heartbeat: async () => {
214
+ await runDevDeliveryCommand({
215
+ command: "heartbeat",
216
+ repository,
217
+ branch,
218
+ fencingToken,
219
+ leaseGeneration,
220
+ leaseSeconds,
221
+ execute: true,
222
+ });
223
+ },
224
+ });
225
+ fs.mkdirSync(path.dirname(output), { recursive: true });
226
+ fs.writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
227
+ process.stdout.write(`Native heartbeat run: ${result.receiptRoot}\n`);
228
+ }
229
+
230
+ if (import.meta.url === `file://${process.argv[1]}`) {
231
+ main().catch((error) => {
232
+ console.error(`buildchain dev native: ${error.message}`);
233
+ process.exit(1);
234
+ });
235
+ }