@intentius/chant 0.124.42 → 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/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +1 -0
- 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 -3
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +2 -2
- package/dist/op/stage-hooks.d.ts +20 -0
- package/dist/op/stage-hooks.d.ts.map +1 -1
- package/dist/op/stage-hooks.js +76 -19
- package/dist/pr-forge.d.ts +31 -3
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +78 -8
- package/dist/pr-requires.d.ts +11 -2
- package/dist/pr-requires.d.ts.map +1 -1
- package/dist/pr-requires.js +6 -1
- package/dist/wave-runtime.mjs +165 -165
- package/package.json +1 -1
- package/src/cli/handlers/pr.ts +1 -0
- package/src/op/index.ts +4 -0
- package/src/op/op-waves-run.ts +6 -2
- package/src/op/stage-hooks-async.test.ts +79 -0
- package/src/op/stage-hooks.ts +116 -21
- package/src/pr-forge-bundle.test.ts +1 -1
- package/src/pr-forge.ts +113 -13
- package/src/pr-requires.test.ts +64 -0
- package/src/pr-requires.ts +17 -3
package/dist/pr-forge.js
CHANGED
|
@@ -70,6 +70,37 @@ 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"]);
|
|
73
104
|
const short = (sha) => (sha ?? "nothing").slice(0, 8);
|
|
74
105
|
const pass = (reason) => ({ passed: true, reason });
|
|
75
106
|
const refuse = (reason, pending = false) => ({ passed: false, ...(pending ? { pending: true } : {}), reason });
|
|
@@ -363,7 +394,7 @@ function githubShaped(kind, opts) {
|
|
|
363
394
|
return [...standing].filter(([, state]) => state === "APPROVED").map(([login]) => login).sort();
|
|
364
395
|
},
|
|
365
396
|
authors: authorsOf,
|
|
366
|
-
async prApproved(pr, head) {
|
|
397
|
+
async prApproved(pr, head, approvers) {
|
|
367
398
|
const got = await api.call("GET", `${repo}/pulls/${pr}`);
|
|
368
399
|
const at = headOf(got);
|
|
369
400
|
if (at !== head)
|
|
@@ -378,15 +409,30 @@ function githubShaped(kind, opts) {
|
|
|
378
409
|
if (state === "COMMENTED" || state === "COMMENT" || state === "PENDING")
|
|
379
410
|
continue;
|
|
380
411
|
const commit = str(r.commit_id);
|
|
381
|
-
standing.set(login, { state, ...(commit ? { commit } : {}) });
|
|
412
|
+
standing.set(login, { state, ...(commit ? { commit } : {}), ...(typeof r.official === "boolean" ? { official: r.official } : {}) });
|
|
382
413
|
}
|
|
383
414
|
const changes = [...standing].filter(([, r]) => r.state === "CHANGES_REQUESTED" || r.state === "REQUEST_CHANGES").map(([l]) => l).sort();
|
|
384
415
|
if (changes.length > 0)
|
|
385
416
|
return refuse(`${changes.join(", ")} asked for changes`);
|
|
386
417
|
const authors = new Set(await authorsOf(pr));
|
|
387
|
-
const
|
|
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`;
|
|
432
|
+
};
|
|
433
|
+
const { by, not } = await countApprovers(approved, authors, approvers, rule);
|
|
388
434
|
if (by.length === 0)
|
|
389
|
-
return
|
|
435
|
+
return noApprover(`no reviewer other than its authors approved its head ${short(head)}`, not);
|
|
390
436
|
return pass(`approved at ${short(head)} by ${by.join(", ")}`);
|
|
391
437
|
},
|
|
392
438
|
async prMergeable(pr, head) {
|
|
@@ -396,6 +442,11 @@ function githubShaped(kind, opts) {
|
|
|
396
442
|
return moved(`pull request ${pr}`, head, at);
|
|
397
443
|
const m = isObject(got) ? got : {};
|
|
398
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);
|
|
399
450
|
if (m.mergeable === false || m.mergeable_state === "dirty")
|
|
400
451
|
return refuse(`the forge reports conflicts with ${into ?? "its base"}`);
|
|
401
452
|
if (m.mergeable !== true)
|
|
@@ -769,7 +820,7 @@ export function gitlabForge(opts) {
|
|
|
769
820
|
return [...new Set(logins)].sort();
|
|
770
821
|
},
|
|
771
822
|
authors: authorsOf,
|
|
772
|
-
async prApproved(mr, head) {
|
|
823
|
+
async prApproved(mr, head, approvers) {
|
|
773
824
|
const got = await api.call("GET", `${notes}/${mr}`);
|
|
774
825
|
const at = isObject(got) ? str(got.sha) : undefined;
|
|
775
826
|
if (at !== head)
|
|
@@ -793,16 +844,35 @@ export function gitlabForge(opts) {
|
|
|
793
844
|
if (!who || who === self || !what || !Number.isFinite(when))
|
|
794
845
|
continue;
|
|
795
846
|
const before = said.get(who);
|
|
847
|
+
const id = isObject(n.author) ? num(n.author.id) : undefined;
|
|
796
848
|
if (!before || when >= before.at)
|
|
797
|
-
said.set(who, { what, at: when });
|
|
849
|
+
said.set(who, { what, at: when, ...(id !== undefined ? { id } : {}) });
|
|
798
850
|
}
|
|
799
851
|
const changes = [...said].filter(([, v]) => v.what === "changes").map(([w]) => w).sort();
|
|
800
852
|
if (changes.length > 0)
|
|
801
853
|
return refuse(`${changes.join(", ")} requested changes`);
|
|
802
854
|
const authors = new Set(await authorsOf(mr));
|
|
803
|
-
const
|
|
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);
|
|
804
874
|
if (by.length === 0)
|
|
805
|
-
return
|
|
875
|
+
return noApprover(`no member other than its authors approved it after its latest push, ${short(head)}`, not);
|
|
806
876
|
return pass(`approved after ${short(head)} by ${by.join(", ")}`);
|
|
807
877
|
},
|
|
808
878
|
async prMergeable(mr, head) {
|
package/dist/pr-requires.d.ts
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* asked of the forge through its {@link PrForge} method. All four must pass.
|
|
5
5
|
*
|
|
6
6
|
* The checks are terragucci's `apply.requires` (comment-apply.ts:781-860 at
|
|
7
|
-
* INTENTIUS/terragucci e6cb431)
|
|
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
|
|
8
10
|
* forge has not settled, such as GitHub working out whether a pull request
|
|
9
11
|
* can merge, is asked again a few times before it counts as unmet.
|
|
10
12
|
*
|
|
11
13
|
* Like ./pr-forge.ts this imports nothing beyond it, so a product bundles it
|
|
12
14
|
* without the TypeScript toolchain.
|
|
13
15
|
*/
|
|
14
|
-
import type { PrForge, PrRequireResult } from "./pr-forge.js";
|
|
16
|
+
import type { PrApproverFilter, PrForge, PrRequireResult } from "./pr-forge.js";
|
|
15
17
|
/** The four checks, in the order they run and the report lists them. */
|
|
16
18
|
export declare const PR_APPLY_REQUIRES: readonly ["approved", "mergeable", "undiverged", "checks"];
|
|
17
19
|
export type PrApplyRequire = (typeof PR_APPLY_REQUIRES)[number];
|
|
@@ -20,6 +22,13 @@ export interface PrRequireOutcome extends PrRequireResult {
|
|
|
20
22
|
name: PrApplyRequire;
|
|
21
23
|
}
|
|
22
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;
|
|
23
32
|
/** Status names the `checks` require leaves out: the apply's own. */
|
|
24
33
|
ignore?: readonly string[];
|
|
25
34
|
/** How many times a pending check is asked in all. Default 5. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-requires.d.ts","sourceRoot":"","sources":["../src/pr-requires.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/pr-requires.js
CHANGED
|
@@ -11,13 +11,18 @@ export async function checkPrRequires(forge, pr, head, opts = {}) {
|
|
|
11
11
|
const wait = opts.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
12
12
|
const reads = Math.max(1, opts.reads ?? READS);
|
|
13
13
|
const ask = {
|
|
14
|
-
approved: forge.prApproved ? () => forge.prApproved(pr, head) : undefined,
|
|
14
|
+
approved: forge.prApproved ? () => forge.prApproved(pr, head, opts.approvers) : undefined,
|
|
15
15
|
mergeable: forge.prMergeable ? () => forge.prMergeable(pr, head) : undefined,
|
|
16
16
|
undiverged: forge.prUndiverged ? () => forge.prUndiverged(pr, head) : undefined,
|
|
17
17
|
checks: forge.prChecksPassed ? () => forge.prChecksPassed(pr, head, opts.ignore ?? []) : undefined,
|
|
18
18
|
};
|
|
19
19
|
const out = [];
|
|
20
|
+
const asked = new Set(opts.only ?? PR_APPLY_REQUIRES);
|
|
20
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
|
+
}
|
|
21
26
|
const call = ask[name];
|
|
22
27
|
if (!call) {
|
|
23
28
|
out.push({ name, passed: false, reason: `this ${forge.kind} client cannot say` });
|