pr-merge-queue 0.0.0-stage → 0.0.0
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/LICENSE +201 -0
- package/README.md +125 -2
- package/dist/claim-pr/claim-pr.d.mts +45 -0
- package/dist/claim-pr/claim-pr.d.mts.map +1 -0
- package/dist/claim-pr/claim-pr.mjs +217 -0
- package/dist/claim-pr/claim-pr.mjs.map +1 -0
- package/dist/claim-pr/index.d.mts +2 -0
- package/dist/claim-pr/index.d.mts.map +1 -0
- package/dist/claim-pr/index.mjs +2 -0
- package/dist/claim-pr/index.mjs.map +1 -0
- package/dist/cmd/claim-pr.d.mts +3 -0
- package/dist/cmd/claim-pr.d.mts.map +1 -0
- package/dist/cmd/claim-pr.mjs +20 -0
- package/dist/cmd/claim-pr.mjs.map +1 -0
- package/dist/cmd/open-pr.d.mts +3 -0
- package/dist/cmd/open-pr.d.mts.map +1 -0
- package/dist/cmd/open-pr.mjs +20 -0
- package/dist/cmd/open-pr.mjs.map +1 -0
- package/dist/cmd/pr-report.d.mts +3 -0
- package/dist/cmd/pr-report.d.mts.map +1 -0
- package/dist/cmd/pr-report.mjs +20 -0
- package/dist/cmd/pr-report.mjs.map +1 -0
- package/dist/cmd/push-fixed-pr.d.mts +3 -0
- package/dist/cmd/push-fixed-pr.d.mts.map +1 -0
- package/dist/cmd/push-fixed-pr.mjs +20 -0
- package/dist/cmd/push-fixed-pr.mjs.map +1 -0
- package/dist/cmd/report-unowned-prs.d.mts +3 -0
- package/dist/cmd/report-unowned-prs.d.mts.map +1 -0
- package/dist/cmd/report-unowned-prs.mjs +20 -0
- package/dist/cmd/report-unowned-prs.mjs.map +1 -0
- package/dist/cmd/unblock-prs.d.mts +3 -0
- package/dist/cmd/unblock-prs.d.mts.map +1 -0
- package/dist/cmd/unblock-prs.mjs +19 -0
- package/dist/cmd/unblock-prs.mjs.map +1 -0
- package/dist/entry-point.d.mts +3 -0
- package/dist/entry-point.d.mts.map +1 -0
- package/dist/entry-point.mjs +3 -0
- package/dist/entry-point.mjs.map +1 -0
- package/dist/lib/cli-args.d.mts +27 -0
- package/dist/lib/cli-args.d.mts.map +1 -0
- package/dist/lib/cli-args.mjs +55 -0
- package/dist/lib/cli-args.mjs.map +1 -0
- package/dist/lib/github-api.d.mts +153 -0
- package/dist/lib/github-api.d.mts.map +1 -0
- package/dist/lib/github-api.mjs +291 -0
- package/dist/lib/github-api.mjs.map +1 -0
- package/dist/lib/index.d.mts +7 -0
- package/dist/lib/index.d.mts.map +1 -0
- package/dist/lib/index.mjs +7 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/json.d.mts +12 -0
- package/dist/lib/json.d.mts.map +1 -0
- package/dist/lib/json.mjs +28 -0
- package/dist/lib/json.mjs.map +1 -0
- package/dist/lib/log.d.mts +10 -0
- package/dist/lib/log.d.mts.map +1 -0
- package/dist/lib/log.mjs +13 -0
- package/dist/lib/log.mjs.map +1 -0
- package/dist/lib/shell.d.mts +18 -0
- package/dist/lib/shell.d.mts.map +1 -0
- package/dist/lib/shell.mjs +42 -0
- package/dist/lib/shell.mjs.map +1 -0
- package/dist/lib/time.d.mts +17 -0
- package/dist/lib/time.d.mts.map +1 -0
- package/dist/lib/time.mjs +35 -0
- package/dist/lib/time.mjs.map +1 -0
- package/dist/open-pr/github.d.mts +72 -0
- package/dist/open-pr/github.d.mts.map +1 -0
- package/dist/open-pr/github.mjs +249 -0
- package/dist/open-pr/github.mjs.map +1 -0
- package/dist/open-pr/index.d.mts +6 -0
- package/dist/open-pr/index.d.mts.map +1 -0
- package/dist/open-pr/index.mjs +6 -0
- package/dist/open-pr/index.mjs.map +1 -0
- package/dist/open-pr/main.d.mts +36 -0
- package/dist/open-pr/main.d.mts.map +1 -0
- package/dist/open-pr/main.mjs +204 -0
- package/dist/open-pr/main.mjs.map +1 -0
- package/dist/open-pr/options.d.mts +20 -0
- package/dist/open-pr/options.d.mts.map +1 -0
- package/dist/open-pr/options.mjs +66 -0
- package/dist/open-pr/options.mjs.map +1 -0
- package/dist/open-pr/steps.d.mts +23 -0
- package/dist/open-pr/steps.d.mts.map +1 -0
- package/dist/open-pr/steps.mjs +38 -0
- package/dist/open-pr/steps.mjs.map +1 -0
- package/dist/open-pr/types.d.mts +179 -0
- package/dist/open-pr/types.d.mts.map +1 -0
- package/dist/open-pr/types.mjs +20 -0
- package/dist/open-pr/types.mjs.map +1 -0
- package/dist/pr-report/github.d.mts +59 -0
- package/dist/pr-report/github.d.mts.map +1 -0
- package/dist/pr-report/github.mjs +561 -0
- package/dist/pr-report/github.mjs.map +1 -0
- package/dist/pr-report/index.d.mts +8 -0
- package/dist/pr-report/index.d.mts.map +1 -0
- package/dist/pr-report/index.mjs +8 -0
- package/dist/pr-report/index.mjs.map +1 -0
- package/dist/pr-report/linked-issues.d.mts +15 -0
- package/dist/pr-report/linked-issues.d.mts.map +1 -0
- package/dist/pr-report/linked-issues.mjs +39 -0
- package/dist/pr-report/linked-issues.mjs.map +1 -0
- package/dist/pr-report/main.d.mts +42 -0
- package/dist/pr-report/main.d.mts.map +1 -0
- package/dist/pr-report/main.mjs +163 -0
- package/dist/pr-report/main.mjs.map +1 -0
- package/dist/pr-report/options.d.mts +36 -0
- package/dist/pr-report/options.d.mts.map +1 -0
- package/dist/pr-report/options.mjs +90 -0
- package/dist/pr-report/options.mjs.map +1 -0
- package/dist/pr-report/queue.d.mts +28 -0
- package/dist/pr-report/queue.d.mts.map +1 -0
- package/dist/pr-report/queue.mjs +74 -0
- package/dist/pr-report/queue.mjs.map +1 -0
- package/dist/pr-report/render.d.mts +14 -0
- package/dist/pr-report/render.d.mts.map +1 -0
- package/dist/pr-report/render.mjs +318 -0
- package/dist/pr-report/render.mjs.map +1 -0
- package/dist/pr-report/repo-settings.d.mts +12 -0
- package/dist/pr-report/repo-settings.d.mts.map +1 -0
- package/dist/pr-report/repo-settings.mjs +22 -0
- package/dist/pr-report/repo-settings.mjs.map +1 -0
- package/dist/push-fixed-pr/index.d.mts +2 -0
- package/dist/push-fixed-pr/index.d.mts.map +1 -0
- package/dist/push-fixed-pr/index.mjs +2 -0
- package/dist/push-fixed-pr/index.mjs.map +1 -0
- package/dist/push-fixed-pr/push-fixed-pr.d.mts +54 -0
- package/dist/push-fixed-pr/push-fixed-pr.d.mts.map +1 -0
- package/dist/push-fixed-pr/push-fixed-pr.mjs +172 -0
- package/dist/push-fixed-pr/push-fixed-pr.mjs.map +1 -0
- package/dist/report-unowned-prs/index.d.mts +2 -0
- package/dist/report-unowned-prs/index.d.mts.map +1 -0
- package/dist/report-unowned-prs/index.mjs +2 -0
- package/dist/report-unowned-prs/index.mjs.map +1 -0
- package/dist/report-unowned-prs/report-unowned-prs.d.mts +49 -0
- package/dist/report-unowned-prs/report-unowned-prs.d.mts.map +1 -0
- package/dist/report-unowned-prs/report-unowned-prs.mjs +127 -0
- package/dist/report-unowned-prs/report-unowned-prs.mjs.map +1 -0
- package/dist/tsconfig.json +1 -0
- package/dist/unblock-prs/auto-fix.d.mts +45 -0
- package/dist/unblock-prs/auto-fix.d.mts.map +1 -0
- package/dist/unblock-prs/auto-fix.mjs +207 -0
- package/dist/unblock-prs/auto-fix.mjs.map +1 -0
- package/dist/unblock-prs/checks.d.mts +307 -0
- package/dist/unblock-prs/checks.d.mts.map +1 -0
- package/dist/unblock-prs/checks.mjs +372 -0
- package/dist/unblock-prs/checks.mjs.map +1 -0
- package/dist/unblock-prs/claims.d.mts +21 -0
- package/dist/unblock-prs/claims.d.mts.map +1 -0
- package/dist/unblock-prs/claims.mjs +23 -0
- package/dist/unblock-prs/claims.mjs.map +1 -0
- package/dist/unblock-prs/config.d.mts +487 -0
- package/dist/unblock-prs/config.d.mts.map +1 -0
- package/dist/unblock-prs/config.mjs +119 -0
- package/dist/unblock-prs/config.mjs.map +1 -0
- package/dist/unblock-prs/constants.d.mts +28 -0
- package/dist/unblock-prs/constants.d.mts.map +1 -0
- package/dist/unblock-prs/constants.mjs +28 -0
- package/dist/unblock-prs/constants.mjs.map +1 -0
- package/dist/unblock-prs/demotions.d.mts +42 -0
- package/dist/unblock-prs/demotions.d.mts.map +1 -0
- package/dist/unblock-prs/demotions.mjs +50 -0
- package/dist/unblock-prs/demotions.mjs.map +1 -0
- package/dist/unblock-prs/github.d.mts +739 -0
- package/dist/unblock-prs/github.d.mts.map +1 -0
- package/dist/unblock-prs/github.mjs +626 -0
- package/dist/unblock-prs/github.mjs.map +1 -0
- package/dist/unblock-prs/index.d.mts +25 -0
- package/dist/unblock-prs/index.d.mts.map +1 -0
- package/dist/unblock-prs/index.mjs +25 -0
- package/dist/unblock-prs/index.mjs.map +1 -0
- package/dist/unblock-prs/labels.d.mts +49 -0
- package/dist/unblock-prs/labels.d.mts.map +1 -0
- package/dist/unblock-prs/labels.mjs +49 -0
- package/dist/unblock-prs/labels.mjs.map +1 -0
- package/dist/unblock-prs/main.d.mts +179 -0
- package/dist/unblock-prs/main.d.mts.map +1 -0
- package/dist/unblock-prs/main.mjs +640 -0
- package/dist/unblock-prs/main.mjs.map +1 -0
- package/dist/unblock-prs/merge-after.d.mts +46 -0
- package/dist/unblock-prs/merge-after.d.mts.map +1 -0
- package/dist/unblock-prs/merge-after.mjs +65 -0
- package/dist/unblock-prs/merge-after.mjs.map +1 -0
- package/dist/unblock-prs/options.d.mts +35 -0
- package/dist/unblock-prs/options.d.mts.map +1 -0
- package/dist/unblock-prs/options.mjs +88 -0
- package/dist/unblock-prs/options.mjs.map +1 -0
- package/dist/unblock-prs/owner.d.mts +32 -0
- package/dist/unblock-prs/owner.d.mts.map +1 -0
- package/dist/unblock-prs/owner.mjs +63 -0
- package/dist/unblock-prs/owner.mjs.map +1 -0
- package/dist/unblock-prs/quiet.d.mts +21 -0
- package/dist/unblock-prs/quiet.d.mts.map +1 -0
- package/dist/unblock-prs/quiet.mjs +42 -0
- package/dist/unblock-prs/quiet.mjs.map +1 -0
- package/dist/unblock-prs/rebase.d.mts +120 -0
- package/dist/unblock-prs/rebase.d.mts.map +1 -0
- package/dist/unblock-prs/rebase.mjs +576 -0
- package/dist/unblock-prs/rebase.mjs.map +1 -0
- package/dist/unblock-prs/release.d.mts +62 -0
- package/dist/unblock-prs/release.d.mts.map +1 -0
- package/dist/unblock-prs/release.mjs +94 -0
- package/dist/unblock-prs/release.mjs.map +1 -0
- package/dist/unblock-prs/set-aside-comment.d.mts +20 -0
- package/dist/unblock-prs/set-aside-comment.d.mts.map +1 -0
- package/dist/unblock-prs/set-aside-comment.mjs +93 -0
- package/dist/unblock-prs/set-aside-comment.mjs.map +1 -0
- package/dist/unblock-prs/set-aside-detail.d.mts +41 -0
- package/dist/unblock-prs/set-aside-detail.d.mts.map +1 -0
- package/dist/unblock-prs/set-aside-detail.mjs +56 -0
- package/dist/unblock-prs/set-aside-detail.mjs.map +1 -0
- package/dist/unblock-prs/skips.d.mts +85 -0
- package/dist/unblock-prs/skips.d.mts.map +1 -0
- package/dist/unblock-prs/skips.mjs +141 -0
- package/dist/unblock-prs/skips.mjs.map +1 -0
- package/dist/unblock-prs/stack.d.mts +98 -0
- package/dist/unblock-prs/stack.d.mts.map +1 -0
- package/dist/unblock-prs/stack.mjs +141 -0
- package/dist/unblock-prs/stack.mjs.map +1 -0
- package/dist/unblock-prs/triage.d.mts +28 -0
- package/dist/unblock-prs/triage.d.mts.map +1 -0
- package/dist/unblock-prs/triage.mjs +396 -0
- package/dist/unblock-prs/triage.mjs.map +1 -0
- package/dist/unblock-prs/types.d.mts +793 -0
- package/dist/unblock-prs/types.d.mts.map +1 -0
- package/dist/unblock-prs/types.mjs +26 -0
- package/dist/unblock-prs/types.mjs.map +1 -0
- package/dist/unblock-prs/util.d.mts +23 -0
- package/dist/unblock-prs/util.d.mts.map +1 -0
- package/dist/unblock-prs/util.mjs +60 -0
- package/dist/unblock-prs/util.mjs.map +1 -0
- package/dist/unblock-prs/version-pr.d.mts +57 -0
- package/dist/unblock-prs/version-pr.d.mts.map +1 -0
- package/dist/unblock-prs/version-pr.mjs +79 -0
- package/dist/unblock-prs/version-pr.mjs.map +1 -0
- package/dist/unblock-prs/watch.d.mts +17 -0
- package/dist/unblock-prs/watch.d.mts.map +1 -0
- package/dist/unblock-prs/watch.mjs +128 -0
- package/dist/unblock-prs/watch.mjs.map +1 -0
- package/package.json +118 -3
- package/src/claim-pr/claim-pr.mts +380 -0
- package/src/claim-pr/claim-pr.test.mts +176 -0
- package/src/claim-pr/index.mts +1 -0
- package/src/cmd/claim-pr.mts +32 -0
- package/src/cmd/open-pr.mts +28 -0
- package/src/cmd/pr-report.mts +28 -0
- package/src/cmd/push-fixed-pr.mts +36 -0
- package/src/cmd/report-unowned-prs.mts +37 -0
- package/src/cmd/unblock-prs.mts +24 -0
- package/src/entry-point.mts +3 -0
- package/src/lib/cli-args.mts +86 -0
- package/src/lib/cli-args.test.mts +92 -0
- package/src/lib/github-api.mts +463 -0
- package/src/lib/github-api.test.mts +40 -0
- package/src/lib/index.mts +6 -0
- package/src/lib/json.mts +43 -0
- package/src/lib/json.test.mts +34 -0
- package/src/lib/log.mts +19 -0
- package/src/lib/log.test.mts +45 -0
- package/src/lib/shell.mts +52 -0
- package/src/lib/time.mts +49 -0
- package/src/open-pr/README.md +237 -0
- package/src/open-pr/github.mts +444 -0
- package/src/open-pr/index.mts +5 -0
- package/src/open-pr/main.mts +327 -0
- package/src/open-pr/options.mts +101 -0
- package/src/open-pr/options.test.mts +67 -0
- package/src/open-pr/steps.mts +57 -0
- package/src/open-pr/steps.test.mts +59 -0
- package/src/open-pr/types.mts +42 -0
- package/src/pr-report/README.md +244 -0
- package/src/pr-report/github.mts +911 -0
- package/src/pr-report/index.mts +7 -0
- package/src/pr-report/linked-issues.mts +51 -0
- package/src/pr-report/linked-issues.test.mts +65 -0
- package/src/pr-report/main.mts +257 -0
- package/src/pr-report/options.mts +140 -0
- package/src/pr-report/queue.mts +125 -0
- package/src/pr-report/render.mts +428 -0
- package/src/pr-report/render.test.mts +379 -0
- package/src/pr-report/repo-settings.mts +34 -0
- package/src/push-fixed-pr/index.mts +1 -0
- package/src/push-fixed-pr/push-fixed-pr.mts +276 -0
- package/src/push-fixed-pr/push-fixed-pr.test.mts +228 -0
- package/src/report-unowned-prs/index.mts +1 -0
- package/src/report-unowned-prs/report-unowned-prs.mts +190 -0
- package/src/report-unowned-prs/report-unowned-prs.test.mts +87 -0
- package/src/unblock-prs/README.md +1616 -0
- package/src/unblock-prs/auto-fix.mts +334 -0
- package/src/unblock-prs/auto-fix.test.mts +159 -0
- package/src/unblock-prs/checks.mts +538 -0
- package/src/unblock-prs/checks.test.mts +507 -0
- package/src/unblock-prs/claims.mts +33 -0
- package/src/unblock-prs/claims.test.mts +64 -0
- package/src/unblock-prs/config.mts +187 -0
- package/src/unblock-prs/config.schema.json +71 -0
- package/src/unblock-prs/config.test.mts +139 -0
- package/src/unblock-prs/constants.mts +33 -0
- package/src/unblock-prs/demotions.mts +73 -0
- package/src/unblock-prs/demotions.test.mts +100 -0
- package/src/unblock-prs/github.mts +1042 -0
- package/src/unblock-prs/github.test.mts +84 -0
- package/src/unblock-prs/index.mts +24 -0
- package/src/unblock-prs/labels.mts +70 -0
- package/src/unblock-prs/main.mts +1051 -0
- package/src/unblock-prs/merge-after.mts +117 -0
- package/src/unblock-prs/merge-after.test.mts +120 -0
- package/src/unblock-prs/options.mts +157 -0
- package/src/unblock-prs/owner.mts +91 -0
- package/src/unblock-prs/owner.test.mts +114 -0
- package/src/unblock-prs/quiet.mts +62 -0
- package/src/unblock-prs/quiet.test.mts +175 -0
- package/src/unblock-prs/rebase.mts +962 -0
- package/src/unblock-prs/release.mts +149 -0
- package/src/unblock-prs/release.test.mts +209 -0
- package/src/unblock-prs/set-aside-comment.mts +143 -0
- package/src/unblock-prs/set-aside-comment.test.mts +148 -0
- package/src/unblock-prs/set-aside-detail.mts +82 -0
- package/src/unblock-prs/set-aside-detail.test.mts +131 -0
- package/src/unblock-prs/skips.mts +215 -0
- package/src/unblock-prs/skips.test.mts +280 -0
- package/src/unblock-prs/stack.mts +220 -0
- package/src/unblock-prs/stack.test.mts +449 -0
- package/src/unblock-prs/triage.mts +669 -0
- package/src/unblock-prs/triage.test.mts +100 -0
- package/src/unblock-prs/types.mts +358 -0
- package/src/unblock-prs/util.mts +78 -0
- package/src/unblock-prs/util.test.mts +26 -0
- package/src/unblock-prs/version-pr.mts +112 -0
- package/src/unblock-prs/version-pr.test.mts +78 -0
- package/src/unblock-prs/watch.mts +219 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the declared order means for picking a pull request, as a note, and
|
|
3
|
+
* the order the issues the pull requests close add to it. Reading the
|
|
4
|
+
* `Merge-After:` trailers and the `Start-After:` lines, finding the cycles in
|
|
5
|
+
* them and deciding that a pull request waits is `pr-report-core`'s, because
|
|
6
|
+
* the Pull Requests Manager page draws the same queue.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { closingKeywordIssuesIn, waitsFor, type RepoRef } from 'pr-report-core';
|
|
10
|
+
import { Arr } from 'ts-data-forge';
|
|
11
|
+
import type { Classification, IssueOrder, PullRequest } from './types.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The note to report instead of picking this pull request while what it
|
|
15
|
+
* must merge after is still open. Picking is all this constrains: a pull
|
|
16
|
+
* request that is already up to date and merging is left to auto-merge,
|
|
17
|
+
* which takes no notice of anything declared here.
|
|
18
|
+
*/
|
|
19
|
+
export const waitingOnNote = (
|
|
20
|
+
pr: PullRequest,
|
|
21
|
+
on: readonly number[],
|
|
22
|
+
cyclic: boolean,
|
|
23
|
+
inherited: readonly number[] = [],
|
|
24
|
+
): Classification => {
|
|
25
|
+
const fromIssues = on.filter((number) => inherited.includes(number));
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
kind: 'note',
|
|
29
|
+
note: `#${pr.number}: Merge-After ${on.map((number) => `#${number}`).join(', ')}, still open${
|
|
30
|
+
Arr.isNonEmpty(fromIssues)
|
|
31
|
+
? ` (${fromIssues.map((number) => `#${number}`).join(', ')} from the Start-After of an issue it closes)`
|
|
32
|
+
: ''
|
|
33
|
+
}${
|
|
34
|
+
cyclic
|
|
35
|
+
? ' — and on a Merge-After cycle, so nothing here will move it'
|
|
36
|
+
: ''
|
|
37
|
+
}`,
|
|
38
|
+
} as const;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** One issue as the closing-issue reads answer it. */
|
|
42
|
+
export type ClosingIssueAnswer = Readonly<{
|
|
43
|
+
number: number;
|
|
44
|
+
/** `OPEN` or `CLOSED`. */
|
|
45
|
+
state: string;
|
|
46
|
+
body: string;
|
|
47
|
+
/** The issues of this repository GitHub records as blocking it. */
|
|
48
|
+
blockedBy: readonly number[];
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Which open issues each pull request closes, and what those declare with
|
|
53
|
+
* `Start-After:`, for `queueGraph` to inherit.
|
|
54
|
+
*
|
|
55
|
+
* An issue closes when GitHub links it (`linked`, this repository's only) or
|
|
56
|
+
* when the body closes it by keyword, as GitHub marked it in `bodyHtml` —
|
|
57
|
+
* the one way to tell for a stacked pull request, which GitHub links
|
|
58
|
+
* nothing on. `described` is what was read of the keyword ones GitHub did
|
|
59
|
+
* not link; one it would not describe is left out, being unknown.
|
|
60
|
+
*/
|
|
61
|
+
export const issueOrderOf = (
|
|
62
|
+
repo: RepoRef,
|
|
63
|
+
pulls: readonly Readonly<{
|
|
64
|
+
number: number;
|
|
65
|
+
bodyHtml: string;
|
|
66
|
+
linked: readonly ClosingIssueAnswer[];
|
|
67
|
+
}>[],
|
|
68
|
+
described: ReadonlyMap<number, ClosingIssueAnswer>,
|
|
69
|
+
): IssueOrder => {
|
|
70
|
+
const mut_issues = new Map(described);
|
|
71
|
+
|
|
72
|
+
for (const issue of pulls.flatMap(({ linked }) => linked)) {
|
|
73
|
+
mut_issues.set(issue.number, issue);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const issues: ReadonlyMap<number, ClosingIssueAnswer> = mut_issues;
|
|
77
|
+
|
|
78
|
+
const isOpen = (number: number): boolean =>
|
|
79
|
+
issues.get(number)?.state.toUpperCase() === 'OPEN';
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
closes: new Map(
|
|
83
|
+
pulls.map(({ number, bodyHtml, linked }) => [
|
|
84
|
+
number,
|
|
85
|
+
Arr.uniq([
|
|
86
|
+
...linked.map((issue) => issue.number),
|
|
87
|
+
...closingKeywordIssuesIn(repo, bodyHtml),
|
|
88
|
+
]).filter(isOpen),
|
|
89
|
+
]),
|
|
90
|
+
),
|
|
91
|
+
startAfter: new Map(
|
|
92
|
+
Array.from(issues.values(), ({ number, body, blockedBy }) => [
|
|
93
|
+
number,
|
|
94
|
+
waitsFor(body, blockedBy),
|
|
95
|
+
]),
|
|
96
|
+
),
|
|
97
|
+
} as const;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** The keyword-closed issues of `pulls` that GitHub did not link. */
|
|
101
|
+
export const unlinkedClosingIssues = (
|
|
102
|
+
repo: RepoRef,
|
|
103
|
+
pulls: readonly Readonly<{
|
|
104
|
+
bodyHtml: string;
|
|
105
|
+
linked: readonly ClosingIssueAnswer[];
|
|
106
|
+
}>[],
|
|
107
|
+
): readonly number[] => {
|
|
108
|
+
const linked = new Set(
|
|
109
|
+
pulls.flatMap((pull) => pull.linked.map(({ number }) => number)),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return Arr.uniq(
|
|
113
|
+
pulls.flatMap(({ bodyHtml }) => closingKeywordIssuesIn(repo, bodyHtml)),
|
|
114
|
+
)
|
|
115
|
+
.filter((number) => !linked.has(number))
|
|
116
|
+
.toSorted((a, b) => a - b);
|
|
117
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
issueOrderOf,
|
|
3
|
+
unlinkedClosingIssues,
|
|
4
|
+
waitingOnNote,
|
|
5
|
+
type ClosingIssueAnswer,
|
|
6
|
+
} from './merge-after.mjs';
|
|
7
|
+
import type { PullRequest } from './types.mjs';
|
|
8
|
+
|
|
9
|
+
const repo = { owner: 'noshiro-pf', name: 'mono' } as const;
|
|
10
|
+
|
|
11
|
+
describe(waitingOnNote, () => {
|
|
12
|
+
test('names what the order is waiting on', () => {
|
|
13
|
+
assert.deepStrictEqual(waitingOnNote(pullRequest(), [5], false), {
|
|
14
|
+
kind: 'note',
|
|
15
|
+
note: '#10: Merge-After #5, still open',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('says which of them an issue it closes asked for', () => {
|
|
20
|
+
assert.deepStrictEqual(waitingOnNote(pullRequest(), [5, 7], true, [7]), {
|
|
21
|
+
kind: 'note',
|
|
22
|
+
note: '#10: Merge-After #5, #7, still open (#7 from the Start-After of an issue it closes) — and on a Merge-After cycle, so nothing here will move it',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe(issueOrderOf, () => {
|
|
28
|
+
test('reads what GitHub links and what the body closes by keyword', () => {
|
|
29
|
+
const order = issueOrderOf(
|
|
30
|
+
repo,
|
|
31
|
+
[
|
|
32
|
+
{
|
|
33
|
+
number: 10,
|
|
34
|
+
bodyHtml: '',
|
|
35
|
+
// What GitHub records as blocking it counts as the line does.
|
|
36
|
+
linked: [issue(1, 'Start-After: #20', 'OPEN', [21])],
|
|
37
|
+
},
|
|
38
|
+
// Stacked: GitHub links nothing, and marks the keyword.
|
|
39
|
+
{ number: 11, bodyHtml: closes(2), linked: [] },
|
|
40
|
+
],
|
|
41
|
+
new Map([[2, issue(2, 'Start-After: #3, #4')]]),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.deepStrictEqual(
|
|
45
|
+
order.closes,
|
|
46
|
+
new Map([
|
|
47
|
+
[10, [1]],
|
|
48
|
+
[11, [2]],
|
|
49
|
+
]),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert.deepStrictEqual(
|
|
53
|
+
order.startAfter,
|
|
54
|
+
new Map([
|
|
55
|
+
[2, [3, 4]],
|
|
56
|
+
[1, [20, 21]],
|
|
57
|
+
]),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a closed issue, or one GitHub would not describe, closes nothing', () => {
|
|
62
|
+
const order = issueOrderOf(
|
|
63
|
+
repo,
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
number: 10,
|
|
67
|
+
bodyHtml: closes(3),
|
|
68
|
+
linked: [issue(1, 'Start-After: #20', 'CLOSED')],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
new Map(),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.deepStrictEqual(order.closes, new Map([[10, []]]));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe(unlinkedClosingIssues, () => {
|
|
79
|
+
test('lists the keyword-closed issues GitHub did not link, once each', () => {
|
|
80
|
+
assert.deepStrictEqual(
|
|
81
|
+
unlinkedClosingIssues(repo, [
|
|
82
|
+
{ bodyHtml: `${closes(5)}${closes(1)}`, linked: [issue(1, '')] },
|
|
83
|
+
{ bodyHtml: closes(5), linked: [] },
|
|
84
|
+
]),
|
|
85
|
+
[5],
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const pullRequest = (): PullRequest =>
|
|
91
|
+
({
|
|
92
|
+
number: 10,
|
|
93
|
+
id: 'PR_10',
|
|
94
|
+
title: '#10',
|
|
95
|
+
body: '',
|
|
96
|
+
state: 'OPEN',
|
|
97
|
+
headRefName: 'fix/a-thing',
|
|
98
|
+
headRefOid: 'c'.repeat(40),
|
|
99
|
+
baseRefName: 'main',
|
|
100
|
+
isCrossRepository: false,
|
|
101
|
+
isDraft: false,
|
|
102
|
+
mergeStateStatus: 'BEHIND',
|
|
103
|
+
autoMergeRequest: null,
|
|
104
|
+
labels: [{ name: 'merge-queued' }],
|
|
105
|
+
}) as const;
|
|
106
|
+
|
|
107
|
+
const issue = (
|
|
108
|
+
number: number,
|
|
109
|
+
body: string,
|
|
110
|
+
state: string = 'OPEN',
|
|
111
|
+
blockedBy: readonly number[] = [],
|
|
112
|
+
): ClosingIssueAnswer => ({ number, state, body, blockedBy }) as const;
|
|
113
|
+
|
|
114
|
+
/** A closing keyword as GitHub renders one it recognized. */
|
|
115
|
+
const closes = (number: number): string =>
|
|
116
|
+
[
|
|
117
|
+
'<p dir="auto"><span class="issue-keyword">Closes</span> ',
|
|
118
|
+
`<a class="issue-link js-issue-link" data-url="https://github.com/${repo.owner}/${repo.name}/issues/${number}"`,
|
|
119
|
+
` data-hovercard-type="issue" href="https://github.com/${repo.owner}/${repo.name}/issues/${number}">#${number}</a></p>`,
|
|
120
|
+
].join('');
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** What the command line says, and how it is read. */
|
|
2
|
+
|
|
3
|
+
import { MERGE_QUEUED_LABEL, SKIP_CI_LABEL } from 'pr-report-core';
|
|
4
|
+
import { Result } from 'ts-data-forge';
|
|
5
|
+
import { parseCommandArgs, positiveInteger } from '../lib/index.mjs';
|
|
6
|
+
|
|
7
|
+
export type Options = Readonly<{
|
|
8
|
+
/** Run one cycle — survey, act on one pull request, report — and exit. */
|
|
9
|
+
once: boolean;
|
|
10
|
+
/** Survey and say what would be done, without rebasing or pushing. */
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
/** How long to wait between surveys while the list is still changing. */
|
|
13
|
+
activeIntervalSec: number;
|
|
14
|
+
/**
|
|
15
|
+
* How many surveys in a row must see an unchanged list before the wait
|
|
16
|
+
* between them grows to `idleIntervalSec`.
|
|
17
|
+
*/
|
|
18
|
+
idleAfter: number;
|
|
19
|
+
/** How long to wait between surveys once the list has sat still. */
|
|
20
|
+
idleIntervalSec: number;
|
|
21
|
+
/** How often to poll the pull request being watched. */
|
|
22
|
+
pollIntervalSec: number;
|
|
23
|
+
/** How long to watch one pull request before giving up on it. */
|
|
24
|
+
watchTimeoutMin: number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a watched pull request whose failure is only a fixer's diff has
|
|
27
|
+
* the fixer run, amended onto its commit and pushed. See `auto-fix.mts`.
|
|
28
|
+
*/
|
|
29
|
+
autoFix: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The repository, `owner/name`, when it is not the one `origin` names
|
|
32
|
+
* (`lib/github-api.mts`'s `resolveRepo`).
|
|
33
|
+
*/
|
|
34
|
+
repo: string | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export const defaultOptions: Options = {
|
|
38
|
+
once: false,
|
|
39
|
+
dryRun: false,
|
|
40
|
+
activeIntervalSec: 30,
|
|
41
|
+
idleAfter: 10,
|
|
42
|
+
idleIntervalSec: 300,
|
|
43
|
+
pollIntervalSec: 30,
|
|
44
|
+
watchTimeoutMin: 90,
|
|
45
|
+
autoFix: true,
|
|
46
|
+
repo: undefined,
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export const HELP = [
|
|
50
|
+
'Usage: unblock-prs [options]',
|
|
51
|
+
'',
|
|
52
|
+
`Takes the ${MERGE_QUEUED_LABEL} pull requests in the order they declare with`,
|
|
53
|
+
'`Merge-After:`, one at a time: rebases the one that is out of date with the',
|
|
54
|
+
`default branch, takes ${SKIP_CI_LABEL} off it, and waits for GitHub to merge it.`,
|
|
55
|
+
'When its checks fail only because a fix: or gen: entry of the check matrices',
|
|
56
|
+
'left a diff, runs that command, amends the result and pushes it, once.',
|
|
57
|
+
'',
|
|
58
|
+
'Options:',
|
|
59
|
+
' --once run one cycle and exit',
|
|
60
|
+
' --dry-run survey and report what would be done, then exit',
|
|
61
|
+
` --active-interval <sec> wait between surveys while the list is changing (default ${defaultOptions.activeIntervalSec})`,
|
|
62
|
+
` --idle-after <n> unchanged surveys before slowing to --idle-interval (default ${defaultOptions.idleAfter})`,
|
|
63
|
+
` --idle-interval <sec> wait between surveys once the list has sat still (default ${defaultOptions.idleIntervalSec})`,
|
|
64
|
+
` --poll-interval <sec> wait between polls of the watched pull request (default ${defaultOptions.pollIntervalSec})`,
|
|
65
|
+
` --watch-timeout <min> give up on a pull request after this long (default ${defaultOptions.watchTimeoutMin})`,
|
|
66
|
+
" --no-auto-fix leave a failure that is only a fixer's diff as it is",
|
|
67
|
+
' --repo <owner/name> the repository, when origin does not name it',
|
|
68
|
+
' -h, --help show this help',
|
|
69
|
+
].join('\n');
|
|
70
|
+
|
|
71
|
+
export const parseOptions = (
|
|
72
|
+
args: readonly string[],
|
|
73
|
+
): Result<Options | 'help', string> => {
|
|
74
|
+
const parsed = parseCommandArgs(args, {
|
|
75
|
+
once: { type: 'boolean', default: false },
|
|
76
|
+
'dry-run': { type: 'boolean', default: false },
|
|
77
|
+
'active-interval': { type: 'string' },
|
|
78
|
+
'idle-after': { type: 'string' },
|
|
79
|
+
'idle-interval': { type: 'string' },
|
|
80
|
+
'poll-interval': { type: 'string' },
|
|
81
|
+
'watch-timeout': { type: 'string' },
|
|
82
|
+
'no-auto-fix': { type: 'boolean', default: false },
|
|
83
|
+
repo: { type: 'string' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (Result.isErr(parsed)) {
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (parsed.value === 'help') {
|
|
91
|
+
return Result.ok('help');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const values = parsed.value;
|
|
95
|
+
|
|
96
|
+
const activeIntervalSec = positiveInteger(
|
|
97
|
+
'active-interval',
|
|
98
|
+
values['active-interval'],
|
|
99
|
+
defaultOptions.activeIntervalSec,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (Result.isErr(activeIntervalSec)) {
|
|
103
|
+
return activeIntervalSec;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const idleAfter = positiveInteger(
|
|
107
|
+
'idle-after',
|
|
108
|
+
values['idle-after'],
|
|
109
|
+
defaultOptions.idleAfter,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (Result.isErr(idleAfter)) {
|
|
113
|
+
return idleAfter;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const idleIntervalSec = positiveInteger(
|
|
117
|
+
'idle-interval',
|
|
118
|
+
values['idle-interval'],
|
|
119
|
+
defaultOptions.idleIntervalSec,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (Result.isErr(idleIntervalSec)) {
|
|
123
|
+
return idleIntervalSec;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pollIntervalSec = positiveInteger(
|
|
127
|
+
'poll-interval',
|
|
128
|
+
values['poll-interval'],
|
|
129
|
+
defaultOptions.pollIntervalSec,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (Result.isErr(pollIntervalSec)) {
|
|
133
|
+
return pollIntervalSec;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const watchTimeoutMin = positiveInteger(
|
|
137
|
+
'watch-timeout',
|
|
138
|
+
values['watch-timeout'],
|
|
139
|
+
defaultOptions.watchTimeoutMin,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (Result.isErr(watchTimeoutMin)) {
|
|
143
|
+
return watchTimeoutMin;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Result.ok({
|
|
147
|
+
once: values.once,
|
|
148
|
+
dryRun: values['dry-run'],
|
|
149
|
+
activeIntervalSec: activeIntervalSec.value,
|
|
150
|
+
idleAfter: idleAfter.value,
|
|
151
|
+
idleIntervalSec: idleIntervalSec.value,
|
|
152
|
+
pollIntervalSec: pollIntervalSec.value,
|
|
153
|
+
watchTimeoutMin: watchTimeoutMin.value,
|
|
154
|
+
autoFix: !values['no-auto-fix'],
|
|
155
|
+
repo: values.repo,
|
|
156
|
+
});
|
|
157
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who moves each pull request next, as the worker reads it: `pr-report-core`'s
|
|
3
|
+
* `queueOwner`, the one function `pr-report`, `claim-pr` and every other
|
|
4
|
+
* command a session runs read it through, given the facts GitHub returned.
|
|
5
|
+
*
|
|
6
|
+
* The set-aside comes from the records, which by the time triage reads them
|
|
7
|
+
* are what this account's comments say (`settleSkips`): a record the base
|
|
8
|
+
* moved under is pruned as its comment is rewritten as resolved, and one an
|
|
9
|
+
* earlier run left is taken up from its comment. The claims are the ones
|
|
10
|
+
* that still hold (`claims.mts`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { queueOwner, type Claim, type QueueOwner } from 'pr-report-core';
|
|
14
|
+
import type { Classification, PullRequest, SkipRecords } from './types.mjs';
|
|
15
|
+
|
|
16
|
+
export type OwnerContext = Readonly<{
|
|
17
|
+
defaultBranch: string;
|
|
18
|
+
skipped: SkipRecords;
|
|
19
|
+
claimed: ReadonlyMap<number, Claim>;
|
|
20
|
+
/** Which open pull request each stacked one is on: child → parent. */
|
|
21
|
+
stackParents: ReadonlyMap<number, number>;
|
|
22
|
+
/** Every number that sits on a `Merge-After` cycle. */
|
|
23
|
+
cyclic: ReadonlySet<number>;
|
|
24
|
+
nowEpochMs: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
/** Who moves `pr` next, or `undefined` outside the queue. */
|
|
28
|
+
export const ownerOf = (
|
|
29
|
+
pr: PullRequest,
|
|
30
|
+
context: OwnerContext,
|
|
31
|
+
): QueueOwner | undefined => {
|
|
32
|
+
const skip = context.skipped.get(pr.number);
|
|
33
|
+
|
|
34
|
+
const claim = context.claimed.get(pr.number);
|
|
35
|
+
|
|
36
|
+
return queueOwner({
|
|
37
|
+
number: pr.number,
|
|
38
|
+
labels: pr.labels.map(({ name }) => name),
|
|
39
|
+
isDraft: pr.isDraft,
|
|
40
|
+
fromFork: pr.isCrossRepository,
|
|
41
|
+
headRef: pr.headRefName,
|
|
42
|
+
headSha: pr.headRefOid,
|
|
43
|
+
baseRef: pr.baseRefName,
|
|
44
|
+
defaultBranch: context.defaultBranch,
|
|
45
|
+
stackedOn: context.stackParents.get(pr.number),
|
|
46
|
+
inCycle: context.cyclic.has(pr.number),
|
|
47
|
+
setAside:
|
|
48
|
+
skip === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: {
|
|
51
|
+
kind: 'standing',
|
|
52
|
+
setAside: {
|
|
53
|
+
reason: skip.reason,
|
|
54
|
+
headSha: skip.headSha,
|
|
55
|
+
baseSha: skip.baseSha,
|
|
56
|
+
},
|
|
57
|
+
retry: false,
|
|
58
|
+
},
|
|
59
|
+
claim: claim === undefined ? undefined : { kind: 'standing', claim },
|
|
60
|
+
nowEpochMs: context.nowEpochMs,
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What triage says about a pull request that is not the worker's to move: a
|
|
66
|
+
* note naming its owner and why, with the record's detail when one set it
|
|
67
|
+
* aside. `undefined` for one the worker moves, or one outside the queue.
|
|
68
|
+
*/
|
|
69
|
+
export const ownerNote = (
|
|
70
|
+
pr: PullRequest,
|
|
71
|
+
owner: QueueOwner | undefined,
|
|
72
|
+
skipped: SkipRecords,
|
|
73
|
+
): Classification | undefined => {
|
|
74
|
+
if (owner === undefined || owner.owner === 'worker') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const skip = skipped.get(pr.number);
|
|
79
|
+
|
|
80
|
+
const whose =
|
|
81
|
+
owner.owner === 'skill'
|
|
82
|
+
? "the /unblock-prs skill's"
|
|
83
|
+
: owner.owner === 'human'
|
|
84
|
+
? "a person's"
|
|
85
|
+
: "nobody's — a bug";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
kind: 'note',
|
|
89
|
+
note: `#${pr.number}: ${whose} (${owner.reason})${skip === undefined ? '' : `; set aside: ${skip.detail}`}`,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Claim } from 'pr-report-core';
|
|
2
|
+
import { ownerNote, ownerOf, type OwnerContext } from './owner.mjs';
|
|
3
|
+
import type { PullRequest, SkipRecord } from './types.mjs';
|
|
4
|
+
|
|
5
|
+
const HEAD = 'c'.repeat(40);
|
|
6
|
+
|
|
7
|
+
const NOW = Date.parse('2026-10-15T00:30:00Z');
|
|
8
|
+
|
|
9
|
+
const pullRequest = (fields: Partial<PullRequest> = {}): PullRequest =>
|
|
10
|
+
({
|
|
11
|
+
number: 2267,
|
|
12
|
+
id: 'PR_2267',
|
|
13
|
+
title: '#2267',
|
|
14
|
+
body: '',
|
|
15
|
+
state: 'OPEN',
|
|
16
|
+
headRefName: 'fix/a-thing',
|
|
17
|
+
headRefOid: HEAD,
|
|
18
|
+
baseRefName: 'main',
|
|
19
|
+
isCrossRepository: false,
|
|
20
|
+
isDraft: false,
|
|
21
|
+
mergeStateStatus: 'BLOCKED',
|
|
22
|
+
autoMergeRequest: null,
|
|
23
|
+
labels: [{ name: 'merge-queued' }, { name: 'skip-ci' }],
|
|
24
|
+
...fields,
|
|
25
|
+
}) as const;
|
|
26
|
+
|
|
27
|
+
const SKIP: SkipRecord = {
|
|
28
|
+
number: 2267,
|
|
29
|
+
headSha: HEAD,
|
|
30
|
+
baseSha: 'b'.repeat(40),
|
|
31
|
+
reason: 'rebase-failed',
|
|
32
|
+
detail: 'conflicts in a.mts',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
const CLAIM: Claim = {
|
|
36
|
+
headSha: HEAD,
|
|
37
|
+
by: 'unblock-wren',
|
|
38
|
+
at: '2026-10-15T00:00:00Z',
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
const context = (fields: Partial<OwnerContext> = {}): OwnerContext =>
|
|
42
|
+
({
|
|
43
|
+
defaultBranch: 'main',
|
|
44
|
+
skipped: new Map(),
|
|
45
|
+
claimed: new Map(),
|
|
46
|
+
stackParents: new Map(),
|
|
47
|
+
cyclic: new Set(),
|
|
48
|
+
nowEpochMs: NOW,
|
|
49
|
+
...fields,
|
|
50
|
+
}) as const;
|
|
51
|
+
|
|
52
|
+
describe(ownerOf, () => {
|
|
53
|
+
test('is the worker’s with nothing set aside or claimed', () => {
|
|
54
|
+
assert.strictEqual(ownerOf(pullRequest(), context())?.owner, 'worker');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('reads a record as the comment it was taken up from', () => {
|
|
58
|
+
assert.deepStrictEqual(
|
|
59
|
+
ownerOf(pullRequest(), context({ skipped: new Map([[2267, SKIP]]) })),
|
|
60
|
+
{
|
|
61
|
+
owner: 'skill',
|
|
62
|
+
reason: 'set aside `rebase-failed` at its head; the skill fixes it',
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('is the claimant’s while the claim holds', () => {
|
|
68
|
+
assert.strictEqual(
|
|
69
|
+
ownerOf(pullRequest(), context({ claimed: new Map([[2267, CLAIM]]) }))
|
|
70
|
+
?.owner,
|
|
71
|
+
'skill',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('is undefined outside the queue', () => {
|
|
76
|
+
assert.isUndefined(ownerOf(pullRequest({ labels: [] }), context()));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe(ownerNote, () => {
|
|
81
|
+
test('says whose a set-aside pull request is, and why it was set aside', () => {
|
|
82
|
+
const skipped = new Map([[2267, SKIP]]);
|
|
83
|
+
|
|
84
|
+
assert.deepStrictEqual(
|
|
85
|
+
ownerNote(
|
|
86
|
+
pullRequest(),
|
|
87
|
+
ownerOf(pullRequest(), context({ skipped })),
|
|
88
|
+
skipped,
|
|
89
|
+
),
|
|
90
|
+
{
|
|
91
|
+
kind: 'note',
|
|
92
|
+
note: "#2267: the /unblock-prs skill's (set aside `rebase-failed` at its head; the skill fixes it); set aside: conflicts in a.mts",
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('names the claimant', () => {
|
|
98
|
+
const claimed = new Map([[2267, CLAIM]]);
|
|
99
|
+
|
|
100
|
+
const note = ownerNote(
|
|
101
|
+
pullRequest(),
|
|
102
|
+
ownerOf(pullRequest(), context({ claimed })),
|
|
103
|
+
new Map(),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
assert.include(note?.kind === 'note' ? note.note : '', 'unblock-wren');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('says nothing about the worker’s own', () => {
|
|
110
|
+
assert.isUndefined(
|
|
111
|
+
ownerNote(pullRequest(), ownerOf(pullRequest(), context()), new Map()),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long the loop sleeps when there is nothing to do: briefly while the
|
|
3
|
+
* list is still changing, so a pull request queued a moment ago is picked up
|
|
4
|
+
* in seconds, and at the idle interval once it has sat still for a while.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isRecord } from 'ts-data-forge';
|
|
8
|
+
import type { StrictPick } from 'ts-type-forge';
|
|
9
|
+
import type { Options } from './options.mjs';
|
|
10
|
+
import type { Quiet, Survey } from './types.mjs';
|
|
11
|
+
|
|
12
|
+
export const initialQuiet: Quiet = {
|
|
13
|
+
fingerprint: undefined,
|
|
14
|
+
unchanged: 0,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The part of a survey triage reads: the base, and for every open pull
|
|
19
|
+
* request its head, merge state, labels, auto-merge, draft flag and body —
|
|
20
|
+
* the body because that is where `Merge-After:` is declared — and what the
|
|
21
|
+
* reviews hold, which triage reads separately: an approval or a resolved
|
|
22
|
+
* conversation is exactly what a held pull request is waiting for. The title
|
|
23
|
+
* is left out, as is the order GitHub lists things in.
|
|
24
|
+
*/
|
|
25
|
+
export const surveyFingerprint = (
|
|
26
|
+
survey: StrictPick<Survey, 'baseSha' | 'pullRequests'>,
|
|
27
|
+
reviewHolds: readonly string[] = [],
|
|
28
|
+
): string =>
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
baseSha: survey.baseSha,
|
|
31
|
+
reviewHolds: reviewHolds.toSorted(),
|
|
32
|
+
pullRequests: survey.pullRequests
|
|
33
|
+
.toSorted((a, b) => a.number - b.number)
|
|
34
|
+
.map((pr) => [
|
|
35
|
+
pr.number,
|
|
36
|
+
pr.headRefOid,
|
|
37
|
+
pr.baseRefName,
|
|
38
|
+
pr.mergeStateStatus,
|
|
39
|
+
pr.isDraft,
|
|
40
|
+
isRecord(pr.autoMergeRequest),
|
|
41
|
+
pr.labels.map((label) => label.name).toSorted(),
|
|
42
|
+
pr.body,
|
|
43
|
+
]),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const observeSurvey = (quiet: Quiet, fingerprint: string): Quiet =>
|
|
47
|
+
({
|
|
48
|
+
fingerprint,
|
|
49
|
+
unchanged:
|
|
50
|
+
quiet.fingerprint === fingerprint ? quiet.unchanged + 1 : (0 as const),
|
|
51
|
+
}) as const;
|
|
52
|
+
|
|
53
|
+
export const idleWaitSec = (
|
|
54
|
+
quiet: Quiet,
|
|
55
|
+
options: StrictPick<
|
|
56
|
+
Options,
|
|
57
|
+
'activeIntervalSec' | 'idleAfter' | 'idleIntervalSec'
|
|
58
|
+
>,
|
|
59
|
+
): number =>
|
|
60
|
+
quiet.unchanged >= options.idleAfter
|
|
61
|
+
? options.idleIntervalSec
|
|
62
|
+
: options.activeIntervalSec;
|