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