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,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the merge is waiting for, read from the head commit's checks and
|
|
3
|
+
* judged against the contexts the ruleset requires.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
combineContextStates,
|
|
8
|
+
epochMsOf,
|
|
9
|
+
summarizeChecks as summarizeContexts,
|
|
10
|
+
type ContextState,
|
|
11
|
+
} from 'pr-report-core';
|
|
12
|
+
import { Arr, Num, Result } from 'ts-data-forge';
|
|
13
|
+
import * as t from 'ts-fortress';
|
|
14
|
+
import { graphqlQuery, type ApiContext } from '../lib/index.mjs';
|
|
15
|
+
import type { ChecksSummary } from './types.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One check on the head, in the shape `gh pr checks --json` printed, which
|
|
19
|
+
* is what this script read until it called the API itself.
|
|
20
|
+
*/
|
|
21
|
+
export type ReportedCheck = Readonly<{
|
|
22
|
+
name: string;
|
|
23
|
+
/** `pass` | `fail` | `pending` | `skipping` | `cancel` */
|
|
24
|
+
bucket: string;
|
|
25
|
+
link: string;
|
|
26
|
+
/** ISO 8601; GitHub's zero time before the check has started. */
|
|
27
|
+
startedAt: string;
|
|
28
|
+
/** ISO 8601; GitHub's zero time before the check has finished. */
|
|
29
|
+
completedAt: string;
|
|
30
|
+
/** The workflow the job belongs to; empty for a commit status. */
|
|
31
|
+
workflow: string;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A check on the head that has not finished, when it started, and the
|
|
36
|
+
* workflow run it is a job of.
|
|
37
|
+
*/
|
|
38
|
+
export type RunningCheck = Readonly<{
|
|
39
|
+
name: string;
|
|
40
|
+
startedAt: string;
|
|
41
|
+
workflow: string;
|
|
42
|
+
link: string;
|
|
43
|
+
}>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The checks that gate the merge, as GitHub reports them for the head commit.
|
|
47
|
+
* `isRequired` is GitHub's own answer for this pull request, so the list
|
|
48
|
+
* follows the ruleset without this script having to know it.
|
|
49
|
+
*/
|
|
50
|
+
export const listRequiredChecks = async (
|
|
51
|
+
api: ApiContext,
|
|
52
|
+
prNumber: number,
|
|
53
|
+
): Promise<Result<readonly ReportedCheck[], string>> =>
|
|
54
|
+
Result.map(await readCheckContexts(api, prNumber), (contexts) =>
|
|
55
|
+
reportedChecksOf(contexts, 'required'),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The checks on the head commit that have not finished, required or not.
|
|
60
|
+
*
|
|
61
|
+
* A required aggregate has no run in a new round until the jobs it waits on
|
|
62
|
+
* are done, so until then the required ones answer with the round before.
|
|
63
|
+
* When that round was the one `skip-ci` skipped, every aggregate reads
|
|
64
|
+
* `skipped` and the commit looks green halfway through its matrix. Anything
|
|
65
|
+
* still running is what says the round is not over, and when it started is
|
|
66
|
+
* what tells a failure of the round before from one of this round.
|
|
67
|
+
*/
|
|
68
|
+
export const listRunningChecks = async (
|
|
69
|
+
api: ApiContext,
|
|
70
|
+
prNumber: number,
|
|
71
|
+
): Promise<Result<readonly RunningCheck[], string>> =>
|
|
72
|
+
Result.map(await readCheckContexts(api, prNumber), (contexts) =>
|
|
73
|
+
reportedChecksOf(contexts, 'all')
|
|
74
|
+
.filter((check) => check.bucket === 'pending')
|
|
75
|
+
.map(({ name, startedAt, workflow, link }) => ({
|
|
76
|
+
name,
|
|
77
|
+
startedAt,
|
|
78
|
+
workflow,
|
|
79
|
+
link,
|
|
80
|
+
})),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What the head commit's checks say, as `gh pr checks` said it: each check
|
|
85
|
+
* run and commit status in a bucket, and of the runs of one job in one
|
|
86
|
+
* workflow and event only the latest — a re-run, or the round `skip-ci`
|
|
87
|
+
* cancelled under the one now running — so that a superseded failure does
|
|
88
|
+
* not speak for its context. `which` is `'required'` for only what GitHub
|
|
89
|
+
* says this pull request requires.
|
|
90
|
+
*
|
|
91
|
+
* Unlike `gh`, the survivors keep the order GitHub listed them in. A commit
|
|
92
|
+
* status's start and finish are both when it was set, the one time it has.
|
|
93
|
+
*/
|
|
94
|
+
export const reportedChecksOf = (
|
|
95
|
+
contexts: readonly CheckContextNode[],
|
|
96
|
+
which: 'all' | 'required',
|
|
97
|
+
): readonly ReportedCheck[] => {
|
|
98
|
+
const mut_latest = new Map<string, CheckContextNode>();
|
|
99
|
+
|
|
100
|
+
for (const context of contexts) {
|
|
101
|
+
const key = keyOf(context);
|
|
102
|
+
|
|
103
|
+
const kept = mut_latest.get(key);
|
|
104
|
+
|
|
105
|
+
if (kept === undefined || startedAtOf(context) > startedAtOf(kept)) {
|
|
106
|
+
mut_latest.set(key, context);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const survivors = new Set(mut_latest.values());
|
|
111
|
+
|
|
112
|
+
return contexts
|
|
113
|
+
.filter(
|
|
114
|
+
(context) =>
|
|
115
|
+
survivors.has(context) && (which === 'all' || context.isRequired),
|
|
116
|
+
)
|
|
117
|
+
.map(reportedCheckOf);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** A context of the head commit's status check rollup, as read. */
|
|
121
|
+
export type CheckContextNode = t.TypeOf<typeof CheckContextNodeSchema>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Every context of the pull request's head commit, page by page. A head with
|
|
125
|
+
* none reported yet — right after a push — is an empty list.
|
|
126
|
+
*/
|
|
127
|
+
const readCheckContexts = async (
|
|
128
|
+
api: ApiContext,
|
|
129
|
+
prNumber: number,
|
|
130
|
+
): Promise<Result<readonly CheckContextNode[], string>> => {
|
|
131
|
+
const mut_contexts: CheckContextNode[] = [];
|
|
132
|
+
|
|
133
|
+
let mut_after: string | undefined = undefined;
|
|
134
|
+
|
|
135
|
+
for (const _page of Arr.seq(MAX_PAGES)) {
|
|
136
|
+
const rollup = await readRollupPage(api, prNumber, mut_after);
|
|
137
|
+
|
|
138
|
+
if (Result.isErr(rollup)) {
|
|
139
|
+
return rollup;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (rollup.value === null) {
|
|
143
|
+
return Result.ok(mut_contexts);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
mut_contexts.push(...rollup.value.nodes);
|
|
147
|
+
|
|
148
|
+
const { hasNextPage, endCursor } = rollup.value.pageInfo;
|
|
149
|
+
|
|
150
|
+
if (!hasNextPage || endCursor === null) {
|
|
151
|
+
return Result.ok(mut_contexts);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
mut_after = endCursor;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Result.err(
|
|
158
|
+
`#${prNumber} has more than ${MAX_PAGES * PAGE_SIZE} checks on its head`,
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* One page of the head commit's contexts, from `after` on; `null` when the
|
|
164
|
+
* head has no rollup yet.
|
|
165
|
+
*/
|
|
166
|
+
const readRollupPage = async (
|
|
167
|
+
api: ApiContext,
|
|
168
|
+
prNumber: number,
|
|
169
|
+
after: string | undefined,
|
|
170
|
+
): Promise<Result<RollupPage | null, string>> =>
|
|
171
|
+
Result.map(
|
|
172
|
+
await graphqlQuery(
|
|
173
|
+
api,
|
|
174
|
+
CHECKS_QUERY,
|
|
175
|
+
{
|
|
176
|
+
owner: api.repo.owner,
|
|
177
|
+
name: api.repo.name,
|
|
178
|
+
number: prNumber,
|
|
179
|
+
...(after === undefined ? {} : { after }),
|
|
180
|
+
},
|
|
181
|
+
ChecksAnswerSchema,
|
|
182
|
+
),
|
|
183
|
+
(answer) =>
|
|
184
|
+
answer.repository.pullRequest.commits.nodes[0]?.commit.statusCheckRollup
|
|
185
|
+
?.contexts ?? null,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
type RollupPage = Readonly<{
|
|
189
|
+
pageInfo: Readonly<{ hasNextPage: boolean; endCursor: string | null }>;
|
|
190
|
+
nodes: readonly CheckContextNode[];
|
|
191
|
+
}>;
|
|
192
|
+
|
|
193
|
+
const PAGE_SIZE = 100;
|
|
194
|
+
|
|
195
|
+
const MAX_PAGES = 10;
|
|
196
|
+
|
|
197
|
+
const CHECKS_QUERY = [
|
|
198
|
+
'query($owner: String!, $name: String!, $number: Int!, $after: String) {',
|
|
199
|
+
' repository(owner: $owner, name: $name) {',
|
|
200
|
+
' pullRequest(number: $number) {',
|
|
201
|
+
' commits(last: 1) { nodes { commit { statusCheckRollup {',
|
|
202
|
+
` contexts(first: ${PAGE_SIZE}, after: $after) {`,
|
|
203
|
+
' pageInfo { hasNextPage endCursor }',
|
|
204
|
+
' nodes {',
|
|
205
|
+
' __typename',
|
|
206
|
+
' ... on CheckRun {',
|
|
207
|
+
' name status conclusion startedAt completedAt detailsUrl',
|
|
208
|
+
' isRequired(pullRequestNumber: $number)',
|
|
209
|
+
' checkSuite { workflowRun { event workflow { name } } }',
|
|
210
|
+
' }',
|
|
211
|
+
' ... on StatusContext {',
|
|
212
|
+
' context state targetUrl createdAt',
|
|
213
|
+
' isRequired(pullRequestNumber: $number)',
|
|
214
|
+
' }',
|
|
215
|
+
' }',
|
|
216
|
+
' }',
|
|
217
|
+
' } } }',
|
|
218
|
+
' }',
|
|
219
|
+
' }',
|
|
220
|
+
'}',
|
|
221
|
+
].join('\n');
|
|
222
|
+
|
|
223
|
+
const CheckContextNodeSchema = t.union([
|
|
224
|
+
t.record({
|
|
225
|
+
__typename: t.literal('CheckRun'),
|
|
226
|
+
name: t.string(),
|
|
227
|
+
status: t.string(),
|
|
228
|
+
conclusion: t.union([t.string(), t.nullType]),
|
|
229
|
+
startedAt: t.union([t.string(), t.nullType]),
|
|
230
|
+
completedAt: t.union([t.string(), t.nullType]),
|
|
231
|
+
detailsUrl: t.union([t.string(), t.nullType]),
|
|
232
|
+
isRequired: t.boolean(),
|
|
233
|
+
checkSuite: t.union([
|
|
234
|
+
t.record({
|
|
235
|
+
workflowRun: t.union([
|
|
236
|
+
t.record({
|
|
237
|
+
event: t.string(),
|
|
238
|
+
workflow: t.record({ name: t.string() }),
|
|
239
|
+
}),
|
|
240
|
+
t.nullType,
|
|
241
|
+
]),
|
|
242
|
+
}),
|
|
243
|
+
t.nullType,
|
|
244
|
+
]),
|
|
245
|
+
}),
|
|
246
|
+
t.record({
|
|
247
|
+
__typename: t.literal('StatusContext'),
|
|
248
|
+
context: t.string(),
|
|
249
|
+
state: t.string(),
|
|
250
|
+
targetUrl: t.union([t.string(), t.nullType]),
|
|
251
|
+
createdAt: t.string(),
|
|
252
|
+
isRequired: t.boolean(),
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const ChecksAnswerSchema = t.record({
|
|
257
|
+
repository: t.record({
|
|
258
|
+
pullRequest: t.record({
|
|
259
|
+
commits: t.record({
|
|
260
|
+
nodes: t.array(
|
|
261
|
+
t.record({
|
|
262
|
+
commit: t.record({
|
|
263
|
+
statusCheckRollup: t.union([
|
|
264
|
+
t.record({
|
|
265
|
+
contexts: t.record({
|
|
266
|
+
pageInfo: t.record({
|
|
267
|
+
hasNextPage: t.boolean(),
|
|
268
|
+
endCursor: t.union([t.string(), t.nullType]),
|
|
269
|
+
}),
|
|
270
|
+
nodes: t.array(CheckContextNodeSchema),
|
|
271
|
+
}),
|
|
272
|
+
}),
|
|
273
|
+
t.nullType,
|
|
274
|
+
]),
|
|
275
|
+
}),
|
|
276
|
+
}),
|
|
277
|
+
),
|
|
278
|
+
}),
|
|
279
|
+
}),
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/** What `gh` wrote for a time a check has not reached. */
|
|
284
|
+
const ZERO_TIME = '0001-01-01T00:00:00Z';
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Which runs supersede each other: a commit status by its context, a check
|
|
288
|
+
* run by its name, workflow and event, as `gh` told them apart.
|
|
289
|
+
*/
|
|
290
|
+
const keyOf = (context: CheckContextNode): string =>
|
|
291
|
+
context.__typename === 'StatusContext'
|
|
292
|
+
? (`status:${context.context}` as const)
|
|
293
|
+
: [
|
|
294
|
+
'run',
|
|
295
|
+
context.name,
|
|
296
|
+
context.checkSuite?.workflowRun?.workflow.name ?? '',
|
|
297
|
+
context.checkSuite?.workflowRun?.event ?? '',
|
|
298
|
+
].join(':');
|
|
299
|
+
|
|
300
|
+
const startedAtOf = (context: CheckContextNode): string =>
|
|
301
|
+
(context.__typename === 'StatusContext'
|
|
302
|
+
? context.createdAt
|
|
303
|
+
: context.startedAt) ?? ZERO_TIME;
|
|
304
|
+
|
|
305
|
+
const reportedCheckOf = (context: CheckContextNode): ReportedCheck =>
|
|
306
|
+
context.__typename === 'StatusContext'
|
|
307
|
+
? ({
|
|
308
|
+
name: context.context,
|
|
309
|
+
bucket: bucketOf(context.state),
|
|
310
|
+
link: context.targetUrl ?? '',
|
|
311
|
+
startedAt: context.createdAt,
|
|
312
|
+
completedAt: context.createdAt,
|
|
313
|
+
workflow: '',
|
|
314
|
+
} as const)
|
|
315
|
+
: ({
|
|
316
|
+
name: context.name,
|
|
317
|
+
bucket: bucketOf(
|
|
318
|
+
context.status === 'COMPLETED'
|
|
319
|
+
? (context.conclusion ?? '')
|
|
320
|
+
: context.status,
|
|
321
|
+
),
|
|
322
|
+
link: context.detailsUrl ?? '',
|
|
323
|
+
startedAt: context.startedAt ?? ZERO_TIME,
|
|
324
|
+
completedAt: context.completedAt ?? ZERO_TIME,
|
|
325
|
+
workflow: context.checkSuite?.workflowRun?.workflow.name ?? '',
|
|
326
|
+
} as const);
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A check run's conclusion, or its status while it runs, or a commit
|
|
330
|
+
* status's state, in `gh pr checks`'s buckets. Anything else — `QUEUED`,
|
|
331
|
+
* `IN_PROGRESS`, `PENDING`, `EXPECTED`, `STALE` — is waited for.
|
|
332
|
+
*/
|
|
333
|
+
const bucketOf = (state: string): string => {
|
|
334
|
+
switch (state) {
|
|
335
|
+
case 'SUCCESS':
|
|
336
|
+
return 'pass';
|
|
337
|
+
|
|
338
|
+
case 'SKIPPED':
|
|
339
|
+
case 'NEUTRAL':
|
|
340
|
+
return 'skipping';
|
|
341
|
+
|
|
342
|
+
case 'ERROR':
|
|
343
|
+
case 'FAILURE':
|
|
344
|
+
case 'TIMED_OUT':
|
|
345
|
+
case 'ACTION_REQUIRED':
|
|
346
|
+
case 'STARTUP_FAILURE':
|
|
347
|
+
return 'fail';
|
|
348
|
+
|
|
349
|
+
case 'CANCELLED':
|
|
350
|
+
return 'cancel';
|
|
351
|
+
|
|
352
|
+
default:
|
|
353
|
+
return 'pending';
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* `checks` is what {@link listRequiredChecks} reported for the head commit;
|
|
359
|
+
* `requiredContexts` is what the ruleset asks for. The difference between the
|
|
360
|
+
* two is the point: a context in the second and not the first has not
|
|
361
|
+
* reported yet, and GitHub will not merge until it does. `running` is every
|
|
362
|
+
* check on the head that has not finished ({@link listRunningChecks}); while
|
|
363
|
+
* it is not empty the required ones may be the round before speaking, so
|
|
364
|
+
* they cannot pass the pull request.
|
|
365
|
+
*
|
|
366
|
+
* Nor can they fail it when they finished before the round still running
|
|
367
|
+
* began: the round `skip-ci` cancelled, as `open-pr` opens every pull request
|
|
368
|
+
* under it, leaves its aggregates red, and reading that red as this round's
|
|
369
|
+
* set a pull request aside while its first real round was under way (#2246).
|
|
370
|
+
* Such a failure waits for the round to end, as a check not yet reported
|
|
371
|
+
* does. So does one that finished after the round began but whose workflow
|
|
372
|
+
* has a newer run still going: the round `skip-ci` was put on for can take
|
|
373
|
+
* long enough to be cancelled that its aggregate goes red after the round
|
|
374
|
+
* taking its place has started, and only the run that replaced it says it is
|
|
375
|
+
* the round before (#2463). A failure of this round still fails the pull
|
|
376
|
+
* request at once.
|
|
377
|
+
*
|
|
378
|
+
* The verdict itself is `pr-report-core`'s `summarizeChecks`, which
|
|
379
|
+
* `pr-report` and the page reach theirs through, so the worker and the report
|
|
380
|
+
* cannot disagree about one; what is here turns what GitHub reports into
|
|
381
|
+
* the states that reads.
|
|
382
|
+
*/
|
|
383
|
+
export const summarizeChecks = (
|
|
384
|
+
checks: readonly ReportedCheck[],
|
|
385
|
+
requiredContexts: readonly string[],
|
|
386
|
+
running: readonly RunningCheck[],
|
|
387
|
+
): ChecksSummary => {
|
|
388
|
+
const roundStartedMs = earliestStart(running);
|
|
389
|
+
|
|
390
|
+
const isOfRoundBefore = (check: ReportedCheck): boolean => {
|
|
391
|
+
const completedMs = epochMsOf(check.completedAt);
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
(roundStartedMs !== undefined &&
|
|
395
|
+
completedMs !== undefined &&
|
|
396
|
+
completedMs < roundStartedMs) ||
|
|
397
|
+
isSuperseded(check, running)
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const isFailureOfThisRound = (check: ReportedCheck): boolean =>
|
|
402
|
+
isFailure(check) && !isOfRoundBefore(check);
|
|
403
|
+
|
|
404
|
+
const mut_reported = new Map<string, ContextState>();
|
|
405
|
+
|
|
406
|
+
for (const check of checks) {
|
|
407
|
+
const state = stateOf(check, isOfRoundBefore(check));
|
|
408
|
+
|
|
409
|
+
const previous = mut_reported.get(check.name);
|
|
410
|
+
|
|
411
|
+
mut_reported.set(
|
|
412
|
+
check.name,
|
|
413
|
+
previous === undefined ? state : combineContextStates(previous, state),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const pendingNames = new Set(
|
|
418
|
+
checks
|
|
419
|
+
.filter((check) => mut_reported.get(check.name) === 'pending')
|
|
420
|
+
.map((check) => check.name),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const stillRunning = running
|
|
424
|
+
.map(({ name }) => name)
|
|
425
|
+
.filter((name) => !pendingNames.has(name));
|
|
426
|
+
|
|
427
|
+
const summary = summarizeContexts({
|
|
428
|
+
// What the ruleset asks for, and whatever `--required` reported besides:
|
|
429
|
+
// all of it, when the rules could not be read.
|
|
430
|
+
required: Arr.uniq([
|
|
431
|
+
...requiredContexts,
|
|
432
|
+
...checks.map((check) => check.name),
|
|
433
|
+
]),
|
|
434
|
+
reported: mut_reported,
|
|
435
|
+
// `skip-ci` is the queue's to read (`queueStanding`), not the checks'.
|
|
436
|
+
paused: false,
|
|
437
|
+
// No checks at all means they have not been reported yet — which is
|
|
438
|
+
// also all that can be said when the ruleset could not be read.
|
|
439
|
+
running: Arr.isNonEmpty(stillRunning) || !Arr.isNonEmpty(checks),
|
|
440
|
+
// A required context that has not reported is waited for wherever the
|
|
441
|
+
// pull request points: the worker arms what it judges green, and an
|
|
442
|
+
// armed pull request stacked on a branch no ruleset covers merges into
|
|
443
|
+
// it at once.
|
|
444
|
+
baseCovered: true,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
...summary,
|
|
449
|
+
failedRuns: checks
|
|
450
|
+
.filter(isFailureOfThisRound)
|
|
451
|
+
.map(({ name, link }) => ({ name, link })),
|
|
452
|
+
running: stillRunning,
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/** Everything the merge is still waiting for, reported and unreported. */
|
|
457
|
+
export const describeWaitingOn = (summary: ChecksSummary): string => {
|
|
458
|
+
const waiting = [
|
|
459
|
+
...summary.pending,
|
|
460
|
+
...summary.missing.map((context) => `${context} (not reported yet)`),
|
|
461
|
+
...summary.running.map((name) => `${name} (running)`),
|
|
462
|
+
] as const;
|
|
463
|
+
|
|
464
|
+
return Arr.isNonEmpty(waiting) ? waiting.join(', ') : 'checks to be reported';
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const isFailure = (check: ReportedCheck): boolean =>
|
|
468
|
+
check.bucket === 'fail' || check.bucket === 'cancel';
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* What one reported check says of its context. A failure of the round before
|
|
472
|
+
* waits for the round still running, as a check not yet reported does.
|
|
473
|
+
*/
|
|
474
|
+
const stateOf = (check: ReportedCheck, ofRoundBefore: boolean): ContextState =>
|
|
475
|
+
isFailure(check)
|
|
476
|
+
? ofRoundBefore
|
|
477
|
+
? 'pending'
|
|
478
|
+
: 'failed'
|
|
479
|
+
: check.bucket === 'pending'
|
|
480
|
+
? 'pending'
|
|
481
|
+
: check.bucket === 'skipping'
|
|
482
|
+
? 'skipped'
|
|
483
|
+
: 'passed';
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Whether a newer run of the check's own workflow is still running. Run ids
|
|
487
|
+
* only grow, so a higher one is a later run; a commit status has no workflow
|
|
488
|
+
* and no run, and is never superseded.
|
|
489
|
+
*/
|
|
490
|
+
const isSuperseded = (
|
|
491
|
+
check: ReportedCheck,
|
|
492
|
+
running: readonly RunningCheck[],
|
|
493
|
+
): boolean => {
|
|
494
|
+
const runId = workflowRunId(check.link);
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
check.workflow !== '' &&
|
|
498
|
+
runId !== undefined &&
|
|
499
|
+
running.some((other) => {
|
|
500
|
+
const otherRunId = workflowRunId(other.link);
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
other.workflow === check.workflow &&
|
|
504
|
+
otherRunId !== undefined &&
|
|
505
|
+
otherRunId > runId
|
|
506
|
+
);
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/** The run id in a job's link (`…/actions/runs/<id>/job/<id>`). */
|
|
512
|
+
const workflowRunId = (link: string): number | undefined => {
|
|
513
|
+
const id = /\/actions\/runs\/(\d+)\//u.exec(link)?.[1];
|
|
514
|
+
|
|
515
|
+
return id === undefined
|
|
516
|
+
? undefined
|
|
517
|
+
: Result.unwrapOkOr(Num.safeParseInt(id), undefined);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* When the earliest check still running started, in milliseconds; `undefined`
|
|
522
|
+
* when nothing is running or no start time can be read.
|
|
523
|
+
*/
|
|
524
|
+
const earliestStart = (
|
|
525
|
+
running: readonly RunningCheck[],
|
|
526
|
+
): number | undefined => {
|
|
527
|
+
const starts = Arr.generate(function* () {
|
|
528
|
+
for (const { startedAt } of running) {
|
|
529
|
+
const ms = epochMsOf(startedAt);
|
|
530
|
+
|
|
531
|
+
if (ms !== undefined) {
|
|
532
|
+
yield ms;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
return Arr.isNonEmpty(starts) ? Math.min(...starts) : undefined;
|
|
538
|
+
};
|