@kungfu-tech/buildchain 4.0.1-alpha.2 → 4.0.1-alpha.4
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/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
- package/architecture/internal-capabilities.json +94 -0
- package/architecture/maintainability-policy.json +75 -8
- package/architecture/v3-core-mechanism-inventory.json +2 -0
- package/architecture/v4-delivery-authority-parity.json +250 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
- package/bin/buildchain.mjs +9 -1
- package/contracts/dev-delivery-authority-v2.schema.json +662 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +91 -8
- package/dist/site/buildchain-site.json +169 -18
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +46 -2
- package/dist/site/kfd-claims.json +43 -9
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +1694 -165
- package/dist/site/page-registry.json +162 -11
- package/dist/site/public-surface-audit.json +367 -7
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +22 -6
- package/docs/MAP.md +1 -0
- package/docs/cli-reference.md +211 -13
- package/docs/dev-delivery-qualification-landing-adr.md +251 -0
- package/docs/dev-delivery-warrant.md +330 -38
- package/docs/node-api-reference.md +113 -53
- package/package.json +5 -1
- package/packages/core/buildchain-contract.js +3 -2
- package/packages/core/dev-delivery-authority-candidate.js +270 -0
- package/packages/core/dev-delivery-authority-evidence.js +146 -0
- package/packages/core/dev-delivery-authority-landing.js +461 -0
- package/packages/core/dev-delivery-authority-observation.js +48 -0
- package/packages/core/dev-delivery-authority-qualification.js +591 -0
- package/packages/core/dev-delivery-authority-settlement.js +213 -0
- package/packages/core/dev-delivery-authority-state.js +583 -0
- package/packages/core/dev-delivery-candidate-identity.js +13 -0
- package/packages/core/dev-delivery-contract-surface.js +76 -0
- package/packages/core/dev-delivery-execution-failure.js +133 -0
- package/packages/core/dev-delivery-execution-transfer.js +572 -0
- package/packages/core/dev-delivery-landing-admission-core.js +119 -0
- package/packages/core/dev-delivery-landing-readback.js +598 -0
- package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
- package/packages/core/dev-delivery-landing-testing-port.js +6 -0
- package/packages/core/dev-delivery-native-execution.js +110 -0
- package/packages/core/dev-delivery-native-proof.js +546 -0
- package/packages/core/dev-delivery-process-boundary.js +550 -0
- package/packages/core/dev-delivery-provider-attempt.js +127 -0
- package/packages/core/dev-delivery-provider-heartbeat.js +370 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-qualification.js +145 -0
- package/packages/core/dev-delivery-warrant-settlement.js +237 -36
- package/packages/core/dev-delivery-warrant-state.js +564 -0
- package/packages/core/dev-delivery-warrant.js +321 -368
- package/scripts/buildchain-cli-help.mjs +11 -2
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +1 -1
- package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
- package/scripts/dev-delivery-authority-provider.mjs +28 -0
- package/scripts/dev-delivery-authority.mjs +490 -0
- package/scripts/dev-delivery-native-run.mjs +177 -0
- package/scripts/dev-delivery-process-boundary.mjs +260 -0
- package/scripts/dev-delivery-proof.mjs +67 -2
- package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
- package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
- package/scripts/dev-delivery-two-phase.mjs +573 -0
- package/scripts/dev-delivery-warrant-options.mjs +266 -0
- package/scripts/dev-delivery-warrant-store.mjs +227 -0
- package/scripts/dev-delivery-warrant.mjs +227 -193
- package/scripts/dev-pr-auto-merge.mjs +1 -1
- package/scripts/dev-pr-delivery-warrant.mjs +10 -0
- package/scripts/generate-site-bundle.mjs +26 -4
- package/scripts/npm-publish-transaction.mjs +2 -2
- package/scripts/release-train-self-dogfood.mjs +94 -92
- package/scripts/site-capability-metadata.mjs +12 -0
- package/templates/native-dev-delivery.yml +143 -0
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
cancelQueuedDevDeliveryCandidate,
|
|
6
|
+
closeDevDeliveryWarrant,
|
|
7
|
+
createDevDeliveryQueue,
|
|
8
|
+
createNativeCommandContract,
|
|
9
|
+
heartbeatDevDeliveryWarrant,
|
|
10
|
+
observeDevDeliveryQueue,
|
|
11
|
+
qualifyDevDeliveryWarrant,
|
|
12
|
+
recoverExpiredDevDeliveryWarrant,
|
|
13
|
+
selectDevDeliveryWarrant,
|
|
14
|
+
settleDevDeliveryTerminalEvent,
|
|
15
|
+
submitDevDeliveryCandidate,
|
|
16
|
+
} from "../packages/core/dev-delivery-warrant.js";
|
|
5
17
|
import { runV4DeliveryWarrantReadCandidate } from "../packages/core/v4-delivery-warrant-read-candidate.js";
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
import { GitHubDevDeliveryStore } from "./dev-delivery-warrant-store.mjs";
|
|
20
|
+
import { devDeliveryCliOptions } from "./dev-delivery-warrant-options.mjs";
|
|
21
|
+
export { GitHubDevDeliveryStore, devDeliveryCliOptions };
|
|
22
|
+
|
|
8
23
|
const STATE_REF_PREFIX = "buildchain/dev-delivery-warrant/";
|
|
9
24
|
|
|
10
25
|
function text(value = "") {
|
|
11
26
|
return String(value ?? "").trim();
|
|
12
27
|
}
|
|
13
|
-
|
|
14
28
|
function bool(value, fallback = false) {
|
|
15
29
|
if (value === undefined || value === null || value === "") return fallback;
|
|
16
30
|
if (typeof value === "boolean") return value;
|
|
@@ -19,33 +33,41 @@ function bool(value, fallback = false) {
|
|
|
19
33
|
|
|
20
34
|
function positiveInteger(value, label, fallback = 0) {
|
|
21
35
|
const parsed = Number(value ?? fallback);
|
|
22
|
-
if (!Number.isInteger(parsed) || parsed < 1)
|
|
36
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
37
|
+
throw new Error(`${label} must be a positive integer`);
|
|
23
38
|
return parsed;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
function exactRoot(value, label) {
|
|
27
42
|
const normalized = text(value).toLowerCase();
|
|
28
|
-
if (!/^sha256:[0-9a-f]{64}$/.test(normalized))
|
|
43
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(normalized))
|
|
44
|
+
throw new Error(`${label} must be a sha256 content root`);
|
|
29
45
|
return normalized;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
function exactSha(value, label) {
|
|
33
49
|
const normalized = text(value).toLowerCase();
|
|
34
|
-
if (!/^[0-9a-f]{40}$/.test(normalized))
|
|
50
|
+
if (!/^[0-9a-f]{40}$/.test(normalized))
|
|
51
|
+
throw new Error(`${label} must be a 40-character Git SHA`);
|
|
35
52
|
return normalized;
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
function normalizeRepository(value) {
|
|
39
56
|
const normalized = text(value);
|
|
40
57
|
const match = normalized.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
41
|
-
if (!match)
|
|
58
|
+
if (!match)
|
|
59
|
+
throw new Error(
|
|
60
|
+
`repository must be owner/repo, got ${normalized || "<empty>"}`,
|
|
61
|
+
);
|
|
42
62
|
return { owner: match[1], repo: match[2], fullName: normalized };
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
function normalizeBranch(value) {
|
|
46
66
|
const normalized = text(value).replace(/^refs\/heads\//, "");
|
|
47
67
|
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalized)) {
|
|
48
|
-
throw new Error(
|
|
68
|
+
throw new Error(
|
|
69
|
+
`branch must be dev/vN/vN.M, got ${normalized || "<empty>"}`,
|
|
70
|
+
);
|
|
49
71
|
}
|
|
50
72
|
return normalized;
|
|
51
73
|
}
|
|
@@ -56,142 +78,122 @@ export function defaultDevDeliveryStateRef(branch) {
|
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
function normalizeStateRef(value, branch) {
|
|
59
|
-
const normalized = text(value || defaultDevDeliveryStateRef(branch)).replace(
|
|
60
|
-
|
|
81
|
+
const normalized = text(value || defaultDevDeliveryStateRef(branch)).replace(
|
|
82
|
+
/^refs\/heads\//,
|
|
83
|
+
"",
|
|
84
|
+
);
|
|
85
|
+
if (
|
|
86
|
+
!normalized.startsWith(STATE_REF_PREFIX) ||
|
|
87
|
+
normalized.includes("..") ||
|
|
88
|
+
normalized.endsWith("/")
|
|
89
|
+
) {
|
|
61
90
|
throw new Error(`state ref must remain under ${STATE_REF_PREFIX}`);
|
|
62
91
|
}
|
|
63
92
|
return normalized;
|
|
64
93
|
}
|
|
65
94
|
|
|
66
|
-
function
|
|
67
|
-
|
|
95
|
+
function warrantIdentity(queue, options) {
|
|
96
|
+
const active = queue.activeWarrant;
|
|
97
|
+
if (!active) throw new Error("no active Delivery Warrant");
|
|
98
|
+
const fencingToken = exactRoot(options.fencingToken, "fencingToken");
|
|
99
|
+
const generation = positiveInteger(
|
|
100
|
+
options.leaseGeneration,
|
|
101
|
+
"leaseGeneration",
|
|
102
|
+
);
|
|
103
|
+
return { candidateId: active.candidateId, fencingToken, generation };
|
|
68
104
|
}
|
|
69
105
|
|
|
70
|
-
function
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
106
|
+
function jsonFile(file, label) {
|
|
107
|
+
if (!file) throw new Error(`${label} is required`);
|
|
108
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
73
109
|
}
|
|
74
110
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.apiUrl = apiUrl.replace(/\/+$/, "");
|
|
82
|
-
this.fetch = fetchImpl;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async request(method, requestPath, body) {
|
|
86
|
-
const response = await this.fetch(`${this.apiUrl}${requestPath}`, {
|
|
87
|
-
method,
|
|
88
|
-
headers: {
|
|
89
|
-
accept: "application/vnd.github+json",
|
|
90
|
-
authorization: `Bearer ${this.token}`,
|
|
91
|
-
"content-type": "application/json",
|
|
92
|
-
"x-github-api-version": "2022-11-28",
|
|
93
|
-
},
|
|
94
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
95
|
-
});
|
|
96
|
-
const raw = await response.text();
|
|
97
|
-
const data = raw ? JSON.parse(raw) : null;
|
|
98
|
-
if (!response.ok) {
|
|
99
|
-
const error = new Error(data?.message || raw || `${method} ${requestPath} failed`);
|
|
100
|
-
error.status = response.status;
|
|
101
|
-
error.data = data;
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
return data;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async read({ stateRef, protectedBase, now }) {
|
|
108
|
-
let ref;
|
|
109
|
-
try {
|
|
110
|
-
ref = await this.request("GET", `/repos/${this.repository.fullName}/git/ref/heads/${encodeRef(stateRef)}`);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (error.status !== 404) throw error;
|
|
113
|
-
return {
|
|
114
|
-
exists: false,
|
|
115
|
-
commitSha: "",
|
|
116
|
-
queue: createDevDeliveryQueue({
|
|
117
|
-
repository: this.repository.fullName,
|
|
118
|
-
protectedBase,
|
|
119
|
-
now,
|
|
120
|
-
}),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
const commitSha = exactSha(ref?.object?.sha, "state ref commit");
|
|
124
|
-
const readback = await this.readCommit(commitSha);
|
|
125
|
-
return { exists: true, ...readback };
|
|
111
|
+
function jsonList(value, label) {
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(value || "[]");
|
|
115
|
+
} catch (cause) {
|
|
116
|
+
throw new Error(`${label} must be a JSON array: ${cause.message}`);
|
|
126
117
|
}
|
|
118
|
+
if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array`);
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const queue = JSON.parse(decodeBlob(blob));
|
|
136
|
-
return { commitSha, queue };
|
|
122
|
+
function jsonObject(value, label) {
|
|
123
|
+
if (!value) return null;
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
throw new Error(`${label} must be a JSON object: ${cause.message}`);
|
|
137
129
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (queue.stateRoot === expectedStateRoot) throw new Error("state transition did not advance the queue root");
|
|
141
|
-
const blob = await this.request("POST", `/repos/${this.repository.fullName}/git/blobs`, {
|
|
142
|
-
content: `${JSON.stringify(queue, null, 2)}\n`,
|
|
143
|
-
encoding: "utf-8",
|
|
144
|
-
});
|
|
145
|
-
const tree = await this.request("POST", `/repos/${this.repository.fullName}/git/trees`, {
|
|
146
|
-
tree: [{ path: STATE_PATH, mode: "100644", type: "blob", sha: blob.sha }],
|
|
147
|
-
});
|
|
148
|
-
const commit = await this.request("POST", `/repos/${this.repository.fullName}/git/commits`, {
|
|
149
|
-
message: `chore(dev-delivery): advance Warrant queue ${receiptRoot.slice(0, 20)}`,
|
|
150
|
-
tree: tree.sha,
|
|
151
|
-
parents: expectedCommitSha ? [expectedCommitSha] : [],
|
|
152
|
-
});
|
|
153
|
-
if (expectedCommitSha) {
|
|
154
|
-
await this.request("PATCH", `/repos/${this.repository.fullName}/git/refs/heads/${encodeRef(stateRef)}`, { sha: commit.sha, force: false });
|
|
155
|
-
} else {
|
|
156
|
-
await this.request("POST", `/repos/${this.repository.fullName}/git/refs`, {
|
|
157
|
-
ref: `refs/heads/${stateRef}`,
|
|
158
|
-
sha: commit.sha,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
const readback = await this.readCommit(commit.sha);
|
|
162
|
-
if (readback.commitSha !== commit.sha || readback.queue.stateRoot !== queue.stateRoot) {
|
|
163
|
-
throw new Error("dev delivery state commit readback mismatch after expected-old update");
|
|
164
|
-
}
|
|
165
|
-
return { commitSha: commit.sha, stateRoot: readback.queue.stateRoot };
|
|
130
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
131
|
+
throw new Error(`${label} must be a JSON object`);
|
|
166
132
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
function warrantIdentity(queue, options) {
|
|
170
|
-
const active = queue.activeWarrant;
|
|
171
|
-
if (!active) throw new Error("no active Delivery Warrant");
|
|
172
|
-
const fencingToken = exactRoot(options.fencingToken, "fencingToken");
|
|
173
|
-
const generation = positiveInteger(options.leaseGeneration, "leaseGeneration");
|
|
174
|
-
return { candidateId: active.candidateId, fencingToken, generation };
|
|
133
|
+
return parsed;
|
|
175
134
|
}
|
|
176
135
|
|
|
177
136
|
function transitionFor(command, queue, options) {
|
|
178
137
|
if (command === "submit") {
|
|
138
|
+
const nativeCommandContract = options.environmentRoot
|
|
139
|
+
? createNativeCommandContract(options.nativeCommand)
|
|
140
|
+
: null;
|
|
141
|
+
if (
|
|
142
|
+
options.nativeCommandRoot &&
|
|
143
|
+
nativeCommandContract?.commandRoot !==
|
|
144
|
+
exactRoot(options.nativeCommandRoot, "nativeCommandRoot")
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"native command contract root does not match native-command",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
179
150
|
return submitDevDeliveryCandidate(
|
|
180
151
|
queue,
|
|
181
152
|
{
|
|
182
|
-
pullRequestNumber: positiveInteger(
|
|
153
|
+
pullRequestNumber: positiveInteger(
|
|
154
|
+
options.pullRequestNumber,
|
|
155
|
+
"pullRequestNumber",
|
|
156
|
+
),
|
|
183
157
|
sourceHead: exactSha(options.sourceHead, "sourceHead"),
|
|
184
158
|
assignmentRoot: exactRoot(options.assignmentRoot, "assignmentRoot"),
|
|
185
159
|
initiativeRoot: exactRoot(options.initiativeRoot, "initiativeRoot"),
|
|
186
|
-
sourceIdentityRoot: exactRoot(
|
|
160
|
+
sourceIdentityRoot: exactRoot(
|
|
161
|
+
options.sourceIdentityRoot,
|
|
162
|
+
"sourceIdentityRoot",
|
|
163
|
+
),
|
|
187
164
|
sourcePatchRoot: exactRoot(options.sourcePatchRoot, "sourcePatchRoot"),
|
|
188
165
|
sourceProofRoot: exactRoot(options.sourceProofRoot, "sourceProofRoot"),
|
|
189
166
|
planRoot: exactRoot(options.planRoot, "planRoot"),
|
|
190
167
|
closureRoot: exactRoot(options.closureRoot, "closureRoot"),
|
|
191
168
|
dependencyRoot: exactRoot(options.dependencyRoot, "dependencyRoot"),
|
|
192
169
|
toolchainRoot: exactRoot(options.toolchainRoot, "toolchainRoot"),
|
|
170
|
+
...(options.environmentRoot
|
|
171
|
+
? {
|
|
172
|
+
environmentRoot: exactRoot(
|
|
173
|
+
options.environmentRoot,
|
|
174
|
+
"environmentRoot",
|
|
175
|
+
),
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
...(nativeCommandContract ? { nativeCommandContract } : {}),
|
|
179
|
+
affectedPaths: jsonList(options.affectedPaths, "affected paths"),
|
|
180
|
+
shardEvidenceRoots: jsonList(
|
|
181
|
+
options.shardEvidenceRoots,
|
|
182
|
+
"shard evidence roots",
|
|
183
|
+
),
|
|
184
|
+
sourceWorkflowRunId: options.sourceWorkflowRunId
|
|
185
|
+
? positiveInteger(options.sourceWorkflowRunId, "sourceWorkflowRunId")
|
|
186
|
+
: 0,
|
|
193
187
|
deliveryClass: options.deliveryClass,
|
|
194
188
|
priority: options.priority || "ordinary",
|
|
189
|
+
...(options.releaseBlockerPriority
|
|
190
|
+
? {
|
|
191
|
+
releaseBlockerPriority: jsonObject(
|
|
192
|
+
options.releaseBlockerPriority,
|
|
193
|
+
"release blocker priority",
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
195
197
|
},
|
|
196
198
|
{ now: options.now },
|
|
197
199
|
);
|
|
@@ -208,7 +210,25 @@ function transitionFor(command, queue, options) {
|
|
|
208
210
|
leaseSeconds: options.leaseSeconds,
|
|
209
211
|
});
|
|
210
212
|
}
|
|
211
|
-
if (command === "
|
|
213
|
+
if (command === "qualify") {
|
|
214
|
+
return qualifyDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
|
|
215
|
+
nativeProof: jsonFile(options.nativeProofPath, "native proof"),
|
|
216
|
+
reuseDecision: jsonFile(
|
|
217
|
+
options.nativeReuseDecisionPath,
|
|
218
|
+
"native reuse decision",
|
|
219
|
+
),
|
|
220
|
+
current: {
|
|
221
|
+
currentBase: options.currentBase,
|
|
222
|
+
graphKnown: options.graphKnown,
|
|
223
|
+
attributionComplete: options.attributionComplete,
|
|
224
|
+
changedPaths: jsonList(options.changedPaths, "changed paths"),
|
|
225
|
+
renames: jsonList(options.renames, "renames"),
|
|
226
|
+
},
|
|
227
|
+
now: options.now,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (command === "recover")
|
|
231
|
+
return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
|
|
212
232
|
if (command === "close") {
|
|
213
233
|
return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
|
|
214
234
|
outcome: options.outcome,
|
|
@@ -221,14 +241,24 @@ function transitionFor(command, queue, options) {
|
|
|
221
241
|
return settleDevDeliveryTerminalEvent(
|
|
222
242
|
queue,
|
|
223
243
|
{
|
|
224
|
-
pullRequestNumber: positiveInteger(
|
|
225
|
-
|
|
244
|
+
pullRequestNumber: positiveInteger(
|
|
245
|
+
options.pullRequestNumber,
|
|
246
|
+
"pullRequestNumber",
|
|
247
|
+
),
|
|
248
|
+
sourceHead: exactSha(
|
|
249
|
+
options.expectedSourceHead || options.sourceHead,
|
|
250
|
+
"sourceHead",
|
|
251
|
+
),
|
|
226
252
|
fencingToken: options.fencingToken,
|
|
227
253
|
leaseGeneration: options.leaseGeneration,
|
|
228
254
|
outcome: options.outcome,
|
|
229
255
|
eventAction: options.eventAction,
|
|
230
256
|
evidenceRoot: options.evidenceRoot,
|
|
231
257
|
reason: options.reason,
|
|
258
|
+
transferRoot: options.transferRoot,
|
|
259
|
+
finalizerBoundaryRoot: options.finalizerBoundaryRoot,
|
|
260
|
+
nativeJobId: options.nativeJobId,
|
|
261
|
+
sealJobId: options.sealJobId,
|
|
232
262
|
},
|
|
233
263
|
{ now: options.now },
|
|
234
264
|
);
|
|
@@ -238,9 +268,18 @@ function transitionFor(command, queue, options) {
|
|
|
238
268
|
queue,
|
|
239
269
|
{
|
|
240
270
|
candidateId: exactRoot(options.candidateId, "candidateId"),
|
|
241
|
-
pullRequestNumber: positiveInteger(
|
|
242
|
-
|
|
243
|
-
|
|
271
|
+
pullRequestNumber: positiveInteger(
|
|
272
|
+
options.pullRequestNumber,
|
|
273
|
+
"pullRequestNumber",
|
|
274
|
+
),
|
|
275
|
+
expectedSourceHead: exactSha(
|
|
276
|
+
options.expectedSourceHead,
|
|
277
|
+
"expectedSourceHead",
|
|
278
|
+
),
|
|
279
|
+
observedSourceHead: exactSha(
|
|
280
|
+
options.observedSourceHead,
|
|
281
|
+
"observedSourceHead",
|
|
282
|
+
),
|
|
244
283
|
eventAction: options.eventAction,
|
|
245
284
|
outcome: options.outcome,
|
|
246
285
|
evidenceRoot: exactRoot(options.evidenceRoot, "evidenceRoot"),
|
|
@@ -254,17 +293,26 @@ function transitionFor(command, queue, options) {
|
|
|
254
293
|
|
|
255
294
|
async function observeQueue(loaded, options) {
|
|
256
295
|
const readMode = text(options.readMode || "v3").toLowerCase();
|
|
257
|
-
if (!["v3", "v4"].includes(readMode))
|
|
296
|
+
if (!["v3", "v4"].includes(readMode))
|
|
297
|
+
throw new Error("readMode must be v3 or v4");
|
|
258
298
|
let observation = observeDevDeliveryQueue(loaded.queue, { now: options.now });
|
|
259
299
|
let readCandidate;
|
|
260
300
|
if (readMode === "v4") {
|
|
261
|
-
const qualification =
|
|
301
|
+
const qualification =
|
|
302
|
+
options.readQualification ||
|
|
303
|
+
JSON.parse(fs.readFileSync(options.readQualificationPath, "utf8"));
|
|
262
304
|
const retain =
|
|
263
305
|
options.retainReadEvidence ||
|
|
264
306
|
(async (evidence) => {
|
|
265
|
-
if (!options.readEvidenceOutput)
|
|
266
|
-
|
|
267
|
-
fs.
|
|
307
|
+
if (!options.readEvidenceOutput)
|
|
308
|
+
throw new Error("readEvidenceOutput is required");
|
|
309
|
+
fs.mkdirSync(path.dirname(options.readEvidenceOutput), {
|
|
310
|
+
recursive: true,
|
|
311
|
+
});
|
|
312
|
+
fs.writeFileSync(
|
|
313
|
+
options.readEvidenceOutput,
|
|
314
|
+
`${JSON.stringify(evidence, null, 2)}\n`,
|
|
315
|
+
);
|
|
268
316
|
return { receiptRoot: evidence.evidenceRoot };
|
|
269
317
|
});
|
|
270
318
|
readCandidate = await runV4DeliveryWarrantReadCandidate(loaded.queue, {
|
|
@@ -305,20 +353,35 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
305
353
|
now: new Date(optionsInput.now || Date.now()).toISOString(),
|
|
306
354
|
execute: bool(optionsInput.execute, false),
|
|
307
355
|
};
|
|
356
|
+
if (
|
|
357
|
+
options.command === "submit" &&
|
|
358
|
+
options.execute &&
|
|
359
|
+
options.deliveryClass !== "non-native-fast"
|
|
360
|
+
) {
|
|
361
|
+
exactRoot(options.environmentRoot, "environmentRoot");
|
|
362
|
+
}
|
|
308
363
|
const store =
|
|
309
364
|
clientInput ||
|
|
310
365
|
new GitHubDevDeliveryStore({
|
|
311
366
|
repository: options.repository,
|
|
312
367
|
token: options.token || process.env.GITHUB_TOKEN,
|
|
313
|
-
apiUrl:
|
|
368
|
+
apiUrl:
|
|
369
|
+
options.apiUrl ||
|
|
370
|
+
process.env.GITHUB_API_URL ||
|
|
371
|
+
"https://api.github.com",
|
|
314
372
|
});
|
|
315
373
|
let loaded = await store.read({
|
|
316
374
|
stateRef: options.stateRef,
|
|
317
375
|
protectedBase: options.branch,
|
|
318
376
|
now: options.now,
|
|
319
377
|
});
|
|
320
|
-
if (
|
|
321
|
-
|
|
378
|
+
if (
|
|
379
|
+
options.expectedOldStateRoot &&
|
|
380
|
+
loaded.queue.stateRoot !== options.expectedOldStateRoot
|
|
381
|
+
) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
|
|
384
|
+
);
|
|
322
385
|
}
|
|
323
386
|
if (options.command === "observe") return observeQueue(loaded, options);
|
|
324
387
|
const initialLoaded = loaded;
|
|
@@ -328,7 +391,9 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
328
391
|
let concurrencyRecovery = null;
|
|
329
392
|
if (options.execute && mutates) {
|
|
330
393
|
if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
|
|
331
|
-
throw new Error(
|
|
394
|
+
throw new Error(
|
|
395
|
+
"transition receipt expected-old root does not match the loaded authority",
|
|
396
|
+
);
|
|
332
397
|
}
|
|
333
398
|
try {
|
|
334
399
|
write = await store.write({
|
|
@@ -339,15 +404,21 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
339
404
|
receiptRoot: changed.receiptRoot,
|
|
340
405
|
});
|
|
341
406
|
} catch (error) {
|
|
342
|
-
if (options.command !== "settle" || options.expectedOldStateRoot)
|
|
407
|
+
if (options.command !== "settle" || options.expectedOldStateRoot)
|
|
408
|
+
throw error;
|
|
343
409
|
const latest = await store.read({
|
|
344
410
|
stateRef: options.stateRef,
|
|
345
411
|
protectedBase: options.branch,
|
|
346
412
|
now: options.now,
|
|
347
413
|
});
|
|
348
414
|
const reconciled = transitionFor(options.command, latest.queue, options);
|
|
349
|
-
const reconciledMutates =
|
|
350
|
-
|
|
415
|
+
const reconciledMutates =
|
|
416
|
+
reconciled.queue.stateRoot !== latest.queue.stateRoot;
|
|
417
|
+
if (
|
|
418
|
+
reconciledMutates ||
|
|
419
|
+
reconciled.receipt.action !== "duplicate-terminal-event-noop"
|
|
420
|
+
)
|
|
421
|
+
throw error;
|
|
351
422
|
loaded = latest;
|
|
352
423
|
changed = reconciled;
|
|
353
424
|
mutates = false;
|
|
@@ -381,83 +452,46 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
381
452
|
};
|
|
382
453
|
}
|
|
383
454
|
|
|
384
|
-
function flag(args, name, fallback = "") {
|
|
385
|
-
const index = args.indexOf(`--${name}`);
|
|
386
|
-
return index === -1 ? fallback : args[index + 1] || "";
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function hasFlag(args, name) {
|
|
390
|
-
return args.includes(`--${name}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
394
|
-
const [command = "", ...rest] = args;
|
|
395
|
-
return {
|
|
396
|
-
command,
|
|
397
|
-
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
398
|
-
branch: flag(rest, "branch", environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF),
|
|
399
|
-
stateRef: flag(rest, "state-ref", environment.BUILDCHAIN_DEV_DELIVERY_STATE_REF),
|
|
400
|
-
expectedOldStateRoot: flag(rest, "expected-old", environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_OLD),
|
|
401
|
-
pullRequestNumber: flag(rest, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
|
|
402
|
-
candidateId: flag(rest, "candidate-id", environment.BUILDCHAIN_DEV_DELIVERY_CANDIDATE_ID),
|
|
403
|
-
sourceHead: flag(rest, "source-head", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD),
|
|
404
|
-
expectedSourceHead: flag(rest, "expected-source-head", environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_SOURCE_HEAD),
|
|
405
|
-
observedSourceHead: flag(rest, "observed-source-head", environment.BUILDCHAIN_DEV_DELIVERY_OBSERVED_SOURCE_HEAD),
|
|
406
|
-
assignmentRoot: flag(rest, "assignment-root", environment.BUILDCHAIN_DEV_DELIVERY_ASSIGNMENT_ROOT),
|
|
407
|
-
initiativeRoot: flag(rest, "initiative-root", environment.BUILDCHAIN_DEV_DELIVERY_INITIATIVE_ROOT),
|
|
408
|
-
sourceIdentityRoot: flag(rest, "source-identity-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT),
|
|
409
|
-
sourcePatchRoot: flag(rest, "source-patch-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT),
|
|
410
|
-
sourceProofRoot: flag(rest, "source-proof-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT),
|
|
411
|
-
planRoot: flag(rest, "plan-root", environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT),
|
|
412
|
-
closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
|
|
413
|
-
dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
|
|
414
|
-
toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
|
|
415
|
-
deliveryClass: flag(rest, "delivery-class", environment.BUILDCHAIN_DEV_DELIVERY_CLASS),
|
|
416
|
-
priority: flag(rest, "priority", environment.BUILDCHAIN_DEV_DELIVERY_PRIORITY || "ordinary"),
|
|
417
|
-
fencingToken: flag(rest, "fencing-token", environment.BUILDCHAIN_DEV_DELIVERY_FENCING_TOKEN),
|
|
418
|
-
leaseGeneration: flag(rest, "lease-generation", environment.BUILDCHAIN_DEV_DELIVERY_LEASE_GENERATION),
|
|
419
|
-
leaseSeconds: flag(rest, "lease-seconds", environment.BUILDCHAIN_DEV_DELIVERY_LEASE_SECONDS),
|
|
420
|
-
outcome: flag(rest, "outcome", environment.BUILDCHAIN_DEV_DELIVERY_OUTCOME),
|
|
421
|
-
eventAction: flag(rest, "event-action", environment.BUILDCHAIN_DEV_DELIVERY_EVENT_ACTION),
|
|
422
|
-
evidenceRoot: flag(rest, "evidence-root", environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT),
|
|
423
|
-
reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
|
|
424
|
-
readMode: flag(rest, "read-mode", environment.BUILDCHAIN_V4_WARRANT_READ_MODE || "v3"),
|
|
425
|
-
readQualificationPath: flag(rest, "read-qualification", environment.BUILDCHAIN_V4_WARRANT_READ_QUALIFICATION),
|
|
426
|
-
readQualificationRoot: flag(rest, "read-qualification-root", environment.BUILDCHAIN_V4_WARRANT_READ_QUALIFICATION_ROOT),
|
|
427
|
-
readTypescriptRevision: flag(rest, "read-typescript-revision", environment.BUILDCHAIN_V4_WARRANT_TYPESCRIPT_REVISION),
|
|
428
|
-
readRustRevision: flag(rest, "read-rust-revision", environment.BUILDCHAIN_V4_WARRANT_RUST_REVISION),
|
|
429
|
-
readValidatorVersion: flag(rest, "read-validator-version", environment.BUILDCHAIN_V4_WARRANT_VALIDATOR_VERSION),
|
|
430
|
-
readTimeoutMs: flag(rest, "read-timeout-ms", environment.BUILDCHAIN_V4_WARRANT_READ_TIMEOUT_MS || "5000"),
|
|
431
|
-
readEvidenceOutput: flag(rest, "read-evidence-output", environment.BUILDCHAIN_V4_WARRANT_READ_EVIDENCE || ".buildchain/dev-delivery/v4-read-evidence.json"),
|
|
432
|
-
now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
|
|
433
|
-
outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT || ".buildchain/dev-delivery/result.json"),
|
|
434
|
-
execute: hasFlag(rest, "execute"),
|
|
435
|
-
json: hasFlag(rest, "json"),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
455
|
function usage() {
|
|
440
|
-
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\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
456
|
+
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\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
441
457
|
}
|
|
442
458
|
|
|
443
459
|
async function main() {
|
|
444
460
|
const args = process.argv.slice(2);
|
|
445
|
-
if (args.length === 0 ||
|
|
461
|
+
if (args.length === 0 || args.includes("--help")) {
|
|
446
462
|
process.stdout.write(usage());
|
|
447
463
|
return;
|
|
448
464
|
}
|
|
449
465
|
const options = devDeliveryCliOptions(args);
|
|
450
|
-
if (
|
|
466
|
+
if (
|
|
467
|
+
![
|
|
468
|
+
"submit",
|
|
469
|
+
"select",
|
|
470
|
+
"heartbeat",
|
|
471
|
+
"qualify",
|
|
472
|
+
"recover",
|
|
473
|
+
"close",
|
|
474
|
+
"settle",
|
|
475
|
+
"cancel-queued",
|
|
476
|
+
"observe",
|
|
477
|
+
].includes(options.command)
|
|
478
|
+
) {
|
|
451
479
|
throw new Error(usage().trim());
|
|
452
480
|
}
|
|
453
481
|
const result = await runDevDeliveryCommand(options);
|
|
454
482
|
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
455
483
|
fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
456
|
-
if (options.json)
|
|
484
|
+
if (options.json)
|
|
485
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
457
486
|
else {
|
|
458
|
-
process.stdout.write(
|
|
459
|
-
|
|
460
|
-
|
|
487
|
+
process.stdout.write(
|
|
488
|
+
`Buildchain dev delivery ${options.command}: ${result.receipt?.reason || result.mode}\n`,
|
|
489
|
+
);
|
|
490
|
+
process.stdout.write(
|
|
491
|
+
`State root: ${result.after?.stateRoot || result.observation.stateRoot}\n`,
|
|
492
|
+
);
|
|
493
|
+
if (result.receiptRoot)
|
|
494
|
+
process.stdout.write(`Receipt root: ${result.receiptRoot}\n`);
|
|
461
495
|
process.stdout.write(`Result: ${options.outputPath}\n`);
|
|
462
496
|
}
|
|
463
497
|
}
|
|
@@ -70,7 +70,7 @@ function normalizeOptions(options = {}) {
|
|
|
70
70
|
allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
|
|
71
71
|
requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
|
|
72
72
|
queueAdmissionContext: String(options.queueAdmissionContext || "").trim(),
|
|
73
|
-
activeLeaseContext: String(options.activeLeaseContext || "").trim(),
|
|
73
|
+
activeLeaseContext: String(options.activeLeaseContext || (choiceOption(options.warrantMode, VALID_WARRANT_MODES, "off", "delivery Warrant mode") === "required" ? "Queue family lease/exact" : "")).trim(),
|
|
74
74
|
requireApproval: boolOption(options.requireApproval, true),
|
|
75
75
|
sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
|
|
76
76
|
maxMerges: intOption(options.maxMerges, 1),
|
|
@@ -52,6 +52,11 @@ function exactWarrantBinding({ result, warrant, candidate, options, pullRequest
|
|
|
52
52
|
requireMatch(Number.isInteger(Number(warrant.generation)) && Number(warrant.generation) >= 1, "delivery-warrant-generation-invalid");
|
|
53
53
|
requireMatch(Number.isFinite(Date.parse(warrant.expiresAt)) && Date.parse(warrant.expiresAt) > Date.now(), "delivery-warrant-expired");
|
|
54
54
|
requireMatch(ROOT_PATTERN.test(String(result.receiptRoot || "")), "delivery-warrant-receipt-root-missing");
|
|
55
|
+
requireMatch((warrant.phase || "qualified") === "qualified", "delivery-warrant-not-qualified");
|
|
56
|
+
if (warrant.phase === "qualified") {
|
|
57
|
+
requireMatch(ROOT_PATTERN.test(String(warrant.nativeProofRoot || "")), "delivery-warrant-native-proof-missing");
|
|
58
|
+
requireMatch(ROOT_PATTERN.test(String(warrant.nativeProofReuseRoot || "")), "delivery-warrant-native-reuse-missing");
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export function createDevPrAdmissionReceipt({ options, pr = {}, state, reason, readiness, decision = {}, queue = null, warrant = null, labels = [], nextAction }) {
|
|
@@ -120,6 +125,8 @@ export function readDeliveryWarrantResult(options, pullRequest) {
|
|
|
120
125
|
pullRequestNumber: warrant.pullRequestNumber,
|
|
121
126
|
sourceHead: warrant.sourceHead,
|
|
122
127
|
sourceProofRoot: warrant.sourceProofRoot,
|
|
128
|
+
nativeProofRoot: warrant.nativeProofRoot || "",
|
|
129
|
+
nativeProofReuseRoot: warrant.nativeProofReuseRoot || "",
|
|
123
130
|
};
|
|
124
131
|
}
|
|
125
132
|
|
|
@@ -162,6 +169,9 @@ export async function verifyCurrentDeliveryWarrant(client, options, pullRequest,
|
|
|
162
169
|
requireMatch(Number(active?.pullRequestNumber) === Number(pullRequest.number), "delivery-warrant-current-pr-mismatch");
|
|
163
170
|
requireMatch(String(active?.sourceHead || "").toLowerCase() === options.expectedHeadSha, "delivery-warrant-current-head-mismatch");
|
|
164
171
|
requireMatch(active?.sourceProofRoot === warrant.sourceProofRoot, "delivery-warrant-current-source-proof-mismatch");
|
|
172
|
+
requireMatch((active?.phase || "qualified") === "qualified", "delivery-warrant-current-not-qualified");
|
|
173
|
+
requireMatch((active?.nativeProofRoot || "") === (warrant.nativeProofRoot || ""), "delivery-warrant-current-native-proof-mismatch");
|
|
174
|
+
requireMatch((active?.nativeProofReuseRoot || "") === (warrant.nativeProofReuseRoot || ""), "delivery-warrant-current-native-reuse-mismatch");
|
|
165
175
|
requireMatch(Number.isFinite(Date.parse(active?.expiresAt)) && Date.parse(active.expiresAt) > Date.now(), "delivery-warrant-current-expired");
|
|
166
176
|
requireMatch(["selected", "proving", "waiting", "blocked", "qualified"].includes(candidate?.status), "delivery-warrant-current-candidate-not-selected");
|
|
167
177
|
requireMatch(candidate?.sourceHead === active.sourceHead, "delivery-warrant-current-candidate-head-mismatch");
|