@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.
Files changed (127) hide show
  1. package/dist/cli/handlers/build.d.ts.map +1 -1
  2. package/dist/cli/handlers/build.js +3 -0
  3. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  4. package/dist/cli/handlers/fan-out.js +27 -3
  5. package/dist/cli/handlers/pr.d.ts +3 -1
  6. package/dist/cli/handlers/pr.d.ts.map +1 -1
  7. package/dist/cli/handlers/pr.js +113 -2
  8. package/dist/cli/handlers/run.d.ts.map +1 -1
  9. package/dist/cli/handlers/run.js +11 -0
  10. package/dist/cli/main.d.ts.map +1 -1
  11. package/dist/cli/main.js +8 -2
  12. package/dist/cli/parse-args.d.ts.map +1 -1
  13. package/dist/cli/parse-args.js +15 -0
  14. package/dist/cli/registry.d.ts +19 -0
  15. package/dist/cli/registry.d.ts.map +1 -1
  16. package/dist/components/capability.d.ts +47 -0
  17. package/dist/components/capability.d.ts.map +1 -1
  18. package/dist/components/capability.js +44 -0
  19. package/dist/components/cli-support.d.ts +6 -0
  20. package/dist/components/cli-support.d.ts.map +1 -1
  21. package/dist/components/cli-support.js +8 -1
  22. package/dist/components/driver.d.ts +10 -0
  23. package/dist/components/driver.d.ts.map +1 -1
  24. package/dist/components/driver.js +52 -2
  25. package/dist/components/fan-out-run.d.ts +9 -0
  26. package/dist/components/fan-out-run.d.ts.map +1 -1
  27. package/dist/components/fan-out-run.js +17 -1
  28. package/dist/components/gate-plan.d.ts +2 -0
  29. package/dist/components/gate-plan.d.ts.map +1 -1
  30. package/dist/components/gate-plan.js +1 -0
  31. package/dist/components/pr-pipeline.d.ts +20 -0
  32. package/dist/components/pr-pipeline.d.ts.map +1 -1
  33. package/dist/components/pr-pipeline.js +54 -16
  34. package/dist/components/pr-run.d.ts +6 -0
  35. package/dist/components/pr-run.d.ts.map +1 -1
  36. package/dist/components/pr-run.js +3 -0
  37. package/dist/components/wave-plan.d.ts +14 -1
  38. package/dist/components/wave-plan.d.ts.map +1 -1
  39. package/dist/components/wave-plan.js +34 -2
  40. package/dist/op/activities/pr-refs.d.ts +10 -0
  41. package/dist/op/activities/pr-refs.d.ts.map +1 -0
  42. package/dist/op/activities/pr-refs.js +26 -0
  43. package/dist/op/activities/predict-behaviour.d.ts +2 -4
  44. package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
  45. package/dist/op/activities/predict-behaviour.js +2 -21
  46. package/dist/op/activities/review-agent.js +1 -1
  47. package/dist/op/index.d.ts +2 -2
  48. package/dist/op/index.d.ts.map +1 -1
  49. package/dist/op/index.js +1 -1
  50. package/dist/op/op-waves-run.d.ts +3 -79
  51. package/dist/op/op-waves-run.d.ts.map +1 -1
  52. package/dist/op/op-waves-run.js +4 -133
  53. package/dist/op/stage-hooks.d.ts +152 -0
  54. package/dist/op/stage-hooks.d.ts.map +1 -0
  55. package/dist/op/stage-hooks.js +199 -0
  56. package/dist/pr-forge.d.ts +56 -0
  57. package/dist/pr-forge.d.ts.map +1 -1
  58. package/dist/pr-forge.js +320 -45
  59. package/dist/pr-loop.d.ts +18 -1
  60. package/dist/pr-loop.d.ts.map +1 -1
  61. package/dist/pr-loop.js +7 -0
  62. package/dist/pr-requires.d.ts +47 -0
  63. package/dist/pr-requires.d.ts.map +1 -0
  64. package/dist/pr-requires.js +53 -0
  65. package/dist/wave-runtime.mjs +213 -213
  66. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
  67. package/dist/workspace/checks/ci-workflow.js +19 -4
  68. package/dist/workspace/ci-init.d.ts.map +1 -1
  69. package/dist/workspace/ci-init.js +5 -4
  70. package/dist/workspace/ci-run.d.ts.map +1 -1
  71. package/dist/workspace/ci-run.js +2 -1
  72. package/dist/workspace/ci-workflow-at.d.ts +14 -0
  73. package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
  74. package/dist/workspace/ci-workflow-at.js +17 -0
  75. package/dist/workspace/ci-workflow.d.ts +11 -3
  76. package/dist/workspace/ci-workflow.d.ts.map +1 -1
  77. package/dist/workspace/ci-workflow.js +10 -2
  78. package/dist/workspace/declaration.d.ts +6 -3
  79. package/dist/workspace/declaration.d.ts.map +1 -1
  80. package/dist/workspace/declaration.schema.json +2 -2
  81. package/dist/workspace/pr-report.schema.json +23 -3
  82. package/dist/workspace/reason-codes.d.ts +1 -1
  83. package/dist/workspace/reason-codes.d.ts.map +1 -1
  84. package/dist/workspace/reason-codes.js +1 -1
  85. package/package.json +1 -1
  86. package/src/cli/handlers/build.ts +3 -0
  87. package/src/cli/handlers/fan-out.ts +27 -3
  88. package/src/cli/handlers/pr-before-merge.test.ts +276 -0
  89. package/src/cli/handlers/pr.ts +110 -2
  90. package/src/cli/handlers/run.ts +10 -0
  91. package/src/cli/main.ts +8 -2
  92. package/src/cli/parse-args.ts +11 -0
  93. package/src/cli/registry.ts +19 -0
  94. package/src/components/capability-targets.test.ts +162 -0
  95. package/src/components/capability.ts +71 -0
  96. package/src/components/cli-support.ts +14 -1
  97. package/src/components/driver.ts +54 -3
  98. package/src/components/fan-out-run.ts +19 -1
  99. package/src/components/gate-plan.ts +3 -0
  100. package/src/components/pr-pipeline.ts +64 -16
  101. package/src/components/pr-run.ts +9 -0
  102. package/src/components/wave-plan.ts +38 -3
  103. package/src/op/activities/pr-refs.ts +28 -0
  104. package/src/op/activities/predict-behaviour.ts +3 -23
  105. package/src/op/activities/review-agent.ts +1 -1
  106. package/src/op/index.ts +4 -0
  107. package/src/op/op-waves-run.ts +24 -228
  108. package/src/op/stage-hooks-async.test.ts +79 -0
  109. package/src/op/stage-hooks.ts +346 -0
  110. package/src/pr-forge-bundle.test.ts +2 -2
  111. package/src/pr-forge.ts +334 -36
  112. package/src/pr-loop-bundle.test.ts +2 -2
  113. package/src/pr-loop.ts +25 -1
  114. package/src/pr-requires.test.ts +248 -0
  115. package/src/pr-requires.ts +93 -0
  116. package/src/review-agent-bundle.test.ts +35 -0
  117. package/src/stage-hooks-bundle.test.ts +38 -0
  118. package/src/workspace/checks/ci-workflow.test.ts +9 -0
  119. package/src/workspace/checks/ci-workflow.ts +18 -4
  120. package/src/workspace/ci-init.ts +5 -4
  121. package/src/workspace/ci-run.ts +2 -1
  122. package/src/workspace/ci-workflow-at.ts +23 -0
  123. package/src/workspace/ci-workflow.ts +13 -2
  124. package/src/workspace/declaration.schema.json +2 -2
  125. package/src/workspace/declaration.ts +6 -3
  126. package/src/workspace/pr-report.schema.json +23 -3
  127. 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
