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,334 @@
|
|
|
1
|
+
// cspell:ignore tmpfs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixing a failure that is only a fixer's diff: running the fixer, amending
|
|
5
|
+
* what it wrote onto the pull request's one commit, and pushing it.
|
|
6
|
+
*
|
|
7
|
+
* Every check job ends with `z:assert-repo-is-clean`, so a `fix:` or `gen:`
|
|
8
|
+
* entry of the check matrices fails by _changing_ files rather than by
|
|
9
|
+
* reporting anything, and the fix is to run the same command and commit what
|
|
10
|
+
* it wrote. That much is mechanical; everything else a failing check asks for
|
|
11
|
+
* is the skill's, or a person's, and is left to them. Which jobs are shaped
|
|
12
|
+
* like that, and what reproduces each, is the repository's to say
|
|
13
|
+
* (`config.mts`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { statesFromCheckRuns, type ContextState } from 'pr-report-core';
|
|
19
|
+
import { Arr, Num, Result } from 'ts-data-forge';
|
|
20
|
+
import { git, log, sh, type ApiContext } from '../lib/index.mjs';
|
|
21
|
+
import { CONFIG_PATH, type AutoFixConfig } from './config.mjs';
|
|
22
|
+
import { listCheckRuns, remoteSha, removeWorktree } from './github.mjs';
|
|
23
|
+
import type { AutoFixed, PullRequest } from './types.mjs';
|
|
24
|
+
import { lastLines, stopRequested } from './util.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tries once to fix the checks that failed on `head` by running the fixers
|
|
28
|
+
* their matrix entries name, and pushes the result when there is one.
|
|
29
|
+
*
|
|
30
|
+
* Only when **every** failed job is such an entry: one real failure beside
|
|
31
|
+
* them means the push would put the pull request through a matrix that fails
|
|
32
|
+
* anyway, so the whole of it is left to whoever fixes the real one.
|
|
33
|
+
*
|
|
34
|
+
* The fixers run in a throwaway worktree, installed and built as CI installs
|
|
35
|
+
* and builds, so the checkout this runs from is never touched. Where it goes
|
|
36
|
+
* is {@link WORKTREE_ROOT}'s to say.
|
|
37
|
+
*
|
|
38
|
+
* The branch has to be exactly one commit on top of the base, and the fix is
|
|
39
|
+
* amended onto it — a pull request is one commit (`Validate commit count`), and
|
|
40
|
+
* squashing several means writing the message that lands on `main`, which is
|
|
41
|
+
* not a mechanical act.
|
|
42
|
+
*/
|
|
43
|
+
export const autoFix = async (
|
|
44
|
+
api: ApiContext,
|
|
45
|
+
pr: PullRequest,
|
|
46
|
+
head: string,
|
|
47
|
+
defaultBranch: string,
|
|
48
|
+
config: AutoFixConfig | undefined,
|
|
49
|
+
): Promise<AutoFixed> => {
|
|
50
|
+
const runs = await listCheckRuns(api, head);
|
|
51
|
+
|
|
52
|
+
if (Result.isErr(runs)) {
|
|
53
|
+
return declined(`cannot read the check runs: ${runs.value}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const plan = planAutoFix(statesFromCheckRuns(runs.value), config);
|
|
57
|
+
|
|
58
|
+
if (Result.isErr(plan)) {
|
|
59
|
+
return declined(plan.value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const worktreeDir = path.join(WORKTREE_ROOT, `auto-fix-pr-${pr.number}`);
|
|
63
|
+
|
|
64
|
+
// A previous run may have been interrupted with this worktree in place.
|
|
65
|
+
await removeWorktree(worktreeDir);
|
|
66
|
+
|
|
67
|
+
const branch = pr.headRefName;
|
|
68
|
+
|
|
69
|
+
const fetched = await git(
|
|
70
|
+
`git fetch --quiet origin ${sh(defaultBranch)} ${sh(branch)}`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (Result.isErr(fetched)) {
|
|
74
|
+
return declined(`fetch failed: ${fetched.value}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const remoteHead = await git(`git rev-parse ${sh(`origin/${branch}`)}`);
|
|
78
|
+
|
|
79
|
+
if (Result.isErr(remoteHead)) {
|
|
80
|
+
return declined(`cannot resolve origin/${branch}: ${remoteHead.value}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (remoteHead.value.trim() !== head) {
|
|
84
|
+
return { kind: 'moved' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const added = await git(
|
|
88
|
+
`git worktree add --detach ${sh(worktreeDir)} ${sh(head)}`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (Result.isErr(added)) {
|
|
92
|
+
return declined(`checkout failed: ${added.value}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log(
|
|
96
|
+
`#${pr.number}: every failed job is a fixer's diff; running ${plan.value.commands.join(', ')} in ${worktreeDir}.`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = await fixInWorktree(
|
|
100
|
+
pr,
|
|
101
|
+
head,
|
|
102
|
+
defaultBranch,
|
|
103
|
+
plan.value,
|
|
104
|
+
worktreeDir,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await removeWorktree(worktreeDir);
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* What to run for the jobs that failed, or why nothing is to be run.
|
|
114
|
+
*
|
|
115
|
+
* `states` is every context on the head commit. A failed context the
|
|
116
|
+
* configuration ignores (an aggregate, only the echo of a job that failed
|
|
117
|
+
* under it) is not read; every other failure has to match one of its fixers,
|
|
118
|
+
* whose command is the whole of what reproduces the job — a job that runs a
|
|
119
|
+
* command and then asserts a clean tree. `config` is the repository's
|
|
120
|
+
* (`config.mts`); without one nothing is fixed.
|
|
121
|
+
*/
|
|
122
|
+
export const planAutoFix = (
|
|
123
|
+
states: ReadonlyMap<string, ContextState>,
|
|
124
|
+
config: AutoFixConfig | undefined,
|
|
125
|
+
): Result<AutoFixPlan, string> => {
|
|
126
|
+
if (config === undefined) {
|
|
127
|
+
return Result.err(`the repository has no autoFix in ${CONFIG_PATH}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const failed = Array.from(states)
|
|
131
|
+
.filter(
|
|
132
|
+
([name, state]) =>
|
|
133
|
+
state === 'failed' &&
|
|
134
|
+
config.ignore.every((pattern) => !pattern.test(name)),
|
|
135
|
+
)
|
|
136
|
+
.map(([name]) => name);
|
|
137
|
+
|
|
138
|
+
if (!Arr.isNonEmpty(failed)) {
|
|
139
|
+
return Result.err('no job failed apart from the ignored ones');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entries = failed.map((name) => ({
|
|
143
|
+
name,
|
|
144
|
+
fixer: fixerFor(name, config),
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const others = entries
|
|
148
|
+
.filter(({ fixer }) => fixer === undefined)
|
|
149
|
+
.map(({ name }) => name);
|
|
150
|
+
|
|
151
|
+
if (Arr.isNonEmpty(others)) {
|
|
152
|
+
return Result.err(
|
|
153
|
+
`${others.join(', ')} failed, which is not a fixer's diff`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const fixers = entries
|
|
158
|
+
.map(({ fixer }) => fixer)
|
|
159
|
+
.filter((fixer) => fixer !== undefined);
|
|
160
|
+
|
|
161
|
+
return Result.ok({
|
|
162
|
+
before: config.before,
|
|
163
|
+
prepare: Arr.uniq(
|
|
164
|
+
config.prepare
|
|
165
|
+
.filter(({ context }) => failed.some((name) => context.test(name)))
|
|
166
|
+
.map(({ run }) => run),
|
|
167
|
+
),
|
|
168
|
+
commands: Arr.uniq(
|
|
169
|
+
fixers
|
|
170
|
+
.toSorted((a, b) =>
|
|
171
|
+
a.stage === b.stage
|
|
172
|
+
? a.command.localeCompare(b.command)
|
|
173
|
+
: a.stage - b.stage,
|
|
174
|
+
)
|
|
175
|
+
.map(({ command }) => command),
|
|
176
|
+
),
|
|
177
|
+
after: config.after,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type AutoFixPlan = Readonly<{
|
|
182
|
+
/** What runs first, always. */
|
|
183
|
+
before: readonly string[];
|
|
184
|
+
/** What the failed jobs were given before they ran, each once. */
|
|
185
|
+
prepare: readonly string[];
|
|
186
|
+
/** The fixers, by stage. */
|
|
187
|
+
commands: readonly string[];
|
|
188
|
+
/** What settles their output. */
|
|
189
|
+
after: readonly string[];
|
|
190
|
+
}>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Not the OS temp directory the rebase uses: an install is a `node_modules`
|
|
194
|
+
* tree, which the pnpm store hard-links only within one filesystem, and the
|
|
195
|
+
* temp directory is often a tmpfs. Not the git directory or `~/.cache`
|
|
196
|
+
* either: `fix:fmt:diff` skips every file with a `.git`, `.cache`, `dist`,
|
|
197
|
+
* `build` or `out` segment anywhere in its absolute path, so a worktree under
|
|
198
|
+
* one is formatted by nothing.
|
|
199
|
+
*/
|
|
200
|
+
const WORKTREE_ROOT = path.join(os.homedir(), '.local', 'state', 'unblock-prs');
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The first fixer `name` matches, with its command: `$1` to `$9` replaced by
|
|
204
|
+
* the capture groups, shell-quoted, since a context's name is whatever the
|
|
205
|
+
* job that reported it chose.
|
|
206
|
+
*/
|
|
207
|
+
const fixerFor = (
|
|
208
|
+
name: string,
|
|
209
|
+
config: AutoFixConfig,
|
|
210
|
+
): Readonly<{ command: string; stage: number }> | undefined => {
|
|
211
|
+
const matched = config.fixers
|
|
212
|
+
.map((candidate) => ({ candidate, groups: candidate.context.exec(name) }))
|
|
213
|
+
.find((entry) => entry.groups !== null);
|
|
214
|
+
|
|
215
|
+
const fixer = matched?.candidate;
|
|
216
|
+
|
|
217
|
+
const groups = matched?.groups ?? undefined;
|
|
218
|
+
|
|
219
|
+
if (fixer === undefined || groups === undefined) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
command: fixer.run.replaceAll(/\$([1-9])/gu, (_, index) =>
|
|
225
|
+
sh(
|
|
226
|
+
groups[Result.unwrapOkOr(Num.safeParseInt(index ?? ''), Number.NaN)] ??
|
|
227
|
+
'',
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
stage: fixer.stage,
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const declined = (detail: string): AutoFixed =>
|
|
235
|
+
({ kind: 'declined', detail }) as const;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The part that happens inside the worktree, so that removing it afterwards
|
|
239
|
+
* is one statement in the caller. Every command here resolves to a `Result`,
|
|
240
|
+
* so the caller's next line always runs.
|
|
241
|
+
*/
|
|
242
|
+
const fixInWorktree = async (
|
|
243
|
+
pr: PullRequest,
|
|
244
|
+
head: string,
|
|
245
|
+
defaultBranch: string,
|
|
246
|
+
plan: AutoFixPlan,
|
|
247
|
+
worktreeDir: string,
|
|
248
|
+
): Promise<AutoFixed> => {
|
|
249
|
+
const commits = await git(
|
|
250
|
+
`git rev-list --count ${sh(`origin/${defaultBranch}..HEAD`)}`,
|
|
251
|
+
worktreeDir,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (Result.isErr(commits)) {
|
|
255
|
+
return declined(`cannot count the commits: ${commits.value}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (commits.value.trim() !== '1') {
|
|
259
|
+
return declined(
|
|
260
|
+
`the branch is ${commits.value.trim()} commits on ${defaultBranch}, and only one is amended`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const steps = [
|
|
265
|
+
...plan.before,
|
|
266
|
+
...plan.prepare,
|
|
267
|
+
...plan.commands,
|
|
268
|
+
...plan.after,
|
|
269
|
+
] as const;
|
|
270
|
+
|
|
271
|
+
for (const step of steps) {
|
|
272
|
+
if (stopRequested()) {
|
|
273
|
+
return declined('stopped before it finished');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
log(`#${pr.number}: ${step}`);
|
|
277
|
+
|
|
278
|
+
const ran = await git(step, worktreeDir);
|
|
279
|
+
|
|
280
|
+
if (Result.isErr(ran)) {
|
|
281
|
+
return declined(`\`${step}\` failed: ${lastLines(ran.value, 5)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const status = await git('git status --porcelain', worktreeDir);
|
|
286
|
+
|
|
287
|
+
if (Result.isErr(status)) {
|
|
288
|
+
return declined(`cannot read what changed: ${status.value}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (status.value.trim() === '') {
|
|
292
|
+
return declined(
|
|
293
|
+
`${plan.commands.join(', ')} changed nothing here, so the failure is not the diff`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const committed = await git(
|
|
298
|
+
'git add --all && git commit --quiet --amend --no-edit',
|
|
299
|
+
worktreeDir,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (Result.isErr(committed)) {
|
|
303
|
+
return declined(`cannot amend: ${lastLines(committed.value, 5)}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const newHead = await git('git rev-parse HEAD', worktreeDir);
|
|
307
|
+
|
|
308
|
+
if (Result.isErr(newHead)) {
|
|
309
|
+
return declined(`cannot read the amended head: ${newHead.value}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (stopRequested()) {
|
|
313
|
+
return declined('stopped before pushing');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const sha = newHead.value.trim();
|
|
317
|
+
|
|
318
|
+
const branch = pr.headRefName;
|
|
319
|
+
|
|
320
|
+
const pushed = await git(
|
|
321
|
+
`git push --force-with-lease=${sh(`${branch}:${head}`)} origin ${sh(`${sha}:refs/heads/${branch}`)}`,
|
|
322
|
+
worktreeDir,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (Result.isErr(pushed)) {
|
|
326
|
+
const remoteHeadNow = await remoteSha(branch);
|
|
327
|
+
|
|
328
|
+
return Result.isOk(remoteHeadNow) && remoteHeadNow.value !== head
|
|
329
|
+
? { kind: 'moved' }
|
|
330
|
+
: declined(`push refused: ${lastLines(pushed.value, 5)}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { kind: 'pushed', head: sha, commands: plan.commands };
|
|
334
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ContextState } from 'pr-report-core';
|
|
2
|
+
import { Result } from 'ts-data-forge';
|
|
3
|
+
import { planAutoFix } from './auto-fix.mjs';
|
|
4
|
+
import { readConfig } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
// The repository's own `.github/unblock-prs.json`: these tests are what its
|
|
7
|
+
// rules mean for the matrices it has.
|
|
8
|
+
const config = await readConfig().then((read) =>
|
|
9
|
+
Result.isOk(read) ? read.value?.autoFix : undefined,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const build = 'pnpm run ws:build';
|
|
13
|
+
|
|
14
|
+
const states = (
|
|
15
|
+
entries: readonly (readonly [string, ContextState])[],
|
|
16
|
+
): ReadonlyMap<string, ContextState> => new Map(entries);
|
|
17
|
+
|
|
18
|
+
describe(planAutoFix, () => {
|
|
19
|
+
test('runs the command a failed matrix entry names, with the build CI gave it', () => {
|
|
20
|
+
const plan = planAutoFix(
|
|
21
|
+
states([
|
|
22
|
+
['code-check (fix:codemod:full)', 'failed'],
|
|
23
|
+
['code-check (ws:check:types)', 'passed'],
|
|
24
|
+
['code-check-result / result', 'failed'],
|
|
25
|
+
['style-check (fix:fmt:full)', 'passed'],
|
|
26
|
+
]),
|
|
27
|
+
config,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
assert.isTrue(Result.isOk(plan));
|
|
31
|
+
|
|
32
|
+
assert.deepStrictEqual(plan.value, {
|
|
33
|
+
before: ['pnpm install --frozen-lockfile'],
|
|
34
|
+
prepare: [build],
|
|
35
|
+
commands: ["pnpm run 'fix:codemod:full'"],
|
|
36
|
+
after: ['pnpm run fix:fmt:diff'],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('builds for a style-check entry only where CI does', () => {
|
|
41
|
+
const formatOnly = planAutoFix(
|
|
42
|
+
states([
|
|
43
|
+
['style-check (fix:fmt:full)', 'failed'],
|
|
44
|
+
['style-check (strict-lib:fix:fmt)', 'failed'],
|
|
45
|
+
['style-check-result / result', 'failed'],
|
|
46
|
+
]),
|
|
47
|
+
config,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.isTrue(Result.isOk(formatOnly));
|
|
51
|
+
|
|
52
|
+
assert.deepStrictEqual(formatOnly.value.prepare, []);
|
|
53
|
+
|
|
54
|
+
const generated = planAutoFix(
|
|
55
|
+
states([
|
|
56
|
+
['style-check (ws:gen)', 'failed'],
|
|
57
|
+
['style-check (gen:deps-graph)', 'failed'],
|
|
58
|
+
]),
|
|
59
|
+
config,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
assert.isTrue(Result.isOk(generated));
|
|
63
|
+
|
|
64
|
+
assert.deepStrictEqual(generated.value.prepare, [build]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('runs generators first and formatters last', () => {
|
|
68
|
+
const plan = planAutoFix(
|
|
69
|
+
states([
|
|
70
|
+
['style-check (fix:fmt:full)', 'failed'],
|
|
71
|
+
['code-check (ws:fix:lint)', 'failed'],
|
|
72
|
+
['style-check (ws:gen)', 'failed'],
|
|
73
|
+
['style-check (strict-lib:fix:fmt)', 'failed'],
|
|
74
|
+
['code-check (fix:codemod:full)', 'failed'],
|
|
75
|
+
['style-check (gen:license-files)', 'failed'],
|
|
76
|
+
]),
|
|
77
|
+
config,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
assert.isTrue(Result.isOk(plan));
|
|
81
|
+
|
|
82
|
+
assert.deepStrictEqual(plan.value.commands, [
|
|
83
|
+
"pnpm run 'gen:license-files'",
|
|
84
|
+
"pnpm run 'ws:gen'",
|
|
85
|
+
"pnpm run 'fix:codemod:full'",
|
|
86
|
+
"pnpm run 'ws:fix:lint'",
|
|
87
|
+
"pnpm run 'fix:fmt:full'",
|
|
88
|
+
"pnpm run 'strict-lib:fix:fmt'",
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('declines when any failure is not a fixer, naming it', () => {
|
|
93
|
+
const plan = planAutoFix(
|
|
94
|
+
states([
|
|
95
|
+
['code-check (fix:codemod:full)', 'failed'],
|
|
96
|
+
['code-check (ws:check:types)', 'failed'],
|
|
97
|
+
['code-check-result / result', 'failed'],
|
|
98
|
+
]),
|
|
99
|
+
config,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
assert.isTrue(Result.isErr(plan));
|
|
103
|
+
|
|
104
|
+
expect(plan.value).toContain('code-check (ws:check:types)');
|
|
105
|
+
|
|
106
|
+
expect(plan.value).not.toContain('fix:codemod:full');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test.each([
|
|
110
|
+
['a check command', 'code-check (ws:check:test:cov)'],
|
|
111
|
+
['`ws:doc`, whose output is untracked', 'style-check (ws:doc)'],
|
|
112
|
+
['a job of another workflow', 'Validate commit count'],
|
|
113
|
+
['a matrix entry of another workflow', 'test-node-versions (fix:x)'],
|
|
114
|
+
] as const)('declines %s', (_, name) => {
|
|
115
|
+
const plan = planAutoFix(states([[name, 'failed']]), config);
|
|
116
|
+
|
|
117
|
+
assert.isTrue(Result.isErr(plan));
|
|
118
|
+
|
|
119
|
+
expect(plan.value).toContain(name);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('declines when nothing but an aggregate failed', () => {
|
|
123
|
+
const plan = planAutoFix(
|
|
124
|
+
states([
|
|
125
|
+
['code-check (fix:codemod:full)', 'passed'],
|
|
126
|
+
['code-check-result / result', 'failed'],
|
|
127
|
+
]),
|
|
128
|
+
config,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
assert.isTrue(Result.isErr(plan));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('reads only what failed; pending and skipped entries are not a plan', () => {
|
|
135
|
+
const plan = planAutoFix(
|
|
136
|
+
states([
|
|
137
|
+
['style-check (fix:fmt:full)', 'failed'],
|
|
138
|
+
['code-check (ws:check:types)', 'pending'],
|
|
139
|
+
['code-check (ws:check:e2e)', 'skipped'],
|
|
140
|
+
]),
|
|
141
|
+
config,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
assert.isTrue(Result.isOk(plan));
|
|
145
|
+
|
|
146
|
+
assert.deepStrictEqual(plan.value.commands, ["pnpm run 'fix:fmt:full'"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('declines everything where the repository configures no auto-fix', () => {
|
|
150
|
+
const plan = planAutoFix(
|
|
151
|
+
states([['style-check (fix:fmt:full)', 'failed']]),
|
|
152
|
+
undefined,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
assert.isTrue(Result.isErr(plan));
|
|
156
|
+
|
|
157
|
+
expect(plan.value).toContain('.github/unblock-prs.json');
|
|
158
|
+
});
|
|
159
|
+
});
|