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,1042 @@
|
|
|
1
|
+
// cspell:ignore retarget retargeted
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Everything that reads or writes GitHub or `git`, and nothing that decides.
|
|
5
|
+
* The API client and the `git` runner are shared with the other commands
|
|
6
|
+
* (`tools/scripts/lib/`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isSafeRefName,
|
|
11
|
+
parseClaimComment,
|
|
12
|
+
parseSetAsideComment,
|
|
13
|
+
requirementsOfRules,
|
|
14
|
+
SET_ASIDE_COMMENT_SCAN,
|
|
15
|
+
SKIP_CI_LABEL,
|
|
16
|
+
type CheckRunReport,
|
|
17
|
+
type Claim,
|
|
18
|
+
type NativeStackEntry,
|
|
19
|
+
type ReviewState,
|
|
20
|
+
type RulesetRequirements,
|
|
21
|
+
} from 'pr-report-core';
|
|
22
|
+
import { Arr, isRecord, Result } from 'ts-data-forge';
|
|
23
|
+
import * as t from 'ts-fortress';
|
|
24
|
+
import {
|
|
25
|
+
addLabel,
|
|
26
|
+
git,
|
|
27
|
+
graphql,
|
|
28
|
+
graphqlQuery,
|
|
29
|
+
resolveApiContext,
|
|
30
|
+
rest,
|
|
31
|
+
sh,
|
|
32
|
+
type ApiContext,
|
|
33
|
+
type GraphqlVariables,
|
|
34
|
+
} from '../lib/index.mjs';
|
|
35
|
+
import {
|
|
36
|
+
issueOrderOf,
|
|
37
|
+
unlinkedClosingIssues,
|
|
38
|
+
type ClosingIssueAnswer,
|
|
39
|
+
} from './merge-after.mjs';
|
|
40
|
+
import {
|
|
41
|
+
PullRequestSchema,
|
|
42
|
+
type BaseChange,
|
|
43
|
+
type IssueOrder,
|
|
44
|
+
type OwnSetAsideComment,
|
|
45
|
+
type PullRequest,
|
|
46
|
+
} from './types.mjs';
|
|
47
|
+
import { headShaFromListing } from './util.mjs';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The credential and the repository every call here takes, and the branch
|
|
51
|
+
* the queue lands on. `repoOption` is `--repo owner/name`, when given.
|
|
52
|
+
*/
|
|
53
|
+
export const checkPreflight = async (
|
|
54
|
+
repoOption: string | undefined,
|
|
55
|
+
): Promise<
|
|
56
|
+
Result<Readonly<{ api: ApiContext; defaultBranch: string }>, string>
|
|
57
|
+
> => {
|
|
58
|
+
const api = await resolveApiContext(repoOption);
|
|
59
|
+
|
|
60
|
+
if (Result.isErr(api)) {
|
|
61
|
+
return api;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const repository = await rest(
|
|
65
|
+
api.value,
|
|
66
|
+
'GET',
|
|
67
|
+
'',
|
|
68
|
+
t.record({ default_branch: t.string() }),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (Result.isErr(repository)) {
|
|
72
|
+
return Result.err(`cannot read the default branch: ${repository.value}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const name = repository.value.default_branch;
|
|
76
|
+
|
|
77
|
+
if (!isSafeRefName(name)) {
|
|
78
|
+
return Result.err(
|
|
79
|
+
`unexpected default branch name: ${JSON.stringify(name)}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Result.ok({ api: api.value, defaultBranch: name });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The open pull requests, newest first, at most {@link OPEN_LIMIT} of them
|
|
88
|
+
* as `gh pr list` read. GraphQL, because `mergeStateStatus` is not in REST.
|
|
89
|
+
*/
|
|
90
|
+
export const listPullRequests = async (
|
|
91
|
+
api: ApiContext,
|
|
92
|
+
): Promise<Result<readonly PullRequest[], string>> =>
|
|
93
|
+
Result.map(
|
|
94
|
+
await askRepositoryOf(
|
|
95
|
+
api,
|
|
96
|
+
[
|
|
97
|
+
'query($owner: String!, $name: String!) {',
|
|
98
|
+
' repository(owner: $owner, name: $name) {',
|
|
99
|
+
` pullRequests(states: OPEN, first: ${OPEN_LIMIT}, orderBy: { field: CREATED_AT, direction: DESC }) {`,
|
|
100
|
+
` nodes { ${PULL_REQUEST_FIELDS} }`,
|
|
101
|
+
' }',
|
|
102
|
+
' }',
|
|
103
|
+
'}',
|
|
104
|
+
].join('\n'),
|
|
105
|
+
{},
|
|
106
|
+
t.record({
|
|
107
|
+
pullRequests: t.record({ nodes: t.array(PullRequestNodeSchema) }),
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
({ pullRequests }) => pullRequests.nodes.map(pullRequestOfNode),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
export const viewPullRequest = async (
|
|
114
|
+
api: ApiContext,
|
|
115
|
+
prNumber: number,
|
|
116
|
+
): Promise<Result<PullRequest, string>> =>
|
|
117
|
+
Result.map(
|
|
118
|
+
await askRepositoryOf(
|
|
119
|
+
api,
|
|
120
|
+
[
|
|
121
|
+
'query($owner: String!, $name: String!, $number: Int!) {',
|
|
122
|
+
' repository(owner: $owner, name: $name) {',
|
|
123
|
+
` pullRequest(number: $number) { ${PULL_REQUEST_FIELDS} }`,
|
|
124
|
+
' }',
|
|
125
|
+
'}',
|
|
126
|
+
].join('\n'),
|
|
127
|
+
{ number: prNumber },
|
|
128
|
+
t.record({ pullRequest: PullRequestNodeSchema }),
|
|
129
|
+
),
|
|
130
|
+
({ pullRequest }) => pullRequestOfNode(pullRequest),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `REVIEW_REQUIRED`, `CHANGES_REQUESTED`, `APPROVED`, or empty when the
|
|
135
|
+
* rules ask for no review. Read only when a green pull request will not
|
|
136
|
+
* merge, to say what holds it.
|
|
137
|
+
*/
|
|
138
|
+
export const viewReviewDecision = async (
|
|
139
|
+
api: ApiContext,
|
|
140
|
+
prNumber: number,
|
|
141
|
+
): Promise<Result<string, string>> =>
|
|
142
|
+
Result.map(
|
|
143
|
+
await askRepositoryOf(
|
|
144
|
+
api,
|
|
145
|
+
[
|
|
146
|
+
'query($owner: String!, $name: String!, $number: Int!) {',
|
|
147
|
+
' repository(owner: $owner, name: $name) {',
|
|
148
|
+
' pullRequest(number: $number) { reviewDecision }',
|
|
149
|
+
' }',
|
|
150
|
+
'}',
|
|
151
|
+
].join('\n'),
|
|
152
|
+
{ number: prNumber },
|
|
153
|
+
t.record({
|
|
154
|
+
pullRequest: t.record({
|
|
155
|
+
reviewDecision: t.union([t.string(), t.nullType]),
|
|
156
|
+
}),
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
({ pullRequest }) => pullRequest.reviewDecision ?? '',
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A pull request as GraphQL answers it, in the shape every module here reads
|
|
164
|
+
* ({@link PullRequestSchema}): only the labels differ, a connection there and
|
|
165
|
+
* a list here.
|
|
166
|
+
*/
|
|
167
|
+
export const pullRequestOfNode = (
|
|
168
|
+
node: t.TypeOf<typeof PullRequestNodeSchema>,
|
|
169
|
+
): PullRequest =>
|
|
170
|
+
({
|
|
171
|
+
...node,
|
|
172
|
+
labels: node.labels.nodes,
|
|
173
|
+
}) as const;
|
|
174
|
+
|
|
175
|
+
/** How many open pull requests one survey reads, as many as `gh` did. */
|
|
176
|
+
const OPEN_LIMIT = 100;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The fields of {@link PullRequestSchema}, read off it so that a field added
|
|
180
|
+
* there is asked for here; `labels` and `autoMergeRequest` are objects, and
|
|
181
|
+
* name what of them is read.
|
|
182
|
+
*/
|
|
183
|
+
const PULL_REQUEST_FIELDS = Object.keys(PullRequestSchema.shape)
|
|
184
|
+
.map((key) =>
|
|
185
|
+
key === 'labels'
|
|
186
|
+
? 'labels(first: 100) { nodes { name } }'
|
|
187
|
+
: key === 'autoMergeRequest'
|
|
188
|
+
? 'autoMergeRequest { enabledAt }'
|
|
189
|
+
: key,
|
|
190
|
+
)
|
|
191
|
+
.join(' ');
|
|
192
|
+
|
|
193
|
+
const PullRequestNodeSchema = t.record({
|
|
194
|
+
...PullRequestSchema.shape,
|
|
195
|
+
labels: t.record({ nodes: PullRequestSchema.shape.labels }),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The tip of `branch` on `origin`, by its whole ref name
|
|
200
|
+
* (`headShaFromListing`): the default branch read as the version pull
|
|
201
|
+
* request's branch is a set-aside recorded against a base that never moves.
|
|
202
|
+
*/
|
|
203
|
+
export const remoteSha = async (
|
|
204
|
+
branch: string,
|
|
205
|
+
): Promise<Result<string, string>> => {
|
|
206
|
+
const listed = await git(
|
|
207
|
+
`git ls-remote origin ${sh(`refs/heads/${branch}`)}`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (Result.isErr(listed)) {
|
|
211
|
+
return listed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const sha = headShaFromListing(listed.value, branch);
|
|
215
|
+
|
|
216
|
+
return sha === undefined
|
|
217
|
+
? Result.err(`origin has no branch named ${branch}`)
|
|
218
|
+
: Result.ok(sha);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* What GitHub enforces on the branch, every ruleset that applies to it
|
|
223
|
+
* together — rather than what `repo-settings/` declares, which changes
|
|
224
|
+
* nothing until it is applied.
|
|
225
|
+
*/
|
|
226
|
+
export const readBranchRules = async (
|
|
227
|
+
api: ApiContext,
|
|
228
|
+
branch: string,
|
|
229
|
+
): Promise<Result<RulesetRequirements, string>> => {
|
|
230
|
+
// One path segment, whatever the name holds: `release/1.x` included. The
|
|
231
|
+
// rule's premise, building a query string, is not what this is.
|
|
232
|
+
// eslint-disable-next-line no-restricted-globals
|
|
233
|
+
const segment = encodeURIComponent(branch);
|
|
234
|
+
|
|
235
|
+
return Result.map(
|
|
236
|
+
await rest(api, 'GET', `/rules/branches/${segment}`, t.array(t.unknown())),
|
|
237
|
+
requirementsOfRules,
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `CODEOWNERS` on the default branch, and what `pr-report-core`'s
|
|
243
|
+
* `review-hold.mts` reads about each of `numbers`, in one GraphQL request.
|
|
244
|
+
* `undefined` for a repository without `.github/CODEOWNERS`, the one of the
|
|
245
|
+
* three places GitHub looks that this repository uses.
|
|
246
|
+
*
|
|
247
|
+
* Every number goes in as a variable, and each pull request comes back under
|
|
248
|
+
* its own alias.
|
|
249
|
+
*/
|
|
250
|
+
export const readReviewStates = async (
|
|
251
|
+
api: ApiContext,
|
|
252
|
+
numbers: readonly number[],
|
|
253
|
+
defaultBranch: string,
|
|
254
|
+
): Promise<
|
|
255
|
+
Result<
|
|
256
|
+
Readonly<{
|
|
257
|
+
codeOwners: string | undefined;
|
|
258
|
+
states: ReadonlyMap<number, ReviewState>;
|
|
259
|
+
}>,
|
|
260
|
+
string
|
|
261
|
+
>
|
|
262
|
+
> => {
|
|
263
|
+
const query = [
|
|
264
|
+
`query ReviewStates(${['$owner: String!', '$name: String!', '$codeOwners: String!', ...numbers.map((n) => `$pr_${n}: Int!`)].join(', ')}) {`,
|
|
265
|
+
' repository(owner: $owner, name: $name) {',
|
|
266
|
+
' codeOwners: object(expression: $codeOwners) { ... on Blob { text } }',
|
|
267
|
+
...numbers.map(
|
|
268
|
+
(n) => ` pr_${n}: pullRequest(number: $pr_${n}) { ...ReviewState }`,
|
|
269
|
+
),
|
|
270
|
+
' }',
|
|
271
|
+
'}',
|
|
272
|
+
'fragment ReviewState on PullRequest {',
|
|
273
|
+
' author { login }',
|
|
274
|
+
' latestOpinionatedReviews(first: 20, writersOnly: true) { nodes { state author { login } } }',
|
|
275
|
+
` files(first: ${PAGE_SIZE}) { pageInfo { hasNextPage } nodes { path } }`,
|
|
276
|
+
// A pull request with more conversations than one page is counted from
|
|
277
|
+
// the first page. Undercounting only ever holds less, and holding less
|
|
278
|
+
// is what happened before this was read at all.
|
|
279
|
+
` reviewThreads(first: ${PAGE_SIZE}) { nodes { isResolved } }`,
|
|
280
|
+
'}',
|
|
281
|
+
].join('\n');
|
|
282
|
+
|
|
283
|
+
const answered = await askRepositoryOf(
|
|
284
|
+
api,
|
|
285
|
+
query,
|
|
286
|
+
{
|
|
287
|
+
codeOwners: `${defaultBranch}:${CODE_OWNERS_PATH}`,
|
|
288
|
+
...Object.fromEntries(numbers.map((n) => [`pr_${n}`, n] as const)),
|
|
289
|
+
},
|
|
290
|
+
t.unknown(),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (Result.isErr(answered)) {
|
|
294
|
+
return answered;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const repository = answered.value;
|
|
298
|
+
|
|
299
|
+
if (!isRecord(repository)) {
|
|
300
|
+
return Result.err('GitHub answered without the repository');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const codeOwners = CodeOwnersBlobSchema.validate(repository['codeOwners']);
|
|
304
|
+
|
|
305
|
+
const nodes = numbers.map(
|
|
306
|
+
(n) => [n, ReviewStateNodeSchema.validate(repository[`pr_${n}`])] as const,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const invalid = Arr.generate(function* () {
|
|
310
|
+
for (const [n, node] of nodes) {
|
|
311
|
+
if (Result.isErr(node)) {
|
|
312
|
+
yield `#${n}: ${t.validationErrorsToMessages(node.value).join('\n')}`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (Arr.isNonEmpty(invalid)) {
|
|
318
|
+
return Result.err(invalid.join('\n'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const states: ReadonlyMap<number, ReviewState> = new Map(
|
|
322
|
+
Arr.generate(function* () {
|
|
323
|
+
for (const [n, node] of nodes) {
|
|
324
|
+
if (Result.isOk(node)) {
|
|
325
|
+
yield [n, reviewStateOf(node.value)] as const;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return Result.ok({
|
|
332
|
+
codeOwners:
|
|
333
|
+
Result.isOk(codeOwners) && codeOwners.value !== null
|
|
334
|
+
? codeOwners.value.text
|
|
335
|
+
: undefined,
|
|
336
|
+
states,
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* What the issues the open pull requests close add to the merge order
|
|
342
|
+
* (`merge-after.mts`'s `issueOrderOf`): one GraphQL request for what GitHub
|
|
343
|
+
* links and how it marked the bodies, and one more for the keyword-closed
|
|
344
|
+
* issues it did not link, only when there are any.
|
|
345
|
+
*/
|
|
346
|
+
export const readIssueOrder = async (
|
|
347
|
+
api: ApiContext,
|
|
348
|
+
numbers: readonly number[],
|
|
349
|
+
): Promise<Result<IssueOrder, string>> => {
|
|
350
|
+
if (!Arr.isNonEmpty(numbers)) {
|
|
351
|
+
return Result.ok({ closes: new Map(), startAfter: new Map() });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const pulls = await askRepository(
|
|
355
|
+
api,
|
|
356
|
+
'ClosingIssues',
|
|
357
|
+
numbers.map((n) => ({
|
|
358
|
+
alias: `pr_${n}`,
|
|
359
|
+
number: n,
|
|
360
|
+
field: `pullRequest(number: $pr_${n}) { bodyHTML closingIssuesReferences(first: 10) { nodes { ${CLOSING_ISSUE_FIELDS} repository { nameWithOwner } } } }`,
|
|
361
|
+
})),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (Result.isErr(pulls)) {
|
|
365
|
+
return pulls;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { repo, fields } = pulls.value;
|
|
369
|
+
|
|
370
|
+
const own = `${repo.owner}/${repo.name}`.toLowerCase();
|
|
371
|
+
|
|
372
|
+
const read = Arr.generate(function* () {
|
|
373
|
+
for (const n of numbers) {
|
|
374
|
+
const node = ClosingIssuesNodeSchema.validate(fields(`pr_${n}`));
|
|
375
|
+
|
|
376
|
+
if (Result.isOk(node)) {
|
|
377
|
+
yield {
|
|
378
|
+
number: n,
|
|
379
|
+
bodyHtml: node.value.bodyHTML,
|
|
380
|
+
linked: node.value.closingIssuesReferences.nodes
|
|
381
|
+
.filter(
|
|
382
|
+
(issue) => issue.repository.nameWithOwner.toLowerCase() === own,
|
|
383
|
+
)
|
|
384
|
+
.map((issue) => closingIssueAnswer(own, issue)),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const unlinked = unlinkedClosingIssues(repo, read);
|
|
391
|
+
|
|
392
|
+
if (!Arr.isNonEmpty(unlinked)) {
|
|
393
|
+
return Result.ok(issueOrderOf(repo, read, new Map()));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const issues = await askRepository(
|
|
397
|
+
api,
|
|
398
|
+
'UnlinkedIssues',
|
|
399
|
+
unlinked.map((n) => ({
|
|
400
|
+
alias: `issue_${n}`,
|
|
401
|
+
number: n,
|
|
402
|
+
field: `issue(number: $issue_${n}) { ${CLOSING_ISSUE_FIELDS} }`,
|
|
403
|
+
})),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (Result.isErr(issues)) {
|
|
407
|
+
return issues;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const described = new Map(
|
|
411
|
+
Arr.generate(function* () {
|
|
412
|
+
for (const n of unlinked) {
|
|
413
|
+
const node = ClosingIssueSchema.validate(
|
|
414
|
+
issues.value.fields(`issue_${n}`),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (Result.isOk(node)) {
|
|
418
|
+
yield [n, closingIssueAnswer(own, node.value)] as const;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return Result.ok(issueOrderOf(repo, read, described));
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* What an issue waits for is its `Start-After:` and the issues GitHub
|
|
429
|
+
* records as blocking it, read as one (`pr-report-core`'s `waitsFor`).
|
|
430
|
+
*/
|
|
431
|
+
const CLOSING_ISSUE_FIELDS =
|
|
432
|
+
'number state body blockedBy(first: 20) { nodes { number repository { nameWithOwner } } }';
|
|
433
|
+
|
|
434
|
+
const BlockedBySchema = t.record({
|
|
435
|
+
nodes: t.array(
|
|
436
|
+
t.record({
|
|
437
|
+
number: t.number(),
|
|
438
|
+
repository: t.record({ nameWithOwner: t.string() }),
|
|
439
|
+
}),
|
|
440
|
+
),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const ClosingIssueSchema = t.record({
|
|
444
|
+
number: t.number(),
|
|
445
|
+
state: t.string(),
|
|
446
|
+
body: t.string(),
|
|
447
|
+
blockedBy: BlockedBySchema,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
/** An issue as read, with only `own`'s issues among what blocks it. */
|
|
451
|
+
const closingIssueAnswer = (
|
|
452
|
+
own: string,
|
|
453
|
+
{ number, state, body, blockedBy }: t.TypeOf<typeof ClosingIssueSchema>,
|
|
454
|
+
): ClosingIssueAnswer =>
|
|
455
|
+
({
|
|
456
|
+
number,
|
|
457
|
+
state,
|
|
458
|
+
body,
|
|
459
|
+
blockedBy: blockedBy.nodes
|
|
460
|
+
.filter(
|
|
461
|
+
({ repository }) => repository.nameWithOwner.toLowerCase() === own,
|
|
462
|
+
)
|
|
463
|
+
.map((node) => node.number),
|
|
464
|
+
}) as const;
|
|
465
|
+
|
|
466
|
+
const ClosingIssuesNodeSchema = t.record({
|
|
467
|
+
bodyHTML: t.string(),
|
|
468
|
+
closingIssuesReferences: t.record({
|
|
469
|
+
nodes: t.array(
|
|
470
|
+
t.record({
|
|
471
|
+
number: t.number(),
|
|
472
|
+
state: t.string(),
|
|
473
|
+
body: t.string(),
|
|
474
|
+
blockedBy: BlockedBySchema,
|
|
475
|
+
repository: t.record({ nameWithOwner: t.string() }),
|
|
476
|
+
}),
|
|
477
|
+
),
|
|
478
|
+
}),
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* One GraphQL request of aliased fields under the repository of the working
|
|
483
|
+
* directory, each taking one number as `$<alias>`, with the repository's own
|
|
484
|
+
* name read back so that a body's references can be told apart from another
|
|
485
|
+
* repository's.
|
|
486
|
+
*/
|
|
487
|
+
const askRepository = async (
|
|
488
|
+
api: ApiContext,
|
|
489
|
+
operationName: string,
|
|
490
|
+
parts: readonly Readonly<{ alias: string; number: number; field: string }>[],
|
|
491
|
+
): Promise<
|
|
492
|
+
Result<
|
|
493
|
+
Readonly<{
|
|
494
|
+
repo: Readonly<{ owner: string; name: string }>;
|
|
495
|
+
fields: (alias: string) => unknown;
|
|
496
|
+
}>,
|
|
497
|
+
string
|
|
498
|
+
>
|
|
499
|
+
> => {
|
|
500
|
+
const query = [
|
|
501
|
+
`query ${operationName}(${['$owner: String!', '$name: String!', ...parts.map(({ alias }) => `$${alias}: Int!`)].join(', ')}) {`,
|
|
502
|
+
' repository(owner: $owner, name: $name) {',
|
|
503
|
+
' nameWithOwner',
|
|
504
|
+
...parts.map(({ alias, field }) => ` ${alias}: ${field}`),
|
|
505
|
+
' }',
|
|
506
|
+
'}',
|
|
507
|
+
].join('\n');
|
|
508
|
+
|
|
509
|
+
const answered = await askRepositoryOf(
|
|
510
|
+
api,
|
|
511
|
+
query,
|
|
512
|
+
Object.fromEntries(
|
|
513
|
+
parts.map(({ alias, number }) => [alias, number] as const),
|
|
514
|
+
),
|
|
515
|
+
t.unknown(),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (Result.isErr(answered)) {
|
|
519
|
+
return answered;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const repository = answered.value;
|
|
523
|
+
|
|
524
|
+
const nameWithOwner = isRecord(repository)
|
|
525
|
+
? repository['nameWithOwner']
|
|
526
|
+
: undefined;
|
|
527
|
+
|
|
528
|
+
const [owner, name] =
|
|
529
|
+
typeof nameWithOwner === 'string'
|
|
530
|
+
? nameWithOwner.split('/')
|
|
531
|
+
: ([] as const);
|
|
532
|
+
|
|
533
|
+
if (owner === undefined || name === undefined || !isRecord(repository)) {
|
|
534
|
+
return Result.err('GitHub answered without the repository');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return Result.ok({
|
|
538
|
+
repo: { owner, name },
|
|
539
|
+
fields: (alias) => repository[alias],
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* One GraphQL request whose `$owner` and `$name` are the repository's, to be
|
|
545
|
+
* declared by `query`, resolving to what it answered under `repository`.
|
|
546
|
+
*/
|
|
547
|
+
const askRepositoryOf = async <A,>(
|
|
548
|
+
api: ApiContext,
|
|
549
|
+
query: string,
|
|
550
|
+
variables: GraphqlVariables,
|
|
551
|
+
schema: t.Type<A>,
|
|
552
|
+
): Promise<Result<A, string>> =>
|
|
553
|
+
Result.map(
|
|
554
|
+
await graphqlQuery(
|
|
555
|
+
api,
|
|
556
|
+
query,
|
|
557
|
+
{ owner: api.repo.owner, name: api.repo.name, ...variables },
|
|
558
|
+
t.record({ repository: schema }),
|
|
559
|
+
),
|
|
560
|
+
({ repository }) => repository,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const CODE_OWNERS_PATH = '.github/CODEOWNERS';
|
|
564
|
+
|
|
565
|
+
/** One page of a list inside a pull request. */
|
|
566
|
+
const PAGE_SIZE = 100;
|
|
567
|
+
|
|
568
|
+
const CodeOwnersBlobSchema = t.union([
|
|
569
|
+
t.record({ text: t.string() }),
|
|
570
|
+
t.nullType,
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
const LoginSchema = t.union([t.record({ login: t.string() }), t.nullType]);
|
|
574
|
+
|
|
575
|
+
const ReviewStateNodeSchema = t.record({
|
|
576
|
+
author: LoginSchema,
|
|
577
|
+
latestOpinionatedReviews: t.record({
|
|
578
|
+
nodes: t.array(t.record({ state: t.string(), author: LoginSchema })),
|
|
579
|
+
}),
|
|
580
|
+
files: t.record({
|
|
581
|
+
pageInfo: t.record({ hasNextPage: t.boolean() }),
|
|
582
|
+
nodes: t.array(t.record({ path: t.string() })),
|
|
583
|
+
}),
|
|
584
|
+
reviewThreads: t.record({
|
|
585
|
+
nodes: t.array(t.record({ isResolved: t.boolean() })),
|
|
586
|
+
}),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const reviewStateOf = (
|
|
590
|
+
node: t.TypeOf<typeof ReviewStateNodeSchema>,
|
|
591
|
+
): ReviewState =>
|
|
592
|
+
({
|
|
593
|
+
author: node.author?.login ?? '',
|
|
594
|
+
approvers: Arr.generate(function* () {
|
|
595
|
+
for (const { state, author } of node.latestOpinionatedReviews.nodes) {
|
|
596
|
+
if (state === 'APPROVED' && author !== null) {
|
|
597
|
+
yield author.login;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}),
|
|
601
|
+
files: node.files.nodes.map(({ path }) => path),
|
|
602
|
+
filesComplete: !node.files.pageInfo.hasNextPage,
|
|
603
|
+
unresolvedConversations: node.reviewThreads.nodes.filter(
|
|
604
|
+
({ isResolved }) => !isResolved,
|
|
605
|
+
).length,
|
|
606
|
+
}) as const;
|
|
607
|
+
|
|
608
|
+
export const addSkipCiLabel = async (
|
|
609
|
+
api: ApiContext,
|
|
610
|
+
prNumber: number,
|
|
611
|
+
): Promise<Result<undefined, string>> => addLabel(api, prNumber, SKIP_CI_LABEL);
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* This account's set-aside comment on each of `numbers` that has one. See
|
|
615
|
+
* {@link readOwnComments}.
|
|
616
|
+
*/
|
|
617
|
+
export const readSetAsideComments = async (
|
|
618
|
+
api: ApiContext,
|
|
619
|
+
numbers: readonly number[],
|
|
620
|
+
): Promise<Result<ReadonlyMap<number, OwnSetAsideComment>, string>> =>
|
|
621
|
+
Result.map(await readOwnComments(api, numbers), ({ setAside }) => setAside);
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* This account's set-aside comment, and the standing claim a session wrote
|
|
625
|
+
* (`claims.mts`), on each of `numbers` that has one, in one GraphQL request:
|
|
626
|
+
* every number goes in as a variable and each pull request comes back under
|
|
627
|
+
* its own alias. `pr-report-core`'s `set-aside.mts` says why only
|
|
628
|
+
* `viewerDidAuthor` counts; of several of a kind, the oldest is the one.
|
|
629
|
+
*/
|
|
630
|
+
export const readOwnComments = async (
|
|
631
|
+
api: ApiContext,
|
|
632
|
+
numbers: readonly number[],
|
|
633
|
+
): Promise<
|
|
634
|
+
Result<
|
|
635
|
+
Readonly<{
|
|
636
|
+
setAside: ReadonlyMap<number, OwnSetAsideComment>;
|
|
637
|
+
claims: ReadonlyMap<number, Claim>;
|
|
638
|
+
}>,
|
|
639
|
+
string
|
|
640
|
+
>
|
|
641
|
+
> => {
|
|
642
|
+
if (!Arr.isNonEmpty(numbers)) {
|
|
643
|
+
return Result.ok({ setAside: new Map(), claims: new Map() });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const query = [
|
|
647
|
+
`query SetAsideComments(${['$owner: String!', '$name: String!', ...numbers.map((n) => `$pr_${n}: Int!`)].join(', ')}) {`,
|
|
648
|
+
' repository(owner: $owner, name: $name) {',
|
|
649
|
+
...numbers.map(
|
|
650
|
+
(n) =>
|
|
651
|
+
` pr_${n}: pullRequest(number: $pr_${n}) { comments(last: ${SET_ASIDE_COMMENT_SCAN}) { nodes { databaseId body viewerDidAuthor } } }`,
|
|
652
|
+
),
|
|
653
|
+
' }',
|
|
654
|
+
'}',
|
|
655
|
+
].join('\n');
|
|
656
|
+
|
|
657
|
+
const answered = await askRepositoryOf(
|
|
658
|
+
api,
|
|
659
|
+
query,
|
|
660
|
+
Object.fromEntries(numbers.map((n) => [`pr_${n}`, n] as const)),
|
|
661
|
+
t.unknown(),
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
if (Result.isErr(answered)) {
|
|
665
|
+
return answered;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const repository = answered.value;
|
|
669
|
+
|
|
670
|
+
if (!isRecord(repository)) {
|
|
671
|
+
return Result.err('GitHub answered without the repository');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const nodes = numbers.map(
|
|
675
|
+
(n) => [n, CommentsNodeSchema.validate(repository[`pr_${n}`])] as const,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
const invalid = Arr.generate(function* () {
|
|
679
|
+
for (const [n, node] of nodes) {
|
|
680
|
+
if (Result.isErr(node)) {
|
|
681
|
+
yield `#${n}: ${t.validationErrorsToMessages(node.value).join('\n')}`;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
if (Arr.isNonEmpty(invalid)) {
|
|
687
|
+
return Result.err(invalid.join('\n'));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const own = Arr.generate(function* () {
|
|
691
|
+
for (const [n, node] of nodes) {
|
|
692
|
+
if (Result.isOk(node)) {
|
|
693
|
+
yield [
|
|
694
|
+
n,
|
|
695
|
+
node.value.comments.nodes.filter(
|
|
696
|
+
({ viewerDidAuthor }) => viewerDidAuthor,
|
|
697
|
+
),
|
|
698
|
+
] as const;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
return Result.ok({
|
|
704
|
+
setAside: new Map(
|
|
705
|
+
Arr.generate(function* () {
|
|
706
|
+
for (const [n, comments] of own) {
|
|
707
|
+
const found = ownSetAsideComment(comments);
|
|
708
|
+
|
|
709
|
+
if (found !== undefined) {
|
|
710
|
+
yield [n, found] as const;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}),
|
|
714
|
+
),
|
|
715
|
+
claims: new Map(
|
|
716
|
+
Arr.generate(function* () {
|
|
717
|
+
for (const [n, comments] of own) {
|
|
718
|
+
const says = Arr.generate(function* () {
|
|
719
|
+
for (const { body } of comments) {
|
|
720
|
+
const claim = parseClaimComment(body);
|
|
721
|
+
|
|
722
|
+
if (claim !== undefined) {
|
|
723
|
+
yield claim;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}).at(0);
|
|
727
|
+
|
|
728
|
+
if (says?.kind === 'standing') {
|
|
729
|
+
yield [n, says.claim] as const;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}),
|
|
733
|
+
),
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const ownSetAsideComment = (
|
|
738
|
+
comments: readonly t.TypeOf<typeof CommentSchema>[],
|
|
739
|
+
): OwnSetAsideComment | undefined =>
|
|
740
|
+
Arr.generate(function* () {
|
|
741
|
+
for (const { databaseId, body } of comments) {
|
|
742
|
+
const says = parseSetAsideComment(body);
|
|
743
|
+
|
|
744
|
+
if (databaseId !== null && says !== undefined) {
|
|
745
|
+
yield { databaseId, says };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}).at(0);
|
|
749
|
+
|
|
750
|
+
const CommentSchema = t.record({
|
|
751
|
+
databaseId: t.union([t.number(), t.nullType]),
|
|
752
|
+
body: t.string(),
|
|
753
|
+
viewerDidAuthor: t.boolean(),
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const CommentsNodeSchema = t.record({
|
|
757
|
+
comments: t.record({ nodes: t.array(CommentSchema) }),
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Writes `body` as the pull request's set-aside comment: over `existing`,
|
|
762
|
+
* this account's own, when there is one — a pull request has one, edited in
|
|
763
|
+
* place — and as a new comment otherwise.
|
|
764
|
+
*/
|
|
765
|
+
export const writeSetAsideCommentOn = async (
|
|
766
|
+
api: ApiContext,
|
|
767
|
+
prNumber: number,
|
|
768
|
+
existing: number | undefined,
|
|
769
|
+
body: string,
|
|
770
|
+
): Promise<Result<undefined, string>> =>
|
|
771
|
+
Result.map(
|
|
772
|
+
await rest(
|
|
773
|
+
api,
|
|
774
|
+
existing === undefined ? 'POST' : 'PATCH',
|
|
775
|
+
existing === undefined
|
|
776
|
+
? `/issues/${prNumber}/comments`
|
|
777
|
+
: `/issues/comments/${existing}`,
|
|
778
|
+
t.unknown(),
|
|
779
|
+
{ body: { body } },
|
|
780
|
+
),
|
|
781
|
+
() => undefined,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Every change of the pull request's base its timeline records, oldest
|
|
786
|
+
* first; `stack.mts` reads which branch GitHub moved it off.
|
|
787
|
+
*
|
|
788
|
+
* GraphQL, because the REST timeline does not say which branches a change of
|
|
789
|
+
* base was between.
|
|
790
|
+
*/
|
|
791
|
+
export const readBaseChanges = async (
|
|
792
|
+
api: ApiContext,
|
|
793
|
+
prNumber: number,
|
|
794
|
+
): Promise<Result<readonly BaseChange[], string>> =>
|
|
795
|
+
Result.map(
|
|
796
|
+
await askRepositoryOf(
|
|
797
|
+
api,
|
|
798
|
+
TIMELINE_QUERY,
|
|
799
|
+
{ number: prNumber },
|
|
800
|
+
TimelineSchema,
|
|
801
|
+
),
|
|
802
|
+
({ pullRequest }) =>
|
|
803
|
+
pullRequest.timelineItems.nodes.map(
|
|
804
|
+
(node) =>
|
|
805
|
+
({
|
|
806
|
+
from: node.previousRefName ?? '',
|
|
807
|
+
to: node.currentRefName ?? '',
|
|
808
|
+
}) as const,
|
|
809
|
+
),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
const TIMELINE_QUERY = [
|
|
813
|
+
'query($owner: String!, $name: String!, $number: Int!) {',
|
|
814
|
+
' repository(owner: $owner, name: $name) {',
|
|
815
|
+
' pullRequest(number: $number) {',
|
|
816
|
+
' timelineItems(last: 100, itemTypes: [BASE_REF_CHANGED_EVENT]) {',
|
|
817
|
+
' nodes {',
|
|
818
|
+
' ... on BaseRefChangedEvent { previousRefName currentRefName }',
|
|
819
|
+
' }',
|
|
820
|
+
' }',
|
|
821
|
+
' }',
|
|
822
|
+
' }',
|
|
823
|
+
'}',
|
|
824
|
+
].join(' ');
|
|
825
|
+
|
|
826
|
+
const TimelineNodeSchema = t.record({
|
|
827
|
+
previousRefName: t.optional(t.union([t.string(), t.nullType])),
|
|
828
|
+
currentRefName: t.optional(t.union([t.string(), t.nullType])),
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const TimelineSchema = t.record({
|
|
832
|
+
pullRequest: t.record({
|
|
833
|
+
timelineItems: t.record({
|
|
834
|
+
nodes: t.array(TimelineNodeSchema),
|
|
835
|
+
}),
|
|
836
|
+
}),
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* The pull request's place in one of GitHub's native stacks, or `undefined`
|
|
841
|
+
* when it is in none. GraphQL, because REST does not know them.
|
|
842
|
+
*/
|
|
843
|
+
export const readNativeStack = async (
|
|
844
|
+
api: ApiContext,
|
|
845
|
+
prNumber: number,
|
|
846
|
+
): Promise<Result<NativeStackEntry | undefined, string>> =>
|
|
847
|
+
Result.map(
|
|
848
|
+
await askRepositoryOf(
|
|
849
|
+
api,
|
|
850
|
+
NATIVE_STACK_QUERY,
|
|
851
|
+
{ number: prNumber },
|
|
852
|
+
NativeStackSchema,
|
|
853
|
+
),
|
|
854
|
+
({ pullRequest: { stackEntry } }) =>
|
|
855
|
+
stackEntry === null
|
|
856
|
+
? undefined
|
|
857
|
+
: {
|
|
858
|
+
stack: stackEntry.stack.number,
|
|
859
|
+
position: stackEntry.position,
|
|
860
|
+
size: stackEntry.stack.size,
|
|
861
|
+
},
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
const NATIVE_STACK_QUERY = [
|
|
865
|
+
'query($owner: String!, $name: String!, $number: Int!) {',
|
|
866
|
+
' repository(owner: $owner, name: $name) {',
|
|
867
|
+
' pullRequest(number: $number) {',
|
|
868
|
+
' stackEntry { position stack { number size } }',
|
|
869
|
+
' }',
|
|
870
|
+
' }',
|
|
871
|
+
'}',
|
|
872
|
+
].join(' ');
|
|
873
|
+
|
|
874
|
+
const NativeStackSchema = t.record({
|
|
875
|
+
pullRequest: t.record({
|
|
876
|
+
stackEntry: t.union([
|
|
877
|
+
t.record({
|
|
878
|
+
position: t.number(),
|
|
879
|
+
stack: t.record({ number: t.number(), size: t.number() }),
|
|
880
|
+
}),
|
|
881
|
+
t.nullType,
|
|
882
|
+
]),
|
|
883
|
+
}),
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* The head the last merged pull request from `branch` was merged at, and the
|
|
888
|
+
* commit its merge made, or `undefined` when none was — the layer a
|
|
889
|
+
* retargeted pull request was stacked on, whose commits it may still carry.
|
|
890
|
+
*/
|
|
891
|
+
export const mergedHeadOf = async (
|
|
892
|
+
api: ApiContext,
|
|
893
|
+
branch: string,
|
|
894
|
+
): Promise<Result<MergedHead | undefined, string>> =>
|
|
895
|
+
Result.map(
|
|
896
|
+
await rest(api, 'GET', '/pulls', t.array(MergedPullSchema), {
|
|
897
|
+
query: {
|
|
898
|
+
state: 'closed',
|
|
899
|
+
head: `${api.repo.owner}:${branch}`,
|
|
900
|
+
sort: 'updated',
|
|
901
|
+
direction: 'desc',
|
|
902
|
+
per_page: '5',
|
|
903
|
+
},
|
|
904
|
+
}),
|
|
905
|
+
latestMergedHead,
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* The last merged of `pulls`, closed pull requests from one branch, or
|
|
910
|
+
* `undefined` when none of them merged.
|
|
911
|
+
*/
|
|
912
|
+
export const latestMergedHead = (
|
|
913
|
+
pulls: readonly t.TypeOf<typeof MergedPullSchema>[],
|
|
914
|
+
): MergedHead | undefined => {
|
|
915
|
+
const [latest] = pulls
|
|
916
|
+
.filter(
|
|
917
|
+
(pull): pull is typeof pull & Readonly<{ merged_at: string }> =>
|
|
918
|
+
pull.merged_at !== null,
|
|
919
|
+
)
|
|
920
|
+
.toSorted((a, b) => b.merged_at.localeCompare(a.merged_at));
|
|
921
|
+
|
|
922
|
+
return latest === undefined
|
|
923
|
+
? undefined
|
|
924
|
+
: {
|
|
925
|
+
number: latest.number,
|
|
926
|
+
headSha: latest.head.sha,
|
|
927
|
+
mergeCommit: latest.merge_commit_sha ?? undefined,
|
|
928
|
+
};
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
export type MergedHead = Readonly<{
|
|
932
|
+
number: number;
|
|
933
|
+
headSha: string;
|
|
934
|
+
/** The squash commit on the base; GitHub may not have it for a moment. */
|
|
935
|
+
mergeCommit: string | undefined;
|
|
936
|
+
}>;
|
|
937
|
+
|
|
938
|
+
const MergedPullSchema = t.record({
|
|
939
|
+
number: t.number(),
|
|
940
|
+
head: t.record({ sha: t.string() }),
|
|
941
|
+
merge_commit_sha: t.union([t.string(), t.nullType]),
|
|
942
|
+
merged_at: t.union([t.string(), t.nullType]),
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Arms auto-merge, squash being the one method the ruleset allows. The
|
|
947
|
+
* mutation refuses when nothing holds the pull request, where
|
|
948
|
+
* `gh pr merge --auto` would merge it on the spot.
|
|
949
|
+
*/
|
|
950
|
+
export const armAutoMerge = async (
|
|
951
|
+
api: ApiContext,
|
|
952
|
+
nodeId: string,
|
|
953
|
+
): Promise<Result<undefined, string>> =>
|
|
954
|
+
graphql(api, ARM_MUTATION, { id: nodeId });
|
|
955
|
+
|
|
956
|
+
const ARM_MUTATION =
|
|
957
|
+
'mutation($id: ID!) { enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: SQUASH }) { clientMutationId } }';
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Every check run GitHub Actions reported on one commit, every job of every
|
|
961
|
+
* workflow rather than only the required aggregates, so that a failure can
|
|
962
|
+
* be traced to the matrix entry under the aggregate. Other apps' runs — the
|
|
963
|
+
* codecov ones — are left out: nothing here can act on them, and none is
|
|
964
|
+
* required.
|
|
965
|
+
*/
|
|
966
|
+
export const listCheckRuns = async (
|
|
967
|
+
api: ApiContext,
|
|
968
|
+
headSha: string,
|
|
969
|
+
): Promise<Result<readonly CheckRunReport[], string>> => {
|
|
970
|
+
if (!SHA.test(headSha)) {
|
|
971
|
+
return Result.err(`unexpected head SHA: ${JSON.stringify(headSha)}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const mut_collected: CheckRunReport[] = [];
|
|
975
|
+
|
|
976
|
+
for (const page of Arr.seq(MAX_CHECK_RUN_PAGES)) {
|
|
977
|
+
const answered = await rest(
|
|
978
|
+
api,
|
|
979
|
+
'GET',
|
|
980
|
+
`/commits/${headSha}/check-runs`,
|
|
981
|
+
CheckRunPageSchema,
|
|
982
|
+
{
|
|
983
|
+
query: { per_page: PAGE_SIZE.toString(), page: (page + 1).toString() },
|
|
984
|
+
},
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
if (Result.isErr(answered)) {
|
|
988
|
+
return answered;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
for (const run of answered.value.check_runs) {
|
|
992
|
+
if (run.app?.slug === 'github-actions') {
|
|
993
|
+
mut_collected.push({
|
|
994
|
+
id: run.id,
|
|
995
|
+
checkSuiteId: run.check_suite.id,
|
|
996
|
+
name: run.name,
|
|
997
|
+
status: run.status,
|
|
998
|
+
conclusion: run.conclusion ?? undefined,
|
|
999
|
+
// Not read: a fix is planned once the checks have failed, not
|
|
1000
|
+
// while a newer round is running.
|
|
1001
|
+
round: undefined,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (
|
|
1007
|
+
Arr.isEmpty(answered.value.check_runs) ||
|
|
1008
|
+
(page + 1) * PAGE_SIZE >= answered.value.total_count
|
|
1009
|
+
) {
|
|
1010
|
+
return Result.ok(mut_collected);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return Result.err(
|
|
1015
|
+
`${headSha} has more than ${PAGE_SIZE * MAX_CHECK_RUN_PAGES} check runs, which is more than this reads`,
|
|
1016
|
+
);
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const MAX_CHECK_RUN_PAGES = 10;
|
|
1020
|
+
|
|
1021
|
+
const CheckRunPageSchema = t.record({
|
|
1022
|
+
total_count: t.number(),
|
|
1023
|
+
check_runs: t.array(
|
|
1024
|
+
t.record({
|
|
1025
|
+
id: t.number(),
|
|
1026
|
+
check_suite: t.record({ id: t.number() }),
|
|
1027
|
+
app: t.union([t.record({ slug: t.string() }), t.nullType]),
|
|
1028
|
+
name: t.string(),
|
|
1029
|
+
status: t.string(),
|
|
1030
|
+
conclusion: t.union([t.string(), t.nullType]),
|
|
1031
|
+
}),
|
|
1032
|
+
),
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
export const removeWorktree = async (worktreeDir: string): Promise<void> => {
|
|
1036
|
+
// Both fail harmlessly when there is nothing to remove.
|
|
1037
|
+
await git(`git worktree remove --force ${sh(worktreeDir)}`);
|
|
1038
|
+
|
|
1039
|
+
await git('git worktree prune');
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const SHA = /^[0-9a-f]{40}$/u;
|