@intentius/chant 0.124.42 → 0.124.44

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/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 by = [...standing].filter(([l, r]) => r.state === "APPROVED" && r.commit === head && !authors.has(l)).map(([l]) => l).sort();
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 refuse(`no reviewer other than its authors approved its head ${short(head)}`);
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 by = [...said].filter(([w, v]) => v.what === "approved" && v.at > since && !authors.has(w)).map(([w]) => w).sort();
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 refuse(`no member other than its authors approved it after its latest push, ${short(head)}`);
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) {
@@ -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), with no setting to drop one. A check the
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;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE3D,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,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,CA6BzI;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,GAAG,IAAI,CAIlF"}
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"}
@@ -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` });