@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.
- package/actions/promote-buildchain-ref/README.md +4 -0
- package/bin/buildchain.mjs +9 -1
- package/bin/internal/trust-release-release-handlers.mjs +1 -0
- package/contracts/dev-delivery-authority-v2.schema.json +253 -0
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +92 -27
- package/dist/site/buildchain-site.json +351 -38
- package/dist/site/capability-registry.json +7 -6
- package/dist/site/cli-registry.json +43 -3
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +223 -12
- package/dist/site/kfd-upstream-aggregate.json +9 -9
- package/dist/site/manual-registry.json +23 -7
- package/dist/site/node-api-registry.json +7926 -4533
- package/dist/site/page-registry.json +332 -27
- package/dist/site/public-surface-audit.json +359 -11
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-passport-check-manifest.json +1 -0
- package/dist/site/release-provenance.json +14 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
- package/dist/site/schemas/release-passport-v1.schema.json +3 -0
- package/dist/site/site-manifest.json +19 -11
- package/dist/site/workflow-registry.json +21 -10
- package/docs/MAP.md +5 -2
- package/docs/adopter-delivery-gate.md +106 -0
- package/docs/aws-us-elastic-runner-burst-plane.md +30 -3
- package/docs/cli-reference.md +202 -15
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/dev-delivery-qualification-landing-adr.md +137 -0
- package/docs/dev-delivery-warrant.md +149 -22
- package/docs/kfd-support.md +7 -2
- package/docs/next-development-transition.md +119 -0
- package/docs/node-api-reference.md +402 -204
- package/docs/release-governance.md +10 -7
- package/docs/release-passport.md +18 -2
- package/docs/versioning.md +4 -1
- package/package.json +23 -6
- package/packages/core/README.md +25 -0
- package/packages/core/adopter-delivery-gate.js +576 -0
- package/packages/core/adopter-delivery-json.js +66 -0
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/artifact-verification-envelope.js +5 -5
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-channel-identity.js +2 -3
- package/packages/core/buildchain-compatibility-proof.js +22 -0
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +52 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/dev-delivery-authority.js +996 -0
- package/packages/core/dev-delivery-native-proof.js +418 -0
- package/packages/core/dev-delivery-warrant-settlement.js +67 -20
- package/packages/core/dev-delivery-warrant.js +129 -46
- package/packages/core/kfd-adopter-category-driver.js +167 -0
- package/packages/core/kfd-adopter-manifest.js +49 -50
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +726 -0
- package/packages/core/next-development-projection.js +288 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +7 -4
- package/packages/core/paper.js +14 -7
- package/packages/core/published-delivery-authority.js +247 -0
- package/packages/core/release-passport-contract.js +10 -6
- package/packages/core/release-passport.js +37 -13
- package/scripts/aws-macos-jit-controller-core.mjs +239 -4
- package/scripts/aws-macos-jit-controller.mjs +198 -2
- package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
- package/scripts/aws-macos-jit-source-rebind.mjs +235 -14
- package/scripts/aws-macos-jit.mjs +12 -0
- package/scripts/buildchain-cli-help.mjs +10 -2
- package/scripts/check-internal-architecture.mjs +1 -0
- package/scripts/check-inventory.mjs +12 -1
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/dev-delivery-authority.mjs +368 -0
- package/scripts/dev-delivery-native-run.mjs +235 -0
- package/scripts/dev-delivery-proof.mjs +64 -2
- package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
- package/scripts/dev-delivery-two-phase.mjs +577 -0
- package/scripts/dev-delivery-warrant.mjs +48 -6
- package/scripts/dev-pr-delivery-warrant.mjs +19 -14
- package/scripts/generate-buildchain-kfd-witnesses.mjs +2 -2
- package/scripts/generate-channel-build-workflow.mjs +4 -0
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-site-bundle.mjs +8 -3
- package/scripts/init-repo.mjs +135 -62
- package/scripts/next-development-self-dogfood-harness.mjs +409 -0
- package/scripts/next-development-self-dogfood.mjs +532 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/release-candidate-tail-reseal.mjs +426 -0
- package/scripts/seal-artifact-signing-requests.mjs +7 -4
- package/scripts/site-capability-metadata.mjs +25 -1
- package/scripts/stable-candidate-qualification.mjs +7 -7
- package/scripts/workflow-call-contract.mjs +1 -1
- 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(
|
|
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
|
+
}
|