@kungfu-tech/buildchain 4.0.1-alpha.37 → 4.0.1-alpha.39
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/maintainability-policy.json +4 -4
- package/dist/site/buildchain-contract.json +6 -6
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +61 -11
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +2 -2
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +4 -4
- package/docs/node-api-reference.md +11 -9
- package/package.json +1 -1
- package/packages/core/dev-delivery-warrant-settlement.js +234 -2
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +8 -0
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant.mjs +45 -5
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
observeDevDeliveryQueue,
|
|
11
11
|
qualifyDevDeliveryWarrant,
|
|
12
12
|
recoverExpiredDevDeliveryWarrant,
|
|
13
|
+
reconcileDevDeliveryTerminalEvidence,
|
|
13
14
|
selectDevDeliveryWarrant,
|
|
14
15
|
settleDevDeliveryTerminalEvent,
|
|
15
16
|
submitDevDeliveryCandidate,
|
|
@@ -52,6 +53,43 @@ function exactSha(value, label) {
|
|
|
52
53
|
return normalized;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function terminalSourceHead(options) {
|
|
57
|
+
return exactSha(
|
|
58
|
+
options.expectedSourceHead || options.sourceHead,
|
|
59
|
+
"sourceHead",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reconcileTerminalEvidenceCommand(queue, options) {
|
|
64
|
+
return reconcileDevDeliveryTerminalEvidence(
|
|
65
|
+
queue,
|
|
66
|
+
{
|
|
67
|
+
candidateId: exactRoot(options.candidateId, "candidateId"),
|
|
68
|
+
expectedPriorEvidenceRoot: exactRoot(
|
|
69
|
+
options.expectedPriorEvidenceRoot,
|
|
70
|
+
"expectedPriorEvidenceRoot",
|
|
71
|
+
),
|
|
72
|
+
integrationProof:
|
|
73
|
+
options.integrationProof ||
|
|
74
|
+
jsonFile(options.integrationProofPath, "integration proof"),
|
|
75
|
+
reason: options.reason,
|
|
76
|
+
},
|
|
77
|
+
{ now: options.now },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireTerminalEvidenceCas(options) {
|
|
82
|
+
if (
|
|
83
|
+
options.command === "reconcile-terminal-evidence" &&
|
|
84
|
+
options.execute &&
|
|
85
|
+
!options.expectedOldStateRoot
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"terminal evidence reconciliation execute requires expected-old CAS",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
function normalizeRepository(value) {
|
|
56
94
|
const normalized = text(value);
|
|
57
95
|
const match = normalized.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
@@ -245,10 +283,7 @@ function transitionFor(command, queue, options) {
|
|
|
245
283
|
options.pullRequestNumber,
|
|
246
284
|
"pullRequestNumber",
|
|
247
285
|
),
|
|
248
|
-
sourceHead:
|
|
249
|
-
options.expectedSourceHead || options.sourceHead,
|
|
250
|
-
"sourceHead",
|
|
251
|
-
),
|
|
286
|
+
sourceHead: terminalSourceHead(options),
|
|
252
287
|
fencingToken: options.fencingToken,
|
|
253
288
|
leaseGeneration: options.leaseGeneration,
|
|
254
289
|
outcome: options.outcome,
|
|
@@ -263,6 +298,9 @@ function transitionFor(command, queue, options) {
|
|
|
263
298
|
{ now: options.now },
|
|
264
299
|
);
|
|
265
300
|
}
|
|
301
|
+
if (command === "reconcile-terminal-evidence") {
|
|
302
|
+
return reconcileTerminalEvidenceCommand(queue, options);
|
|
303
|
+
}
|
|
266
304
|
if (command === "cancel-queued") {
|
|
267
305
|
return cancelQueuedDevDeliveryCandidate(
|
|
268
306
|
queue,
|
|
@@ -356,6 +394,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
356
394
|
now: new Date(optionsInput.now || Date.now()).toISOString(),
|
|
357
395
|
execute: bool(optionsInput.execute, false),
|
|
358
396
|
};
|
|
397
|
+
requireTerminalEvidenceCas(options);
|
|
359
398
|
if (
|
|
360
399
|
options.command === "submit" &&
|
|
361
400
|
options.execute &&
|
|
@@ -457,7 +496,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
457
496
|
}
|
|
458
497
|
|
|
459
498
|
function usage() {
|
|
460
|
-
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";
|
|
499
|
+
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|reconcile-terminal-evidence|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";
|
|
461
500
|
}
|
|
462
501
|
|
|
463
502
|
async function main() {
|
|
@@ -476,6 +515,7 @@ async function main() {
|
|
|
476
515
|
"recover",
|
|
477
516
|
"close",
|
|
478
517
|
"settle",
|
|
518
|
+
"reconcile-terminal-evidence",
|
|
479
519
|
"cancel-queued",
|
|
480
520
|
"observe",
|
|
481
521
|
].includes(options.command)
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const GIT_OID = /^[0-9a-f]{40}$/u;
|
|
8
|
+
const CONTENT_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const FAMILY_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
|
|
10
|
+
const FAMILY_MARKER =
|
|
11
|
+
/<!-- kungfu-family-queue-lease:v1 ([A-Za-z0-9_-]+) -->/gu;
|
|
12
|
+
const FAMILY_LEASE_SCHEMA = "kungfu.project-cut.family-queue-lease/v1";
|
|
13
|
+
|
|
14
|
+
export class ProjectCutAdmissionError extends Error {
|
|
15
|
+
constructor(reasonCode, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ProjectCutAdmissionError";
|
|
18
|
+
this.reasonCode = reasonCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stableValue(value) {
|
|
23
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value)
|
|
27
|
+
.filter(([, entry]) => entry !== undefined)
|
|
28
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
29
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function canonicalBytes(value) {
|
|
36
|
+
return Buffer.from(`${JSON.stringify(stableValue(value))}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentRoot(value) {
|
|
40
|
+
return `sha256:${crypto.createHash("sha256").update(canonicalBytes(value)).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exactRoot(value, label) {
|
|
44
|
+
const normalized = String(value || "")
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
if (!CONTENT_ROOT.test(normalized))
|
|
48
|
+
throw new Error(`${label} must be an exact SHA-256 root`);
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function familyId(value, label) {
|
|
53
|
+
const normalized = String(value || "").trim();
|
|
54
|
+
if (!FAMILY_ID.test(normalized)) throw new Error(`${label} is invalid`);
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function git(
|
|
59
|
+
cwd,
|
|
60
|
+
args,
|
|
61
|
+
{
|
|
62
|
+
env = {},
|
|
63
|
+
input = undefined,
|
|
64
|
+
encoding = "utf8",
|
|
65
|
+
reason = "git-failed",
|
|
66
|
+
} = {},
|
|
67
|
+
) {
|
|
68
|
+
const result = spawnSync("git", args, {
|
|
69
|
+
cwd,
|
|
70
|
+
env: { ...process.env, ...env },
|
|
71
|
+
input,
|
|
72
|
+
encoding,
|
|
73
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
77
|
+
throw new ProjectCutAdmissionError(
|
|
78
|
+
reason,
|
|
79
|
+
`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return encoding === null ? result.stdout : String(result.stdout || "").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCommit(cwd, revision, label) {
|
|
86
|
+
const oid = git(cwd, ["rev-parse", "--verify", `${revision}^{commit}`], {
|
|
87
|
+
reason: `${label}-unresolved`,
|
|
88
|
+
});
|
|
89
|
+
if (!GIT_OID.test(oid))
|
|
90
|
+
throw new ProjectCutAdmissionError(
|
|
91
|
+
`${label}-unresolved`,
|
|
92
|
+
`${label} is not an exact commit`,
|
|
93
|
+
);
|
|
94
|
+
return oid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function linearSourceCommits(cwd, fork, head) {
|
|
98
|
+
const commits = git(
|
|
99
|
+
cwd,
|
|
100
|
+
["rev-list", "--reverse", "--topo-order", `${fork}..${head}`],
|
|
101
|
+
{
|
|
102
|
+
reason: "source-history-unreadable",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
.split(/\s+/u)
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (commits.length === 0)
|
|
108
|
+
throw new ProjectCutAdmissionError(
|
|
109
|
+
"empty-source-range",
|
|
110
|
+
"source range is empty",
|
|
111
|
+
);
|
|
112
|
+
let expectedParent = fork;
|
|
113
|
+
for (const commit of commits) {
|
|
114
|
+
const [observed, ...parents] = git(
|
|
115
|
+
cwd,
|
|
116
|
+
["rev-list", "--parents", "-n", "1", commit],
|
|
117
|
+
{
|
|
118
|
+
reason: "source-history-unreadable",
|
|
119
|
+
},
|
|
120
|
+
).split(/\s+/u);
|
|
121
|
+
if (
|
|
122
|
+
observed !== commit ||
|
|
123
|
+
parents.length !== 1 ||
|
|
124
|
+
parents[0] !== expectedParent
|
|
125
|
+
) {
|
|
126
|
+
throw new ProjectCutAdmissionError(
|
|
127
|
+
"nonlinear-source-range",
|
|
128
|
+
"source range is not one immutable linear patch series",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
expectedParent = commit;
|
|
132
|
+
}
|
|
133
|
+
return commits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function patchId(cwd, patch) {
|
|
137
|
+
const output = git(cwd, ["patch-id", "--stable"], {
|
|
138
|
+
input: patch,
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
reason: "patch-id-failed",
|
|
141
|
+
});
|
|
142
|
+
const id = output.split(/\s+/u)[0] || "";
|
|
143
|
+
if (!GIT_OID.test(id))
|
|
144
|
+
throw new ProjectCutAdmissionError(
|
|
145
|
+
"patch-id-failed",
|
|
146
|
+
"patch has no stable identity",
|
|
147
|
+
);
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function virtualCommitOid(cwd, tree, base, head) {
|
|
152
|
+
const body = [
|
|
153
|
+
`tree ${tree}`,
|
|
154
|
+
`parent ${base}`,
|
|
155
|
+
"author Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
156
|
+
"committer Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
157
|
+
"",
|
|
158
|
+
`Replay ${head} onto ${base}`,
|
|
159
|
+
"",
|
|
160
|
+
].join("\n");
|
|
161
|
+
return git(cwd, ["hash-object", "-t", "commit", "--stdin"], {
|
|
162
|
+
input: body,
|
|
163
|
+
reason: "candidate-commit-hash-failed",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function replayProjectCut(cwd, base, head) {
|
|
168
|
+
const fork = git(cwd, ["merge-base", base, head], {
|
|
169
|
+
reason: "no-common-fork",
|
|
170
|
+
});
|
|
171
|
+
if (!GIT_OID.test(fork))
|
|
172
|
+
throw new ProjectCutAdmissionError(
|
|
173
|
+
"no-common-fork",
|
|
174
|
+
"base and head have no exact fork",
|
|
175
|
+
);
|
|
176
|
+
const commits = linearSourceCommits(cwd, fork, head);
|
|
177
|
+
const temporary = fs.mkdtempSync(
|
|
178
|
+
path.join(os.tmpdir(), "buildchain-project-cut-"),
|
|
179
|
+
);
|
|
180
|
+
const indexPath = path.join(temporary, "index");
|
|
181
|
+
const indexEnvironment = { GIT_INDEX_FILE: indexPath };
|
|
182
|
+
try {
|
|
183
|
+
git(cwd, ["read-tree", base], {
|
|
184
|
+
env: indexEnvironment,
|
|
185
|
+
reason: "base-tree-unreadable",
|
|
186
|
+
});
|
|
187
|
+
const sourcePatch = git(
|
|
188
|
+
cwd,
|
|
189
|
+
["diff", "--binary", "--full-index", "--no-ext-diff", fork, head],
|
|
190
|
+
{
|
|
191
|
+
encoding: null,
|
|
192
|
+
reason: "source-patch-unreadable",
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
const emptySourcePatch = sourcePatch.length === 0;
|
|
196
|
+
if (emptySourcePatch && commits.length !== 1)
|
|
197
|
+
throw new ProjectCutAdmissionError("empty-source-composition", "an empty source composition must be exactly one linear commit");
|
|
198
|
+
if (!emptySourcePatch) git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
|
|
199
|
+
env: indexEnvironment, input: sourcePatch, encoding: "utf8", reason: "project-cut-conflict",
|
|
200
|
+
});
|
|
201
|
+
const candidateTreeOid = git(cwd, ["write-tree"], {
|
|
202
|
+
env: indexEnvironment,
|
|
203
|
+
reason: "candidate-tree-unreadable",
|
|
204
|
+
});
|
|
205
|
+
const replayPatch = git(
|
|
206
|
+
cwd,
|
|
207
|
+
["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", base],
|
|
208
|
+
{
|
|
209
|
+
env: indexEnvironment,
|
|
210
|
+
encoding: null,
|
|
211
|
+
reason: "replay-patch-unreadable",
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
const sourceNames = git(
|
|
215
|
+
cwd,
|
|
216
|
+
["diff", "--name-status", "--no-renames", fork, head],
|
|
217
|
+
{
|
|
218
|
+
reason: "source-paths-unreadable",
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
const replayNames = git(
|
|
222
|
+
cwd,
|
|
223
|
+
["diff", "--cached", "--name-status", "--no-renames", base],
|
|
224
|
+
{
|
|
225
|
+
env: indexEnvironment,
|
|
226
|
+
reason: "replay-paths-unreadable",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
const compositionDrifted = emptySourcePatch ? replayPatch.length !== 0 ||
|
|
230
|
+
sourceNames !== "" || replayNames !== "" : patchId(cwd, sourcePatch) !==
|
|
231
|
+
patchId(cwd, replayPatch) || sourceNames !== replayNames;
|
|
232
|
+
if (compositionDrifted) {
|
|
233
|
+
throw new ProjectCutAdmissionError(
|
|
234
|
+
"composition-drift",
|
|
235
|
+
"latest-base replay changes the immutable source composition",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
baseCommitOid: base,
|
|
240
|
+
headCommitOid: head,
|
|
241
|
+
forkCommitOid: fork,
|
|
242
|
+
candidateCommitOid: virtualCommitOid(cwd, candidateTreeOid, base, head),
|
|
243
|
+
candidateTreeOid,
|
|
244
|
+
replayedCommitCount: commits.length,
|
|
245
|
+
};
|
|
246
|
+
} finally {
|
|
247
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function encodeMarker(payload) {
|
|
252
|
+
return canonicalBytes(payload)
|
|
253
|
+
.toString("base64")
|
|
254
|
+
.replace(/\+/gu, "-")
|
|
255
|
+
.replace(/\//gu, "_")
|
|
256
|
+
.replace(/=+$/gu, "");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function decodeMarker(token) {
|
|
260
|
+
const padded = `${token}${"=".repeat((4 - (token.length % 4)) % 4)}`
|
|
261
|
+
.replace(/-/gu, "+")
|
|
262
|
+
.replace(/_/gu, "/");
|
|
263
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function familyLease(input, replay) {
|
|
267
|
+
const binding = {
|
|
268
|
+
schema: FAMILY_LEASE_SCHEMA,
|
|
269
|
+
state: "active",
|
|
270
|
+
initiativeId: familyId(input.initiativeId, "initiativeId"),
|
|
271
|
+
assignmentId: familyId(input.assignmentId, "assignmentId"),
|
|
272
|
+
deliveryClass: familyId(input.deliveryClass, "deliveryClass"),
|
|
273
|
+
admissionProofRoot: exactRoot(
|
|
274
|
+
input.admissionProofRoot,
|
|
275
|
+
"admissionProofRoot",
|
|
276
|
+
),
|
|
277
|
+
queueAttempt: String(input.queueAttempt || "").trim(),
|
|
278
|
+
pullRequestHead: replay.headCommitOid,
|
|
279
|
+
baseCommitOid: replay.baseCommitOid,
|
|
280
|
+
forkCommitOid: replay.forkCommitOid,
|
|
281
|
+
candidateTreeOid: replay.candidateTreeOid,
|
|
282
|
+
};
|
|
283
|
+
const expectedAttempt = `${replay.headCommitOid}@${replay.baseCommitOid}`;
|
|
284
|
+
if (binding.queueAttempt !== expectedAttempt) {
|
|
285
|
+
throw new ProjectCutAdmissionError(
|
|
286
|
+
"queue-attempt-drift",
|
|
287
|
+
`queueAttempt must equal ${expectedAttempt}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const bindingRoot = contentRoot(binding);
|
|
291
|
+
const statusContext = `Queue family lease/${bindingRoot.slice(7, 23)}`;
|
|
292
|
+
const payload = { ...binding, bindingRoot, statusContext };
|
|
293
|
+
const leaseRoot = contentRoot(payload);
|
|
294
|
+
const sealed = { ...payload, leaseRoot };
|
|
295
|
+
return {
|
|
296
|
+
marker: `<!-- kungfu-family-queue-lease:v1 ${encodeMarker(sealed)} -->`,
|
|
297
|
+
leaseRoot,
|
|
298
|
+
statusContext,
|
|
299
|
+
pullRequestHead: replay.headCommitOid,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function parseFamilyQueueLeaseMarker(text) {
|
|
304
|
+
const matches = [...String(text || "").matchAll(FAMILY_MARKER)];
|
|
305
|
+
if (matches.length !== 1) return null;
|
|
306
|
+
try {
|
|
307
|
+
const payload = decodeMarker(matches[0][1]);
|
|
308
|
+
const { leaseRoot, bindingRoot, statusContext, ...binding } = payload;
|
|
309
|
+
if (payload.schema !== FAMILY_LEASE_SCHEMA || payload.state !== "active")
|
|
310
|
+
return null;
|
|
311
|
+
if (contentRoot(binding) !== bindingRoot) return null;
|
|
312
|
+
if (statusContext !== `Queue family lease/${bindingRoot.slice(7, 23)}`)
|
|
313
|
+
return null;
|
|
314
|
+
if (contentRoot({ ...binding, bindingRoot, statusContext }) !== leaseRoot)
|
|
315
|
+
return null;
|
|
316
|
+
if (encodeMarker(payload) !== matches[0][1]) return null;
|
|
317
|
+
exactRoot(leaseRoot, "leaseRoot");
|
|
318
|
+
exactRoot(payload.admissionProofRoot, "admissionProofRoot");
|
|
319
|
+
for (const field of [
|
|
320
|
+
"pullRequestHead",
|
|
321
|
+
"baseCommitOid",
|
|
322
|
+
"forkCommitOid",
|
|
323
|
+
"candidateTreeOid",
|
|
324
|
+
]) {
|
|
325
|
+
if (!GIT_OID.test(String(payload[field] || ""))) return null;
|
|
326
|
+
}
|
|
327
|
+
return payload;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function releaseFamilyQueueLease(markerText, input = {}) {
|
|
334
|
+
const lease = parseFamilyQueueLeaseMarker(markerText);
|
|
335
|
+
if (!lease) throw new Error("family queue lease marker is invalid");
|
|
336
|
+
const expectedHead = String(input.expectedPullRequestHead || "")
|
|
337
|
+
.trim()
|
|
338
|
+
.toLowerCase();
|
|
339
|
+
if (!GIT_OID.test(expectedHead) || expectedHead !== lease.pullRequestHead) {
|
|
340
|
+
throw new Error("family queue lease pull request head drifted");
|
|
341
|
+
}
|
|
342
|
+
const terminalReason = String(input.terminalReason || "").trim();
|
|
343
|
+
if (!["controller-failed", "merged"].includes(terminalReason))
|
|
344
|
+
throw new Error("terminalReason is invalid");
|
|
345
|
+
const evidenceRoot = input.evidenceRoot
|
|
346
|
+
? exactRoot(input.evidenceRoot, "evidenceRoot")
|
|
347
|
+
: null;
|
|
348
|
+
if (terminalReason === "merged" && !evidenceRoot)
|
|
349
|
+
throw new Error("merged family lease requires evidenceRoot");
|
|
350
|
+
const receipt = {
|
|
351
|
+
schema: "project.cut.family-queue-release/v1",
|
|
352
|
+
state: "released",
|
|
353
|
+
leaseRoot: lease.leaseRoot,
|
|
354
|
+
pullRequestHead: lease.pullRequestHead,
|
|
355
|
+
terminalReason,
|
|
356
|
+
evidenceRoot,
|
|
357
|
+
};
|
|
358
|
+
return { ...receipt, releaseRoot: contentRoot(receipt) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function qualifyProjectCut(input = {}) {
|
|
362
|
+
const cwd = path.resolve(input.cwd || process.cwd());
|
|
363
|
+
const base = resolveCommit(cwd, input.base, "base");
|
|
364
|
+
const head = resolveCommit(cwd, input.head, "head");
|
|
365
|
+
const replay = replayProjectCut(cwd, base, head);
|
|
366
|
+
const familyInputs = [
|
|
367
|
+
input.initiativeId,
|
|
368
|
+
input.assignmentId,
|
|
369
|
+
input.deliveryClass,
|
|
370
|
+
input.queueAttempt,
|
|
371
|
+
input.admissionProofRoot,
|
|
372
|
+
];
|
|
373
|
+
const familyCount = familyInputs.filter((value) =>
|
|
374
|
+
String(value || "").trim(),
|
|
375
|
+
).length;
|
|
376
|
+
if (familyCount !== 0 && familyCount !== familyInputs.length) {
|
|
377
|
+
throw new ProjectCutAdmissionError(
|
|
378
|
+
"partial-family-binding",
|
|
379
|
+
"family admission inputs must be all present or all absent",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
schema: "project.cut.merge-queue-admission/v1",
|
|
384
|
+
ok: true,
|
|
385
|
+
decision: "qualified",
|
|
386
|
+
retryable: false,
|
|
387
|
+
compositionChanged: false,
|
|
388
|
+
reasonCodes: [],
|
|
389
|
+
...replay,
|
|
390
|
+
...(familyCount === familyInputs.length
|
|
391
|
+
? { familyLease: familyLease(input, replay) }
|
|
392
|
+
: {}),
|
|
393
|
+
};
|
|
394
|
+
}
|