@intentius/chant 0.124.41 → 0.124.43
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/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +3 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +27 -3
- package/dist/cli/handlers/pr.d.ts +3 -1
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +113 -2
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +8 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +19 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +9 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +17 -1
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- package/dist/components/pr-pipeline.d.ts +20 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +54 -16
- package/dist/components/pr-run.d.ts +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +3 -0
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- package/dist/op/activities/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- package/dist/op/activities/review-agent.js +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/op-waves-run.d.ts +3 -79
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +4 -133
- package/dist/op/stage-hooks.d.ts +152 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +199 -0
- package/dist/pr-forge.d.ts +56 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +320 -45
- package/dist/pr-loop.d.ts +18 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +7 -0
- package/dist/pr-requires.d.ts +47 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +53 -0
- package/dist/wave-runtime.mjs +213 -213
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +2 -1
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +6 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +2 -2
- package/dist/workspace/pr-report.schema.json +23 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/build.ts +3 -0
- package/src/cli/handlers/fan-out.ts +27 -3
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr.ts +110 -2
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +8 -2
- package/src/cli/parse-args.ts +11 -0
- package/src/cli/registry.ts +19 -0
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +19 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-pipeline.ts +64 -16
- package/src/components/pr-run.ts +9 -0
- package/src/components/wave-plan.ts +38 -3
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.ts +1 -1
- package/src/op/index.ts +4 -0
- package/src/op/op-waves-run.ts +24 -228
- package/src/op/stage-hooks-async.test.ts +79 -0
- package/src/op/stage-hooks.ts +346 -0
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.ts +334 -36
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.ts +25 -1
- package/src/pr-requires.test.ts +248 -0
- package/src/pr-requires.ts +93 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-run.ts +2 -1
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +2 -2
- package/src/workspace/declaration.ts +6 -3
- package/src/workspace/pr-report.schema.json +23 -3
- package/src/workspace/reason-codes.ts +1 -1
package/dist/pr-forge.js
CHANGED
|
@@ -70,6 +70,52 @@ function refusedMerge(err) {
|
|
|
70
70
|
}
|
|
71
71
|
throw err;
|
|
72
72
|
}
|
|
73
|
+
/** Why `login`'s approval does not count under `filter`'s `exclude`, or undefined. */
|
|
74
|
+
function excludedWhy(filter, login) {
|
|
75
|
+
for (const e of filter?.exclude ?? []) {
|
|
76
|
+
const who = typeof e === "string" ? e : e.login;
|
|
77
|
+
if (who.toLowerCase() === login.toLowerCase())
|
|
78
|
+
return typeof e === "string" ? `${login} is excluded` : e.reason;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The approvers who count, and why each other one did not: an author, then
|
|
84
|
+
* `exclude`, then the forge's rule (`rule` gives a reason or undefined).
|
|
85
|
+
*/
|
|
86
|
+
async function countApprovers(approved, authors, filter, rule) {
|
|
87
|
+
const by = [];
|
|
88
|
+
const not = [];
|
|
89
|
+
for (const login of approved) {
|
|
90
|
+
const why = authors.has(login) ? `${login} is one of its authors` : (excludedWhy(filter, login) ?? (filter?.forgeRule ? await rule(login) : undefined));
|
|
91
|
+
if (why)
|
|
92
|
+
not.push(why);
|
|
93
|
+
else
|
|
94
|
+
by.push(login);
|
|
95
|
+
}
|
|
96
|
+
return { by, not };
|
|
97
|
+
}
|
|
98
|
+
/** A refusal for no approver counted, naming each one left out. */
|
|
99
|
+
const noApprover = (line, not) => refuse(not.length > 0 ? `${line}: ${not.join("; ")}` : line);
|
|
100
|
+
/** GitLab's access level for Developer, the least that may push. */
|
|
101
|
+
const GITLAB_DEVELOPER = 30;
|
|
102
|
+
/** GitHub's permissions that may approve under {@link PrApproverFilter.forgeRule}. */
|
|
103
|
+
const GITHUB_WRITERS = new Set(["admin", "maintain", "write"]);
|
|
104
|
+
const short = (sha) => (sha ?? "nothing").slice(0, 8);
|
|
105
|
+
const pass = (reason) => ({ passed: true, reason });
|
|
106
|
+
const refuse = (reason, pending = false) => ({ passed: false, ...(pending ? { pending: true } : {}), reason });
|
|
107
|
+
const moved = (what, head, now) => refuse(`${what}'s head is ${short(now)}, not ${short(head)}, the commit this job checked out`);
|
|
108
|
+
/** The verdict over statuses and checks: failing and still-running names, and how many reported. */
|
|
109
|
+
function checksVerdict(what, head, failing, waiting, count) {
|
|
110
|
+
const names = (l) => [...new Set(l)].sort().join(", ");
|
|
111
|
+
if (failing.length > 0)
|
|
112
|
+
return refuse(`${names(failing)} failed on ${short(head)}`);
|
|
113
|
+
if (waiting.length > 0)
|
|
114
|
+
return refuse(`${names(waiting)} still running on ${short(head)}`, true);
|
|
115
|
+
if (count === 0)
|
|
116
|
+
return refuse(`no check has reported on ${what}'s head ${short(head)} yet`, true);
|
|
117
|
+
return pass(`${count} check${count === 1 ? "" : "s"} passed on ${short(head)}`);
|
|
118
|
+
}
|
|
73
119
|
/** The newest of some ISO 8601 times, ignoring what does not parse. */
|
|
74
120
|
function newestTime(times) {
|
|
75
121
|
return times.filter((t) => typeof t === "string" && !Number.isNaN(Date.parse(t))).sort((a, b) => Date.parse(b) - Date.parse(a))[0];
|
|
@@ -213,6 +259,10 @@ function threadCalls(t) {
|
|
|
213
259
|
upsertIssue: async (marker, title, body) => (await trackIssueWith(t, marker, { title, body }, {}, false)).url,
|
|
214
260
|
};
|
|
215
261
|
}
|
|
262
|
+
/** GitLab's merge statuses while it works out whether a merge request can merge (#4301). */
|
|
263
|
+
const GITLAB_CHECKING = new Set(["checking", "unchecked", "preparing", "approvals_syncing", "cannot_be_merged_recheck"]);
|
|
264
|
+
/** GitLab's commit statuses still running. */
|
|
265
|
+
const GITLAB_RUNNING = new Set(["created", "waiting_for_resource", "preparing", "pending", "running", "scheduled"]);
|
|
216
266
|
/** GitHub keeps a status description to 140 characters; Forgejo follows. */
|
|
217
267
|
const GITHUB_DESCRIPTION = 140;
|
|
218
268
|
/** GitLab keeps a status description to 255 characters. */
|
|
@@ -282,6 +332,38 @@ function githubShaped(kind, opts) {
|
|
|
282
332
|
...(url ? { target_url: url } : {}),
|
|
283
333
|
});
|
|
284
334
|
};
|
|
335
|
+
const authorsOf = async (pr) => {
|
|
336
|
+
const logins = new Set();
|
|
337
|
+
const add = (v) => {
|
|
338
|
+
if (isObject(v) && typeof v.login === "string" && v.login !== "")
|
|
339
|
+
logins.add(v.login);
|
|
340
|
+
};
|
|
341
|
+
const opened = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
342
|
+
if (isObject(opened))
|
|
343
|
+
add(opened.user);
|
|
344
|
+
for (const c of await api.list(`${repo}/pulls/${pr}/commits`, pageParams, pageSize)) {
|
|
345
|
+
if (!isObject(c))
|
|
346
|
+
continue;
|
|
347
|
+
add(c.author);
|
|
348
|
+
add(c.committer);
|
|
349
|
+
}
|
|
350
|
+
return [...logins].sort();
|
|
351
|
+
};
|
|
352
|
+
const branchTip = async (branch) => {
|
|
353
|
+
let b;
|
|
354
|
+
try {
|
|
355
|
+
b = await api.call("GET", `${repo}/branches/${branch.split("/").map(encodeURIComponent).join("/")}`);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
if (err instanceof ForgeApiError && err.status === 404)
|
|
359
|
+
return undefined;
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
362
|
+
// GitHub names the commit `sha`, Forgejo `id`.
|
|
363
|
+
const commit = isObject(b) && isObject(b.commit) ? b.commit : undefined;
|
|
364
|
+
return commit ? str(commit.sha) ?? str(commit.id) : undefined;
|
|
365
|
+
};
|
|
366
|
+
const headOf = (v) => (isObject(v) && isObject(v.head) ? str(v.head.sha) : undefined);
|
|
285
367
|
return {
|
|
286
368
|
kind,
|
|
287
369
|
host,
|
|
@@ -311,22 +393,123 @@ function githubShaped(kind, opts) {
|
|
|
311
393
|
}
|
|
312
394
|
return [...standing].filter(([, state]) => state === "APPROVED").map(([login]) => login).sort();
|
|
313
395
|
},
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
396
|
+
authors: authorsOf,
|
|
397
|
+
async prApproved(pr, head, approvers) {
|
|
398
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
399
|
+
const at = headOf(got);
|
|
400
|
+
if (at !== head)
|
|
401
|
+
return moved(`pull request ${pr}`, head, at);
|
|
402
|
+
// A reviewer's newest deciding review stands, as in approvers; an approval counts only of this head.
|
|
403
|
+
const standing = new Map();
|
|
404
|
+
for (const r of await api.list(`${repo}/pulls/${pr}/reviews`, pageParams, pageSize)) {
|
|
405
|
+
const login = loginOf(r);
|
|
406
|
+
if (!login || !isObject(r) || typeof r.state !== "string")
|
|
407
|
+
continue;
|
|
408
|
+
const state = r.dismissed === true ? "DISMISSED" : r.state.toUpperCase();
|
|
409
|
+
if (state === "COMMENTED" || state === "COMMENT" || state === "PENDING")
|
|
410
|
+
continue;
|
|
411
|
+
const commit = str(r.commit_id);
|
|
412
|
+
standing.set(login, { state, ...(commit ? { commit } : {}), ...(typeof r.official === "boolean" ? { official: r.official } : {}) });
|
|
413
|
+
}
|
|
414
|
+
const changes = [...standing].filter(([, r]) => r.state === "CHANGES_REQUESTED" || r.state === "REQUEST_CHANGES").map(([l]) => l).sort();
|
|
415
|
+
if (changes.length > 0)
|
|
416
|
+
return refuse(`${changes.join(", ")} asked for changes`);
|
|
417
|
+
const authors = new Set(await authorsOf(pr));
|
|
418
|
+
const approved = [...standing].filter(([, r]) => r.state === "APPROVED" && r.commit === head).map(([l]) => l).sort();
|
|
419
|
+
const rule = async (login) => {
|
|
420
|
+
// Forgejo marks a review official when its author may write; terragucci read a missing flag as official too.
|
|
421
|
+
if (kind === "forgejo")
|
|
422
|
+
return standing.get(login)?.official === false ? `${login}'s review is not official` : undefined;
|
|
423
|
+
let got;
|
|
424
|
+
try {
|
|
425
|
+
got = await api.call("GET", `${repo}/collaborators/${encodeURIComponent(login)}/permission`);
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
return `could not read ${login}'s access (${err instanceof Error ? err.message : String(err)})`;
|
|
429
|
+
}
|
|
430
|
+
const p = isObject(got) ? [str(got.permission), str(got.role_name)] : [];
|
|
431
|
+
return p.some((x) => x !== undefined && GITHUB_WRITERS.has(x)) ? undefined : `${login} has no write access`;
|
|
319
432
|
};
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
433
|
+
const { by, not } = await countApprovers(approved, authors, approvers, rule);
|
|
434
|
+
if (by.length === 0)
|
|
435
|
+
return noApprover(`no reviewer other than its authors approved its head ${short(head)}`, not);
|
|
436
|
+
return pass(`approved at ${short(head)} by ${by.join(", ")}`);
|
|
437
|
+
},
|
|
438
|
+
async prMergeable(pr, head) {
|
|
439
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
440
|
+
const at = headOf(got);
|
|
441
|
+
if (at !== head)
|
|
442
|
+
return moved(`pull request ${pr}`, head, at);
|
|
443
|
+
const m = isObject(got) ? got : {};
|
|
444
|
+
const into = isObject(m.base) ? str(m.base.ref) : undefined;
|
|
445
|
+
// Forgejo answers mergeable false while it still checks for conflicts, and names no state that tells the two apart, so its false is asked again.
|
|
446
|
+
if (kind === "forgejo" && m.draft === true)
|
|
447
|
+
return refuse("it is a draft");
|
|
448
|
+
if (kind === "forgejo" && m.mergeable === false)
|
|
449
|
+
return refuse(`Forgejo reports conflicts with ${into ?? "its base"}, or is still checking for them`, true);
|
|
450
|
+
if (m.mergeable === false || m.mergeable_state === "dirty")
|
|
451
|
+
return refuse(`the forge reports conflicts with ${into ?? "its base"}`);
|
|
452
|
+
if (m.mergeable !== true)
|
|
453
|
+
return refuse("the forge has not worked out yet whether it can merge", true);
|
|
454
|
+
if (kind === "github" && m.mergeable_state === "blocked")
|
|
455
|
+
return refuse(`branch protection on ${into ?? "its base"} blocks its merge`);
|
|
456
|
+
return pass(`it merges into ${into ?? "its base"} as it stands`);
|
|
457
|
+
},
|
|
458
|
+
async prUndiverged(pr, head) {
|
|
459
|
+
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
460
|
+
const at = headOf(got);
|
|
461
|
+
if (at !== head)
|
|
462
|
+
return moved(`pull request ${pr}`, head, at);
|
|
463
|
+
const into = isObject(got) && isObject(got.base) ? str(got.base.ref) : undefined;
|
|
464
|
+
if (!into)
|
|
465
|
+
return refuse(`the forge names no branch pull request ${pr} merges into`);
|
|
466
|
+
if (kind === "github") {
|
|
467
|
+
// behind_by: the commits on the base branch that the head does not contain.
|
|
468
|
+
const cmp = await api.call("GET", `${repo}/compare/${encodeURIComponent(into)}...${head}`);
|
|
469
|
+
const behind = isObject(cmp) ? num(cmp.behind_by) : undefined;
|
|
470
|
+
if (behind === undefined)
|
|
471
|
+
return refuse(`the forge did not say how far ${short(head)} is behind ${into}`);
|
|
472
|
+
return behind === 0 ? pass(`${short(head)} contains ${into}`) : refuse(`${short(head)} is ${behind} commit${behind === 1 ? "" : "s"} behind ${into}`);
|
|
473
|
+
}
|
|
474
|
+
// Forgejo: the pull request's merge base is the base branch's tip exactly when the head contains it.
|
|
475
|
+
const tip = await branchTip(into);
|
|
476
|
+
const base = isObject(got) ? str(got.merge_base) : undefined;
|
|
477
|
+
if (!tip || !base)
|
|
478
|
+
return refuse(`the forge did not say where ${into} is or where ${short(head)} left it`);
|
|
479
|
+
return base === tip ? pass(`${short(head)} contains ${into} at ${short(tip)}`) : refuse(`${short(head)} does not contain ${into} at ${short(tip)}`);
|
|
480
|
+
},
|
|
481
|
+
async prChecksPassed(_pr, head, ignore = []) {
|
|
482
|
+
const skip = new Set(ignore);
|
|
483
|
+
const failing = [];
|
|
484
|
+
const waiting = [];
|
|
485
|
+
let count = 0;
|
|
486
|
+
// The combined status holds each context's newest status.
|
|
487
|
+
const combined = await api.call("GET", `${repo}/commits/${head}/status`);
|
|
488
|
+
for (const st of isObject(combined) && Array.isArray(combined.statuses) ? combined.statuses : []) {
|
|
489
|
+
const context = isObject(st) ? str(st.context) : undefined;
|
|
490
|
+
if (!context || skip.has(context))
|
|
325
491
|
continue;
|
|
326
|
-
|
|
327
|
-
|
|
492
|
+
const state = str(st.state) ?? str(st.status) ?? "";
|
|
493
|
+
count++;
|
|
494
|
+
if (state === "failure" || state === "error" || state === "failed")
|
|
495
|
+
failing.push(context);
|
|
496
|
+
else if (state === "pending" || state === "running" || state === "")
|
|
497
|
+
waiting.push(context);
|
|
328
498
|
}
|
|
329
|
-
|
|
499
|
+
if (kind === "github") {
|
|
500
|
+
const runs = await api.call("GET", `${repo}/commits/${head}/check-runs?per_page=100`);
|
|
501
|
+
for (const run of isObject(runs) && Array.isArray(runs.check_runs) ? runs.check_runs : []) {
|
|
502
|
+
const name = (isObject(run) ? str(run.name) : undefined) ?? "a check";
|
|
503
|
+
if (skip.has(name) || !isObject(run))
|
|
504
|
+
continue;
|
|
505
|
+
count++;
|
|
506
|
+
if (run.status !== "completed")
|
|
507
|
+
waiting.push(name);
|
|
508
|
+
else if (!["success", "neutral", "skipped"].includes(String(run.conclusion)))
|
|
509
|
+
failing.push(name);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return checksVerdict(`pull request ${_pr}`, head, failing, waiting, count);
|
|
330
513
|
},
|
|
331
514
|
async pullRequestOpen(pr) {
|
|
332
515
|
try {
|
|
@@ -422,20 +605,7 @@ function githubShaped(kind, opts) {
|
|
|
422
605
|
}
|
|
423
606
|
return out.sort((a, b) => a.number - b.number);
|
|
424
607
|
},
|
|
425
|
-
|
|
426
|
-
let b;
|
|
427
|
-
try {
|
|
428
|
-
b = await api.call("GET", `${repo}/branches/${branch.split("/").map(encodeURIComponent).join("/")}`);
|
|
429
|
-
}
|
|
430
|
-
catch (err) {
|
|
431
|
-
if (err instanceof ForgeApiError && err.status === 404)
|
|
432
|
-
return undefined;
|
|
433
|
-
throw err;
|
|
434
|
-
}
|
|
435
|
-
// GitHub names the commit `sha`, Forgejo `id`.
|
|
436
|
-
const commit = isObject(b) && isObject(b.commit) ? b.commit : undefined;
|
|
437
|
-
return commit ? str(commit.sha) ?? str(commit.id) : undefined;
|
|
438
|
-
},
|
|
608
|
+
branchHead: branchTip,
|
|
439
609
|
async scheduledRuns(q) {
|
|
440
610
|
const file = encodeURIComponent(q.workflow);
|
|
441
611
|
if (kind === "github") {
|
|
@@ -594,6 +764,28 @@ export function gitlabForge(opts) {
|
|
|
594
764
|
await api.call("PUT", `${project}/issues/${n}`, { state_event: "close" });
|
|
595
765
|
},
|
|
596
766
|
};
|
|
767
|
+
const authorsOf = async (mr) => {
|
|
768
|
+
const logins = new Set();
|
|
769
|
+
const merge = await api.call("GET", `${notes}/${mr}`);
|
|
770
|
+
const author = isObject(merge) ? authorOf(merge) : undefined;
|
|
771
|
+
if (author)
|
|
772
|
+
logins.add(author);
|
|
773
|
+
const emails = new Set();
|
|
774
|
+
for (const c of await api.list(`${notes}/${mr}/commits`, pageParams, 100)) {
|
|
775
|
+
if (!isObject(c))
|
|
776
|
+
continue;
|
|
777
|
+
for (const email of [str(c.author_email), str(c.committer_email)])
|
|
778
|
+
if (email)
|
|
779
|
+
emails.add(email.toLowerCase());
|
|
780
|
+
}
|
|
781
|
+
for (const email of [...emails].sort()) {
|
|
782
|
+
const users = await api.call("GET", `/users?search=${encodeURIComponent(email)}`);
|
|
783
|
+
for (const u of Array.isArray(users) ? users : [])
|
|
784
|
+
if (isObject(u) && typeof u.username === "string")
|
|
785
|
+
logins.add(u.username);
|
|
786
|
+
}
|
|
787
|
+
return [...logins].sort();
|
|
788
|
+
};
|
|
597
789
|
return {
|
|
598
790
|
kind: "gitlab",
|
|
599
791
|
host,
|
|
@@ -627,27 +819,110 @@ export function gitlabForge(opts) {
|
|
|
627
819
|
.filter((l) => l !== undefined);
|
|
628
820
|
return [...new Set(logins)].sort();
|
|
629
821
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
822
|
+
authors: authorsOf,
|
|
823
|
+
async prApproved(mr, head, approvers) {
|
|
824
|
+
const got = await api.call("GET", `${notes}/${mr}`);
|
|
825
|
+
const at = isObject(got) ? str(got.sha) : undefined;
|
|
826
|
+
if (at !== head)
|
|
827
|
+
return moved(`merge request !${mr}`, head, at);
|
|
828
|
+
// The token's own user approves for the pipeline, never as a reviewer.
|
|
829
|
+
const me = await api.call("GET", "/user");
|
|
830
|
+
const self = isObject(me) ? str(me.username) : undefined;
|
|
831
|
+
if (!self)
|
|
832
|
+
return refuse("GitLab did not say which user the job's token acts as, so no approval counts");
|
|
833
|
+
// An approval counts once it came after the latest push: the newest version of the diff.
|
|
834
|
+
const versions = await api.call("GET", `${notes}/${mr}/versions`);
|
|
835
|
+
const since = Math.max(-Infinity, ...(Array.isArray(versions) ? versions : []).map((v) => Date.parse((isObject(v) ? str(v.created_at) : undefined) ?? "")).filter(Number.isFinite));
|
|
836
|
+
const said = new Map();
|
|
837
|
+
for (const n of await api.list(`${notes}/${mr}/notes?sort=asc&order_by=created_at`, pageParams, 100)) {
|
|
838
|
+
if (!isObject(n) || n.system !== true)
|
|
839
|
+
continue;
|
|
840
|
+
const who = authorOf(n);
|
|
841
|
+
const body = (str(n.body) ?? "").trim();
|
|
842
|
+
const what = /^approved this merge request/.test(body) ? "approved" : /^unapproved this merge request/.test(body) ? "unapproved" : /^requested changes/.test(body) ? "changes" : undefined;
|
|
843
|
+
const when = Date.parse(str(n.created_at) ?? "");
|
|
844
|
+
if (!who || who === self || !what || !Number.isFinite(when))
|
|
845
|
+
continue;
|
|
846
|
+
const before = said.get(who);
|
|
847
|
+
const id = isObject(n.author) ? num(n.author.id) : undefined;
|
|
848
|
+
if (!before || when >= before.at)
|
|
849
|
+
said.set(who, { what, at: when, ...(id !== undefined ? { id } : {}) });
|
|
850
|
+
}
|
|
851
|
+
const changes = [...said].filter(([, v]) => v.what === "changes").map(([w]) => w).sort();
|
|
852
|
+
if (changes.length > 0)
|
|
853
|
+
return refuse(`${changes.join(", ")} requested changes`);
|
|
854
|
+
const authors = new Set(await authorsOf(mr));
|
|
855
|
+
const approved = [...said].filter(([, v]) => v.what === "approved" && v.at > since).map(([w]) => w).sort();
|
|
856
|
+
const least = approvers?.minAccessLevel ?? GITLAB_DEVELOPER;
|
|
857
|
+
const rule = async (login) => {
|
|
858
|
+
const id = said.get(login)?.id;
|
|
859
|
+
if (id === undefined)
|
|
860
|
+
return `GitLab did not say who ${login} is`;
|
|
861
|
+
let member;
|
|
862
|
+
try {
|
|
863
|
+
member = await api.call("GET", `${project}/members/all/${id}`);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
if (err instanceof ForgeApiError && err.status === 404)
|
|
867
|
+
return `${login} is not a member`;
|
|
868
|
+
return `could not read ${login}'s access (${err instanceof Error ? err.message : String(err)})`;
|
|
869
|
+
}
|
|
870
|
+
const level = isObject(member) ? (num(member.access_level) ?? 0) : 0;
|
|
871
|
+
return level >= least ? undefined : `${login} has access level ${level}, under ${least}`;
|
|
872
|
+
};
|
|
873
|
+
const { by, not } = await countApprovers(approved, authors, approvers, rule);
|
|
874
|
+
if (by.length === 0)
|
|
875
|
+
return noApprover(`no member other than its authors approved it after its latest push, ${short(head)}`, not);
|
|
876
|
+
return pass(`approved after ${short(head)} by ${by.join(", ")}`);
|
|
877
|
+
},
|
|
878
|
+
async prMergeable(mr, head) {
|
|
879
|
+
const got = await api.call("GET", `${notes}/${mr}`);
|
|
880
|
+
const m = isObject(got) ? got : {};
|
|
881
|
+
if (str(m.sha) !== head)
|
|
882
|
+
return moved(`merge request !${mr}`, head, str(m.sha));
|
|
883
|
+
const status = str(m.detailed_merge_status) ?? (m.merge_status === "can_be_merged" ? "mergeable" : (str(m.merge_status) ?? ""));
|
|
884
|
+
const into = str(m.target_branch) ?? "its target branch";
|
|
885
|
+
if (status === "conflict" || m.has_conflicts === true)
|
|
886
|
+
return refuse(`GitLab reports conflicts with ${into}`);
|
|
887
|
+
if (GITLAB_CHECKING.has(status))
|
|
888
|
+
return refuse("GitLab has not worked out yet whether it can merge", true);
|
|
889
|
+
if (status !== "mergeable")
|
|
890
|
+
return refuse(`GitLab says ${status || "nothing"} about merging it`);
|
|
891
|
+
return pass(`it merges into ${into} as it stands`);
|
|
892
|
+
},
|
|
893
|
+
async prUndiverged(mr, head) {
|
|
894
|
+
const got = await api.call("GET", `${notes}/${mr}?include_diverged_commits_count=true`);
|
|
895
|
+
const m = isObject(got) ? got : {};
|
|
896
|
+
if (str(m.sha) !== head)
|
|
897
|
+
return moved(`merge request !${mr}`, head, str(m.sha));
|
|
898
|
+
const into = str(m.target_branch) ?? "its target branch";
|
|
899
|
+
const behind = num(m.diverged_commits_count);
|
|
900
|
+
if (behind === undefined)
|
|
901
|
+
return refuse(`GitLab did not say how far ${short(head)} is behind ${into}`);
|
|
902
|
+
return behind === 0 ? pass(`${short(head)} contains ${into}`) : refuse(`${short(head)} is ${behind} commit${behind === 1 ? "" : "s"} behind ${into}`);
|
|
903
|
+
},
|
|
904
|
+
async prChecksPassed(mr, head, ignore = []) {
|
|
905
|
+
const skip = new Set(ignore);
|
|
906
|
+
// Each name's newest status; a job allowed to fail holds nothing back.
|
|
907
|
+
const newest = new Map();
|
|
908
|
+
for (const st of await api.list(`${project}/repository/commits/${head}/statuses?all=true`, pageParams, 100)) {
|
|
909
|
+
const name = isObject(st) ? str(st.name) : undefined;
|
|
910
|
+
if (!name || skip.has(name) || !isObject(st))
|
|
639
911
|
continue;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
912
|
+
const had = newest.get(name);
|
|
913
|
+
if (!had || (num(st.id) ?? 0) > (num(had.id) ?? 0))
|
|
914
|
+
newest.set(name, st);
|
|
643
915
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
916
|
+
const failing = [];
|
|
917
|
+
const waiting = [];
|
|
918
|
+
for (const [name, st] of newest) {
|
|
919
|
+
const state = str(st.status) ?? "";
|
|
920
|
+
if ((state === "failed" || state === "canceled") && st.allow_failure !== true)
|
|
921
|
+
failing.push(name);
|
|
922
|
+
else if (GITLAB_RUNNING.has(state))
|
|
923
|
+
waiting.push(name);
|
|
649
924
|
}
|
|
650
|
-
return
|
|
925
|
+
return checksVerdict(`merge request !${mr}`, head, failing, waiting, newest.size);
|
|
651
926
|
},
|
|
652
927
|
async pullRequestOpen(mr) {
|
|
653
928
|
try {
|
package/dist/pr-loop.d.ts
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
* `identity.gates` entry covers every member.
|
|
48
48
|
*/
|
|
49
49
|
import type { ChangeSetDocument } from "./change-set.js";
|
|
50
|
+
import type { PrRequireOutcome } from "./pr-requires.js";
|
|
50
51
|
/** The schema a pull-request report names in `$schema`. */
|
|
51
52
|
export declare const PR_REPORT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/pr-report/v1/pr-report.schema.json";
|
|
52
53
|
/** The read-contract version the report follows. */
|
|
@@ -69,6 +70,8 @@ export declare const PR_REPORT_HOOKS_FLOOR = "0.124.41";
|
|
|
69
70
|
export declare const PR_REPORT_OVERDUE_FLOOR = "0.124.41";
|
|
70
71
|
/** The first chant whose report can carry `policy` and the `policy-denied` and `policy-error` refusals (#4294). */
|
|
71
72
|
export declare const PR_REPORT_POLICY_FLOOR = "0.124.41";
|
|
73
|
+
/** The first chant whose apply report can carry `beforeMerge` and the `requires-unmet` and `before-merge-off` refusals (#4301, ws-106). */
|
|
74
|
+
export declare const PR_REPORT_BEFORE_MERGE_FLOOR = "0.124.42";
|
|
72
75
|
/** The gate a pull request's apply waits on, unless the pipeline names another. */
|
|
73
76
|
export declare const PR_APPLY_GATE = "pr-apply";
|
|
74
77
|
/**
|
|
@@ -246,7 +249,15 @@ export type PrRefusal =
|
|
|
246
249
|
/** `ci.policy` at base denied the plan (#4294), and no override of it counts. */
|
|
247
250
|
| "policy-denied"
|
|
248
251
|
/** `ci.policy` at base could not be run, or gave no answer chant can read (#4294). */
|
|
249
|
-
| "policy-error"
|
|
252
|
+
| "policy-error"
|
|
253
|
+
/** `pr-apply --before-merge` (#4301): the pull request is not approved, mergeable, undiverged or green; `beforeMerge` says which. */
|
|
254
|
+
| "requires-unmet"
|
|
255
|
+
/** `pr-apply --before-merge`: the declaration at base does not set `ci.workflow` to `apply-before-merge` for the member. */
|
|
256
|
+
| "before-merge-off";
|
|
257
|
+
/** An apply before merge (#4301, ws-106): the requires checks the forge answered, in order. */
|
|
258
|
+
export interface PrBeforeMerge {
|
|
259
|
+
requires: PrRequireOutcome[];
|
|
260
|
+
}
|
|
250
261
|
/**
|
|
251
262
|
* One wave of an apply with `--wave-gate` (#4003): what it planned, where it
|
|
252
263
|
* got to, and which approval it applied or waits under.
|
|
@@ -424,6 +435,12 @@ export interface PrReport {
|
|
|
424
435
|
* within contract 1 by chant {@link PR_REPORT_POLICY_FLOOR}.
|
|
425
436
|
*/
|
|
426
437
|
policy?: PrPolicyReport;
|
|
438
|
+
/**
|
|
439
|
+
* `pr-apply --before-merge` (#4301): the requires checks. Absent on every
|
|
440
|
+
* other stage. Added within contract 1 by chant
|
|
441
|
+
* {@link PR_REPORT_BEFORE_MERGE_FLOOR}.
|
|
442
|
+
*/
|
|
443
|
+
beforeMerge?: PrBeforeMerge;
|
|
427
444
|
}
|
|
428
445
|
/** The counts a member's entries add up to. */
|
|
429
446
|
export declare function memberCounts(doc: Pick<ChangeSetDocument, "summary">, member: string): PrMemberCounts;
|
package/dist/pr-loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-loop.d.ts","sourceRoot":"","sources":["../src/pr-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAmB,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"pr-loop.d.ts","sourceRoot":"","sources":["../src/pr-loop.ts"],"names":[],"mappings":"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"}
|
package/dist/pr-loop.js
CHANGED
|
@@ -69,6 +69,8 @@ export const PR_REPORT_HOOKS_FLOOR = "0.124.41";
|
|
|
69
69
|
export const PR_REPORT_OVERDUE_FLOOR = "0.124.41";
|
|
70
70
|
/** The first chant whose report can carry `policy` and the `policy-denied` and `policy-error` refusals (#4294). */
|
|
71
71
|
export const PR_REPORT_POLICY_FLOOR = "0.124.41";
|
|
72
|
+
/** The first chant whose apply report can carry `beforeMerge` and the `requires-unmet` and `before-merge-off` refusals (#4301, ws-106). */
|
|
73
|
+
export const PR_REPORT_BEFORE_MERGE_FLOOR = "0.124.42";
|
|
72
74
|
/** The gate a pull request's apply waits on, unless the pipeline names another. */
|
|
73
75
|
export const PR_APPLY_GATE = "pr-apply";
|
|
74
76
|
/**
|
|
@@ -401,6 +403,11 @@ export function prStatusDescription(report) {
|
|
|
401
403
|
text = "refused: an earlier wave has not applied";
|
|
402
404
|
break;
|
|
403
405
|
}
|
|
406
|
+
if (report.refusal === "requires-unmet" || report.refusal === "before-merge-off") {
|
|
407
|
+
const unmet = (report.beforeMerge?.requires ?? []).filter((r) => !r.passed).map((r) => r.name);
|
|
408
|
+
text = report.refusal === "before-merge-off" ? "refused: this workspace does not apply before merge" : `refused: ${unmet.join(", ") || "a require"} unmet for an apply before merge`;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
404
411
|
if (report.refusal === "policy-denied" || report.refusal === "policy-error") {
|
|
405
412
|
text = report.refusal === "policy-denied" ? `refused: the policy denied the plan (${(report.policy?.rules ?? []).join(", ")})` : "refused: the policy could not be checked";
|
|
406
413
|
break;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The requires checks of an apply before merge (#4301, ws-106): the pull
|
|
3
|
+
* request is approved, mergeable, undiverged and its checks passed, each
|
|
4
|
+
* asked of the forge through its {@link PrForge} method. All four must pass.
|
|
5
|
+
*
|
|
6
|
+
* The checks are terragucci's `apply.requires` (comment-apply.ts:781-860 at
|
|
7
|
+
* INTENTIUS/terragucci e6cb431). A product may leave one out with `only`
|
|
8
|
+
* (#4336); chant's own `pr-apply --before-merge` passes no `only`, so it asks
|
|
9
|
+
* all four, as ws-106 requires. A check the
|
|
10
|
+
* forge has not settled, such as GitHub working out whether a pull request
|
|
11
|
+
* can merge, is asked again a few times before it counts as unmet.
|
|
12
|
+
*
|
|
13
|
+
* Like ./pr-forge.ts this imports nothing beyond it, so a product bundles it
|
|
14
|
+
* without the TypeScript toolchain.
|
|
15
|
+
*/
|
|
16
|
+
import type { PrApproverFilter, PrForge, PrRequireResult } from "./pr-forge.js";
|
|
17
|
+
/** The four checks, in the order they run and the report lists them. */
|
|
18
|
+
export declare const PR_APPLY_REQUIRES: readonly ["approved", "mergeable", "undiverged", "checks"];
|
|
19
|
+
export type PrApplyRequire = (typeof PR_APPLY_REQUIRES)[number];
|
|
20
|
+
/** One check as the pull-request report's `beforeMerge.requires` holds it. */
|
|
21
|
+
export interface PrRequireOutcome extends PrRequireResult {
|
|
22
|
+
name: PrApplyRequire;
|
|
23
|
+
}
|
|
24
|
+
export interface PrRequiresOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The checks to ask (#4336). One left out passes with the reason "not
|
|
27
|
+
* required" and makes no forge call. Default: all four.
|
|
28
|
+
*/
|
|
29
|
+
only?: readonly PrApplyRequire[];
|
|
30
|
+
/** Who counts as an approver for `approved` (#4337). Default: anyone other than the authors. */
|
|
31
|
+
approvers?: PrApproverFilter;
|
|
32
|
+
/** Status names the `checks` require leaves out: the apply's own. */
|
|
33
|
+
ignore?: readonly string[];
|
|
34
|
+
/** How many times a pending check is asked in all. Default 5. */
|
|
35
|
+
reads?: number;
|
|
36
|
+
/** The wait between reads. Default 3 seconds. */
|
|
37
|
+
wait?: (ms: number) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ask `forge` each of the four checks for pull request `pr` at `head`. A
|
|
41
|
+
* forge client without a method, or a call that throws, gives an unmet
|
|
42
|
+
* check with the reason, so nothing applies on a check nobody made.
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkPrRequires(forge: PrForge, pr: number, head: string, opts?: PrRequiresOptions): Promise<PrRequireOutcome[]>;
|
|
45
|
+
/** The unmet checks as one line, or null when all four passed. */
|
|
46
|
+
export declare function unmetRequires(outcomes: readonly PrRequireOutcome[]): string | null;
|
|
47
|
+
//# sourceMappingURL=pr-requires.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr-requires.d.ts","sourceRoot":"","sources":["../src/pr-requires.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7E,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,YAAI,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,CAAU,CAAC;AAE5F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,8EAA8E;AAC9E,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,IAAI,EAAE,cAAc,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACjC,gGAAgG;IAChG,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,qEAAqE;IACrE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAKD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkCzI;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,GAAG,IAAI,CAIlF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The four checks, in the order they run and the report lists them. */
|
|
2
|
+
export const PR_APPLY_REQUIRES = ["approved", "mergeable", "undiverged", "checks"];
|
|
3
|
+
const READS = 5;
|
|
4
|
+
const WAIT_MS = 3000;
|
|
5
|
+
/**
|
|
6
|
+
* Ask `forge` each of the four checks for pull request `pr` at `head`. A
|
|
7
|
+
* forge client without a method, or a call that throws, gives an unmet
|
|
8
|
+
* check with the reason, so nothing applies on a check nobody made.
|
|
9
|
+
*/
|
|
10
|
+
export async function checkPrRequires(forge, pr, head, opts = {}) {
|
|
11
|
+
const wait = opts.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
12
|
+
const reads = Math.max(1, opts.reads ?? READS);
|
|
13
|
+
const ask = {
|
|
14
|
+
approved: forge.prApproved ? () => forge.prApproved(pr, head, opts.approvers) : undefined,
|
|
15
|
+
mergeable: forge.prMergeable ? () => forge.prMergeable(pr, head) : undefined,
|
|
16
|
+
undiverged: forge.prUndiverged ? () => forge.prUndiverged(pr, head) : undefined,
|
|
17
|
+
checks: forge.prChecksPassed ? () => forge.prChecksPassed(pr, head, opts.ignore ?? []) : undefined,
|
|
18
|
+
};
|
|
19
|
+
const out = [];
|
|
20
|
+
const asked = new Set(opts.only ?? PR_APPLY_REQUIRES);
|
|
21
|
+
for (const name of PR_APPLY_REQUIRES) {
|
|
22
|
+
if (!asked.has(name)) {
|
|
23
|
+
out.push({ name, passed: true, reason: "not required" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const call = ask[name];
|
|
27
|
+
if (!call) {
|
|
28
|
+
out.push({ name, passed: false, reason: `this ${forge.kind} client cannot say` });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let result;
|
|
32
|
+
for (let read = 1;; read++) {
|
|
33
|
+
try {
|
|
34
|
+
result = await call();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
result = { passed: false, reason: `could not ask ${forge.kind}: ${err instanceof Error ? err.message : String(err)}` };
|
|
38
|
+
}
|
|
39
|
+
if (result.passed || !result.pending || read >= reads)
|
|
40
|
+
break;
|
|
41
|
+
await wait(WAIT_MS);
|
|
42
|
+
}
|
|
43
|
+
out.push({ name, ...result });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** The unmet checks as one line, or null when all four passed. */
|
|
48
|
+
export function unmetRequires(outcomes) {
|
|
49
|
+
const unmet = outcomes.filter((o) => !o.passed);
|
|
50
|
+
if (unmet.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
return unmet.map((o) => `${o.name}: ${o.reason}`).join("; ");
|
|
53
|
+
}
|