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