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,911 @@
|
|
|
1
|
+
/** Everything that talks to GitHub, and nothing that decides. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
anyRunInProgress,
|
|
5
|
+
closingIssuesIn,
|
|
6
|
+
epochMsOf,
|
|
7
|
+
parseMergeAfter,
|
|
8
|
+
readQueueComments,
|
|
9
|
+
referenceStatusOf,
|
|
10
|
+
reportedContexts,
|
|
11
|
+
requirementsOfRules,
|
|
12
|
+
waitsFor,
|
|
13
|
+
type CheckRunReport,
|
|
14
|
+
type Comparison,
|
|
15
|
+
type ContextState,
|
|
16
|
+
type Label,
|
|
17
|
+
type LinkedIssue,
|
|
18
|
+
type MergedPullRequest,
|
|
19
|
+
type OpenIssue,
|
|
20
|
+
type PullRequestFacts,
|
|
21
|
+
type QueueComments,
|
|
22
|
+
type ReferenceStatus,
|
|
23
|
+
type RepoRef,
|
|
24
|
+
} from 'pr-report-core';
|
|
25
|
+
import { Arr, Result } from 'ts-data-forge';
|
|
26
|
+
import * as t from 'ts-fortress';
|
|
27
|
+
import { currentEpochMs, parseJson, send } from '../lib/index.mjs';
|
|
28
|
+
import { parseClosingIssueRefs } from './linked-issues.mjs';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How many pull requests are read. A repository with more open than this has
|
|
32
|
+
* a different problem than an unreadable report, so this fails loudly rather
|
|
33
|
+
* than paginating and reporting half of it as the whole.
|
|
34
|
+
*/
|
|
35
|
+
const PAGE_SIZE = 100;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How many pages of check runs, or of workflow runs, one commit may have. A
|
|
39
|
+
* bound rather than a `while (true)`: a paginating loop against an answer that
|
|
40
|
+
* never says it has ended is the one bug in a read-only report that costs a
|
|
41
|
+
* rate limit.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_CHECK_RUN_PAGES = 10;
|
|
44
|
+
|
|
45
|
+
const PullRequestSchema = t.record({
|
|
46
|
+
number: t.number(),
|
|
47
|
+
title: t.string(),
|
|
48
|
+
body: t.union([t.string(), t.nullType]),
|
|
49
|
+
draft: t.boolean(),
|
|
50
|
+
html_url: t.string(),
|
|
51
|
+
updated_at: t.string(),
|
|
52
|
+
user: t.union([t.record({ login: t.string() }), t.nullType]),
|
|
53
|
+
// The colour is read so that a chip can be the colour GitHub shows; the
|
|
54
|
+
// description so that hovering one says what the label means.
|
|
55
|
+
labels: t.array(
|
|
56
|
+
t.record({
|
|
57
|
+
name: t.string(),
|
|
58
|
+
color: t.string(),
|
|
59
|
+
description: t.union([t.string(), t.nullType]),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
merged_at: t.union([t.string(), t.nullType]),
|
|
63
|
+
// `null` until auto-merge is armed, and an object describing it after.
|
|
64
|
+
// Only its presence is read: what the report answers is whether anything
|
|
65
|
+
// will land the pull request once the checks go green.
|
|
66
|
+
auto_merge: t.union([t.record({}), t.nullType]),
|
|
67
|
+
// `repo` is `null` for a head whose fork has been deleted. Read so that a
|
|
68
|
+
// fork's branch is not taken for the parent of a stacked pull request.
|
|
69
|
+
head: t.record({
|
|
70
|
+
ref: t.string(),
|
|
71
|
+
sha: t.string(),
|
|
72
|
+
repo: t.union([t.record({ full_name: t.string() }), t.nullType]),
|
|
73
|
+
}),
|
|
74
|
+
base: t.record({ ref: t.string() }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const PullRequestListSchema = t.array(PullRequestSchema);
|
|
78
|
+
|
|
79
|
+
const ComparisonSchema = t.record({
|
|
80
|
+
ahead_by: t.number(),
|
|
81
|
+
behind_by: t.number(),
|
|
82
|
+
/**
|
|
83
|
+
* The commits the head has that the base does not, oldest first, so the
|
|
84
|
+
* last is the head commit — read for its date, which comes free with the
|
|
85
|
+
* counts rather than as a request of its own. Truncated on a long branch,
|
|
86
|
+
* which is why its length is checked against `ahead_by` before use.
|
|
87
|
+
*/
|
|
88
|
+
commits: t.array(
|
|
89
|
+
t.record({
|
|
90
|
+
commit: t.record({ committer: t.record({ date: t.string() }) }),
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const CheckRunsSchema = t.record({
|
|
96
|
+
total_count: t.number(),
|
|
97
|
+
check_runs: t.array(
|
|
98
|
+
t.record({
|
|
99
|
+
// Read so that two runs of one name can be told apart; which of them
|
|
100
|
+
// counts is decided in `checks.mts`.
|
|
101
|
+
id: t.number(),
|
|
102
|
+
check_suite: t.record({ id: t.number() }),
|
|
103
|
+
name: t.string(),
|
|
104
|
+
status: t.string(),
|
|
105
|
+
conclusion: t.union([t.string(), t.nullType]),
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* How many issues block one, which says whether asking which is worth a
|
|
112
|
+
* request. Absent where GitHub does not send it.
|
|
113
|
+
*/
|
|
114
|
+
const DependenciesSummarySchema = t.optional(
|
|
115
|
+
t.record({ total_blocked_by: t.number() }),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* GitHub answers pull requests from the issues endpoint too, and the only
|
|
120
|
+
* thing telling one apart in the answer is the `pull_request` key.
|
|
121
|
+
*/
|
|
122
|
+
const IssueListSchema = t.array(
|
|
123
|
+
t.record({
|
|
124
|
+
number: t.number(),
|
|
125
|
+
title: t.string(),
|
|
126
|
+
/** Where `Start-After:` is read from. */
|
|
127
|
+
body: t.union([t.string(), t.nullType]),
|
|
128
|
+
issue_dependencies_summary: DependenciesSummarySchema,
|
|
129
|
+
html_url: t.string(),
|
|
130
|
+
created_at: t.string(),
|
|
131
|
+
updated_at: t.string(),
|
|
132
|
+
comments: t.number(),
|
|
133
|
+
user: t.union([t.record({ login: t.string() }), t.nullType]),
|
|
134
|
+
labels: t.array(
|
|
135
|
+
t.record({
|
|
136
|
+
name: t.string(),
|
|
137
|
+
color: t.string(),
|
|
138
|
+
description: t.union([t.string(), t.nullType]),
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
pull_request: t.optional(t.record({})),
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const IssueBodySchema = t.record({
|
|
146
|
+
state: t.string(),
|
|
147
|
+
body: t.union([t.string(), t.nullType]),
|
|
148
|
+
pull_request: t.optional(t.record({})),
|
|
149
|
+
issue_dependencies_summary: DependenciesSummarySchema,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** The issues blocking one, of whichever repository. */
|
|
153
|
+
const BlockedByListSchema = t.array(
|
|
154
|
+
t.record({ number: t.number(), repository_url: t.string() }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
/** An issue or a pull request, as the issues route answers for either. */
|
|
158
|
+
const ReferenceSchema = t.record({
|
|
159
|
+
state: t.string(),
|
|
160
|
+
state_reason: t.optional(t.union([t.string(), t.nullType])),
|
|
161
|
+
pull_request: t.optional(
|
|
162
|
+
t.record({ merged_at: t.union([t.string(), t.nullType]) }),
|
|
163
|
+
),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const WorkflowRunsSchema = t.record({
|
|
167
|
+
total_count: t.number(),
|
|
168
|
+
workflow_runs: t.array(
|
|
169
|
+
t.record({
|
|
170
|
+
check_suite_id: t.union([t.number(), t.nullType]),
|
|
171
|
+
workflow_id: t.number(),
|
|
172
|
+
status: t.union([t.string(), t.nullType]),
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const CombinedStatusSchema = t.record({
|
|
178
|
+
statuses: t.array(t.record({ context: t.string(), state: t.string() })),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const CommentListSchema = t.array(
|
|
182
|
+
t.record({
|
|
183
|
+
id: t.number(),
|
|
184
|
+
body: t.union([t.string(), t.nullType]),
|
|
185
|
+
user: t.union([t.record({ login: t.string() }), t.nullType]),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const ClosingIssuesSchema = t.record({
|
|
190
|
+
data: t.record({
|
|
191
|
+
repository: t.record({
|
|
192
|
+
pullRequests: t.record({
|
|
193
|
+
nodes: t.array(
|
|
194
|
+
t.record({
|
|
195
|
+
number: t.number(),
|
|
196
|
+
// `null` for an issue the token may not see, which
|
|
197
|
+
// `closingIssuesIn` skips: one unreadable issue used to fail this
|
|
198
|
+
// whole answer and drop every pull request's list with it.
|
|
199
|
+
closingIssuesReferences: t.record({
|
|
200
|
+
nodes: t.array(
|
|
201
|
+
t.union([
|
|
202
|
+
t.record({
|
|
203
|
+
number: t.number(),
|
|
204
|
+
title: t.string(),
|
|
205
|
+
url: t.string(),
|
|
206
|
+
state: t.string(),
|
|
207
|
+
repository: t.record({ nameWithOwner: t.string() }),
|
|
208
|
+
}),
|
|
209
|
+
t.nullType,
|
|
210
|
+
]),
|
|
211
|
+
),
|
|
212
|
+
}),
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
}),
|
|
216
|
+
}),
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export type Client = Readonly<{
|
|
221
|
+
/** Whether a token was found, which decides how much can be read. */
|
|
222
|
+
authenticated: boolean;
|
|
223
|
+
/** What pull requests target unless they are stacked. */
|
|
224
|
+
defaultBranch: (repo: RepoRef) => Promise<Result<string, string>>;
|
|
225
|
+
/**
|
|
226
|
+
* The contexts GitHub requires on `branch`, every ruleset that applies to
|
|
227
|
+
* it together (`pr-report-core`'s `requirementsOfRules`).
|
|
228
|
+
*/
|
|
229
|
+
requiredContexts: (
|
|
230
|
+
repo: RepoRef,
|
|
231
|
+
branch: string,
|
|
232
|
+
) => Promise<Result<readonly string[], string>>;
|
|
233
|
+
facts: (
|
|
234
|
+
repo: RepoRef,
|
|
235
|
+
) => Promise<Result<readonly PullRequestFacts[], string>>;
|
|
236
|
+
/**
|
|
237
|
+
* The pull requests that landed in the last `withinDays` days, newest
|
|
238
|
+
* first. One request: the closed list, sorted by when it last changed.
|
|
239
|
+
*/
|
|
240
|
+
merged: (
|
|
241
|
+
repo: RepoRef,
|
|
242
|
+
withinDays: number,
|
|
243
|
+
limit: number,
|
|
244
|
+
) => Promise<Result<readonly MergedPullRequest[], string>>;
|
|
245
|
+
/**
|
|
246
|
+
* What the queue's comments say on each of `pulls` — the set-aside comment
|
|
247
|
+
* `unblock-prs` writes and the claim of whoever is fixing it — by the
|
|
248
|
+
* token's own account, which is what both are trusted by. Empty without a
|
|
249
|
+
* token, or one whose account cannot be read.
|
|
250
|
+
*/
|
|
251
|
+
queueComments: (
|
|
252
|
+
repo: RepoRef,
|
|
253
|
+
defaultBranch: string,
|
|
254
|
+
pulls: readonly PullRequestFacts[],
|
|
255
|
+
) => Promise<ReadonlyMap<number, QueueComments>>;
|
|
256
|
+
/** The open issues, most recently updated first, at most `limit`. */
|
|
257
|
+
issues: (
|
|
258
|
+
repo: RepoRef,
|
|
259
|
+
limit: number,
|
|
260
|
+
) => Promise<Result<readonly OpenIssue[], string>>;
|
|
261
|
+
/**
|
|
262
|
+
* Whether each of `numbers` is an open issue, and what its body declares
|
|
263
|
+
* with `Start-After:`: one request each, for the issues the open pull
|
|
264
|
+
* requests close that the open list did not cover. One that cannot be read
|
|
265
|
+
* is left out, and declares nothing.
|
|
266
|
+
*/
|
|
267
|
+
issueBodies: (
|
|
268
|
+
repo: RepoRef,
|
|
269
|
+
numbers: readonly number[],
|
|
270
|
+
) => Promise<ReadonlyMap<number, IssueBody>>;
|
|
271
|
+
/**
|
|
272
|
+
* Where each of `numbers` — what the issues' `Start-After:` names beyond
|
|
273
|
+
* the open lists — stands: one request each, an issue or a pull request
|
|
274
|
+
* alike. One that cannot be read is left out, and is unknown.
|
|
275
|
+
*/
|
|
276
|
+
references: (
|
|
277
|
+
repo: RepoRef,
|
|
278
|
+
numbers: readonly number[],
|
|
279
|
+
) => Promise<ReadonlyMap<number, ReferenceStatus>>;
|
|
280
|
+
}>;
|
|
281
|
+
|
|
282
|
+
/** What {@link Client.issueBodies} reads of one issue. */
|
|
283
|
+
export type IssueBody = Readonly<{
|
|
284
|
+
open: boolean;
|
|
285
|
+
startAfter: readonly number[];
|
|
286
|
+
}>;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A client for the public REST API, using a token when one is in the
|
|
290
|
+
* environment and going without when there is not.
|
|
291
|
+
*
|
|
292
|
+
* Unauthenticated works because the repository is public, and one report a
|
|
293
|
+
* day fits inside the 60 requests an hour an anonymous caller gets — roughly
|
|
294
|
+
* three per pull request, for the comparison and the two kinds of check.
|
|
295
|
+
* A token raises that to 5000 and, through one GraphQL query, replaces the
|
|
296
|
+
* issue links read out of the bodies with the ones GitHub itself holds.
|
|
297
|
+
*/
|
|
298
|
+
export const createClient = (
|
|
299
|
+
token: string | undefined,
|
|
300
|
+
baseUrl = 'https://api.github.com',
|
|
301
|
+
): Client => {
|
|
302
|
+
/**
|
|
303
|
+
* Set by the first answer that says the quota is spent. Every call after
|
|
304
|
+
* one of those fails the same way, and each failure is swallowed into
|
|
305
|
+
* "nothing reported" — a branch with no commit counts and a pull request
|
|
306
|
+
* with no checks. A report that says that is worse than no report, so the
|
|
307
|
+
* flag turns the whole run into one error naming the cause.
|
|
308
|
+
*/
|
|
309
|
+
const mut_state = { rateLimited: false };
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* One call. A body makes it a POST, which is only ever the GraphQL query —
|
|
313
|
+
* everything else this reads is a GET, so there is nothing to configure.
|
|
314
|
+
*/
|
|
315
|
+
const request = async (
|
|
316
|
+
route: string,
|
|
317
|
+
body?: string,
|
|
318
|
+
): Promise<Result<string, string>> => {
|
|
319
|
+
const response = await send({
|
|
320
|
+
url: `${baseUrl}${route}`,
|
|
321
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
322
|
+
token,
|
|
323
|
+
accept: 'application/vnd.github+json',
|
|
324
|
+
body,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (Result.isOk(response)) {
|
|
328
|
+
return response;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (response.value.rateLimited) {
|
|
332
|
+
mut_state.rateLimited = true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const refused = `${route} answered ${response.value.message}` as const;
|
|
336
|
+
|
|
337
|
+
return Result.err(
|
|
338
|
+
response.value.rateLimited
|
|
339
|
+
? `${refused}\nThe rate limit is spent. Set GH_TOKEN or GITHUB_TOKEN to raise it from 60 requests an hour to 5000.`
|
|
340
|
+
: refused,
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const getJson = async <A,>(
|
|
345
|
+
route: string,
|
|
346
|
+
schema: t.Type<A>,
|
|
347
|
+
): Promise<Result<A, string>> => {
|
|
348
|
+
const text = await request(route);
|
|
349
|
+
|
|
350
|
+
return Result.isErr(text) ? text : parseJson(text.value, schema, route);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const comparison = async (
|
|
354
|
+
repo: RepoRef,
|
|
355
|
+
base: string,
|
|
356
|
+
headSha: string,
|
|
357
|
+
): Promise<
|
|
358
|
+
Readonly<{
|
|
359
|
+
comparison: Comparison | undefined;
|
|
360
|
+
headCommittedAt: string | undefined;
|
|
361
|
+
}>
|
|
362
|
+
> => {
|
|
363
|
+
const compared = await getJson(
|
|
364
|
+
// A base with a slash in it has to be escaped, and only the slash:
|
|
365
|
+
// GitHub reads `compare/{base}...{head}` as one path segment.
|
|
366
|
+
`/repos/${repo.owner}/${repo.name}/compare/${base.replaceAll('/', '%2F')}...${headSha}`,
|
|
367
|
+
ComparisonSchema,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (Result.isErr(compared)) {
|
|
371
|
+
return { comparison: undefined, headCommittedAt: undefined };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const { ahead_by, behind_by, commits } = compared.value;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
comparison: { aheadBy: ahead_by, behindBy: behind_by },
|
|
378
|
+
// Only when the list is whole: on a branch too long for one answer
|
|
379
|
+
// the last commit listed is not the head. A branch level with its
|
|
380
|
+
// base has no commit of its own to date.
|
|
381
|
+
headCommittedAt:
|
|
382
|
+
commits.length === ahead_by
|
|
383
|
+
? commits.at(-1)?.commit.committer.date
|
|
384
|
+
: undefined,
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Every check run on one commit, across as many pages as it takes.
|
|
390
|
+
*
|
|
391
|
+
* One page is not enough and has not been for a while: each package
|
|
392
|
+
* contributes two codecov runs, and the aggregates, the matrix jobs and
|
|
393
|
+
* the gates are on top of that. A commit that overflows the page would
|
|
394
|
+
* quietly lose whichever required contexts fell off the end, and the
|
|
395
|
+
* report would call them "missing" — the one wording that reads as "CI has
|
|
396
|
+
* not got to it yet" rather than "this report did not look".
|
|
397
|
+
*/
|
|
398
|
+
const checkRuns = async (
|
|
399
|
+
prefix: string,
|
|
400
|
+
rounds: ReadonlyMap<number, CheckRunReport['round']>,
|
|
401
|
+
): Promise<Result<readonly CheckRunReport[], string>> => {
|
|
402
|
+
const mut_collected: CheckRunReport[] = [];
|
|
403
|
+
|
|
404
|
+
for (const page of Arr.seq(MAX_CHECK_RUN_PAGES)) {
|
|
405
|
+
const answered = await getJson(
|
|
406
|
+
`${prefix}/check-runs?per_page=${PAGE_SIZE}&page=${page + 1}`,
|
|
407
|
+
CheckRunsSchema,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (Result.isErr(answered)) {
|
|
411
|
+
return answered;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const run of answered.value.check_runs) {
|
|
415
|
+
mut_collected.push({
|
|
416
|
+
id: run.id,
|
|
417
|
+
checkSuiteId: run.check_suite.id,
|
|
418
|
+
name: run.name,
|
|
419
|
+
status: run.status,
|
|
420
|
+
conclusion: run.conclusion ?? undefined,
|
|
421
|
+
round: rounds.get(run.check_suite.id),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
Arr.isEmpty(answered.value.check_runs) ||
|
|
427
|
+
mut_collected.length >= answered.value.total_count
|
|
428
|
+
) {
|
|
429
|
+
return Result.ok(mut_collected);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return Result.err(
|
|
434
|
+
`${prefix}/check-runs has more than ${PAGE_SIZE * MAX_CHECK_RUN_PAGES} runs, which is more than this reads.`,
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Which workflow each check suite on one commit is a run of, and whether
|
|
440
|
+
* that run has finished — what the check runs do not say, and what tells
|
|
441
|
+
* a newer round still going from an unrelated suite. Empty when it cannot
|
|
442
|
+
* be read, which leaves the verdict as it was before rounds were read.
|
|
443
|
+
*/
|
|
444
|
+
const workflowRounds = async (
|
|
445
|
+
repo: RepoRef,
|
|
446
|
+
sha: string,
|
|
447
|
+
): Promise<ReadonlyMap<number, CheckRunReport['round']>> => {
|
|
448
|
+
const mut_rounds = new Map<number, CheckRunReport['round']>();
|
|
449
|
+
|
|
450
|
+
for (const page of Arr.seq(MAX_CHECK_RUN_PAGES)) {
|
|
451
|
+
const answered = await getJson(
|
|
452
|
+
`/repos/${repo.owner}/${repo.name}/actions/runs?head_sha=${sha}&per_page=${PAGE_SIZE}&page=${page + 1}`,
|
|
453
|
+
WorkflowRunsSchema,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (Result.isErr(answered)) {
|
|
457
|
+
return new Map();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const run of answered.value.workflow_runs) {
|
|
461
|
+
if (run.check_suite_id !== null) {
|
|
462
|
+
mut_rounds.set(run.check_suite_id, {
|
|
463
|
+
workflowId: run.workflow_id,
|
|
464
|
+
completed: run.status === 'completed',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (
|
|
470
|
+
Arr.isEmpty(answered.value.workflow_runs) ||
|
|
471
|
+
(page + 1) * PAGE_SIZE >= answered.value.total_count
|
|
472
|
+
) {
|
|
473
|
+
return mut_rounds;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return new Map();
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* What every context has reported on one commit, from both places GitHub
|
|
482
|
+
* keeps them. The aggregate jobs are check runs; `no-skip-ci-label` is a
|
|
483
|
+
* commit status, so reading only the first would report the context that
|
|
484
|
+
* holds every labelled pull request as missing.
|
|
485
|
+
*/
|
|
486
|
+
const contextStates = async (
|
|
487
|
+
repo: RepoRef,
|
|
488
|
+
sha: string,
|
|
489
|
+
): Promise<
|
|
490
|
+
Readonly<{ reported: ReadonlyMap<string, ContextState>; running: boolean }>
|
|
491
|
+
> => {
|
|
492
|
+
const prefix = `/repos/${repo.owner}/${repo.name}/commits/${sha}` as const;
|
|
493
|
+
|
|
494
|
+
const runs = await checkRuns(prefix, await workflowRounds(repo, sha));
|
|
495
|
+
|
|
496
|
+
const statuses = await getJson(
|
|
497
|
+
`${prefix}/status?per_page=${PAGE_SIZE}`,
|
|
498
|
+
CombinedStatusSchema,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const runList = Result.isErr(runs) ? ([] as const) : runs.value;
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
reported: reportedContexts(
|
|
505
|
+
runList,
|
|
506
|
+
Result.isErr(statuses) ? [] : statuses.value.statuses,
|
|
507
|
+
),
|
|
508
|
+
running: anyRunInProgress(runList),
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The issues of `repo` GitHub records as blocking `issue` — its issue
|
|
514
|
+
* dependencies, read as its `Start-After:` is (`waitsFor`). Asked only
|
|
515
|
+
* when the issue's summary says it has any, so an issue with none costs
|
|
516
|
+
* nothing; one that cannot be read is taken as having none.
|
|
517
|
+
*/
|
|
518
|
+
const blockedByOf = async (
|
|
519
|
+
repo: RepoRef,
|
|
520
|
+
issue: Readonly<{
|
|
521
|
+
number: number;
|
|
522
|
+
issue_dependencies_summary?:
|
|
523
|
+
Readonly<{ total_blocked_by: number }> | undefined;
|
|
524
|
+
}>,
|
|
525
|
+
): Promise<readonly number[]> => {
|
|
526
|
+
if ((issue.issue_dependencies_summary?.total_blocked_by ?? 0) === 0) {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const listed = await getJson(
|
|
531
|
+
`/repos/${repo.owner}/${repo.name}/issues/${issue.number}/dependencies/blocked_by?per_page=${PAGE_SIZE}`,
|
|
532
|
+
BlockedByListSchema,
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const own =
|
|
536
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}`.toLowerCase();
|
|
537
|
+
|
|
538
|
+
return Result.isErr(listed)
|
|
539
|
+
? []
|
|
540
|
+
: listed.value
|
|
541
|
+
.filter(({ repository_url: url }) => url.toLowerCase() === own)
|
|
542
|
+
.map(({ number }) => number);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* The issues GitHub itself says each pull request closes — the sidebar
|
|
547
|
+
* list, which includes a link made by hand and so knows more than the body
|
|
548
|
+
* does. Only GraphQL exposes it, and GraphQL refuses an anonymous caller,
|
|
549
|
+
* which is why this is the one thing a token buys.
|
|
550
|
+
*/
|
|
551
|
+
const closingIssues = async (
|
|
552
|
+
repo: RepoRef,
|
|
553
|
+
): Promise<ReadonlyMap<number, readonly LinkedIssue[]>> => {
|
|
554
|
+
const query = [
|
|
555
|
+
'query($owner: String!, $name: String!) {',
|
|
556
|
+
' repository(owner: $owner, name: $name) {',
|
|
557
|
+
` pullRequests(states: OPEN, first: ${PAGE_SIZE}) {`,
|
|
558
|
+
' nodes {',
|
|
559
|
+
' number',
|
|
560
|
+
' closingIssuesReferences(first: 10) {',
|
|
561
|
+
' nodes { number title url state repository { nameWithOwner } }',
|
|
562
|
+
' }',
|
|
563
|
+
' }',
|
|
564
|
+
' }',
|
|
565
|
+
' }',
|
|
566
|
+
'}',
|
|
567
|
+
].join('\n');
|
|
568
|
+
|
|
569
|
+
const answered = await request(
|
|
570
|
+
'/graphql',
|
|
571
|
+
JSON.stringify({
|
|
572
|
+
query,
|
|
573
|
+
variables: { owner: repo.owner, name: repo.name },
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
if (Result.isErr(answered)) {
|
|
578
|
+
return new Map();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const parsed = parseJson(answered.value, ClosingIssuesSchema, '/graphql');
|
|
582
|
+
|
|
583
|
+
if (Result.isErr(parsed)) {
|
|
584
|
+
return new Map();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return new Map(
|
|
588
|
+
parsed.value.data.repository.pullRequests.nodes.map((node) => [
|
|
589
|
+
node.number,
|
|
590
|
+
closingIssuesIn(repo, node.closingIssuesReferences.nodes),
|
|
591
|
+
]),
|
|
592
|
+
);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
authenticated: token !== undefined,
|
|
597
|
+
|
|
598
|
+
defaultBranch: async (repo) => {
|
|
599
|
+
const answered = await getJson(
|
|
600
|
+
`/repos/${repo.owner}/${repo.name}`,
|
|
601
|
+
t.record({ default_branch: t.string() }),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
return Result.isErr(answered)
|
|
605
|
+
? answered
|
|
606
|
+
: Result.ok(answered.value.default_branch);
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
requiredContexts: async (repo, branch) => {
|
|
610
|
+
// One path segment, whatever the name holds: `release/1.x` included.
|
|
611
|
+
// The rule's premise, building a query string, is not what this is.
|
|
612
|
+
// eslint-disable-next-line no-restricted-globals
|
|
613
|
+
const segment = encodeURIComponent(branch);
|
|
614
|
+
|
|
615
|
+
const answered = await getJson(
|
|
616
|
+
`/repos/${repo.owner}/${repo.name}/rules/branches/${segment}`,
|
|
617
|
+
t.array(t.unknown()),
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
return Result.map(
|
|
621
|
+
answered,
|
|
622
|
+
(rules) => requirementsOfRules(rules).requiredContexts,
|
|
623
|
+
);
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
queueComments: async (repo, defaultBranch, pulls) => {
|
|
627
|
+
// Without a token this is refused, and nothing is trusted.
|
|
628
|
+
const viewer = await getJson('/user', t.record({ login: t.string() }));
|
|
629
|
+
|
|
630
|
+
const tip = await getJson(
|
|
631
|
+
`/repos/${repo.owner}/${repo.name}/git/ref/heads/${defaultBranch}`,
|
|
632
|
+
t.record({ object: t.record({ sha: t.string() }) }),
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
if (Result.isErr(viewer) || Result.isErr(tip)) {
|
|
636
|
+
return new Map();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const nowEpochMs = currentEpochMs();
|
|
640
|
+
|
|
641
|
+
const mut_read = new Map<number, QueueComments>();
|
|
642
|
+
|
|
643
|
+
for (const pr of pulls) {
|
|
644
|
+
// The first page, oldest first: each comment is edited in place, so
|
|
645
|
+
// it stays where it was first posted.
|
|
646
|
+
const listed = await getJson(
|
|
647
|
+
`/repos/${repo.owner}/${repo.name}/issues/${pr.number}/comments?per_page=${PAGE_SIZE}`,
|
|
648
|
+
CommentListSchema,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
if (Result.isOk(listed)) {
|
|
652
|
+
mut_read.set(
|
|
653
|
+
pr.number,
|
|
654
|
+
readQueueComments(
|
|
655
|
+
Arr.generate(function* () {
|
|
656
|
+
for (const { id, body, user } of listed.value) {
|
|
657
|
+
if (user?.login === viewer.value.login) {
|
|
658
|
+
yield { id, body: body ?? '' };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}),
|
|
662
|
+
{
|
|
663
|
+
headSha: pr.headSha,
|
|
664
|
+
baseTip: tip.value.object.sha,
|
|
665
|
+
nowEpochMs,
|
|
666
|
+
},
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return mut_read;
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
issues: async (repo, limit) => {
|
|
676
|
+
// A full page rather than `limit`: pull requests come back from this
|
|
677
|
+
// endpoint too and are filtered out after, so asking for exactly the
|
|
678
|
+
// cap would let a run of recently updated pull requests crowd the
|
|
679
|
+
// issues out.
|
|
680
|
+
const listed = await getJson(
|
|
681
|
+
`/repos/${repo.owner}/${repo.name}/issues?state=open&per_page=${PAGE_SIZE}&sort=updated&direction=desc`,
|
|
682
|
+
IssueListSchema,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
if (Result.isErr(listed)) {
|
|
686
|
+
return listed;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const mut_issues: OpenIssue[] = [];
|
|
690
|
+
|
|
691
|
+
for (const issue of listed.value
|
|
692
|
+
.filter((each) => each.pull_request === undefined)
|
|
693
|
+
.slice(0, limit)) {
|
|
694
|
+
const blockedBy = await blockedByOf(repo, issue);
|
|
695
|
+
|
|
696
|
+
mut_issues.push({
|
|
697
|
+
number: issue.number,
|
|
698
|
+
title: issue.title,
|
|
699
|
+
author: issue.user?.login ?? 'unknown',
|
|
700
|
+
url: issue.html_url,
|
|
701
|
+
labels: labelsOf(issue.labels),
|
|
702
|
+
createdAt: issue.created_at,
|
|
703
|
+
updatedAt: issue.updated_at,
|
|
704
|
+
comments: issue.comments,
|
|
705
|
+
startAfter: waitsFor(issue.body ?? '', blockedBy),
|
|
706
|
+
misplacedMergeAfter: parseMergeAfter(issue.body ?? ''),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return Result.ok(mut_issues);
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
issueBodies: async (repo, numbers) => {
|
|
714
|
+
const mut_read = new Map<number, IssueBody>();
|
|
715
|
+
|
|
716
|
+
for (const number of numbers) {
|
|
717
|
+
const read = await getJson(
|
|
718
|
+
`/repos/${repo.owner}/${repo.name}/issues/${number}`,
|
|
719
|
+
IssueBodySchema,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// A pull request answers here too, and closes nothing it inherits.
|
|
723
|
+
if (Result.isOk(read) && read.value.pull_request === undefined) {
|
|
724
|
+
mut_read.set(number, {
|
|
725
|
+
open: read.value.state === 'open',
|
|
726
|
+
startAfter: waitsFor(
|
|
727
|
+
read.value.body ?? '',
|
|
728
|
+
await blockedByOf(repo, { number, ...read.value }),
|
|
729
|
+
),
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return mut_read;
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
references: async (repo, numbers) => {
|
|
738
|
+
const mut_read = new Map<number, ReferenceStatus>();
|
|
739
|
+
|
|
740
|
+
for (const number of numbers) {
|
|
741
|
+
const read = await getJson(
|
|
742
|
+
`/repos/${repo.owner}/${repo.name}/issues/${number}`,
|
|
743
|
+
ReferenceSchema,
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
if (Result.isErr(read)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const {
|
|
751
|
+
state,
|
|
752
|
+
state_reason: stateReason,
|
|
753
|
+
pull_request: pull,
|
|
754
|
+
} = read.value;
|
|
755
|
+
|
|
756
|
+
mut_read.set(
|
|
757
|
+
number,
|
|
758
|
+
referenceStatusOf(
|
|
759
|
+
pull === undefined
|
|
760
|
+
? { kind: 'issue', state, stateReason: stateReason ?? undefined }
|
|
761
|
+
: {
|
|
762
|
+
kind: 'pull-request',
|
|
763
|
+
state,
|
|
764
|
+
merged: pull.merged_at !== null,
|
|
765
|
+
},
|
|
766
|
+
),
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return mut_read;
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
merged: async (repo, withinDays, limit) => {
|
|
774
|
+
// Sorted by `updated`, not by when they merged: GitHub does not sort
|
|
775
|
+
// closed pull requests by merge time, and `updated` is the closest
|
|
776
|
+
// thing that is monotonic enough for a window this short. The filter
|
|
777
|
+
// below is what decides membership; the sort only decides what one
|
|
778
|
+
// page of a hundred contains, and a hundred pull requests updated more
|
|
779
|
+
// recently than a merge inside the window is not a state this
|
|
780
|
+
// repository reaches.
|
|
781
|
+
const listed = await getJson(
|
|
782
|
+
`/repos/${repo.owner}/${repo.name}/pulls?state=closed&per_page=${PAGE_SIZE}&sort=updated&direction=desc`,
|
|
783
|
+
PullRequestListSchema,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
if (Result.isErr(listed)) {
|
|
787
|
+
return listed;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const cutoff = currentEpochMs() - withinDays * 24 * 60 * 60 * 1000;
|
|
791
|
+
|
|
792
|
+
const within = listed.value.filter((pr) => {
|
|
793
|
+
if (pr.merged_at === null) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const at = epochMsOf(pr.merged_at);
|
|
798
|
+
|
|
799
|
+
return at !== undefined && at >= cutoff;
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
return Result.ok(
|
|
803
|
+
within
|
|
804
|
+
.map((pr) => ({
|
|
805
|
+
number: pr.number,
|
|
806
|
+
title: pr.title,
|
|
807
|
+
author: pr.user?.login ?? 'unknown',
|
|
808
|
+
url: pr.html_url,
|
|
809
|
+
headRef: pr.head.ref,
|
|
810
|
+
baseRef: pr.base.ref,
|
|
811
|
+
mergedAt: pr.merged_at ?? '',
|
|
812
|
+
labels: labelsOf(pr.labels),
|
|
813
|
+
linkedIssues: parseClosingIssueRefs(pr.body ?? '', repo).map(
|
|
814
|
+
(number) => ({
|
|
815
|
+
number,
|
|
816
|
+
title: '',
|
|
817
|
+
url: `https://github.com/${repo.owner}/${repo.name}/issues/${number}`,
|
|
818
|
+
state: 'unknown' as const,
|
|
819
|
+
}),
|
|
820
|
+
),
|
|
821
|
+
}))
|
|
822
|
+
.toSorted((a, b) => b.mergedAt.localeCompare(a.mergedAt))
|
|
823
|
+
.slice(0, limit),
|
|
824
|
+
);
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
facts: async (repo) => {
|
|
828
|
+
const listed = await getJson(
|
|
829
|
+
`/repos/${repo.owner}/${repo.name}/pulls?state=open&per_page=${PAGE_SIZE}&sort=created&direction=asc`,
|
|
830
|
+
PullRequestListSchema,
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
if (Result.isErr(listed)) {
|
|
834
|
+
return listed;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (Arr.isFixedLengthArray(PAGE_SIZE, listed.value)) {
|
|
838
|
+
return Result.err(
|
|
839
|
+
`There are at least ${PAGE_SIZE} open pull requests; this reads one page and would report a part of them as the whole.`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const linked =
|
|
844
|
+
token === undefined
|
|
845
|
+
? new Map<number, readonly LinkedIssue[]>()
|
|
846
|
+
: await closingIssues(repo);
|
|
847
|
+
|
|
848
|
+
const mut_facts: PullRequestFacts[] = [];
|
|
849
|
+
|
|
850
|
+
for (const pr of listed.value) {
|
|
851
|
+
const body = pr.body ?? '';
|
|
852
|
+
|
|
853
|
+
const compared = await comparison(repo, pr.base.ref, pr.head.sha);
|
|
854
|
+
|
|
855
|
+
const checks = await contextStates(repo, pr.head.sha);
|
|
856
|
+
|
|
857
|
+
mut_facts.push({
|
|
858
|
+
number: pr.number,
|
|
859
|
+
title: pr.title,
|
|
860
|
+
body,
|
|
861
|
+
author: pr.user?.login ?? 'unknown',
|
|
862
|
+
isDraft: pr.draft,
|
|
863
|
+
labels: labelsOf(pr.labels),
|
|
864
|
+
autoMerge: pr.auto_merge !== null,
|
|
865
|
+
headRef: pr.head.ref,
|
|
866
|
+
headSha: pr.head.sha,
|
|
867
|
+
baseRef: pr.base.ref,
|
|
868
|
+
fromFork: pr.head.repo?.full_name !== `${repo.owner}/${repo.name}`,
|
|
869
|
+
url: pr.html_url,
|
|
870
|
+
updatedAt: pr.updated_at,
|
|
871
|
+
headCommittedAt: compared.headCommittedAt,
|
|
872
|
+
comparison: compared.comparison,
|
|
873
|
+
reported: checks.reported,
|
|
874
|
+
checksRunning: checks.running,
|
|
875
|
+
linkedIssues:
|
|
876
|
+
linked.get(pr.number) ??
|
|
877
|
+
parseClosingIssueRefs(body, repo).map((number) => ({
|
|
878
|
+
number,
|
|
879
|
+
title: '',
|
|
880
|
+
url: `https://github.com/${repo.owner}/${repo.name}/issues/${number}`,
|
|
881
|
+
state: 'unknown' as const,
|
|
882
|
+
})),
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return mut_state.rateLimited
|
|
887
|
+
? Result.err(
|
|
888
|
+
'The GitHub rate limit was spent part way through, so this report would have been missing commit counts and check verdicts without saying which.\nSet GH_TOKEN or GITHUB_TOKEN to raise it from 60 requests an hour to 5000.',
|
|
889
|
+
)
|
|
890
|
+
: Result.ok(mut_facts);
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* The labels, as the report passes them on. GitHub sends a `null` description
|
|
897
|
+
* for a label that has none; the report carries the empty string, so that
|
|
898
|
+
* nothing downstream has to know the difference.
|
|
899
|
+
*/
|
|
900
|
+
const labelsOf = (
|
|
901
|
+
labels: readonly Readonly<{
|
|
902
|
+
name: string;
|
|
903
|
+
color: string;
|
|
904
|
+
description: string | null;
|
|
905
|
+
}>[],
|
|
906
|
+
): readonly Label[] =>
|
|
907
|
+
labels.map(({ name, color, description }) => ({
|
|
908
|
+
name,
|
|
909
|
+
color,
|
|
910
|
+
description: description ?? '',
|
|
911
|
+
}));
|