- async authors(pr) {
315
- const logins = new Set();
316
- const add = (v) => {
317
- if (isObject(v) && typeof v.login === "string" && v.login !== "")
318
- logins.add(v.login);
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 opened = await api.call("GET", `${repo}/pulls/${pr}`);
321
- if (isObject(opened))
322
- add(opened.user);
323
- for (const c of await api.list(`${repo}/pulls/${pr}/commits`, pageParams, pageSize)) {
324
- if (!isObject(c))
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
- add(c.author);
327
- add(c.committer);
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
- return [...logins].sort();
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
- async branchHead(branch) {
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
- async authors(mr) {
631
- const logins = new Set();
632
- const merge = await api.call("GET", `${notes}/${mr}`);
633
- const author = isObject(merge) ? authorOf(merge) : undefined;
634
- if (author)
635
- logins.add(author);
636
- const emails = new Set();
637
- for (const c of await api.list(`${notes}/${mr}/commits`, pageParams, 100)) {
638
- if (!isObject(c))
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
- for (const email of [str(c.author_email), str(c.committer_email)])
641
- if (email)
642
- emails.add(email.toLowerCase());
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
- for (const email of [...emails].sort()) {
645
- const users = await api.call("GET", `/users?search=${encodeURIComponent(email)}`);
646
- for (const u of Array.isArray(users) ? users : [])
647
- if (isObject(u) && typeof u.username === "string")
648
- logins.add(u.username);
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 [...logins].sort();
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;
@@ -1 +1 @@
1
- {"version":3,"file":"pr-loop.d.ts","sourceRoot":"","sources":["../src/pr-loop.ts"],"names":[],"mappings":"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"}
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
+ }