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,1616 @@
|
|
|
1
|
+
<!-- cspell:ignore unlabel, gpgsign, tmpfs -->
|
|
2
|
+
|
|
3
|
+
# `unblock-prs`
|
|
4
|
+
|
|
5
|
+
`pnpm run unblock-prs` — the worker that lands the merge queue. The
|
|
6
|
+
`/unblock-prs` skill fixes what it sets aside, and nothing else.
|
|
7
|
+
|
|
8
|
+
- [日本語](#日本語)
|
|
9
|
+
- [English](#english)
|
|
10
|
+
|
|
11
|
+
The authority for the conventions this reads is `CLAUDE.md`: "CI" and
|
|
12
|
+
"Commits and pull requests". This file describes what the script does with
|
|
13
|
+
them.
|
|
14
|
+
|
|
15
|
+
## 日本語
|
|
16
|
+
|
|
17
|
+
### 何をするスクリプトか
|
|
18
|
+
|
|
19
|
+
`merge-queued` ラベルの付いた PR を、PR 自身が宣言した順に **1本ずつ** 前に進
|
|
20
|
+
めます。1サイクルで触るのは常に1本だけです。マージした瞬間に `main` が動き、
|
|
21
|
+
他のブランチはすべて `BEHIND` に戻るため、まとめて rebase しても1本を除いて捨
|
|
22
|
+
てることになるからです。
|
|
23
|
+
|
|
24
|
+
**やらないこと**: マージ(auto-merge の仕事)、`merge-queued` を付けること、
|
|
25
|
+
レビュー承認、失敗したチェックの修正(skill の仕事。例外は fixer の差分だけで落
|
|
26
|
+
ちた場合で、「4a」を参照)。rebase は使い捨ての `git worktree` の中で行うので、
|
|
27
|
+
実行元のチェックアウトの作業ツリーには触れません。
|
|
28
|
+
|
|
29
|
+
### PR 側で宣言する6つのこと
|
|
30
|
+
|
|
31
|
+
**`merge-queued` ラベル** が対象範囲そのものです。付いていない PR は何も言わず
|
|
32
|
+
に無視します(例外は2つで、下の層がマージされて `main` に付け替えられた層の
|
|
33
|
+
積み直し(下の「base」の項)と、`auto-rebase` の付いた PR を `main` に追従させる
|
|
34
|
+
こと(下の「`auto-rebase`」の項)です)。付いていて、かつ動かせない PR(draft / base が `main` でも open な
|
|
35
|
+
PR のブランチでもない)は理由をログに出します —
|
|
36
|
+
ラベルは明示的な依頼なので、答えが「できない」なら黙っていてはいけないから
|
|
37
|
+
です。
|
|
38
|
+
|
|
39
|
+
**auto-merge を張るのはこのスクリプトだけです**( `rebase.mts` の `armOnPick` )。
|
|
40
|
+
`open-pr` はどの PR にも張らず、ここが `merge-queued` の PR を pick したときに
|
|
41
|
+
張ります — rebase する候補なら push の後・`skip-ci` を外す前に、up-to-date で
|
|
42
|
+
watch する PR なら watch の前に。ruleset は承認を要求しないので、張られた PR は
|
|
43
|
+
緑になった瞬間にマージされます。キューに入れる前に張らないのはそのためで、
|
|
44
|
+
stack の層は base が ruleset の掛からないブランチなので、張った瞬間に下の層へ
|
|
45
|
+
マージされかねません。マージしてよいかを表すのは `merge-queued` だけなので、
|
|
46
|
+
auto-merge が外れた PR(base の変更で GitHub が外した場合も、人が切った場合も)
|
|
47
|
+
は次に pick されたときに張り直します。止めたいときは `merge-queued` を外します。
|
|
48
|
+
|
|
49
|
+
**`Merge-After:` トレーラ** が順序を宣言します。行頭に書き、複数の番号を1行に
|
|
50
|
+
並べても、行を分けても構いません。
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
Merge-After: #1901, #1903
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- 指定した PR が **すべて** open でなくなるまで、その PR は pick されません
|
|
57
|
+
(AND です)。
|
|
58
|
+
- 制約するのは pick だけです。すでに up-to-date でマージ進行中の PR は従来通り
|
|
59
|
+
watch します。auto-merge は何が書かれていようとマージするので、そこで見送る
|
|
60
|
+
と「直後にマージされて無効になるブランチ」を rebase しに行くだけです。
|
|
61
|
+
- 1つの PR が複数の先行 PR を指定でき、複数の PR が同じ先行 PR を指定できるの
|
|
62
|
+
で、宣言されるのは列ではなくグラフです。**閉路は検出して名指しで報告**し、
|
|
63
|
+
そこに含まれる PR は放置します(スクリプトには解消できないため)。
|
|
64
|
+
- **コードフェンス内のトレーラは読みません。** 宣言とその実例は同じ文字列なの
|
|
65
|
+
で、この機能を説明する文書が本物の制約になってしまわないようにするためです。
|
|
66
|
+
- **閉じる issue の `Start-After:` も引き継ぎます**(`docs/issue-start-after.md`
|
|
67
|
+
の「pull request への引き継ぎ」)。名指されたのが開いた PR ならその PR を、
|
|
68
|
+
開いた issue ならそれを閉じる開いた PR を、`Merge-After:` で名指したのと同じに
|
|
69
|
+
扱います。閉じる issue は GitHub のリンクと、本文の closing keyword
|
|
70
|
+
(stack された PR を含む)で決め、survey ごとに読み直します。GitHub の issue の
|
|
71
|
+
依存関係(blocked by)も `Start-After:` と同じに扱います。読めなかった
|
|
72
|
+
survey では何も引き継がず、その旨を出力します。`note` には、どの番号が
|
|
73
|
+
issue から来たかを書きます。
|
|
74
|
+
|
|
75
|
+
**base(stacked PR)** も順序を宣言します。base が別の open な PR のブランチ
|
|
76
|
+
である PR は、その PR の上に積まれた層です(GitHub の stacked pull request)。
|
|
77
|
+
差分はその層の分だけになり、レビューしやすくなります。
|
|
78
|
+
|
|
79
|
+
- 下の層がマージされるまで pick されません。判定は base だけで行うので、
|
|
80
|
+
`Merge-After:` の無い stack も認識します。`open-pr --base` は下の層を
|
|
81
|
+
`Merge-After:` にも書きますが、base と同じ意味で、二重に数えることはありません。
|
|
82
|
+
- 下の層がマージされると、GitHub がこの PR を `main` に付け替えます。そこから
|
|
83
|
+
先は普通の PR です。
|
|
84
|
+
- **付け替えられた層は、キューに入れなくても積み直します。** 付け替えられた PR
|
|
85
|
+
はマージされた下の層のコミットをまだ含んでいるので、差分に下の層の変更まで
|
|
86
|
+
表示されます。次の survey で、下の層がマージされたときの head から、その
|
|
87
|
+
squash commit の上へ `--onto` で rebase し、上の層も一緒に運びます(「1a」)。
|
|
88
|
+
- ラベルは問いません。これは GitHub が付け替えたことの後始末で、順番が来た
|
|
89
|
+
わけではないからです。`main` の新しい変更は持ち込まず、レビューが終わって
|
|
90
|
+
いる必要もありません。
|
|
91
|
+
- ruleset は up-to-date のブランチしか、squash でしかマージさせないので、
|
|
92
|
+
squash commit のツリーはマージされた head のツリーと同じです。したがって
|
|
93
|
+
積み直した層のツリーも元と同じで、変わるのは履歴(と差分の見え方)だけで
|
|
94
|
+
す。チェック済みのツリーは再チェックされないので、`skip-ci` が外れていても
|
|
95
|
+
CI は増えません。
|
|
96
|
+
- `skip-ci` も auto-merge も触りません。`merge-queued` の付いた層は対象外で
|
|
97
|
+
す。pick されたときの rebase が同じコミットを同じ方法で落とすからです。
|
|
98
|
+
- **GitHub のネイティブ stack は扱いません。** GitHub はネイティブ stack に
|
|
99
|
+
入った PR に auto-merge を張らせず( "Auto-merge is not supported for stacked
|
|
100
|
+
pull requests" )、下の層がマージされて `main` に付け替えられた後も stack
|
|
101
|
+
から外しません。そういう PR は pick せず、手でマージするよう `note` で報告
|
|
102
|
+
します。stack は base だけで作ってください。
|
|
103
|
+
- 積まれている間は pick されないので、auto-merge も張られません。張られるのは
|
|
104
|
+
`main` に付け替えられて順番が来たときです。
|
|
105
|
+
- **下の層を rebase したら、上の層も一緒に運びます。** `git rebase --onto <新しい
|
|
106
|
+
head> <古い head>` で各層を自分のコミットだけ積み直して push します。こうしない
|
|
107
|
+
と上の層の差分に下の層の古いコミットが混ざり、下の層がマージされたときの
|
|
108
|
+
GitHub の rebase も上の層が含んでいない head から始まることになります。
|
|
109
|
+
`merge-queued` の有無は問いません — stack は1つの単位として動きます。
|
|
110
|
+
- 付け替えられた PR に、GitHub が rebase しなかったために下の層のコミットが
|
|
111
|
+
残っていれば、マージされた head から `--onto` で rebase します。patch が
|
|
112
|
+
squash commit と一致することに頼らないためです。
|
|
113
|
+
- fork のブランチは親にならず、積み直しの対象にもなりません。
|
|
114
|
+
|
|
115
|
+
**`blocks-release` ラベル** は「次のリリースにはこれが入っていなければならな
|
|
116
|
+
い」という宣言です。これが付いた PR が1本でも open な間、version PR
|
|
117
|
+
( `changeset-release/main` から changesets が開く PR )は pick されません。
|
|
118
|
+
|
|
119
|
+
- 制約するのは version PR だけです。ラベルの付いた PR 自身は何も制約されませ
|
|
120
|
+
ん。
|
|
121
|
+
- **draft でも数えます。** キューに入れていない PR、まだ書きかけの PR でリリー
|
|
122
|
+
スを止められるのがこのラベルの目的だからです。裏返すと、忘れられた draft が
|
|
123
|
+
1本あるとリリースは止まり続けるので、毎サイクル名指しで報告します。
|
|
124
|
+
- 外す運用はありません。制約は「 open であること」なので、マージでもクローズで
|
|
125
|
+
も PR が閉じれば自動的に解除されます。
|
|
126
|
+
- version PR 自身に付いた場合は無視します(自分で自分を止め続けるだけで、誰も
|
|
127
|
+
外せないため)。
|
|
128
|
+
|
|
129
|
+
**なぜ本文ではなくラベルなのか。** version PR は唯一、本文に何も書けない PR で
|
|
130
|
+
す。`changesets/action` は main への push のたびに `updatePullRequest` で title
|
|
131
|
+
と body を丸ごと上書きするので、そこに書いた `Merge-After:` は消えます。しかも
|
|
132
|
+
消えるのは「キューに他の PR があって先にマージされた時」、つまり順序宣言が効い
|
|
133
|
+
ていてほしいまさにその瞬間です。だから宣言は逆側 — リリースが待っている当の
|
|
134
|
+
PR — から行います。public repository なので、コメントではなく write 権限の要る
|
|
135
|
+
ラベルであることにも意味があります。
|
|
136
|
+
|
|
137
|
+
**`auto-rebase` ラベル** は「レビューや順番を待っている間も `main` に追従させ
|
|
138
|
+
てほしい」という宣言です。差分を自分の変更だけにしておき、レビューしやすくする
|
|
139
|
+
ためのものです(「3a」)。
|
|
140
|
+
|
|
141
|
+
- `skip-ci` と一緒のときだけ効きます。`skip-ci` が付いていれば push しても CI
|
|
142
|
+
は走りません。
|
|
143
|
+
- `merge-queued` の付いた PR も同じ扱いです。pick されない間(`Merge-After`
|
|
144
|
+
やレビュー待ち)も、`skip-ci` が付いていれば `main` に追従させます。
|
|
145
|
+
- queue に動かす候補が無いときだけ動きます。`main` がすぐまた動くときに push
|
|
146
|
+
しても無駄になるからです。
|
|
147
|
+
- `open-pr` が stack の全層に付けます。一番下より上の層は下の層と一緒に運ば
|
|
148
|
+
れるだけで、このラベルが効くのは、下の層がマージされてその層が `main` に付
|
|
149
|
+
け替えられてからです。付け替えのときに付ける仕組みは無いので、最初から付け
|
|
150
|
+
ておきます。
|
|
151
|
+
- 印を付けた PR だけが対象です(opt-in)。寝かせてあるブランチを付け忘れで動
|
|
152
|
+
かしてしまわないためです。レビューを頼む直前に付ければ足ります。
|
|
153
|
+
|
|
154
|
+
**`priority:high` / `priority:low` ラベル** は「pick できるものの中で優先して
|
|
155
|
+
ほしい / 後回しで良い」という宣言です。
|
|
156
|
+
|
|
157
|
+
- pick できる候補の中で、`priority:high` を先頭に、`priority:low` を末尾に並
|
|
158
|
+
べます。並び替えるだけで、ゲートではありません。`Merge-After`、stack、
|
|
159
|
+
`blocks-release`、レビュー待ちで止まっている PR は止まったままで、進行中の PR
|
|
160
|
+
は何が付いていても今まで通り watch します。
|
|
161
|
+
- version PR にも効きます。`priority:high` を付ければ順番待ちの変更より先にリ
|
|
162
|
+
リースし、`priority:low` の付いた変更は普通の version PR より後ろ、つまり次
|
|
163
|
+
のリリースに回ります。
|
|
164
|
+
- `DIRTY` と version PR を後ろに回す並びより優先し、番号順はその内側です。
|
|
165
|
+
green のまま止まった PR(「5」)を最後に回す規則だけはこれより強く、
|
|
166
|
+
`priority:high` でも最後です。
|
|
167
|
+
- 解放されている PR が2本になったときにどちらを残すかも、この順で決めます。
|
|
168
|
+
- 両方付いている PR は、矛盾しているのでどちらも無いものとして扱い、triage の
|
|
169
|
+
出力でそう言います。
|
|
170
|
+
- GitHub にある `priority:mid` は読みません。ラベル無しと同じです。
|
|
171
|
+
|
|
172
|
+
**`skip-ci` は対象範囲の判定には使いません。** キューに入った PR を一時停止さ
|
|
173
|
+
せるだけで、順番が来たら外すのがこのスクリプトの仕事です。「3a」で `main` に追
|
|
174
|
+
従させるかどうかの条件には使います(push しても CI が走らないことの保証として)。
|
|
175
|
+
|
|
176
|
+
### 解放されている PR は1本まで
|
|
177
|
+
|
|
178
|
+
`merge-queued` が付いていて `skip-ci` の無い PR を「解放されている」と呼びます。
|
|
179
|
+
push されてもラベルが外れても、フルマトリクスが走る PR です。**解放されている
|
|
180
|
+
PR は常に1本まで** に保ちます。`main` が動く前にマージできるのは1本だけなの
|
|
181
|
+
で、2本目の実行は捨てることになるからです。
|
|
182
|
+
|
|
183
|
+
#### なぜ必要か — 起きていたこと
|
|
184
|
+
|
|
185
|
+
このスクリプトと `/unblock-prs` skill は、同じキューを互いに見えないまま並行し
|
|
186
|
+
て動かします。ルールが無いと、例えば次のことが起きます。
|
|
187
|
+
|
|
188
|
+
1. `merge-queued` の PR A の `skip-ci` を外し、CI を走らせる。CI が失敗してマー
|
|
189
|
+
ジが止まる。A は `skip-ci` が外れたまま見送られる。
|
|
190
|
+
2. このスクリプトが次の `merge-queued` PR B を pick し、`skip-ci` を外す。
|
|
191
|
+
3. skill が A の失敗を直して push する。A の `skip-ci` は外れたままなので、この
|
|
192
|
+
push で A のマトリクスも走る。
|
|
193
|
+
4. A と B で CI が同時に走り、先にマージされた方が `main` を動かす。もう片方は
|
|
194
|
+
`BEHIND` に戻り、その CI 実行は丸ごと無駄になる。
|
|
195
|
+
|
|
196
|
+
どちらの書き手も単独では正しく動いています。無駄は、「CI を走らせてよいのは1本
|
|
197
|
+
だけ」という状態がどこにも記録されていないことから生まれます。`skip-ci` をその
|
|
198
|
+
記録として使います。
|
|
199
|
+
|
|
200
|
+
今の skill は見送られた PR だけを直し、push は必ず `skip-ci` を付けてから行う
|
|
201
|
+
ので、この例の手順 3 は skill からは起きません。人の push や、
|
|
202
|
+
`pnpm-update.yml` のようにラベル無しで開かれる PR があるので、ルールはそのまま
|
|
203
|
+
です。
|
|
204
|
+
|
|
205
|
+
#### ルール
|
|
206
|
+
|
|
207
|
+
**このスクリプトが `skip-ci` を付けるのは、自分で外したばかりのラベルを戻して、
|
|
208
|
+
同時に解放された別の PR に譲るときだけです。** 他の PR には付けません。以前は解放する前に他の解放済み PR 全部に `skip-ci` を付け
|
|
209
|
+
ていましたが、bot が解放済みで開く依存更新 PR や、人が意図して解放した PR まで
|
|
210
|
+
止めてしまうので、やめました。
|
|
211
|
+
|
|
212
|
+
- **in flight の PR があれば、他を pick しない。** triage が in flight と判断
|
|
213
|
+
した PR があれば、それを watch して rebase も解放もしません(「3」)。
|
|
214
|
+
- **解放した直後にもう一度見る。** 同じ瞬間に、担当者が `worker` の別の PR も
|
|
215
|
+
解放されていて、そちらが pick の順(`priority:high` が先、`priority:low` が
|
|
216
|
+
後、その中で番号の小さい順、version PR は最後)で先なら、**自分の PR に**
|
|
217
|
+
`skip-ci` を付け直して譲り、survey からやり直します(`release.mts` の
|
|
218
|
+
`yieldsTo`)。見送り中・claim 中の PR(skill か人の担当)は、直した push まで
|
|
219
|
+
CI を走らせないので数えません。数えると、誰も走らせない PR の後ろでキューが
|
|
220
|
+
止まります。survey の時点で既に解放されていた PR(人が `skip-ci` を外した、
|
|
221
|
+
`skip-ci` なしの PR に `merge-queued` を付けた、など)も数えません。in flight
|
|
222
|
+
でないそれ(`Merge-After:` 待ち、review 待ち、conflict など)は次の survey でも
|
|
223
|
+
解放されたままで、譲ると同じ PR を pick しては `skip-ci` を戻す無限ループに
|
|
224
|
+
なります。
|
|
225
|
+
- 人や bot が解放した PR はそのまま走ります。無駄になりうるのはマトリクス1回分
|
|
226
|
+
で、誤ったマージは起きません。上の例の手順 3 は、skill が push の前に自分の
|
|
227
|
+
PR に `skip-ci` を付けるので起きません。
|
|
228
|
+
|
|
229
|
+
この規則が保証するのは CI 時間だけで、マージの正しさは保証しません。それは
|
|
230
|
+
ruleset(最新の `main` の上で必須チェックが全部緑)が別に保証しています。
|
|
231
|
+
|
|
232
|
+
### 起動時
|
|
233
|
+
|
|
234
|
+
トークンとリポジトリを決め、デフォルトブランチを読みます。GitHub へは `gh` を介さ
|
|
235
|
+
ず API を直接呼び(`../lib/github-api.mts`)、トークンは `open-pr`
|
|
236
|
+
と同じく **`GH_TOKEN` → `GITHUB_TOKEN` → `gh auth token`** の順に探します。リポ
|
|
237
|
+
ジトリは `--repo owner/name`、無ければ `origin`、それも GitHub でなければルートの
|
|
238
|
+
`package.json` です。SIGINT / SIGTERM のハンドラを設置
|
|
239
|
+
します(停止要求後は現在のステップを終えてから抜けます。2回目のシグナルは即
|
|
240
|
+
終了)。
|
|
241
|
+
|
|
242
|
+
### 1サイクル
|
|
243
|
+
|
|
244
|
+
#### 1. survey
|
|
245
|
+
|
|
246
|
+
GraphQL で open な PR を新しい順に100件まで取得します(`body` を含む — 順序をそこから読
|
|
247
|
+
むため)。`mergeStateStatus` が `UNKNOWN` の PR があれば10秒おきに最大6回引き
|
|
248
|
+
直します。GitHub が計算中の状態を「up-to-date」と誤読しないためです。あわせて
|
|
249
|
+
`origin/main` の SHA と、`main` に適用されている rule を取得します。rule から
|
|
250
|
+
読むのは、必須の status check の一覧と、レビューへの2つの要件(code owner の承
|
|
251
|
+
認、会話の解決)です。宣言ファイルではなく GitHub が実際に適用しているものを読
|
|
252
|
+
みます。`origin/main` の SHA は ref 名全体(`refs/heads/main`)で読みます。
|
|
253
|
+
`git ls-remote origin main` は名前の末尾で一致させるので
|
|
254
|
+
`changeset-release/main` も拾い、しかもそちらが先に並びます。version PR の
|
|
255
|
+
head を `main` と取り違えると、見送りの記録が動かない base に紐付き、`main` が
|
|
256
|
+
動いても再試行されません。
|
|
257
|
+
|
|
258
|
+
#### 1a. 付け替えられた層の積み直し
|
|
259
|
+
|
|
260
|
+
前回の survey で stack の層だった PR(下の層がマージされ、まだ付け替えられて
|
|
261
|
+
いないものも含む)のうち、今回 `main` を base にしていて `merge-queued` の無い
|
|
262
|
+
ものについて、次を確かめます。
|
|
263
|
+
|
|
264
|
+
- 付け替え元のブランチから最後にマージされた PR の head を、まだ含んでいるか。
|
|
265
|
+
- 含んでいれば、その head から squash commit の上へ `--onto` で rebase し、
|
|
266
|
+
`--force-with-lease`(survey が見た head を明示)で push します。その上の層
|
|
267
|
+
も「3」の2と同じように積み直します。
|
|
268
|
+
- 何か動いたら、survey をもう一度取ってから triage に進みます。読んだ head が
|
|
269
|
+
古くなっているからです。
|
|
270
|
+
|
|
271
|
+
起動直後の1回目には比べる前回がないので、`main` を base にしている PR すべて
|
|
272
|
+
について timeline からどのブランチから付け替えられたかを読みます。含んでいな
|
|
273
|
+
い(GitHub が rebase した、または積み直し済み)PR は黙って通り過ぎます。
|
|
274
|
+
conflict や fork など積み直せない層は理由をログに出して残し、次からは追いませ
|
|
275
|
+
ん(記録は作りません。そのまま置いておけば、GitHub が付け替えたときの状態で
|
|
276
|
+
す)。`--dry-run` では何をするかを出力するだけです。
|
|
277
|
+
|
|
278
|
+
#### 1b. 見送りの comment を読む
|
|
279
|
+
|
|
280
|
+
`merge-queued` の PR と、このプロセスが見送りの記録を持っている PR について、
|
|
281
|
+
自分のアカウントが書いた見送りの comment(「見送った PR はどこで分かるか」)を
|
|
282
|
+
GraphQL 1回でまとめて読み、記録と突き合わせます(`skips.mts` の
|
|
283
|
+
`settleSkips`)。
|
|
284
|
+
|
|
285
|
+
- comment が見送った head から branch が動いた、または見送った base から
|
|
286
|
+
`main` が動いた(`checks-failed` と `awaiting-bypass` を除く)なら、comment
|
|
287
|
+
を1行の「解消済み」に書き換えます。
|
|
288
|
+
- 「再試行」のチェックボックスに人がチェックを入れていたら、comment を
|
|
289
|
+
「解消済み」に書き換え、その PR の見送りの記録を捨てます。今回の triage は
|
|
290
|
+
見送られていなかったものとしてその PR を扱います。また失敗すれば、また
|
|
291
|
+
見送って comment に書きます。
|
|
292
|
+
- まだ有効なのにこのプロセスが記録を持っていない comment は、前のプロセスが
|
|
293
|
+
残したものです。記録として引き継ぎ、試し直しません。同じ head と base の
|
|
294
|
+
rebase は同じように conflict するからです。
|
|
295
|
+
- 逆に、このプロセスが記録を持っているのに、それを言う comment が無い(書き込
|
|
296
|
+
みに失敗した)PR には comment を書き直します(`unannouncedSkips`)。放って
|
|
297
|
+
おくと、このスクリプトは見送り中として扱い、comment を読む skill は担当者を
|
|
298
|
+
`worker` と読むので、互いに相手に任せたままになります。
|
|
299
|
+
|
|
300
|
+
同じリクエストで、セッションが書いた **claim**(「見送った PR を直すセッション
|
|
301
|
+
の claim」)も読みます。まだ有効な claim は見送りの記録とは別に持ち、その PR は
|
|
302
|
+
pick も「3a」の rebase もしません。
|
|
303
|
+
|
|
304
|
+
読めなければログに出して、記録はそのままで続行します(claim は無いものとしま
|
|
305
|
+
す)。`--dry-run` では書き換えません。
|
|
306
|
+
|
|
307
|
+
#### 2. triage
|
|
308
|
+
|
|
309
|
+
1. 全 PR の body から `Merge-After:` を、base から stack を、閉じる issue から
|
|
310
|
+
`Start-After:` を読んで依存グラフを作り、閉路を検出( version PR の body
|
|
311
|
+
だけは読みません — 上書きされるので)。
|
|
312
|
+
2. **範囲判定** — `merge-queued` が無ければ `ignore`(無言)。あって動かせなけ
|
|
313
|
+
れば `note`(理由を出力)。
|
|
314
|
+
3. **担当者** — 担当者(「担当者(owner)」)が `worker` でない PR は、担当者と
|
|
315
|
+
理由を添えて `note`。見送りの記録が head に残っている PR と、セッションが
|
|
316
|
+
claim している PR がこれにあたります。
|
|
317
|
+
4. **stack** — 積まれている PR は「下の層が先」と `note`。
|
|
318
|
+
**auto-merge** — 無い PR は、GitHub のネイティブ stack に入っていれば
|
|
319
|
+
`note`(手でマージ)。そうでなければ先へ進み、pick されたときに張られます。
|
|
320
|
+
5. **`Merge-After` ゲート** — 指定先が1つでも open なら `note`。
|
|
321
|
+
6. **version PR** — `changeset-release/<デフォルトブランチ>` から来た PR は専
|
|
322
|
+
用の扱いになります。`blocks-release` の付いた open な PR があれば `note`。
|
|
323
|
+
無ければ、そのブランチが **main の現在の先端の上に建っているか** を
|
|
324
|
+
`git merge-base` で確かめ、建っていなければ `note`( Release workflow がま
|
|
325
|
+
だ作り直していない)。どちらでもなければ、`skip-ci` が付いていれば
|
|
326
|
+
**candidate**、付いていなければ必須チェックを読んで **failing** か
|
|
327
|
+
**in-flight**。`mergeStateStatus` を使わないのは、これを読み違えた代償が
|
|
328
|
+
「feature 抜きのリリース」だからです( GitHub の答えは非同期に計算された
|
|
329
|
+
キャッシュ)。
|
|
330
|
+
7. **振り分け**
|
|
331
|
+
- `skip-ci` 付き → **candidate**。merge state は見ません。
|
|
332
|
+
`no-skip-ci-label` が pending なので、どれだけ準備できていても必ず
|
|
333
|
+
`BLOCKED` になり、「スキップされたチェック」と「実行中のチェック」が区別
|
|
334
|
+
できないからです。
|
|
335
|
+
- `BEHIND` / `DIRTY` → **candidate**。`DIRTY` は GitHub が非同期に計算して
|
|
336
|
+
キャッシュした答えで、しかも聞いている質問が違います(3-way merge が衝突
|
|
337
|
+
するか/rebase が衝突するか)。除外理由にはせず、実際に rebase させて答
|
|
338
|
+
えさせます。ただし確実な `BEHIND` より後ろに並べます。
|
|
339
|
+
- `CLEAN` / `UNSTABLE` / `HAS_HOOKS` → **in-flight**。
|
|
340
|
+
- `BLOCKED` → 必須チェックを読み、失敗なら **failing**、未完なら
|
|
341
|
+
**in-flight**。新しい round が実行中なら、前の round が残した赤も未完と
|
|
342
|
+
数えます。
|
|
343
|
+
8. **レビュー待ち** — **candidate** になった PR と、`BLOCKED` のまま
|
|
344
|
+
**in-flight** になった PR について、レビューがマージを止めていないかを
|
|
345
|
+
GraphQL 1回でまとめて読みます。止めていれば **held**(理由を出力)にして、
|
|
346
|
+
rebase も解放も watch もしません。読むのは ruleset が要求する2つです。
|
|
347
|
+
- **code owner の承認** — 変更したパスのうち `.github/CODEOWNERS` に所有者
|
|
348
|
+
がいるもの全部に、所有者の承認が要ります。GitHub の `reviewDecision` は
|
|
349
|
+
branch protection の答えで、ruleset のこのリポジトリでは常に `null` なの
|
|
350
|
+
で、`CODEOWNERS` と変更ファイルと承認から計算します。Pull Requests
|
|
351
|
+
Manager と同じ `pr-report-core` の `codeOwnerReview` です。作者自身が所有
|
|
352
|
+
者なら、作者は自分の PR を承認できないので、そう出力します。
|
|
353
|
+
- **未解決の会話** — 1件でもあれば。
|
|
354
|
+
|
|
355
|
+
例外は、承認待ちのパスの所有者が**作者だけ**の PR です(未解決の会話が無
|
|
356
|
+
い場合)。作者は自分の PR を承認できないので、承認は永遠に来ず、ruleset
|
|
357
|
+
bypass でマージするしかありません。held にすると `skip-ci` が付いたまま
|
|
358
|
+
になり、bypass merge の判断材料になるチェック結果が一度も出ないので、held
|
|
359
|
+
にはしません。ほかの PR と同じく rebase、`skip-ci` 外し、watch まで進めます
|
|
360
|
+
が、**auto-merge は arm しません**(承認が来ないので発火しないため)。
|
|
361
|
+
green になったら `awaiting-bypass` で見送り、bypass merge を人に頼む
|
|
362
|
+
comment を書きます。
|
|
363
|
+
|
|
364
|
+
チェックでは分からない理由でマージできない PR を解放すると、マトリクスを
|
|
365
|
+
1回丸ごと使い、watch の間キューを止めます。しかも `main` が動くたびに
|
|
366
|
+
`BEHIND` として先頭に戻ってくるので、他の PR がマージされるたびに同じこと
|
|
367
|
+
を繰り返します。承認や会話の解決があれば次の survey で答えが変わるので、
|
|
368
|
+
skip 記録は作りません。変更ファイルが1ページ(100件)に収まらず判定しきれ
|
|
369
|
+
ない場合と、読み取りに失敗した場合は、止めません(以前の動作)。GitHub が
|
|
370
|
+
mergeable と言っている in-flight の PR は、それ自体が「チェック以外に残っ
|
|
371
|
+
ているものは無い」という答えなので、今まで通り watch します。
|
|
372
|
+
|
|
373
|
+
このスクリプトは **version PR を rebase しません。** `changeset-release/main`
|
|
374
|
+
は Release workflow が main の先端から毎回作り直して force-push するブランチで、
|
|
375
|
+
ここで rebase すると (a) 2つの自動化が同じブランチを force-push し、(b) 古い
|
|
376
|
+
version commit が「まだ消費していない changeset を含む先端」の上に乗るため、
|
|
377
|
+
マージすると **その changeset 抜きのリリース** が出ます。やることは `skip-ci`
|
|
378
|
+
を外すことだけです。候補の並びでも最後に回します — こちらはゲートではなく順序
|
|
379
|
+
なので、リリースを止めることはありません(キュー済みの変更とリリースが同時に準
|
|
380
|
+
備できた時、変更を先に入れるだけ)。
|
|
381
|
+
|
|
382
|
+
#### 3. 1本だけ動かす
|
|
383
|
+
|
|
384
|
+
**in-flight があればそれを watch して終わり** — rebase はしません。auto-merge
|
|
385
|
+
が無ければ watch の前に張ります。
|
|
386
|
+
|
|
387
|
+
なければ candidate の先頭に対して、次の手順を行います。並びは番号順で、後ろに回
|
|
388
|
+
すものが3種類あります。`DIRTY`、その後ろに version PR、さらにその後ろに green
|
|
389
|
+
のまま止まった PR(「5」)です。green のまま止まった PR 以外は、まず
|
|
390
|
+
`priority:high` / ラベル無し / `priority:low` の順に分け、その中で今の並びに
|
|
391
|
+
します。
|
|
392
|
+
|
|
393
|
+
1. 使い捨て worktree で `origin/main` に rebase し、`--force-with-lease`
|
|
394
|
+
(survey が見た head を明示)で push。stack から降ろされた PR がマージ済みの
|
|
395
|
+
下の層の head をまだ含んでいれば、その head からの `--onto` にします。
|
|
396
|
+
2. head が変わったなら、その上に積まれた層を下から順に `--onto` で積み直して
|
|
397
|
+
push します。積み直せない層(衝突、fork、下の層が動かなかった)は理由を
|
|
398
|
+
ログに出して残し、この PR 自体は止めません。
|
|
399
|
+
3. auto-merge が無ければ、再確認(open / `main` 向き / head が今 push したもの /
|
|
400
|
+
`merge-queued` がまだ付いている)してから張ります。`skip-ci` を外す前なので、
|
|
401
|
+
張っている間もマージはラベルが止めています。
|
|
402
|
+
4. `skip-ci` が付いていれば、**push の後で** 再確認(まだ open か / head が今
|
|
403
|
+
push したものか / 両ラベルがまだ付いているか)してからラベルを外します。
|
|
404
|
+
順序が要点です。ラベルが付いている間の push は全チェックが skip される
|
|
405
|
+
`synchronize` を起こすだけで無料ですが、ラベルを外すと `unlabeled` が発火
|
|
406
|
+
し、**実際にマージされる head の上で** マトリクスが1回だけ走ります。逆順だ
|
|
407
|
+
と rebase 前の head でフルマトリクスが起動し、直後の push がそれをキャンセ
|
|
408
|
+
ルします。
|
|
409
|
+
5. rebase が no-op で、かつ `skip-ci` も付いていなかった場合だけ「GitHub の
|
|
410
|
+
merge state が古かった」と判断し、30秒待って survey からやり直します。
|
|
411
|
+
|
|
412
|
+
失敗(conflict / push 拒否 / auto-merge を張れない / ラベル除去失敗)した場合は記録して **同じサイクル
|
|
413
|
+
内で次の候補へ** 進みます。キューが止まるわけではありません。
|
|
414
|
+
|
|
415
|
+
例外は **作業中にブランチが動いた場合** です(rebase 前の fetch で head が
|
|
416
|
+
survey と違う / lease 負けで push が拒否され、remote の head が変わっている /
|
|
417
|
+
ラベルを外す前の再確認で head が違う)。これは skill や人が同じ PR を動かして
|
|
418
|
+
いるということなので、失敗として記録せず、次の候補にも進まずに survey からやり
|
|
419
|
+
直します。次の候補を rebase すると、相手が起動するマトリクスの横でもう1本走ら
|
|
420
|
+
せることになるからです。`/unblock-prs` skill はこのスクリプトと並行して動く前
|
|
421
|
+
提で書かれています。
|
|
422
|
+
|
|
423
|
+
#### 3a. 待っている PR を main に積み直す
|
|
424
|
+
|
|
425
|
+
「3」で動かす候補が無いとき(進行中の PR も、pick する候補も無いとき)だけ、
|
|
426
|
+
次の PR を `main` へ rebase します。`main` がすぐまた動くときに push しても無
|
|
427
|
+
駄になるからです。レビュー中や順番待ちの PR の差分を、自分の変更だけにしてお
|
|
428
|
+
くためのものです。
|
|
429
|
+
|
|
430
|
+
- 対象: **`skip-ci` が付いていて**(push しても CI が走らない)、**
|
|
431
|
+
`auto-rebase` か `merge-queued` のどちらかが付いている** PR。そのうち open、
|
|
432
|
+
draft でない、同じリポジトリのブランチ、base が `main`、version PR ではない、
|
|
433
|
+
担当者が `worker` のもの(見送りの記録も、セッションの claim も無いもの)。
|
|
434
|
+
`merge-queued` の PR がここまで来るのは、`Merge-After` やレビュー待ちなどで
|
|
435
|
+
pick されないときです。pick される前に rebase しておいても、`skip-ci` が付
|
|
436
|
+
いていれば損はありません。
|
|
437
|
+
- `skip-ci` が付いていると merge state は `BLOCKED` のままなので、遅れている
|
|
438
|
+
かどうかは `BEHIND` / `DIRTY` ではなく、`git merge-base --is-ancestor` で
|
|
439
|
+
`origin/main` が head の祖先かどうかで判断します。
|
|
440
|
+
- 普通の `git rebase origin/main` を試し、`--force-with-lease`(survey が見た
|
|
441
|
+
head を明示)で push します。その上の層は「3」の2と同じ `restack()` で積み直
|
|
442
|
+
します。衝突や push の拒否は見送りとして記録し、comment で知らせます(head
|
|
443
|
+
か `main` が動くか、retry の欄にチェックが入るまで試し直しません)。
|
|
444
|
+
- 何か動いたら、triage の前に survey をやり直します。
|
|
445
|
+
|
|
446
|
+
**`auto-rebase` ラベル**は `open-pr` が stack の全層に付けます。対象は base が
|
|
447
|
+
`main` の層だけなので、上の層は下の層がマージされるまで一緒に運ばれるだけです。
|
|
448
|
+
付いていない PR は、寝かせてあるブランチも含めて、この節では動かしません。
|
|
449
|
+
|
|
450
|
+
#### 4. watch
|
|
451
|
+
|
|
452
|
+
`--poll-interval`(既定30秒)待ってから、マージするか「しない」と確定するまで
|
|
453
|
+
ポーリングします。各回で次の順に判定し、該当した時点で抜けます。
|
|
454
|
+
|
|
455
|
+
| 条件 | 結果 |
|
|
456
|
+
| :--------------------------------- | :---------------------------- |
|
|
457
|
+
| PR の再取得に失敗 | 連続5回で `error`、未満は継続 |
|
|
458
|
+
| `MERGED` | `merged` |
|
|
459
|
+
| `OPEN` でない | `closed` |
|
|
460
|
+
| auto-merge が外れた(※) | `auto-merge-disabled` |
|
|
461
|
+
| `skip-ci` が再付与された | `skip-ci-labelled` |
|
|
462
|
+
| head が push したものと違う | `head-moved` |
|
|
463
|
+
| `BEHIND` / `DIRTY` | `behind-again` |
|
|
464
|
+
| 必須チェックが fail / cancel | `checks-failed` |
|
|
465
|
+
| 全 green(bypass merge 待ちの PR) | `awaiting-bypass` |
|
|
466
|
+
| 全 green が規定回数続く | `not-merging` |
|
|
467
|
+
| `--watch-timeout`(既定90分)超過 | `timeout` |
|
|
468
|
+
| SIGINT / SIGTERM | `stopped` |
|
|
469
|
+
|
|
470
|
+
※ bypass merge 待ちの PR(「2」の8)は arm していないので、この行は見ません。
|
|
471
|
+
|
|
472
|
+
順序には意味があります。ラベルの再付与やブランチの移動をチェック結果より先に
|
|
473
|
+
見るのは、その状態の PR のチェックを読んでも意味がないからです。
|
|
474
|
+
|
|
475
|
+
**green の判定は「ruleset が要求する context 全部」** であり、報告済みのものだ
|
|
476
|
+
けではありません。head commit にまだ check run が無い必須 context は
|
|
477
|
+
head のチェックの一覧に pending として並ぶのではなく **行ごと現れません**。報
|
|
478
|
+
告済みだけで判定すると、25分かかるマトリクスの3分目に「完了」と誤読します。
|
|
479
|
+
|
|
480
|
+
**head 上で何か一つでも実行中なら green とは数えません**(必須かどうかを問わず)。
|
|
481
|
+
必須の集約ジョブは、待っているジョブが終わるまで新しい round に check run を
|
|
482
|
+
持たないので、その間 `--required` は前の round の結果を返します。前の round が
|
|
483
|
+
`skip-ci` で飛ばされたものなら集約は全部 `skipped` で、マトリクスの途中なのに
|
|
484
|
+
green に見えます(#2069 はこれで `not-merging` になりました)。fail はこの間も
|
|
485
|
+
すぐ判定しますが、実行中のうち最も早く始まったものより前に終わった fail は前の
|
|
486
|
+
round のものとして、round が終わるまで待ちます。`open-pr` が開いた直後に
|
|
487
|
+
`skip-ci` で止めた round は集約を赤く残すので、それを今の round の fail と読むと、
|
|
488
|
+
最初の本番の round の途中で脇に置いてしまいます(#2246 はこれで `checks-failed`
|
|
489
|
+
になりました)。同じ workflow のもっと新しい run がまだ実行中の fail も前の round
|
|
490
|
+
のものとして待ちます。`skip-ci` を付けた round のキャンセルが遅れると、その集約は
|
|
491
|
+
新しい round が始まった後に赤くなるので、時刻では見分けられません(#2463 はこれで
|
|
492
|
+
`checks-failed` になりました)。
|
|
493
|
+
|
|
494
|
+
全部 green なのにマージされない場合は rebase では直せないので、連続 green 回
|
|
495
|
+
数で打ち切ります。原因のうち未解決の会話と code owner の承認不足は triage が先
|
|
496
|
+
に見つけて解放しないので(「2」の8)、ここに来るのはそれ以外です。たとえばマー
|
|
497
|
+
ジ権限の無い人が auto-merge を有効にした、このスクリプトが読まない rule に引っ
|
|
498
|
+
かかっている、などです。GitHub 自身が mergeable と言っている場合は3回、`BLOCKED` の
|
|
499
|
+
まま green の場合は8回。後者が長いのは、check run が superseded された直後の一
|
|
500
|
+
瞬がこの形になるためです。
|
|
501
|
+
|
|
502
|
+
#### 4a. fixer の差分だけで落ちたなら直す
|
|
503
|
+
|
|
504
|
+
watch が `checks-failed` で終わったら、その head の check run を集約 job の下の
|
|
505
|
+
matrix の job まで読みます。どの失敗なら直せるか、何を実行すれば直るかはリポジ
|
|
506
|
+
トリの `.github/unblock-prs.json` の `autoFix` が決めます(スキーマは
|
|
507
|
+
`config.mts` から `gen:config-schema` が生成する
|
|
508
|
+
[`config.schema.json`](./config.schema.json))。ファイルが無いか `autoFix` が無
|
|
509
|
+
ければ自動修正はせず、キュー、rebase、`Merge-After:`、stack、見送りコメントは
|
|
510
|
+
そのまま動きます。ラベル名、`Merge-After:`、version PR のブランチ名
|
|
511
|
+
(`changeset-release/<base>`)は PR Manager のページも読む規約なので、設定には
|
|
512
|
+
しません。
|
|
513
|
+
|
|
514
|
+
| キー | 意味 |
|
|
515
|
+
| :-------- | :----------------------------------------------------------------------------------------------------- |
|
|
516
|
+
| `ignore` | 失敗していても読まない context の正規表現(集約 job など) |
|
|
517
|
+
| `before` | 常に最初に実行するコマンド(install) |
|
|
518
|
+
| `prepare` | 直す context が `context` に一致したときだけ、`before` の後に1回実行するコマンド(build) |
|
|
519
|
+
| `fixers` | 残りの失敗はすべて、どれかの `context` に一致しなければならない。最初に一致したものの `run` を実行する |
|
|
520
|
+
| `after` | 常に最後に実行するコマンド(フォーマッタ) |
|
|
521
|
+
|
|
522
|
+
`run` の `$1`〜`$9` は `context` のキャプチャで、シェル用にクォートして埋め込み
|
|
523
|
+
ます。`fixers` は `stage` の小さい順、同じ `stage` ならコマンド順に実行します。
|
|
524
|
+
|
|
525
|
+
このリポジトリの設定では、失敗した job が **すべて** `code-check (X)` か
|
|
526
|
+
`style-check (X)` で、`X` が `fix:` か `gen:` のコマンド(`ws:` /
|
|
527
|
+
`strict-lib:` 付きも含む)なら直します。どの job も最後に
|
|
528
|
+
`z:assert-repo-is-clean` を実行するので、それはコマンドがファイルを書き換えた
|
|
529
|
+
だけの失敗だからです。その場合に限り、次のことをします。
|
|
530
|
+
|
|
531
|
+
1. 使い捨ての worktree で `pnpm install --frozen-lockfile` し、CI がビルドする
|
|
532
|
+
entry(`code-check` のすべてと `ws:gen`)があれば `ws:build` を実行します。
|
|
533
|
+
worktree は `~/.local/state/unblock-prs/` に作ります。OS の temp は tmpfs
|
|
534
|
+
のことが多く、pnpm store から `node_modules` をハードリンクできません。git
|
|
535
|
+
ディレクトリや `~/.cache` の下も使えません。`fix:fmt:diff` は絶対パスのどこ
|
|
536
|
+
かに `.git` / `.cache` / `dist` / `build` / `out` を含むファイルをすべて飛ば
|
|
537
|
+
すためです。
|
|
538
|
+
2. 失敗したコマンドを、生成系 → その他 → フォーマッタの順に実行し、最後に
|
|
539
|
+
`fix:fmt:diff` を実行します。
|
|
540
|
+
3. 差分があれば PR の1コミットに `--amend` し、失敗した head を lease にして
|
|
541
|
+
`--force-with-lease` で push します。
|
|
542
|
+
4. 新しい head を watch します。**次の PR は pick しません。**
|
|
543
|
+
|
|
544
|
+
次の場合は何もせず、理由をログに出して従来通り `checks-failed` として見送り
|
|
545
|
+
ます。
|
|
546
|
+
|
|
547
|
+
- リポジトリに `autoFix` の設定が無い。
|
|
548
|
+
- 失敗した job に1つでも fixer 以外がある(型、テスト、`ws:doc`、
|
|
549
|
+
`Validate commit count` など)。push しても落ちるマトリクスを1回走らせるだけ
|
|
550
|
+
なので、全体を skill に任せます。
|
|
551
|
+
- ブランチが1コミットでない。squash はメッセージを書く行為なので skill の仕事
|
|
552
|
+
です。
|
|
553
|
+
- コマンドが失敗した、または差分が出なかった。
|
|
554
|
+
- 自動修正で push した head がまた落ちた。1回の watch につき1回までです。
|
|
555
|
+
|
|
556
|
+
対象は watch している1本、つまりこのサイクルが pick した PR だけです。
|
|
557
|
+
`merge-queued` で CI が落ちている他の PR には触れません。
|
|
558
|
+
|
|
559
|
+
- **`ws:doc` は対象外です。** `doc` が書くのは untracked な出力で、tree を汚す
|
|
560
|
+
のはその中で呼ばれる生成器(`ws:gen` でも走る)です。しかも実行に10分かか
|
|
561
|
+
ります。
|
|
562
|
+
- **oxfmt。** `fix:fmt:diff` は Prettier だけで、`strict-lib/` は整形しませ
|
|
563
|
+
ん。fixer のうち `strict-lib/` に書くのは `strict-lib:fix:fmt` 自身だけなの
|
|
564
|
+
で、それが落ちていればそのコマンドが実行されます。
|
|
565
|
+
|
|
566
|
+
#### 5. 記憶して次のサイクルへ
|
|
567
|
+
|
|
568
|
+
判定は **その head かつその base のまま** の間だけ有効です。
|
|
569
|
+
|
|
570
|
+
| 記録される理由 | push で解除 | `main` の移動で解除 |
|
|
571
|
+
| :-------------------------------- | :---------: | :-----------------: |
|
|
572
|
+
| `rebase-failed`(実際に衝突した) | ✓ | ✓ |
|
|
573
|
+
| `push-failed` | ✓ | ✓ |
|
|
574
|
+
| `unlabel-failed` | ✓ | ✓ |
|
|
575
|
+
| `arm-failed` | ✓ | ✓ |
|
|
576
|
+
| `already-in-base` | ✓ | ✓ |
|
|
577
|
+
| `not-merging` | ✓ | ✓ |
|
|
578
|
+
| `watch-timeout` | ✓ | ✓ |
|
|
579
|
+
| `checks-failed` | ✓ | **✗(意図的)** |
|
|
580
|
+
| `awaiting-bypass` | ✓ | **✗(意図的)** |
|
|
581
|
+
|
|
582
|
+
衝突の判定がキャッシュされないのはこのためです。衝突していた相手のコミットが
|
|
583
|
+
base の移動と一緒に消えたかもしれないので、`main` が動けば記録は捨てられ、次の
|
|
584
|
+
サイクルで再試行されます。`checks-failed` は base に紐付いていません —
|
|
585
|
+
rebase しても同じマトリクスが同じように落ちるだけで、直すのは人間の仕事だから
|
|
586
|
+
です。`awaiting-bypass` も同じく base に紐付けません。待っている bypass merge
|
|
587
|
+
は base の移動を待たないので、他の PR がマージされるたびに rebase してマトリク
|
|
588
|
+
スを回し直しても無駄になるだけです。最新の `main` の上で確かめ直したいときは、
|
|
589
|
+
comment の再試行ボックスにチェックを入れます。
|
|
590
|
+
|
|
591
|
+
`not-merging` の PR は、記録が消えた後も **candidate の最後**(version PR より
|
|
592
|
+
後ろ)に回されます。`main` が動けば記録は消えて `BEHIND` の candidate に戻り
|
|
593
|
+
ます。しかし原因は triage が読めなかったものなので、次もマージされない見込みが
|
|
594
|
+
高いです。先頭に置いたままだと、他の PR がマージされるたびにこの PR がマトリク
|
|
595
|
+
スを1回使い、green になって打ち切られる、という流れを繰り返します。除外ではな
|
|
596
|
+
く最後に回すだけなのは、推測が外れることがあるからです(原因がその後に解消して
|
|
597
|
+
いる場合)。この降格は **誰かが push するまで** 続きます。このスクリプト自身の
|
|
598
|
+
rebase や「4a」の push では解けません。記憶はプロセスの中だけにあるので、再起
|
|
599
|
+
動すると忘れます。
|
|
600
|
+
|
|
601
|
+
このサイクルで触った PR が次の survey で open 一覧から消えていれば、マージされ
|
|
602
|
+
たのかクローズされたのかを報告します。watch を諦めた数分後にマージされた PR を
|
|
603
|
+
取りこぼさないためです。
|
|
604
|
+
|
|
605
|
+
何もすることがなければ待ってから再 survey します。待ち時間は一覧が動いている
|
|
606
|
+
かどうかで変わります。open な PR の一覧(head、merge state、ラベル、
|
|
607
|
+
auto-merge、draft、本文)、レビュー待ちの理由、`main` の tip が
|
|
608
|
+
`--idle-after`(既定10回)続けて
|
|
609
|
+
前回の survey と同じになるまでは `--active-interval`(既定30秒)、それ以降は
|
|
610
|
+
`--idle-interval`(既定300秒)です。何か変われば短い間隔に戻ります。今キュー
|
|
611
|
+
に入れた PR を数分放置しないためです。
|
|
612
|
+
|
|
613
|
+
### オプション
|
|
614
|
+
|
|
615
|
+
| オプション | 既定 | 意味 |
|
|
616
|
+
| :------------------------ | ---: | :------------------------------------------- |
|
|
617
|
+
| `--once` | | 1サイクルだけ実行して終了 |
|
|
618
|
+
| `--dry-run` | | survey と報告だけ行い、何も変更しない |
|
|
619
|
+
| `--active-interval <sec>` | 30 | 一覧が動いている間の再 survey までの待ち時間 |
|
|
620
|
+
| `--idle-after <n>` | 10 | 何回続けて変化が無ければ長い間隔にするか |
|
|
621
|
+
| `--idle-interval <sec>` | 300 | 一覧が止まった後の再 survey までの待ち時間 |
|
|
622
|
+
| `--poll-interval <sec>` | 30 | watch 中のポーリング間隔 |
|
|
623
|
+
| `--watch-timeout <min>` | 90 | 1本を諦めるまでの時間 |
|
|
624
|
+
| `--no-auto-fix` | | 「4a」の自動修正をしない |
|
|
625
|
+
| `--repo <owner/name>` | | `origin` が指さないリポジトリを扱う |
|
|
626
|
+
|
|
627
|
+
ログの各行の時刻はマシンの既定のタイムゾーンで、UTC からのオフセット付きで出ます
|
|
628
|
+
(例: `[2026-09-27T21:03:04.500+09:00]`)。別のタイムゾーンで読みたいときは
|
|
629
|
+
`TZ=Asia/Tokyo pnpm run unblock-prs` のように `TZ` で指定します。
|
|
630
|
+
|
|
631
|
+
### 見送った PR はどこで分かるか
|
|
632
|
+
|
|
633
|
+
このスクリプトは手元で動き、何をしたかは標準出力にしか出ません。そこで PR を
|
|
634
|
+
見送るたびに、**その PR に comment を書きます**。comment は PR ごとに1本で、
|
|
635
|
+
2回目からは同じ comment を書き換えます。**GitHub Pull Requests Manager**
|
|
636
|
+
(<https://noshiro-pf.github.io/mono/pr-manager/>)がそれを読んで PR ごとに
|
|
637
|
+
表示するので、端末を見に行かなくても止まっている理由が分かります。
|
|
638
|
+
|
|
639
|
+
- **1行目は機械が読む隠しコメント**です(
|
|
640
|
+
`<!-- unblock-prs:set-aside reason=… head=… base=… -->`)。理由と、見送った
|
|
641
|
+
時点の head と base の SHA を書きます。書式は `pr-report-core` の
|
|
642
|
+
`set-aside.mts` にあり、このスクリプトとページが同じものを読みます。その後の
|
|
643
|
+
文章は人が読むためのもので、機械は読みません(`set-aside-comment.mts`)。
|
|
644
|
+
理由の1文(`set-aside-detail.mts`: `checks-failed` は落ちたチェック名、
|
|
645
|
+
`not-merging` は何が止めているか、`watch-timeout` はまだ待っていたもの、
|
|
646
|
+
`rebase-failed` は conflict したファイル)、落ちた必須チェックそれぞれの
|
|
647
|
+
run へのリンク、失敗したコマンドの出力(`<details>` に畳み、末尾60行まで)、
|
|
648
|
+
何があれば解けるか、を書きます。
|
|
649
|
+
- **最終行はチェックボックス「Retry」**です。チェックを入れると、次の
|
|
650
|
+
survey がその PR の見送りを捨てて試し直します(「1」の1b)。他人の comment
|
|
651
|
+
のチェックボックスを切り替えられるのは write 権限のある人だけです。
|
|
652
|
+
`checks-failed` のまま up to date な PR は、試し直しても同じ失敗を見つけて
|
|
653
|
+
また見送るだけです。直すのは push です。
|
|
654
|
+
- **読むのは自分のアカウントが書いた comment だけ**です(GraphQL の
|
|
655
|
+
`viewerDidAuthor`)。リポジトリが public なので、誰でも同じ隠しコメントで
|
|
656
|
+
始まる comment を書けるからです。ページも同じ規則で読みます。
|
|
657
|
+
- **本文ではなく comment** なのは、本文は作者やセッションや `open-pr` も書き
|
|
658
|
+
換え、REST の PATCH には条件付き更新がないので人の編集を消しかねないからです。
|
|
659
|
+
version PR の本文は push のたびに上書きされます。以前の commit status(
|
|
660
|
+
context `unblock-prs`、state `failure`)をやめたのは、必須でもないのに PR の
|
|
661
|
+
checks に ✗ が残り、140字しか書けず、人が消すことも試し直しを頼むこともでき
|
|
662
|
+
なかったからです。
|
|
663
|
+
- **GitHub の `mergeable` ではなくこちら**を見るのは、あれが merge の可否で、
|
|
664
|
+
このスクリプトがするのは rebase だからです(`DIRTY` を信用しない理由と同じ)。
|
|
665
|
+
- **解けたら1行の「解消済み」に書き換えます。** head か base が動いた(
|
|
666
|
+
`checks-failed` と `awaiting-bypass` は head だけ — `skips.mts` と同じ規則を
|
|
667
|
+
`pr-report-core` の `setAsideApplies` で共有しています)、または再試行を頼ま
|
|
668
|
+
れたときです。消さないのは、timeline で経緯を追えるようにするためです。ペー
|
|
669
|
+
ジは、head が動いた comment は表示せず、base が動いたものは灰色の古い記録と
|
|
670
|
+
して表示します。
|
|
671
|
+
- マージは止めません。
|
|
672
|
+
- 書くのは新しく見送ったときだけで、同じ状態のまま見送り続けている間は書き直し
|
|
673
|
+
ません。`--dry-run` では書きません。書けなかったときはその旨をログに出して
|
|
674
|
+
続行します。書き込むのはトークンのアカウントなので、そのアカウントが作者の PR
|
|
675
|
+
では作者に通知は飛びません。PR を購読しているセッションは comment で起こされ
|
|
676
|
+
るので、自分の PR が見送られたことに気づけます。
|
|
677
|
+
- 以前の commit status はすでに付いているものを API で消せませんが、次の push
|
|
678
|
+
で head が変われば消えるので放ってあります。
|
|
679
|
+
|
|
680
|
+
code owner の承認待ちで止まっている PR は、変更したパスと `.github/CODEOWNERS`
|
|
681
|
+
からページが自分で判定します。このスクリプトも同じ判定で、その PR を解放せずに
|
|
682
|
+
残します(「2」の8)。この場合 comment は書きません。承認されれば次の survey で
|
|
683
|
+
動き出すからです。所有者が作者だけの PR はその例外で、チェックを回して green に
|
|
684
|
+
なった時点で `awaiting-bypass` の comment を書きます。
|
|
685
|
+
|
|
686
|
+
### 見送った PR を直すセッションの claim
|
|
687
|
+
|
|
688
|
+
見送った PR は `/unblock-prs` skill が直します(PR を開いたセッションが直すこ
|
|
689
|
+
ともあります)。直している間にこのスクリプトが同じ PR を試し直す(`main` が動
|
|
690
|
+
いた、Retry にチェックが入った)と、同じ conflict を2か所で解くことになるので、
|
|
691
|
+
直す側は先に **claim** を書きます(`pnpm run claim-pr -- <番号> --by <名前>`)。
|
|
692
|
+
|
|
693
|
+
- 書式は `pr-report-core` の `claim.mts` で、1行目が
|
|
694
|
+
`<!-- unblock-prs:claim head=… by=… at=… -->` の comment です。見送りの
|
|
695
|
+
comment と同じく PR ごとに1本、書き換えて使い、自分のアカウントのものだけ
|
|
696
|
+
を読みます。`by` が同じアカウントで書く claim の書き手を区別します。
|
|
697
|
+
- **有効なのは、claim した head のままで、120分以内の間だけ**です。直した
|
|
698
|
+
push で head が動けばそれだけで終わり、PR はこのスクリプトに戻ります(push
|
|
699
|
+
で戻るのは以前と同じです)。120分は push せずに止まったセッションのため
|
|
700
|
+
で、長くかかるなら同じコマンドで書き直します。諦めるときは `--release`。
|
|
701
|
+
- 有効な claim がある PR は、pick も「3a」の rebase もせず、ラベルにも触りま
|
|
702
|
+
せん(skill は自分で `skip-ci` を付けてから push します)。
|
|
703
|
+
- `claim-pr` は、担当者が skill でない PR(このスクリプトがまさに動かそうとし
|
|
704
|
+
ている PR や、人の手が要る PR)と、他の書き手の claim が有効な PR には書き
|
|
705
|
+
ません。書いた後に読み直し、同時に書いた相手に負けていれば失敗します。
|
|
706
|
+
- `pnpm run pr-report -- --format json` の各 entry の `owner` が、skill が直す
|
|
707
|
+
PR を選ぶときに読むものです。
|
|
708
|
+
|
|
709
|
+
### 担当者(owner)
|
|
710
|
+
|
|
711
|
+
キューにある PR(`merge-queued`、または `skip-ci` と `auto-rebase`)には、次に
|
|
712
|
+
動かす担当者が必ず1つ決まります。決めるのは `pr-report-core` の
|
|
713
|
+
`queue-owner.mts` の `queueOwner` だけで、このスクリプト(`owner.mts`)も
|
|
714
|
+
`pr-report` / `claim-pr` / `push-fixed-pr` / `report-unowned-prs`
|
|
715
|
+
(`pr-report/queue.mts`)も同じ関数に同じ事実を渡して読みます。#2267 では、こ
|
|
716
|
+
のスクリプトと skill がそれぞれ別の読み方の `main` で見送りの有効性を判定し、
|
|
717
|
+
互いに相手の担当だと判断して誰も動かしませんでした。
|
|
718
|
+
|
|
719
|
+
| `owner` | 次に動かすもの |
|
|
720
|
+
| :------- | :-------------------------------------------------------------------------------------------------------- |
|
|
721
|
+
| `worker` | このスクリプト。順番待ち、stack の上の層、見送りが解消済み |
|
|
722
|
+
| `skill` | `/unblock-prs` skill。claim 中、または `rebase-failed` / `checks-failed` で見送り中(依存更新 PR も含む) |
|
|
723
|
+
| `human` | 人。draft、fork、閉路、bypass merge 待ち、設定や権限で止まっている見送り |
|
|
724
|
+
| `none` | 誰でもない。規則の想定外の状態で、ツールのバグ |
|
|
725
|
+
|
|
726
|
+
- **読むのは comment と PR だけで、base の先端は読みません。** 見送りの
|
|
727
|
+
comment が受け渡しの印です。このスクリプトが「解消済み」に書き換える(head
|
|
728
|
+
が動いた、再試行を頼まれた、base に紐付く理由で `main` が動いた)までは
|
|
729
|
+
skill か人の担当です。このスクリプトが止まっていても、担当者が誰でもなくなる
|
|
730
|
+
ことはありません。
|
|
731
|
+
- 見送りの理由ごとの担当者は `setAsideOwner` が網羅します。理由の一覧
|
|
732
|
+
(`SET_ASIDE_REASONS`)もそこにあり、このスクリプトはそれを使うので、担当者
|
|
733
|
+
の無い理由を書くことはできません(コンパイルが通りません)。`none` になるの
|
|
734
|
+
は、このファイルの知らない理由が comment にあるときです。
|
|
735
|
+
- 依存更新 PR(`chore/pnpm-update`)も他の PR と同じく skill の担当です。
|
|
736
|
+
ただし `rebase-failed` は rebase で解かず、`pnpm run rebuild-pnpm-update` で
|
|
737
|
+
`pnpm-update.yml` に `main` の先端から作り直させます。lockfile を含めて生成
|
|
738
|
+
されるブランチなので、手で解いた conflict は `minimumReleaseAge` の下で解決
|
|
739
|
+
されたことを誰も確かめていないからです。
|
|
740
|
+
- `none` は `pnpm run report-unowned-prs` が issue にします(PR ごとに1本、
|
|
741
|
+
本文の隠し行で既存の issue を見つけます)。`/unblock-prs` skill が毎回実行
|
|
742
|
+
します。
|
|
743
|
+
|
|
744
|
+
### ファイル構成
|
|
745
|
+
|
|
746
|
+
| ファイル | 役割 |
|
|
747
|
+
| :---------------------- | :------------------------------------------------------------ |
|
|
748
|
+
| `main.mts` | ループ本体とコマンドライン(入口) |
|
|
749
|
+
| `triage.mts` | 1回の survey が各 PR について何を言うか(判定の読み込み) |
|
|
750
|
+
| `merge-after.mts` | 宣言された順序が pick に何を言うか |
|
|
751
|
+
| `stack.mts` | stacked PR — いつ待たせ、いつ張り、何を運ぶか、いつ積み直すか |
|
|
752
|
+
| `version-pr.mts` | version PR と、それを止めているもの |
|
|
753
|
+
| `rebase.mts` | ブランチを動かす — worktree 内の rebase と `skip-ci` 除去 |
|
|
754
|
+
| `release.mts` | 解放されている PR を1本に保つ |
|
|
755
|
+
| `watch.mts` | 1本をマージまでポーリング |
|
|
756
|
+
| `auto-fix.mts` | fixer の差分だけの失敗を直して push |
|
|
757
|
+
| `config.mts` | `.github/unblock-prs.json` を読む |
|
|
758
|
+
| `quiet.mts` | 何もない時にどれだけ待つか |
|
|
759
|
+
| `checks.mts` | マージが何を待っているか |
|
|
760
|
+
| `claims.mts` | セッションの claim のうち有効なもの |
|
|
761
|
+
| `owner.mts` | 各 PR の担当者(`pr-report-core` の `queueOwner`) |
|
|
762
|
+
| `github.mts` | GitHub と `git` を読み書きするもの全部。判断はしない |
|
|
763
|
+
| `labels.mts` | ラベルがその PR について何を言うか |
|
|
764
|
+
| `skips.mts` | 諦めた PR を何をもって覚え続けるか、comment との突き合わせ |
|
|
765
|
+
| `set-aside-comment.mts` | 見送りの comment の文章 |
|
|
766
|
+
| `demotions.mts` | green のまま止まった PR をいつまで最後に回すか |
|
|
767
|
+
| `options.mts` | コマンドライン |
|
|
768
|
+
| `types.mts` | 共有される型とスキーマ |
|
|
769
|
+
| `constants.mts` | 待ち時間と諦めるまでの回数 |
|
|
770
|
+
| `util.mts` | 停止シグナル |
|
|
771
|
+
|
|
772
|
+
API を呼ぶ `rest()` / `graphqlQuery()`、`git` を走らせる `git()`、quoting の
|
|
773
|
+
`sh()`、ログの `log()` は `open-pr` などほかのコマンドも使うので
|
|
774
|
+
`../lib/` にあります。
|
|
775
|
+
|
|
776
|
+
トレーラのパーサと閉路検出、どの PR がどの PR に積まれているか、ラベルの文字列、
|
|
777
|
+
`CODEOWNERS` の判定、見送りの comment の隠しコメントとチェックボックスの書式、
|
|
778
|
+
そして triage の判定そのもの(各 PR が queue のどこにいるか、レビューが止めているか、
|
|
779
|
+
候補をどの順に pick するか: `queue-order.mts` と `review-hold.mts`)は、
|
|
780
|
+
同じ queue を描く Pull Requests Manager と共有するため `libs/pr-report-core`
|
|
781
|
+
にあります。`triage.mts` はその判定が求めるものを GitHub と `git` から読むだけです。
|
|
782
|
+
|
|
783
|
+
`index.mts` はありません。`ws:gen` は workspace メンバーしか歩かず `tools/` は
|
|
784
|
+
意図的にメンバーではないので、手で維持するだけの barrel になります。
|
|
785
|
+
|
|
786
|
+
## English
|
|
787
|
+
|
|
788
|
+
### What it does
|
|
789
|
+
|
|
790
|
+
Takes the pull requests labelled `merge-queued` forward **one at a time**, in
|
|
791
|
+
the order those pull requests declare. One per cycle, always: merging any of
|
|
792
|
+
them moves `main` and puts every other branch back to `BEHIND`, so a batch
|
|
793
|
+
rebase runs a full CI matrix per branch and throws all but the first away.
|
|
794
|
+
|
|
795
|
+
**What it never does**: merge (auto-merge's job), add `merge-queued`, approve a
|
|
796
|
+
review, or fix a failing check (the skill's job — except a failure that is only
|
|
797
|
+
a fixer's diff, see "4a"). The rebase happens in a throwaway `git worktree`, so
|
|
798
|
+
the checkout it runs from is never touched.
|
|
799
|
+
|
|
800
|
+
### The six things a pull request declares
|
|
801
|
+
|
|
802
|
+
**The `merge-queued` label is the scope rule.** A pull request without it is
|
|
803
|
+
passed over in silence — with two exceptions: a layer GitHub moved onto
|
|
804
|
+
`main` when the layer below it merged, which is rebased off that layer's
|
|
805
|
+
commits whatever its labels (see "the base" below), and a pull request
|
|
806
|
+
labelled `auto-rebase`, which is kept on `main` (see "`auto-rebase`" below). One that has it and cannot be acted on — a draft, or a
|
|
807
|
+
base that is neither `main` nor an open pull request's branch — is reported,
|
|
808
|
+
because the label asked for something and the answer is no.
|
|
809
|
+
|
|
810
|
+
**This script is the only thing that arms auto-merge** (`armOnPick` in
|
|
811
|
+
`rebase.mts`).
|
|
812
|
+
`open-pr` arms nothing; this script arms a queued pull request when it picks
|
|
813
|
+
it — one it rebases after the push and before `skip-ci` comes off, one already
|
|
814
|
+
up to date before it is watched. The ruleset asks for no approvals, so an
|
|
815
|
+
armed pull request merges the moment it goes green, which is why nothing is
|
|
816
|
+
armed before it is queued; and a layer of a stack, onto a branch no ruleset
|
|
817
|
+
covers, could merge into the layer below the moment it was armed.
|
|
818
|
+
`merge-queued` is the only thing read as permission, so one that has lost its
|
|
819
|
+
auto-merge — GitHub disarms it when the base changes, and a person may switch
|
|
820
|
+
it off — is armed again when it is next picked. Holding one back is taking
|
|
821
|
+
`merge-queued` off.
|
|
822
|
+
|
|
823
|
+
**The `Merge-After:` trailer declares the order.** On its own line, naming as
|
|
824
|
+
many numbers as it likes, on one line or several:
|
|
825
|
+
|
|
826
|
+
```markdown
|
|
827
|
+
Merge-After: #1901, #1903
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
- The pull request is not picked until **every** pull request it names has
|
|
831
|
+
left the open list. It is an AND, not an OR.
|
|
832
|
+
- It constrains picking and nothing else. One that is already up to date and
|
|
833
|
+
merging is watched as ever, because auto-merge will merge it whatever is
|
|
834
|
+
declared here — and passing it over would only send the cycle off to rebase
|
|
835
|
+
a branch that merge is about to invalidate.
|
|
836
|
+
- One pull request may name several predecessors and several may name the
|
|
837
|
+
same one, so what is declared is a graph rather than a list. **A cycle is
|
|
838
|
+
detected and reported by name**, and everything caught in it is left alone,
|
|
839
|
+
because nothing here can move it.
|
|
840
|
+
- **A trailer inside a fenced code block is not read**, so a document
|
|
841
|
+
describing the convention does not become a constraint.
|
|
842
|
+
- **The `Start-After:` of the issues it closes is inherited**
|
|
843
|
+
(`docs/issue-start-after.md`, "pull request への引き継ぎ"): an open pull
|
|
844
|
+
request named there is waited for as if `Merge-After:` named it, and an open
|
|
845
|
+
issue named stands for the open pull requests closing it. The issues it
|
|
846
|
+
closes are GitHub's links and the closing keywords in its body, a stacked
|
|
847
|
+
one's included, read again on every survey, and GitHub's issue
|
|
848
|
+
dependencies ("blocked by") count as `Start-After:` does; a survey that cannot read them
|
|
849
|
+
inherits nothing and says so. The `note` says which numbers came from an
|
|
850
|
+
issue.
|
|
851
|
+
|
|
852
|
+
**The base declares an order too.** A pull request onto another open pull
|
|
853
|
+
request's branch is a layer stacked on it — GitHub's stacked pull requests —
|
|
854
|
+
and its diff is its own layer and nothing below it, which is what makes it
|
|
855
|
+
reviewable.
|
|
856
|
+
|
|
857
|
+
- It is not picked until the layer below it has merged. The base alone says
|
|
858
|
+
so, so a stack with no `Merge-After:` is recognized too; `open-pr --base`
|
|
859
|
+
writes the layer below into `Merge-After:` as well, which means the same and
|
|
860
|
+
is not counted twice.
|
|
861
|
+
- When the layer below merges, GitHub moves this one onto `main`. From then
|
|
862
|
+
on it is an ordinary pull request.
|
|
863
|
+
- **A layer moved onto `main` is restacked without being queued.** It still
|
|
864
|
+
carries the merged layer's commits, so its diff shows that layer's changes
|
|
865
|
+
as its own. The next survey rebases it with `--onto` from the head that
|
|
866
|
+
layer merged at onto its squash commit, carrying the layers above along
|
|
867
|
+
("1a").
|
|
868
|
+
- No label is asked: this undoes GitHub's move, and is not the pull
|
|
869
|
+
request's turn. It brings in nothing new from `main`, and needs no review
|
|
870
|
+
to have finished.
|
|
871
|
+
- The ruleset merges only an up-to-date branch, and only by squash, so the
|
|
872
|
+
squash commit's tree is the merged head's, and the restacked layer's tree
|
|
873
|
+
is the one it had. Only its history, and so its diff, changes; a tree
|
|
874
|
+
already checked is not checked again, so it costs no CI even with
|
|
875
|
+
`skip-ci` off.
|
|
876
|
+
- `skip-ci` and auto-merge are left as they are. A layer labelled
|
|
877
|
+
`merge-queued` is left to its rebase when it is picked, which drops the
|
|
878
|
+
same commits the same way.
|
|
879
|
+
- **GitHub's native stacks are not landed.** GitHub refuses auto-merge on a
|
|
880
|
+
pull request in one ("Auto-merge is not supported for stacked pull
|
|
881
|
+
requests"), and keeps a layer in its stack after the layer below has merged
|
|
882
|
+
and it has been moved onto `main`. Such a pull request is not picked but
|
|
883
|
+
reported as a `note`, to be merged by hand. Make a stack with the base
|
|
884
|
+
alone.
|
|
885
|
+
- A stacked pull request is never picked, so never armed. It is armed when
|
|
886
|
+
its turn comes, after GitHub has moved it onto `main`.
|
|
887
|
+
- **When a layer is rebased, the layers above it are carried along.** Each is
|
|
888
|
+
replayed with `git rebase --onto <new head> <old head>` — its own commits and
|
|
889
|
+
nothing else — and pushed. Otherwise each would show the layer below's old
|
|
890
|
+
commits in its diff, and GitHub's rebase when that layer merges would start
|
|
891
|
+
from a head the layer above does not contain. `merge-queued` is not asked:
|
|
892
|
+
a stack moves as one.
|
|
893
|
+
- A pull request moved off a stack that GitHub did not rebase still carries
|
|
894
|
+
the merged layer's commits; it is rebased with `--onto` from the head that
|
|
895
|
+
layer merged at, rather than trusting the patches to match the squash
|
|
896
|
+
commit.
|
|
897
|
+
- A fork's branch is never a parent, and never restacked.
|
|
898
|
+
|
|
899
|
+
**The `blocks-release` label says the next release must contain this pull
|
|
900
|
+
request.** While one carrying it is open, the version pull request — the one
|
|
901
|
+
`changesets/action` opens from `changeset-release/main` — is not picked.
|
|
902
|
+
|
|
903
|
+
- It constrains the version pull request and nothing else. The pull request
|
|
904
|
+
carrying it is not constrained at all.
|
|
905
|
+
- **A draft counts.** Holding a release for something not queued, or not
|
|
906
|
+
finished, is the whole point of the label. The other side of that is a
|
|
907
|
+
forgotten draft holding every release, so the blockers are named in the
|
|
908
|
+
report on every cycle.
|
|
909
|
+
- Nothing ever takes it off: the constraint is "open", so merging or closing
|
|
910
|
+
the pull request ends it and leaves no state to clean up.
|
|
911
|
+
- On the version pull request itself it is ignored — it would hold it forever
|
|
912
|
+
with nothing able to take it off, which is a `Merge-After` cycle by another
|
|
913
|
+
name.
|
|
914
|
+
|
|
915
|
+
**Why a label, and not the body.** The version pull request is the one pull
|
|
916
|
+
request that cannot declare anything in its body: `changesets/action`
|
|
917
|
+
overwrites its title and body through `updatePullRequest` on every push to the
|
|
918
|
+
base, so a `Merge-After:` written there is wiped — and wiped precisely when
|
|
919
|
+
another queued pull request merged first, which is when the order mattered. So
|
|
920
|
+
the declaration is made from the other side, by the pull request the release is
|
|
921
|
+
waiting for. A label rather than a comment for a second reason: this repository
|
|
922
|
+
is public, and a label needs write access while a comment does not.
|
|
923
|
+
|
|
924
|
+
**The `auto-rebase` label asks for the pull request to be kept on `main`
|
|
925
|
+
while it waits** for review or for its turn, so that its diff stays its own
|
|
926
|
+
changes and easy to review ("3a").
|
|
927
|
+
|
|
928
|
+
- It works only together with `skip-ci`, under which a push runs no checks.
|
|
929
|
+
- A `merge-queued` pull request is treated the same: while it cannot be picked
|
|
930
|
+
(waiting on a `Merge-After` or its review), it is kept on `main` as long as
|
|
931
|
+
`skip-ci` is on.
|
|
932
|
+
- It acts only when the queue has nothing to do, because a push while `main`
|
|
933
|
+
is about to move again is wasted.
|
|
934
|
+
- `open-pr` puts it on every layer of a stack. A layer above the bottom is
|
|
935
|
+
only carried along until the layer below merges and it is moved onto
|
|
936
|
+
`main`; that is when its own label starts to count, and nothing adds one
|
|
937
|
+
then, so it is there from the start.
|
|
938
|
+
- It is opt-in, so that a branch left to sleep is never moved for want of a
|
|
939
|
+
label. Putting it on just before asking for a review is enough.
|
|
940
|
+
|
|
941
|
+
**The `priority:high` and `priority:low` labels say which of the pickable
|
|
942
|
+
pull requests to take first, and which can wait.**
|
|
943
|
+
|
|
944
|
+
- Among the candidates, one labelled `priority:high` goes first and one
|
|
945
|
+
labelled `priority:low` last. It is an ordering, not a gate: what
|
|
946
|
+
`Merge-After`, a stack, `blocks-release` or a review holds stays held, and
|
|
947
|
+
one in flight is watched whatever it carries.
|
|
948
|
+
- The version pull request reads it too. `priority:high` on it releases
|
|
949
|
+
before the queued changes; a change labelled `priority:low` goes after an
|
|
950
|
+
unlabelled version pull request, so into the release after.
|
|
951
|
+
- It outranks the ordering that moves `DIRTY` and the version pull request
|
|
952
|
+
back, and the numbers order within it. Only the rule that puts last one that
|
|
953
|
+
sat green without merging ("5") is stronger: that one is last even with
|
|
954
|
+
`priority:high`.
|
|
955
|
+
- When two pull requests are released at once, the same order says which
|
|
956
|
+
keeps its release.
|
|
957
|
+
- One carrying both contradicts itself and counts as neither, and the triage
|
|
958
|
+
output says so.
|
|
959
|
+
- `priority:mid`, which also exists on GitHub, is not read: it is the same as
|
|
960
|
+
no label.
|
|
961
|
+
|
|
962
|
+
**`skip-ci` is not a scope rule.** It pauses a queued pull request, and taking
|
|
963
|
+
it off when its turn comes is the job. It is a condition of "3a", as the
|
|
964
|
+
guarantee that the push runs no checks.
|
|
965
|
+
|
|
966
|
+
### One released pull request at a time
|
|
967
|
+
|
|
968
|
+
A pull request labelled `merge-queued` without `skip-ci` is **released**:
|
|
969
|
+
pushing to it, or taking the label off, runs the full matrix. **At most one is
|
|
970
|
+
released at any time.** Only one can merge before `main` moves, so a second
|
|
971
|
+
one's run is thrown away.
|
|
972
|
+
|
|
973
|
+
#### Why — what happened without it
|
|
974
|
+
|
|
975
|
+
This script and the `/unblock-prs` skill move the same queue side by side,
|
|
976
|
+
and neither can see the other. Without the rule:
|
|
977
|
+
|
|
978
|
+
1. Queued pull request A has its `skip-ci` taken off, and its matrix runs. A
|
|
979
|
+
check fails and the merge is blocked. A is set aside with the label off.
|
|
980
|
+
2. This script picks the next queued pull request, B, and takes its `skip-ci`
|
|
981
|
+
off.
|
|
982
|
+
3. The skill fixes A's failure and pushes. A has no label, so the push starts
|
|
983
|
+
A's matrix too.
|
|
984
|
+
4. A and B run at once. Whichever merges first moves `main`, the other goes
|
|
985
|
+
back to `BEHIND`, and the whole of its run is wasted.
|
|
986
|
+
|
|
987
|
+
Each writer was right on its own. The waste comes from "only one may run" being
|
|
988
|
+
written down nowhere, and `skip-ci` is where it is now written.
|
|
989
|
+
|
|
990
|
+
The skill now fixes only set-aside pull requests and always pushes under
|
|
991
|
+
`skip-ci`, so step 3 no longer comes from it. A person's push and a pull
|
|
992
|
+
request opened without the label, as `pnpm-update.yml` opens one, still can,
|
|
993
|
+
so the rule stays.
|
|
994
|
+
|
|
995
|
+
#### The rule
|
|
996
|
+
|
|
997
|
+
**This script puts `skip-ci` on a pull request only to give back the label it
|
|
998
|
+
has just taken off, yielding to another released in the same moment.** It
|
|
999
|
+
puts it on no other pull request. It used to pause every other released pull request before releasing its own,
|
|
1000
|
+
which stopped pull requests nobody had handed it: the dependency update the
|
|
1001
|
+
bot opens released, and one a person had released on purpose.
|
|
1002
|
+
|
|
1003
|
+
- **Pick nothing while one is in flight.** When triage finds a pull request
|
|
1004
|
+
in flight, this script watches it and rebases and releases nothing else
|
|
1005
|
+
("3").
|
|
1006
|
+
- **Look again straight after releasing.** When another pull request the
|
|
1007
|
+
worker owns was released in the same moment and comes first in the pick
|
|
1008
|
+
order (`priority:high` first and `priority:low` last, then the lowest
|
|
1009
|
+
number, the version pull request last), this script puts `skip-ci` back on
|
|
1010
|
+
**its own** and surveys again (`yieldsTo` in `release.mts`). A set-aside or
|
|
1011
|
+
claimed pull request, a skill's or a person's, runs nothing until its fix is
|
|
1012
|
+
pushed, so it does not count; counting it would stall the queue behind a
|
|
1013
|
+
pull request nobody is about to run. Nor does one already released when the
|
|
1014
|
+
survey ran (a person took `skip-ci` off it, or labelled `merge-queued` one
|
|
1015
|
+
that never had it): not in flight — waiting on `Merge-After:`, held by its
|
|
1016
|
+
review, conflicting — it is still released on the next survey, and yielding
|
|
1017
|
+
to it would pick and pause the same pull request forever.
|
|
1018
|
+
- A pull request a person or a bot released runs as released. What that can
|
|
1019
|
+
cost is a matrix, not a wrong merge. Step 3 of the example no longer
|
|
1020
|
+
happens, because the skill puts `skip-ci` on its own pull request before it
|
|
1021
|
+
pushes.
|
|
1022
|
+
|
|
1023
|
+
The rule protects CI time, not correctness. The ruleset protects correctness
|
|
1024
|
+
on its own: every required check green, on the current `main`.
|
|
1025
|
+
|
|
1026
|
+
### At startup
|
|
1027
|
+
|
|
1028
|
+
The token, the repository and its default branch. GitHub is called directly
|
|
1029
|
+
(`../lib/github-api.mts`), not through `gh`, and the token is
|
|
1030
|
+
looked for as `open-pr` looks for it: **`GH_TOKEN`, then `GITHUB_TOKEN`, then
|
|
1031
|
+
`gh auth token`**. The repository is `--repo owner/name`, else `origin`, else
|
|
1032
|
+
the root `package.json` when `origin` is not GitHub. SIGINT / SIGTERM handlers are
|
|
1033
|
+
installed: the first signal finishes the current step and stops, a second one
|
|
1034
|
+
exits immediately.
|
|
1035
|
+
|
|
1036
|
+
### One cycle
|
|
1037
|
+
|
|
1038
|
+
#### 1. Survey
|
|
1039
|
+
|
|
1040
|
+
GraphQL for the open pull requests, newest first and up to a hundred, `body` included — that is where the
|
|
1041
|
+
order is read from. While any of them reports `mergeStateStatus: UNKNOWN` the
|
|
1042
|
+
list is fetched again, up to six times at ten-second intervals, so a state
|
|
1043
|
+
GitHub is still computing is never read as up to date. The tip of
|
|
1044
|
+
`origin/main` is read too, and so are the rules GitHub applies to `main`: the
|
|
1045
|
+
contexts they require, and whether they require a code owner's approval and
|
|
1046
|
+
every conversation resolved. These are the rules GitHub enforces, not the ones
|
|
1047
|
+
declared under `repo-settings/`. The tip is read by its whole ref name
|
|
1048
|
+
(`refs/heads/main`): `git ls-remote origin main` matches on the tail of the
|
|
1049
|
+
name, so it lists `changeset-release/main` too, and first. Read as `main`, the
|
|
1050
|
+
version pull request's head ties each set-aside to a base that never moves,
|
|
1051
|
+
and nothing is tried again when `main` does.
|
|
1052
|
+
|
|
1053
|
+
#### 1a. Restack the layers GitHub moved
|
|
1054
|
+
|
|
1055
|
+
For each pull request the last survey saw as a layer — including one whose
|
|
1056
|
+
parent had merged and which GitHub had yet to move — that is now onto `main`
|
|
1057
|
+
and not labelled `merge-queued`:
|
|
1058
|
+
|
|
1059
|
+
- Does it still contain the head the last pull request merged from the branch
|
|
1060
|
+
it was moved off?
|
|
1061
|
+
- If so, rebase it with `--onto` from that head onto the squash commit, push
|
|
1062
|
+
with `--force-with-lease` against the head the survey saw, and restack the
|
|
1063
|
+
layers above it as in step 2 of "3".
|
|
1064
|
+
- If anything moved, survey again before triage, since the heads it read are
|
|
1065
|
+
stale.
|
|
1066
|
+
|
|
1067
|
+
The first cycle of a run has no previous survey, so it reads the timeline of
|
|
1068
|
+
every pull request onto `main` for the branch it was moved off. One that no
|
|
1069
|
+
longer contains the merged head — GitHub rebased it, or it was restacked
|
|
1070
|
+
already — is passed over in silence. A layer that cannot be restacked — a
|
|
1071
|
+
conflict, a fork — is logged with the reason and left, and not followed
|
|
1072
|
+
again: no record is kept, and left alone it is where GitHub put it. With
|
|
1073
|
+
`--dry-run` it only says what it would do.
|
|
1074
|
+
|
|
1075
|
+
#### 1b. Read the set-aside comments
|
|
1076
|
+
|
|
1077
|
+
For the `merge-queued` pull requests and any other this process holds a record
|
|
1078
|
+
for, read the set-aside comments this account wrote ("Where a passed-over pull
|
|
1079
|
+
request shows") in one GraphQL request, and settle the records with them
|
|
1080
|
+
(`settleSkips` in `skips.mts`):
|
|
1081
|
+
|
|
1082
|
+
- A comment set aside at a head the branch has moved from, or against a base
|
|
1083
|
+
`main` has moved from (except `checks-failed` and `awaiting-bypass`), is
|
|
1084
|
+
rewritten as one resolved line.
|
|
1085
|
+
- A comment whose "Retry" box a person ticked is rewritten as resolved, and
|
|
1086
|
+
that pull request's record is dropped: this cycle's triage treats it as if
|
|
1087
|
+
it had never been set aside. One that fails again is set aside again, and
|
|
1088
|
+
its comment says so again.
|
|
1089
|
+
- A comment still standing that this process holds no record for was left by
|
|
1090
|
+
an earlier run. It is taken up as a record rather than tried again: a rebase
|
|
1091
|
+
of the same head onto the same base conflicts the same way.
|
|
1092
|
+
- A record this process holds that no comment says — the write failed — has
|
|
1093
|
+
its comment written again (`unannouncedSkips`). Left alone, this script
|
|
1094
|
+
holds the pull request aside while the skill, which reads the comment, reads
|
|
1095
|
+
its owner as `worker`, and each leaves it to the other.
|
|
1096
|
+
|
|
1097
|
+
The same request reads the **claims** sessions wrote ("A session's claim on a
|
|
1098
|
+
set-aside pull request"). Those still holding are kept beside the records, and
|
|
1099
|
+
the pull request is neither picked nor rebased by "3a".
|
|
1100
|
+
|
|
1101
|
+
Comments that cannot be read are logged, and the records stand as they were
|
|
1102
|
+
(and nothing is claimed). With `--dry-run` nothing is rewritten.
|
|
1103
|
+
|
|
1104
|
+
#### 2. Triage
|
|
1105
|
+
|
|
1106
|
+
1. Build the dependency graph from every body's `Merge-After:`, every base's
|
|
1107
|
+
stack and the `Start-After:` of the issues each closes, and find the cycles
|
|
1108
|
+
in it. The version pull request's body is
|
|
1109
|
+
the one that is not read, because it is overwritten; others may still name
|
|
1110
|
+
it.
|
|
1111
|
+
2. **Scope** — no `merge-queued` means `ignore`, silently. Queued but not
|
|
1112
|
+
actionable means a `note` naming the reason.
|
|
1113
|
+
3. **Owner** — one whose owner ("Who owns a pull request in the queue") is not
|
|
1114
|
+
`worker` is a `note` naming its owner and why: one set aside at its head,
|
|
1115
|
+
and one a session has claimed.
|
|
1116
|
+
4. **Stacks** — a stacked pull request is a `note` saying the layer below goes
|
|
1117
|
+
first. **Auto-merge** — one without it is a `note` if it is in one of
|
|
1118
|
+
GitHub's native stacks (to be merged by hand); otherwise it goes on, and is
|
|
1119
|
+
armed when picked.
|
|
1120
|
+
5. **The `Merge-After` gate** — a `note` while anything it names is open.
|
|
1121
|
+
6. **The version pull request** — one whose branch is
|
|
1122
|
+
`changeset-release/<default branch>` is handled on its own. A `note` while
|
|
1123
|
+
any pull request labelled `blocks-release` is open. Otherwise `git
|
|
1124
|
+
merge-base` says whether the branch was built on the current tip of the
|
|
1125
|
+
base, and a `note` while it was not — the release workflow has not rebuilt
|
|
1126
|
+
it yet. Past both: **candidate** if it carries `skip-ci`, and otherwise
|
|
1127
|
+
**failing** or **in flight** by its required checks. `mergeStateStatus` is
|
|
1128
|
+
not what answers this one: GitHub's answer is computed asynchronously and
|
|
1129
|
+
cached, and being wrong here releases a set the branch was built before.
|
|
1130
|
+
7. **The rest**
|
|
1131
|
+
- Carrying `skip-ci` → **candidate**, whatever the merge state says. With
|
|
1132
|
+
`no-skip-ci-label` pending it reads `BLOCKED` however ready it is, and
|
|
1133
|
+
checks that were skipped are indistinguishable from checks still
|
|
1134
|
+
running.
|
|
1135
|
+
- `BEHIND` / `DIRTY` → **candidate**. `DIRTY` is an asynchronously
|
|
1136
|
+
computed, cached answer to a different question — whether _merging_
|
|
1137
|
+
conflicts, not whether a rebase will — so it is never a reason to
|
|
1138
|
+
exclude. The rebase answers it; it is only ordered last.
|
|
1139
|
+
- `CLEAN` / `UNSTABLE` / `HAS_HOOKS` → **in flight**.
|
|
1140
|
+
- `BLOCKED` → read the required checks: **failing** if one failed,
|
|
1141
|
+
**in flight** if they are still running — a red left by the round before
|
|
1142
|
+
included, while the checks on the head say a new round is running.
|
|
1143
|
+
8. **Its review** — for every **candidate**, and every **in flight** that
|
|
1144
|
+
GitHub still calls `BLOCKED`, one GraphQL request reads whether its review
|
|
1145
|
+
holds the merge. If it does, the pull request is **held**, with the reason
|
|
1146
|
+
in the output, and is not rebased, released or watched. Two things are
|
|
1147
|
+
read, the two the ruleset asks for:
|
|
1148
|
+
- **A code owner's approval** of every changed path that
|
|
1149
|
+
`.github/CODEOWNERS` gives an owner. GitHub's `reviewDecision` answers
|
|
1150
|
+
for branch protection, not a ruleset, and is `null` for every pull
|
|
1151
|
+
request here. So this is worked out from `CODEOWNERS`, the changed files
|
|
1152
|
+
and the approvals, by `pr-report-core`'s `codeOwnerReview`, the same
|
|
1153
|
+
code the Pull Requests Manager uses. When the author owns the path it
|
|
1154
|
+
says so, since an author may not approve their own pull request.
|
|
1155
|
+
- **An unresolved conversation**, any at all.
|
|
1156
|
+
|
|
1157
|
+
The exception is a pull request whose waiting paths **only its author**
|
|
1158
|
+
owns, with no conversation open. No approval will ever come, so it merges
|
|
1159
|
+
by a ruleset bypass or not at all, and holding it would leave it under
|
|
1160
|
+
`skip-ci` with no verdict for whoever makes that merge. So it is not
|
|
1161
|
+
held: it is rebased, released and watched like any other, but **never
|
|
1162
|
+
armed** — auto-merge would wait for the same approval — and once its
|
|
1163
|
+
checks are green it is set aside `awaiting-bypass`, with a comment asking
|
|
1164
|
+
a person for the bypass merge.
|
|
1165
|
+
|
|
1166
|
+
Releasing a pull request that cannot merge for a reason no check reports
|
|
1167
|
+
runs a whole matrix and holds the queue for the length of a watch. And
|
|
1168
|
+
since `main` moving puts it back at the front as `BEHIND`, it would do so
|
|
1169
|
+
again every time anything else merged. An approval or a resolved
|
|
1170
|
+
conversation changes the answer on the next survey, so nothing is
|
|
1171
|
+
recorded. A pull request whose changed files run past one page (100) is
|
|
1172
|
+
not held on a guess, and neither is anything when the request fails; that
|
|
1173
|
+
is how the script behaved before it asked. One in flight that GitHub calls
|
|
1174
|
+
mergeable is watched as ever: that state is GitHub's own answer that
|
|
1175
|
+
nothing but the checks is left.
|
|
1176
|
+
|
|
1177
|
+
**The version pull request is never rebased.** `changeset-release/main` is
|
|
1178
|
+
rebuilt from the tip of the base and force-pushed by the release workflow on
|
|
1179
|
+
every push to it, so a rebase here would be (a) a second thing force-pushing
|
|
1180
|
+
one branch and (b) the old version commit landing on a tip that carries a
|
|
1181
|
+
changeset it never consumed — merge that and the release is missing the change
|
|
1182
|
+
the queue was assembled for. Taking `skip-ci` off is the whole of what it is
|
|
1183
|
+
given, and it is ordered last among the candidates. That last part is an
|
|
1184
|
+
ordering and not a gate, so it never stops a release: it only means that when
|
|
1185
|
+
a queued change and the release are both ready, the change goes in first.
|
|
1186
|
+
|
|
1187
|
+
#### 3. Act on exactly one
|
|
1188
|
+
|
|
1189
|
+
**If anything is in flight, watch that and stop** — do not rebase another.
|
|
1190
|
+
Arm it first if it has no auto-merge.
|
|
1191
|
+
|
|
1192
|
+
Otherwise take the first candidate. Candidates go lowest number first, with
|
|
1193
|
+
three kinds moved back: `DIRTY` after the rest, the version pull request after
|
|
1194
|
+
that, and one that sat green without merging (step 5) last of all. Everything
|
|
1195
|
+
but that last kind is first split into `priority:high`, unlabelled and
|
|
1196
|
+
`priority:low`, in that order, and ordered as above within each. Then:
|
|
1197
|
+
|
|
1198
|
+
1. Rebase onto `origin/main` in a throwaway worktree and push with
|
|
1199
|
+
`--force-with-lease`, leased against the head the survey saw. A pull
|
|
1200
|
+
request moved off a stack that still contains the head its merged layer
|
|
1201
|
+
merged at is rebased `--onto` from that head.
|
|
1202
|
+
2. If the head moved, restack the layers above it, lowest first, each with
|
|
1203
|
+
`--onto`, and push them. A layer that cannot be moved — a conflict, a fork,
|
|
1204
|
+
a layer below that did not move — is logged and left; the pull request
|
|
1205
|
+
whose turn it is goes on regardless.
|
|
1206
|
+
3. If it has no auto-merge, ask GitHub once more — still open, onto `main`,
|
|
1207
|
+
head is the commit just pushed, `merge-queued` still on — and arm it. This
|
|
1208
|
+
comes before `skip-ci` is taken off, so the label holds the merge while it
|
|
1209
|
+
happens.
|
|
1210
|
+
4. If it carries `skip-ci`, ask GitHub once more — still open, head is the
|
|
1211
|
+
commit just pushed, both labels still on — and then take the label off.
|
|
1212
|
+
The order is the point: while the label is on, the push fires a
|
|
1213
|
+
`synchronize` whose every check skips, so it is free; taking the label off
|
|
1214
|
+
then fires `unlabeled` and runs the matrix once, **on the head that will
|
|
1215
|
+
actually be merged**. The other order starts a full matrix on the
|
|
1216
|
+
pre-rebase head and has the push cancel it.
|
|
1217
|
+
5. A rebase that changed nothing, on a pull request with no label to take off
|
|
1218
|
+
either, means GitHub's merge state was stale: wait thirty seconds and
|
|
1219
|
+
survey again.
|
|
1220
|
+
|
|
1221
|
+
A failure — a real conflict, a refused push, auto-merge that could not be
|
|
1222
|
+
armed, a label that could not be removed — is recorded and **the next candidate is tried in the same cycle**.
|
|
1223
|
+
The queue does not stop.
|
|
1224
|
+
|
|
1225
|
+
The exception is **a branch that moved under it**: the head the fetch finds
|
|
1226
|
+
is not the one the survey saw, a push lost its lease to a head that has
|
|
1227
|
+
since changed, or the re-read before taking the label off finds a different
|
|
1228
|
+
head. That is the skill or a person moving the same pull request, so nothing
|
|
1229
|
+
is recorded, and the cycle surveys again rather than moving on — rebasing the
|
|
1230
|
+
next candidate would start a second matrix beside the one the other writer is
|
|
1231
|
+
about to start. The `/unblock-prs` skill is written to run alongside this
|
|
1232
|
+
script.
|
|
1233
|
+
|
|
1234
|
+
#### 3a. Keep waiting pull requests on the tip
|
|
1235
|
+
|
|
1236
|
+
Only when step 3 has nothing to act on — nothing in flight and no candidate to
|
|
1237
|
+
pick — rebase the following onto `main`, because a push while `main` is about
|
|
1238
|
+
to move again is wasted. It keeps the diff of a pull request under review, or
|
|
1239
|
+
waiting its turn, to its own changes.
|
|
1240
|
+
|
|
1241
|
+
- Eligible: **labelled `skip-ci`** (so the push runs no checks) **and either
|
|
1242
|
+
`auto-rebase` or `merge-queued`**; open, not a draft, a branch of this
|
|
1243
|
+
repository, based on `main`, not the version pull request, and whose owner
|
|
1244
|
+
is `worker` — no set-aside standing at its head, and no session's claim. A
|
|
1245
|
+
queued one gets
|
|
1246
|
+
here only when it cannot be picked — waiting on a `Merge-After` or its
|
|
1247
|
+
review — and rebasing it early under `skip-ci` costs nothing.
|
|
1248
|
+
- `skip-ci` leaves the merge state at `BLOCKED`, so whether it is behind is
|
|
1249
|
+
read from git, not from `BEHIND` / `DIRTY`: `git merge-base --is-ancestor`
|
|
1250
|
+
says whether `origin/main` is in its head.
|
|
1251
|
+
- A plain `git rebase origin/main`, pushed with `--force-with-lease` against
|
|
1252
|
+
the head the survey saw; the layers above are carried by `restack()`, as in
|
|
1253
|
+
step 2 of "3". A conflict or a refused push is a set-aside, announced in its
|
|
1254
|
+
comment and not tried again until the head or `main` moves or the retry box
|
|
1255
|
+
is ticked.
|
|
1256
|
+
- If anything moved, survey again before triage.
|
|
1257
|
+
|
|
1258
|
+
**The `auto-rebase` label** is on every layer of a stack, where `open-pr` puts
|
|
1259
|
+
it; only a layer based on `main` is a target, so the layers above are carried
|
|
1260
|
+
along until the one below merges. A pull request with neither label — a
|
|
1261
|
+
branch left to sleep included — is not moved by this step.
|
|
1262
|
+
|
|
1263
|
+
#### 4. Watch
|
|
1264
|
+
|
|
1265
|
+
After one `--poll-interval` (30s by default), poll until it merges or until
|
|
1266
|
+
something says it will not. Each poll tests these in order and leaves on the
|
|
1267
|
+
first that matches:
|
|
1268
|
+
|
|
1269
|
+
| Condition | Outcome |
|
|
1270
|
+
| :--------------------------------- | :----------------------------------- |
|
|
1271
|
+
| re-reading the pull request failed | `error` after 5 in a row, else retry |
|
|
1272
|
+
| `MERGED` | `merged` |
|
|
1273
|
+
| not `OPEN` | `closed` |
|
|
1274
|
+
| auto-merge switched off (\*) | `auto-merge-disabled` |
|
|
1275
|
+
| `skip-ci` went back on | `skip-ci-labelled` |
|
|
1276
|
+
| head is not the commit pushed | `head-moved` |
|
|
1277
|
+
| `BEHIND` / `DIRTY` | `behind-again` |
|
|
1278
|
+
| a required check failed | `checks-failed` |
|
|
1279
|
+
| green, and only a bypass merges it | `awaiting-bypass` |
|
|
1280
|
+
| green for the whole budget | `not-merging` |
|
|
1281
|
+
| past `--watch-timeout` (90m) | `timeout` |
|
|
1282
|
+
| SIGINT / SIGTERM | `stopped` |
|
|
1283
|
+
|
|
1284
|
+
(\*) Not read for one only a bypass merges (step 2, item 8), which is never
|
|
1285
|
+
armed.
|
|
1286
|
+
|
|
1287
|
+
The order matters: a pull request whose label went back on, or whose branch
|
|
1288
|
+
moved, is one whose check results say nothing.
|
|
1289
|
+
|
|
1290
|
+
**"Green" means the whole list the ruleset requires**, not the part of it that
|
|
1291
|
+
has reported. A required context with no check run on the head commit is not
|
|
1292
|
+
pending in the head's checks — it is _absent_ from them, so judging by what has
|
|
1293
|
+
reported writes a pull request off three minutes into a twenty-five minute
|
|
1294
|
+
matrix.
|
|
1295
|
+
|
|
1296
|
+
**Nothing on the head may still be running**, required or not. A required
|
|
1297
|
+
aggregate has no run in a new round until the jobs it waits on are done, so
|
|
1298
|
+
until then `--required` answers with the round before, and when that round
|
|
1299
|
+
was the one `skip-ci` skipped, every aggregate reads `skipped` and the commit
|
|
1300
|
+
looks green halfway through its matrix (#2069 was set aside `not-merging` so).
|
|
1301
|
+
A failure is still acted on at once, unless it finished before the earliest
|
|
1302
|
+
check still running started: that one is the round before, and waits for the
|
|
1303
|
+
round to end. The round `skip-ci` cancels as `open-pr` opens a pull request
|
|
1304
|
+
leaves its aggregates red, and reading that red as this round's set the first
|
|
1305
|
+
real round aside halfway through (#2246 was set aside `checks-failed` so).
|
|
1306
|
+
So is a failure whose workflow has a newer run still running: when the round
|
|
1307
|
+
`skip-ci` was put on for is slow to cancel, its aggregate goes red after the
|
|
1308
|
+
round replacing it has started, and the time alone cannot tell them apart
|
|
1309
|
+
(#2463 was set aside `checks-failed` so).
|
|
1310
|
+
|
|
1311
|
+
A pull request that is green and still open is held by something a rebase
|
|
1312
|
+
cannot fix, so it is given a budget of consecutive green polls. An unresolved
|
|
1313
|
+
conversation or a missing code-owner approval is caught by triage before
|
|
1314
|
+
release (step 2, item 8), so what reaches this point is anything else, such as
|
|
1315
|
+
auto-merge armed by someone who may not merge, or a rule this script does not
|
|
1316
|
+
read. The budget is three when GitHub itself calls it mergeable, eight while it still reads
|
|
1317
|
+
`BLOCKED`, because that combination is also what a check run that has just
|
|
1318
|
+
been superseded looks like for a moment.
|
|
1319
|
+
|
|
1320
|
+
#### 4a. Fix a failure that is only a fixer's diff
|
|
1321
|
+
|
|
1322
|
+
When the watch ends in `checks-failed`, the head's check runs are read down to
|
|
1323
|
+
the matrix jobs under the aggregates. Which failures can be fixed, and what
|
|
1324
|
+
fixes each, is the repository's `.github/unblock-prs.json`, under `autoFix`
|
|
1325
|
+
(schema: [`config.schema.json`](./config.schema.json), generated from
|
|
1326
|
+
`config.mts` by `gen:config-schema`). Without the file, or
|
|
1327
|
+
without `autoFix` in it, nothing is fixed, and the queue, rebasing,
|
|
1328
|
+
`Merge-After:`, stacks and set-aside comments work as ever. The label names,
|
|
1329
|
+
`Merge-After:` and the version pull request's branch
|
|
1330
|
+
(`changeset-release/<base>`) are conventions the PR Manager page reads too, so
|
|
1331
|
+
they are not configured.
|
|
1332
|
+
|
|
1333
|
+
| Key | Meaning |
|
|
1334
|
+
| :-------- | :-------------------------------------------------------------------------------------------- |
|
|
1335
|
+
| `ignore` | regular expressions for failed contexts that are not read (aggregates) |
|
|
1336
|
+
| `before` | commands run first, always (an install) |
|
|
1337
|
+
| `prepare` | commands run once after `before` when a context being fixed matches `context` (a build) |
|
|
1338
|
+
| `fixers` | every other failure has to match one `context`; the first that matches gives the `run` to run |
|
|
1339
|
+
| `after` | commands run last, always (a formatter) |
|
|
1340
|
+
|
|
1341
|
+
In `run`, `$1` to `$9` are `context`'s capture groups, shell-quoted. Fixers run
|
|
1342
|
+
by `stage`, lowest first, and within a stage by command.
|
|
1343
|
+
|
|
1344
|
+
This repository's configuration fixes a failure when **every** job that failed
|
|
1345
|
+
is a `code-check (X)` or `style-check (X)` entry whose `X` is a `fix:` or
|
|
1346
|
+
`gen:` command (under `ws:` or `strict-lib:` too): every job ends with
|
|
1347
|
+
`z:assert-repo-is-clean`, so the failure is the command having rewritten
|
|
1348
|
+
files. Then, and only then:
|
|
1349
|
+
|
|
1350
|
+
1. In a throwaway worktree, `pnpm install --frozen-lockfile`, and `ws:build`
|
|
1351
|
+
when CI builds for one of the entries (every `code-check` entry, and
|
|
1352
|
+
`ws:gen`). The worktree goes in `~/.local/state/unblock-prs/`. Not the OS
|
|
1353
|
+
temp directory, which is often a tmpfs, and the pnpm store hard-links
|
|
1354
|
+
`node_modules` only within one filesystem. Not the git directory or
|
|
1355
|
+
`~/.cache` either, because `fix:fmt:diff` skips every file with a `.git`,
|
|
1356
|
+
`.cache`, `dist`, `build` or `out` segment anywhere in its absolute path.
|
|
1357
|
+
2. Run the failed commands, generators first and formatters last, then
|
|
1358
|
+
`fix:fmt:diff`.
|
|
1359
|
+
3. If anything changed, amend it onto the pull request's one commit and push
|
|
1360
|
+
with `--force-with-lease` against the head that failed.
|
|
1361
|
+
4. Watch the new head. **The next pull request is not picked.**
|
|
1362
|
+
|
|
1363
|
+
Nothing is done, and the pull request is set aside as `checks-failed` as
|
|
1364
|
+
before with the reason logged, when:
|
|
1365
|
+
|
|
1366
|
+
- The repository configures no `autoFix`.
|
|
1367
|
+
- Any failed job is not a fixer (types, tests, `ws:doc`, `Validate commit
|
|
1368
|
+
count`, …). Pushing would run a matrix that fails anyway, so the whole of it
|
|
1369
|
+
is left to the skill.
|
|
1370
|
+
- The branch is not one commit. Squashing writes a message, which is the
|
|
1371
|
+
skill's to do.
|
|
1372
|
+
- A command failed, or none of them changed anything.
|
|
1373
|
+
- The head the fix pushed failed again. It is once per watch.
|
|
1374
|
+
|
|
1375
|
+
It applies to the one pull request being watched, the one this cycle picked,
|
|
1376
|
+
and never to the other queued pull requests whose checks are failing.
|
|
1377
|
+
|
|
1378
|
+
- **`ws:doc` is not a fixer.** What `doc` writes is untracked; what leaves the
|
|
1379
|
+
tree dirty is a generator it calls, which `ws:gen` runs too, and it takes ten
|
|
1380
|
+
minutes.
|
|
1381
|
+
- **oxfmt.** `fix:fmt:diff` is Prettier only and leaves `strict-lib/` alone.
|
|
1382
|
+
Of the fixers only `strict-lib:fix:fmt` itself writes there, so when that
|
|
1383
|
+
entry failed it is the command that runs.
|
|
1384
|
+
|
|
1385
|
+
#### 5. Remember, and go round again
|
|
1386
|
+
|
|
1387
|
+
A verdict lasts as long as the state it was reached in.
|
|
1388
|
+
|
|
1389
|
+
| Recorded reason | Cleared by a push | Cleared when `main` moves |
|
|
1390
|
+
| :-------------------------------- | :---------------: | :-----------------------: |
|
|
1391
|
+
| `rebase-failed` (a real conflict) | ✓ | ✓ |
|
|
1392
|
+
| `push-failed` | ✓ | ✓ |
|
|
1393
|
+
| `unlabel-failed` | ✓ | ✓ |
|
|
1394
|
+
| `arm-failed` | ✓ | ✓ |
|
|
1395
|
+
| `already-in-base` | ✓ | ✓ |
|
|
1396
|
+
| `not-merging` | ✓ | ✓ |
|
|
1397
|
+
| `watch-timeout` | ✓ | ✓ |
|
|
1398
|
+
| `checks-failed` | ✓ | **✗ (deliberate)** |
|
|
1399
|
+
| `awaiting-bypass` | ✓ | **✗ (deliberate)** |
|
|
1400
|
+
|
|
1401
|
+
So a conflict verdict is never cached across a base that moved: the commit it
|
|
1402
|
+
conflicted with may have gone with it. `checks-failed` is the exception —
|
|
1403
|
+
rebasing would put the same failing matrix through again, and fixing the
|
|
1404
|
+
failure is a person's job, so the push that carries the fix is what clears it.
|
|
1405
|
+
`awaiting-bypass` is not tied to the base either: the bypass merge it waits for
|
|
1406
|
+
does not wait for the base, and rebasing it each time anything else merged
|
|
1407
|
+
would run the matrix again for nothing. Its comment's retry box is how a person
|
|
1408
|
+
asks for a run on the current `main`.
|
|
1409
|
+
|
|
1410
|
+
A `not-merging` pull request is also **picked last** from then on, after the
|
|
1411
|
+
version pull request, and this outlives the record. `main` moving clears the
|
|
1412
|
+
record and makes it a `BEHIND` candidate again. But what held it is something
|
|
1413
|
+
triage could not read, so it will most likely not merge next time either. Left
|
|
1414
|
+
at the front, it would run a matrix, sit green and be given up on each time
|
|
1415
|
+
anything else merged. It is moved back rather than excluded, because the
|
|
1416
|
+
guess is wrong for one whose cause has since cleared. The demotion lasts until
|
|
1417
|
+
**someone else pushes**: this script's own rebase, or a "4a" push, carries it
|
|
1418
|
+
along. It is kept in memory only, so a restart forgets it.
|
|
1419
|
+
|
|
1420
|
+
A pull request this run acted on that has left the open list by the next
|
|
1421
|
+
survey is reported as merged or closed. Nothing else would notice one that
|
|
1422
|
+
merged minutes after the watch gave up on it.
|
|
1423
|
+
|
|
1424
|
+
With nothing to do, the loop sleeps and surveys again, for how long depending
|
|
1425
|
+
on whether the list is still moving. It sleeps `--active-interval` (30s) until
|
|
1426
|
+
`--idle-after` (10) surveys in a row have seen the same open pull requests
|
|
1427
|
+
(head, merge state, labels, auto-merge, draft flag, body), the same reasons a
|
|
1428
|
+
review holds any of them, and the same tip of `main`, and `--idle-interval` (300s) from then on. Any change brings it back
|
|
1429
|
+
to the short interval, so a pull request queued a moment ago is not left for
|
|
1430
|
+
minutes.
|
|
1431
|
+
|
|
1432
|
+
### Options
|
|
1433
|
+
|
|
1434
|
+
| Option | Default | Meaning |
|
|
1435
|
+
| :------------------------ | ------: | :----------------------------------------- |
|
|
1436
|
+
| `--once` | | run one cycle and exit |
|
|
1437
|
+
| `--dry-run` | | survey and report, change nothing |
|
|
1438
|
+
| `--active-interval <sec>` | 30 | wait between surveys while the list moves |
|
|
1439
|
+
| `--idle-after <n>` | 10 | unchanged surveys before the long wait |
|
|
1440
|
+
| `--idle-interval <sec>` | 300 | wait between surveys once it has sat still |
|
|
1441
|
+
| `--poll-interval <sec>` | 30 | wait between polls of the watched one |
|
|
1442
|
+
| `--watch-timeout <min>` | 90 | give up on one pull request after this |
|
|
1443
|
+
| `--no-auto-fix` | | skip the fix in "4a" |
|
|
1444
|
+
| `--repo <owner/name>` | | the repository, when `origin` is not it |
|
|
1445
|
+
|
|
1446
|
+
Each log line is stamped with the time in the machine's time zone, with its
|
|
1447
|
+
offset from UTC (`[2026-09-27T21:03:04.500+09:00]`). To read it in another
|
|
1448
|
+
zone, set `TZ`: `TZ=Asia/Tokyo pnpm run unblock-prs`.
|
|
1449
|
+
|
|
1450
|
+
### Where a passed-over pull request shows
|
|
1451
|
+
|
|
1452
|
+
This script runs on someone's machine and says everything it does on standard
|
|
1453
|
+
output. So each time it sets a pull request aside it also **writes a comment
|
|
1454
|
+
on that pull request** — one per pull request, edited in place from the second
|
|
1455
|
+
time on. The **GitHub Pull Requests Manager** page
|
|
1456
|
+
(<https://noshiro-pf.github.io/mono/pr-manager/>) reads it and shows it on
|
|
1457
|
+
the pull request, so finding out why one is not moving does not mean finding
|
|
1458
|
+
the terminal the script ran in.
|
|
1459
|
+
|
|
1460
|
+
- **The first line is a hidden record for machines**
|
|
1461
|
+
(`<!-- unblock-prs:set-aside reason=… head=… base=… -->`): the reason, and
|
|
1462
|
+
the head's and the base's SHAs at the time. Its format is `set-aside.mts` in
|
|
1463
|
+
`pr-report-core`, which this script and the page both read with. The prose
|
|
1464
|
+
after it is for people and nothing reads it back (`set-aside-comment.mts`):
|
|
1465
|
+
the reason's one sentence (`set-aside-detail.mts`: the failed checks for
|
|
1466
|
+
`checks-failed`, what holds it for `not-merging`, what it was still waiting
|
|
1467
|
+
on for `watch-timeout`, the conflicting files for `rebase-failed`), a link
|
|
1468
|
+
to the run of each failed required check, what the failed command printed
|
|
1469
|
+
(folded in `<details>`, its last 60 lines), and what will clear it.
|
|
1470
|
+
- **The last line is a "Retry" task box.** Ticking it has the next survey drop
|
|
1471
|
+
the pull request's record and try it again (step 1b of "1"). Only those who
|
|
1472
|
+
can write to the repository can tick a box in someone else's comment. A
|
|
1473
|
+
`checks-failed` pull request that is up to date is only found failing and
|
|
1474
|
+
set aside again; what clears that is a push.
|
|
1475
|
+
- **Only comments this account wrote count** (`viewerDidAuthor` in GraphQL):
|
|
1476
|
+
the repository is public, and anyone can post a comment that starts with
|
|
1477
|
+
the same hidden line. The page reads by the same rule.
|
|
1478
|
+
- **A comment, not the body**, because the author, a session and `open-pr`
|
|
1479
|
+
write the body too, and the REST API has no conditional update to keep an
|
|
1480
|
+
edit made in between; the version pull request's body is rewritten on every
|
|
1481
|
+
push. **Not a commit status any more** (context `unblock-prs`, state
|
|
1482
|
+
`failure`, as it was): that left a ✗ among the checks for a context nothing
|
|
1483
|
+
requires, had 140 characters to say why, and could neither be taken off nor
|
|
1484
|
+
asked to try again.
|
|
1485
|
+
- **This rather than GitHub's `mergeable`**, because that answers whether a
|
|
1486
|
+
merge conflicts and this script rebases — the same reason it does not take
|
|
1487
|
+
`DIRTY` at its word.
|
|
1488
|
+
- **Once it no longer applies it is rewritten as one resolved line**: the
|
|
1489
|
+
head or the base moved (the head alone for `checks-failed` and
|
|
1490
|
+
`awaiting-bypass`, by the rule `skips.mts` uses too, shared as
|
|
1491
|
+
`setAsideApplies` in `pr-report-core`), or a retry was asked for. It is kept rather than deleted, so the timeline
|
|
1492
|
+
keeps what happened. The page shows nothing for a comment whose head has
|
|
1493
|
+
moved, and a moved base as a grey, older record.
|
|
1494
|
+
- It holds no merge.
|
|
1495
|
+
- It is written when a pull request is newly set aside, not again on every
|
|
1496
|
+
survey that finds the same record standing; not at all under `--dry-run`;
|
|
1497
|
+
and a comment that could not be written is logged and nothing more. It is
|
|
1498
|
+
written as the token's account, so on a pull request that account opened the
|
|
1499
|
+
author is not notified; a session subscribed to the pull request is woken
|
|
1500
|
+
by it, and so learns its pull request was set aside.
|
|
1501
|
+
- The commit statuses written before cannot be deleted through the API; each
|
|
1502
|
+
goes with its head at the next push, so they are left alone.
|
|
1503
|
+
|
|
1504
|
+
A pull request waiting for a code owner the page works out for itself, from
|
|
1505
|
+
the paths it changes and `.github/CODEOWNERS`. This script works it out the
|
|
1506
|
+
same way and leaves that pull request in the queue (step 2, item 8). It writes
|
|
1507
|
+
no comment for it, because an approval puts it back in the running on the
|
|
1508
|
+
next survey. The exception is one only its author owns, whose checks are run
|
|
1509
|
+
and which gets an `awaiting-bypass` comment once they are green.
|
|
1510
|
+
|
|
1511
|
+
### A session's claim on a set-aside pull request
|
|
1512
|
+
|
|
1513
|
+
What this script sets aside, the `/unblock-prs` skill fixes — and so, at
|
|
1514
|
+
times, does the session that opened the pull request. If this script tried
|
|
1515
|
+
the same pull request again meanwhile (`main` moved, the "Retry" box was
|
|
1516
|
+
ticked), the same conflict would be resolved in two places. So whoever fixes
|
|
1517
|
+
one writes a **claim** first (`pnpm run claim-pr -- <number> --by <name>`).
|
|
1518
|
+
|
|
1519
|
+
- Its format is `claim.mts` in `pr-report-core`: a comment whose first line is
|
|
1520
|
+
`<!-- unblock-prs:claim head=… by=… at=… -->`. Like the set-aside comment
|
|
1521
|
+
it is one per pull request, edited in place, and only this account's count;
|
|
1522
|
+
`by` tells apart the claimants writing as that one account.
|
|
1523
|
+
- **It holds while the head is the one it claimed, for at most 120
|
|
1524
|
+
minutes.** The push carrying the fix moves the head, which ends it and hands
|
|
1525
|
+
the pull request back to this script, as a push always did. The 120 minutes
|
|
1526
|
+
are for a session that stopped without pushing; one that needs longer writes
|
|
1527
|
+
the claim again, and one that gives up runs it with `--release`.
|
|
1528
|
+
- While a claim holds, the pull request is not picked, not rebased by "3a",
|
|
1529
|
+
and its labels are left alone: the skill puts `skip-ci` on before it
|
|
1530
|
+
pushes.
|
|
1531
|
+
- `claim-pr` refuses a pull request whose owner is not a skill — this script
|
|
1532
|
+
is about to move it, or a person has to — and one another claimant holds.
|
|
1533
|
+
It reads back after writing, and fails when someone writing in the same
|
|
1534
|
+
moment won.
|
|
1535
|
+
- `owner` on each entry of `pnpm run pr-report -- --format json` is what the
|
|
1536
|
+
skill reads to choose what to fix.
|
|
1537
|
+
|
|
1538
|
+
### Who owns a pull request in the queue
|
|
1539
|
+
|
|
1540
|
+
Every pull request in the queue (`merge-queued`, or `skip-ci` with
|
|
1541
|
+
`auto-rebase`) has exactly one owner, who moves it next. Only
|
|
1542
|
+
`pr-report-core`'s `queueOwner` (`queue-owner.mts`) decides it, and this
|
|
1543
|
+
script (`owner.mts`), `pr-report`, `claim-pr`, `push-fixed-pr` and
|
|
1544
|
+
`report-unowned-prs` (`pr-report/queue.mts`) all pass it the same facts.
|
|
1545
|
+
This script and the skill used to judge whether a set-aside still stood
|
|
1546
|
+
against their own reading of `main`, and when the readings differed each found
|
|
1547
|
+
the pull request the other's and nobody moved it (#2267).
|
|
1548
|
+
|
|
1549
|
+
| `owner` | who moves it next |
|
|
1550
|
+
| :------- | :---------------------------------------------------------------------------------------------------------------- |
|
|
1551
|
+
| `worker` | this script: waiting its turn, a layer of a stack, a set-aside resolved |
|
|
1552
|
+
| `skill` | the `/unblock-prs` skill: claimed, or set aside `rebase-failed` / `checks-failed`, the dependency update included |
|
|
1553
|
+
| `human` | a person: a draft, a fork, a cycle, a bypass merge, a setting or a permission |
|
|
1554
|
+
| `none` | nobody: a state the rules do not cover, which is a bug in the tooling |
|
|
1555
|
+
|
|
1556
|
+
- **It reads the comments and the pull request, never the tip of the base.**
|
|
1557
|
+
The set-aside comment is the hand-off: until this script rewrites it as
|
|
1558
|
+
resolved — the head moved, a retry was asked for, or `main` moved under a
|
|
1559
|
+
reason tied to the base — the pull request is a skill's or a person's. A
|
|
1560
|
+
stopped script leaves it so, rather than nobody's.
|
|
1561
|
+
- `setAsideOwner` gives every reason an owner. The list of reasons
|
|
1562
|
+
(`SET_ASIDE_REASONS`) is beside it and this script writes from it, so a
|
|
1563
|
+
reason with no owner does not compile. `none` is a reason in a comment that
|
|
1564
|
+
module does not know.
|
|
1565
|
+
- The dependency update (`chore/pnpm-update`) is the skill's like any other,
|
|
1566
|
+
except that a `rebase-failed` on it is not rebased:
|
|
1567
|
+
`pnpm run rebuild-pnpm-update` has `pnpm-update.yml` build it again from the
|
|
1568
|
+
tip of `main`. The branch is generated, lockfile and all, and nothing checks
|
|
1569
|
+
that a conflict resolved by hand was resolved under `minimumReleaseAge`.
|
|
1570
|
+
- `pnpm run report-unowned-prs` files an issue for each `none`, one per pull
|
|
1571
|
+
request, found again by a hidden line in its body. The `/unblock-prs` skill
|
|
1572
|
+
runs it every round.
|
|
1573
|
+
|
|
1574
|
+
### Layout
|
|
1575
|
+
|
|
1576
|
+
| File | Role |
|
|
1577
|
+
| :---------------------- | :------------------------------------------------------------ |
|
|
1578
|
+
| `main.mts` | the loop and the command line (entry point) |
|
|
1579
|
+
| `triage.mts` | what one survey says about each pull request (the reading) |
|
|
1580
|
+
| `merge-after.mts` | what the declared order says about picking |
|
|
1581
|
+
| `stack.mts` | stacked pull requests — when they wait, arm, move and restack |
|
|
1582
|
+
| `version-pr.mts` | the version pull request, and what holds it back |
|
|
1583
|
+
| `rebase.mts` | moving a branch — the worktree rebase, the label removal |
|
|
1584
|
+
| `release.mts` | holding the queue to one released pull request |
|
|
1585
|
+
| `watch.mts` | polling one pull request until it merges, or will not |
|
|
1586
|
+
| `auto-fix.mts` | fixing and pushing a failure that is only a fixer's diff |
|
|
1587
|
+
| `config.mts` | reading `.github/unblock-prs.json` |
|
|
1588
|
+
| `quiet.mts` | how long to sleep when there is nothing to do |
|
|
1589
|
+
| `checks.mts` | what the merge is waiting for |
|
|
1590
|
+
| `claims.mts` | the claims sessions wrote that still hold |
|
|
1591
|
+
| `owner.mts` | who owns each pull request (`pr-report-core`'s `queueOwner`) |
|
|
1592
|
+
| `github.mts` | everything that reads or writes GitHub or `git` |
|
|
1593
|
+
| `labels.mts` | what the labels say about a pull request |
|
|
1594
|
+
| `skips.mts` | what the loop remembers, for how long, and what comments say |
|
|
1595
|
+
| `set-aside-comment.mts` | the prose of the set-aside comment |
|
|
1596
|
+
| `demotions.mts` | how long one that sat green without merging goes last |
|
|
1597
|
+
| `options.mts` | the command line |
|
|
1598
|
+
| `types.mts` | the shapes every module passes around |
|
|
1599
|
+
| `constants.mts` | how long it waits, and how long before it gives up |
|
|
1600
|
+
| `util.mts` | the stop signal |
|
|
1601
|
+
|
|
1602
|
+
The API client (`rest()`, `graphqlQuery()`), the runner for `git` (`git()`),
|
|
1603
|
+
the quoting (`sh()`) and the log line (`log()`) are used by `open-pr` and the
|
|
1604
|
+
other commands too, and live in `../lib/`.
|
|
1605
|
+
|
|
1606
|
+
The trailer parser, the cycle detection, which pull request is stacked on
|
|
1607
|
+
which, the label strings, the `CODEOWNERS` rules, the set-aside comment's
|
|
1608
|
+
hidden record and task box, and triage's decisions themselves — where each
|
|
1609
|
+
pull request stands in the queue, whether its review holds it, and the order
|
|
1610
|
+
candidates are picked in (`queue-order.mts` and `review-hold.mts`) — are in
|
|
1611
|
+
`libs/pr-report-core`, shared with the Pull Requests Manager page, which draws
|
|
1612
|
+
the same queue. `triage.mts` only reads, from GitHub and `git`, what those
|
|
1613
|
+
decisions ask for.
|
|
1614
|
+
|
|
1615
|
+
There is no `index.mts`: `ws:gen` only walks workspace members and `tools/` is
|
|
1616
|
+
deliberately not one, so a barrel here would be hand-maintained for nothing.
|