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.
Files changed (331) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +125 -2
  3. package/dist/claim-pr/claim-pr.d.mts +45 -0
  4. package/dist/claim-pr/claim-pr.d.mts.map +1 -0
  5. package/dist/claim-pr/claim-pr.mjs +217 -0
  6. package/dist/claim-pr/claim-pr.mjs.map +1 -0
  7. package/dist/claim-pr/index.d.mts +2 -0
  8. package/dist/claim-pr/index.d.mts.map +1 -0
  9. package/dist/claim-pr/index.mjs +2 -0
  10. package/dist/claim-pr/index.mjs.map +1 -0
  11. package/dist/cmd/claim-pr.d.mts +3 -0
  12. package/dist/cmd/claim-pr.d.mts.map +1 -0
  13. package/dist/cmd/claim-pr.mjs +20 -0
  14. package/dist/cmd/claim-pr.mjs.map +1 -0
  15. package/dist/cmd/open-pr.d.mts +3 -0
  16. package/dist/cmd/open-pr.d.mts.map +1 -0
  17. package/dist/cmd/open-pr.mjs +20 -0
  18. package/dist/cmd/open-pr.mjs.map +1 -0
  19. package/dist/cmd/pr-report.d.mts +3 -0
  20. package/dist/cmd/pr-report.d.mts.map +1 -0
  21. package/dist/cmd/pr-report.mjs +20 -0
  22. package/dist/cmd/pr-report.mjs.map +1 -0
  23. package/dist/cmd/push-fixed-pr.d.mts +3 -0
  24. package/dist/cmd/push-fixed-pr.d.mts.map +1 -0
  25. package/dist/cmd/push-fixed-pr.mjs +20 -0
  26. package/dist/cmd/push-fixed-pr.mjs.map +1 -0
  27. package/dist/cmd/report-unowned-prs.d.mts +3 -0
  28. package/dist/cmd/report-unowned-prs.d.mts.map +1 -0
  29. package/dist/cmd/report-unowned-prs.mjs +20 -0
  30. package/dist/cmd/report-unowned-prs.mjs.map +1 -0
  31. package/dist/cmd/unblock-prs.d.mts +3 -0
  32. package/dist/cmd/unblock-prs.d.mts.map +1 -0
  33. package/dist/cmd/unblock-prs.mjs +19 -0
  34. package/dist/cmd/unblock-prs.mjs.map +1 -0
  35. package/dist/entry-point.d.mts +3 -0
  36. package/dist/entry-point.d.mts.map +1 -0
  37. package/dist/entry-point.mjs +3 -0
  38. package/dist/entry-point.mjs.map +1 -0
  39. package/dist/lib/cli-args.d.mts +27 -0
  40. package/dist/lib/cli-args.d.mts.map +1 -0
  41. package/dist/lib/cli-args.mjs +55 -0
  42. package/dist/lib/cli-args.mjs.map +1 -0
  43. package/dist/lib/github-api.d.mts +153 -0
  44. package/dist/lib/github-api.d.mts.map +1 -0
  45. package/dist/lib/github-api.mjs +291 -0
  46. package/dist/lib/github-api.mjs.map +1 -0
  47. package/dist/lib/index.d.mts +7 -0
  48. package/dist/lib/index.d.mts.map +1 -0
  49. package/dist/lib/index.mjs +7 -0
  50. package/dist/lib/index.mjs.map +1 -0
  51. package/dist/lib/json.d.mts +12 -0
  52. package/dist/lib/json.d.mts.map +1 -0
  53. package/dist/lib/json.mjs +28 -0
  54. package/dist/lib/json.mjs.map +1 -0
  55. package/dist/lib/log.d.mts +10 -0
  56. package/dist/lib/log.d.mts.map +1 -0
  57. package/dist/lib/log.mjs +13 -0
  58. package/dist/lib/log.mjs.map +1 -0
  59. package/dist/lib/shell.d.mts +18 -0
  60. package/dist/lib/shell.d.mts.map +1 -0
  61. package/dist/lib/shell.mjs +42 -0
  62. package/dist/lib/shell.mjs.map +1 -0
  63. package/dist/lib/time.d.mts +17 -0
  64. package/dist/lib/time.d.mts.map +1 -0
  65. package/dist/lib/time.mjs +35 -0
  66. package/dist/lib/time.mjs.map +1 -0
  67. package/dist/open-pr/github.d.mts +72 -0
  68. package/dist/open-pr/github.d.mts.map +1 -0
  69. package/dist/open-pr/github.mjs +249 -0
  70. package/dist/open-pr/github.mjs.map +1 -0
  71. package/dist/open-pr/index.d.mts +6 -0
  72. package/dist/open-pr/index.d.mts.map +1 -0
  73. package/dist/open-pr/index.mjs +6 -0
  74. package/dist/open-pr/index.mjs.map +1 -0
  75. package/dist/open-pr/main.d.mts +36 -0
  76. package/dist/open-pr/main.d.mts.map +1 -0
  77. package/dist/open-pr/main.mjs +204 -0
  78. package/dist/open-pr/main.mjs.map +1 -0
  79. package/dist/open-pr/options.d.mts +20 -0
  80. package/dist/open-pr/options.d.mts.map +1 -0
  81. package/dist/open-pr/options.mjs +66 -0
  82. package/dist/open-pr/options.mjs.map +1 -0
  83. package/dist/open-pr/steps.d.mts +23 -0
  84. package/dist/open-pr/steps.d.mts.map +1 -0
  85. package/dist/open-pr/steps.mjs +38 -0
  86. package/dist/open-pr/steps.mjs.map +1 -0
  87. package/dist/open-pr/types.d.mts +179 -0
  88. package/dist/open-pr/types.d.mts.map +1 -0
  89. package/dist/open-pr/types.mjs +20 -0
  90. package/dist/open-pr/types.mjs.map +1 -0
  91. package/dist/pr-report/github.d.mts +59 -0
  92. package/dist/pr-report/github.d.mts.map +1 -0
  93. package/dist/pr-report/github.mjs +561 -0
  94. package/dist/pr-report/github.mjs.map +1 -0
  95. package/dist/pr-report/index.d.mts +8 -0
  96. package/dist/pr-report/index.d.mts.map +1 -0
  97. package/dist/pr-report/index.mjs +8 -0
  98. package/dist/pr-report/index.mjs.map +1 -0
  99. package/dist/pr-report/linked-issues.d.mts +15 -0
  100. package/dist/pr-report/linked-issues.d.mts.map +1 -0
  101. package/dist/pr-report/linked-issues.mjs +39 -0
  102. package/dist/pr-report/linked-issues.mjs.map +1 -0
  103. package/dist/pr-report/main.d.mts +42 -0
  104. package/dist/pr-report/main.d.mts.map +1 -0
  105. package/dist/pr-report/main.mjs +163 -0
  106. package/dist/pr-report/main.mjs.map +1 -0
  107. package/dist/pr-report/options.d.mts +36 -0
  108. package/dist/pr-report/options.d.mts.map +1 -0
  109. package/dist/pr-report/options.mjs +90 -0
  110. package/dist/pr-report/options.mjs.map +1 -0
  111. package/dist/pr-report/queue.d.mts +28 -0
  112. package/dist/pr-report/queue.d.mts.map +1 -0
  113. package/dist/pr-report/queue.mjs +74 -0
  114. package/dist/pr-report/queue.mjs.map +1 -0
  115. package/dist/pr-report/render.d.mts +14 -0
  116. package/dist/pr-report/render.d.mts.map +1 -0
  117. package/dist/pr-report/render.mjs +318 -0
  118. package/dist/pr-report/render.mjs.map +1 -0
  119. package/dist/pr-report/repo-settings.d.mts +12 -0
  120. package/dist/pr-report/repo-settings.d.mts.map +1 -0
  121. package/dist/pr-report/repo-settings.mjs +22 -0
  122. package/dist/pr-report/repo-settings.mjs.map +1 -0
  123. package/dist/push-fixed-pr/index.d.mts +2 -0
  124. package/dist/push-fixed-pr/index.d.mts.map +1 -0
  125. package/dist/push-fixed-pr/index.mjs +2 -0
  126. package/dist/push-fixed-pr/index.mjs.map +1 -0
  127. package/dist/push-fixed-pr/push-fixed-pr.d.mts +54 -0
  128. package/dist/push-fixed-pr/push-fixed-pr.d.mts.map +1 -0
  129. package/dist/push-fixed-pr/push-fixed-pr.mjs +172 -0
  130. package/dist/push-fixed-pr/push-fixed-pr.mjs.map +1 -0
  131. package/dist/report-unowned-prs/index.d.mts +2 -0
  132. package/dist/report-unowned-prs/index.d.mts.map +1 -0
  133. package/dist/report-unowned-prs/index.mjs +2 -0
  134. package/dist/report-unowned-prs/index.mjs.map +1 -0
  135. package/dist/report-unowned-prs/report-unowned-prs.d.mts +49 -0
  136. package/dist/report-unowned-prs/report-unowned-prs.d.mts.map +1 -0
  137. package/dist/report-unowned-prs/report-unowned-prs.mjs +127 -0
  138. package/dist/report-unowned-prs/report-unowned-prs.mjs.map +1 -0
  139. package/dist/tsconfig.json +1 -0
  140. package/dist/unblock-prs/auto-fix.d.mts +45 -0
  141. package/dist/unblock-prs/auto-fix.d.mts.map +1 -0
  142. package/dist/unblock-prs/auto-fix.mjs +207 -0
  143. package/dist/unblock-prs/auto-fix.mjs.map +1 -0
  144. package/dist/unblock-prs/checks.d.mts +307 -0
  145. package/dist/unblock-prs/checks.d.mts.map +1 -0
  146. package/dist/unblock-prs/checks.mjs +372 -0
  147. package/dist/unblock-prs/checks.mjs.map +1 -0
  148. package/dist/unblock-prs/claims.d.mts +21 -0
  149. package/dist/unblock-prs/claims.d.mts.map +1 -0
  150. package/dist/unblock-prs/claims.mjs +23 -0
  151. package/dist/unblock-prs/claims.mjs.map +1 -0
  152. package/dist/unblock-prs/config.d.mts +487 -0
  153. package/dist/unblock-prs/config.d.mts.map +1 -0
  154. package/dist/unblock-prs/config.mjs +119 -0
  155. package/dist/unblock-prs/config.mjs.map +1 -0
  156. package/dist/unblock-prs/constants.d.mts +28 -0
  157. package/dist/unblock-prs/constants.d.mts.map +1 -0
  158. package/dist/unblock-prs/constants.mjs +28 -0
  159. package/dist/unblock-prs/constants.mjs.map +1 -0
  160. package/dist/unblock-prs/demotions.d.mts +42 -0
  161. package/dist/unblock-prs/demotions.d.mts.map +1 -0
  162. package/dist/unblock-prs/demotions.mjs +50 -0
  163. package/dist/unblock-prs/demotions.mjs.map +1 -0
  164. package/dist/unblock-prs/github.d.mts +739 -0
  165. package/dist/unblock-prs/github.d.mts.map +1 -0
  166. package/dist/unblock-prs/github.mjs +626 -0
  167. package/dist/unblock-prs/github.mjs.map +1 -0
  168. package/dist/unblock-prs/index.d.mts +25 -0
  169. package/dist/unblock-prs/index.d.mts.map +1 -0
  170. package/dist/unblock-prs/index.mjs +25 -0
  171. package/dist/unblock-prs/index.mjs.map +1 -0
  172. package/dist/unblock-prs/labels.d.mts +49 -0
  173. package/dist/unblock-prs/labels.d.mts.map +1 -0
  174. package/dist/unblock-prs/labels.mjs +49 -0
  175. package/dist/unblock-prs/labels.mjs.map +1 -0
  176. package/dist/unblock-prs/main.d.mts +179 -0
  177. package/dist/unblock-prs/main.d.mts.map +1 -0
  178. package/dist/unblock-prs/main.mjs +640 -0
  179. package/dist/unblock-prs/main.mjs.map +1 -0
  180. package/dist/unblock-prs/merge-after.d.mts +46 -0
  181. package/dist/unblock-prs/merge-after.d.mts.map +1 -0
  182. package/dist/unblock-prs/merge-after.mjs +65 -0
  183. package/dist/unblock-prs/merge-after.mjs.map +1 -0
  184. package/dist/unblock-prs/options.d.mts +35 -0
  185. package/dist/unblock-prs/options.d.mts.map +1 -0
  186. package/dist/unblock-prs/options.mjs +88 -0
  187. package/dist/unblock-prs/options.mjs.map +1 -0
  188. package/dist/unblock-prs/owner.d.mts +32 -0
  189. package/dist/unblock-prs/owner.d.mts.map +1 -0
  190. package/dist/unblock-prs/owner.mjs +63 -0
  191. package/dist/unblock-prs/owner.mjs.map +1 -0
  192. package/dist/unblock-prs/quiet.d.mts +21 -0
  193. package/dist/unblock-prs/quiet.d.mts.map +1 -0
  194. package/dist/unblock-prs/quiet.mjs +42 -0
  195. package/dist/unblock-prs/quiet.mjs.map +1 -0
  196. package/dist/unblock-prs/rebase.d.mts +120 -0
  197. package/dist/unblock-prs/rebase.d.mts.map +1 -0
  198. package/dist/unblock-prs/rebase.mjs +576 -0
  199. package/dist/unblock-prs/rebase.mjs.map +1 -0
  200. package/dist/unblock-prs/release.d.mts +62 -0
  201. package/dist/unblock-prs/release.d.mts.map +1 -0
  202. package/dist/unblock-prs/release.mjs +94 -0
  203. package/dist/unblock-prs/release.mjs.map +1 -0
  204. package/dist/unblock-prs/set-aside-comment.d.mts +20 -0
  205. package/dist/unblock-prs/set-aside-comment.d.mts.map +1 -0
  206. package/dist/unblock-prs/set-aside-comment.mjs +93 -0
  207. package/dist/unblock-prs/set-aside-comment.mjs.map +1 -0
  208. package/dist/unblock-prs/set-aside-detail.d.mts +41 -0
  209. package/dist/unblock-prs/set-aside-detail.d.mts.map +1 -0
  210. package/dist/unblock-prs/set-aside-detail.mjs +56 -0
  211. package/dist/unblock-prs/set-aside-detail.mjs.map +1 -0
  212. package/dist/unblock-prs/skips.d.mts +85 -0
  213. package/dist/unblock-prs/skips.d.mts.map +1 -0
  214. package/dist/unblock-prs/skips.mjs +141 -0
  215. package/dist/unblock-prs/skips.mjs.map +1 -0
  216. package/dist/unblock-prs/stack.d.mts +98 -0
  217. package/dist/unblock-prs/stack.d.mts.map +1 -0
  218. package/dist/unblock-prs/stack.mjs +141 -0
  219. package/dist/unblock-prs/stack.mjs.map +1 -0
  220. package/dist/unblock-prs/triage.d.mts +28 -0
  221. package/dist/unblock-prs/triage.d.mts.map +1 -0
  222. package/dist/unblock-prs/triage.mjs +396 -0
  223. package/dist/unblock-prs/triage.mjs.map +1 -0
  224. package/dist/unblock-prs/types.d.mts +793 -0
  225. package/dist/unblock-prs/types.d.mts.map +1 -0
  226. package/dist/unblock-prs/types.mjs +26 -0
  227. package/dist/unblock-prs/types.mjs.map +1 -0
  228. package/dist/unblock-prs/util.d.mts +23 -0
  229. package/dist/unblock-prs/util.d.mts.map +1 -0
  230. package/dist/unblock-prs/util.mjs +60 -0
  231. package/dist/unblock-prs/util.mjs.map +1 -0
  232. package/dist/unblock-prs/version-pr.d.mts +57 -0
  233. package/dist/unblock-prs/version-pr.d.mts.map +1 -0
  234. package/dist/unblock-prs/version-pr.mjs +79 -0
  235. package/dist/unblock-prs/version-pr.mjs.map +1 -0
  236. package/dist/unblock-prs/watch.d.mts +17 -0
  237. package/dist/unblock-prs/watch.d.mts.map +1 -0
  238. package/dist/unblock-prs/watch.mjs +128 -0
  239. package/dist/unblock-prs/watch.mjs.map +1 -0
  240. package/package.json +118 -3
  241. package/src/claim-pr/claim-pr.mts +380 -0
  242. package/src/claim-pr/claim-pr.test.mts +176 -0
  243. package/src/claim-pr/index.mts +1 -0
  244. package/src/cmd/claim-pr.mts +32 -0
  245. package/src/cmd/open-pr.mts +28 -0
  246. package/src/cmd/pr-report.mts +28 -0
  247. package/src/cmd/push-fixed-pr.mts +36 -0
  248. package/src/cmd/report-unowned-prs.mts +37 -0
  249. package/src/cmd/unblock-prs.mts +24 -0
  250. package/src/entry-point.mts +3 -0
  251. package/src/lib/cli-args.mts +86 -0
  252. package/src/lib/cli-args.test.mts +92 -0
  253. package/src/lib/github-api.mts +463 -0
  254. package/src/lib/github-api.test.mts +40 -0
  255. package/src/lib/index.mts +6 -0
  256. package/src/lib/json.mts +43 -0
  257. package/src/lib/json.test.mts +34 -0
  258. package/src/lib/log.mts +19 -0
  259. package/src/lib/log.test.mts +45 -0
  260. package/src/lib/shell.mts +52 -0
  261. package/src/lib/time.mts +49 -0
  262. package/src/open-pr/README.md +237 -0
  263. package/src/open-pr/github.mts +444 -0
  264. package/src/open-pr/index.mts +5 -0
  265. package/src/open-pr/main.mts +327 -0
  266. package/src/open-pr/options.mts +101 -0
  267. package/src/open-pr/options.test.mts +67 -0
  268. package/src/open-pr/steps.mts +57 -0
  269. package/src/open-pr/steps.test.mts +59 -0
  270. package/src/open-pr/types.mts +42 -0
  271. package/src/pr-report/README.md +244 -0
  272. package/src/pr-report/github.mts +911 -0
  273. package/src/pr-report/index.mts +7 -0
  274. package/src/pr-report/linked-issues.mts +51 -0
  275. package/src/pr-report/linked-issues.test.mts +65 -0
  276. package/src/pr-report/main.mts +257 -0
  277. package/src/pr-report/options.mts +140 -0
  278. package/src/pr-report/queue.mts +125 -0
  279. package/src/pr-report/render.mts +428 -0
  280. package/src/pr-report/render.test.mts +379 -0
  281. package/src/pr-report/repo-settings.mts +34 -0
  282. package/src/push-fixed-pr/index.mts +1 -0
  283. package/src/push-fixed-pr/push-fixed-pr.mts +276 -0
  284. package/src/push-fixed-pr/push-fixed-pr.test.mts +228 -0
  285. package/src/report-unowned-prs/index.mts +1 -0
  286. package/src/report-unowned-prs/report-unowned-prs.mts +190 -0
  287. package/src/report-unowned-prs/report-unowned-prs.test.mts +87 -0
  288. package/src/unblock-prs/README.md +1616 -0
  289. package/src/unblock-prs/auto-fix.mts +334 -0
  290. package/src/unblock-prs/auto-fix.test.mts +159 -0
  291. package/src/unblock-prs/checks.mts +538 -0
  292. package/src/unblock-prs/checks.test.mts +507 -0
  293. package/src/unblock-prs/claims.mts +33 -0
  294. package/src/unblock-prs/claims.test.mts +64 -0
  295. package/src/unblock-prs/config.mts +187 -0
  296. package/src/unblock-prs/config.schema.json +71 -0
  297. package/src/unblock-prs/config.test.mts +139 -0
  298. package/src/unblock-prs/constants.mts +33 -0
  299. package/src/unblock-prs/demotions.mts +73 -0
  300. package/src/unblock-prs/demotions.test.mts +100 -0
  301. package/src/unblock-prs/github.mts +1042 -0
  302. package/src/unblock-prs/github.test.mts +84 -0
  303. package/src/unblock-prs/index.mts +24 -0
  304. package/src/unblock-prs/labels.mts +70 -0
  305. package/src/unblock-prs/main.mts +1051 -0
  306. package/src/unblock-prs/merge-after.mts +117 -0
  307. package/src/unblock-prs/merge-after.test.mts +120 -0
  308. package/src/unblock-prs/options.mts +157 -0
  309. package/src/unblock-prs/owner.mts +91 -0
  310. package/src/unblock-prs/owner.test.mts +114 -0
  311. package/src/unblock-prs/quiet.mts +62 -0
  312. package/src/unblock-prs/quiet.test.mts +175 -0
  313. package/src/unblock-prs/rebase.mts +962 -0
  314. package/src/unblock-prs/release.mts +149 -0
  315. package/src/unblock-prs/release.test.mts +209 -0
  316. package/src/unblock-prs/set-aside-comment.mts +143 -0
  317. package/src/unblock-prs/set-aside-comment.test.mts +148 -0
  318. package/src/unblock-prs/set-aside-detail.mts +82 -0
  319. package/src/unblock-prs/set-aside-detail.test.mts +131 -0
  320. package/src/unblock-prs/skips.mts +215 -0
  321. package/src/unblock-prs/skips.test.mts +280 -0
  322. package/src/unblock-prs/stack.mts +220 -0
  323. package/src/unblock-prs/stack.test.mts +449 -0
  324. package/src/unblock-prs/triage.mts +669 -0
  325. package/src/unblock-prs/triage.test.mts +100 -0
  326. package/src/unblock-prs/types.mts +358 -0
  327. package/src/unblock-prs/util.mts +78 -0
  328. package/src/unblock-prs/util.test.mts +26 -0
  329. package/src/unblock-prs/version-pr.mts +112 -0
  330. package/src/unblock-prs/version-pr.test.mts +78 -0
  331. package/src/unblock-prs/watch.mts +219 -0
@@ -0,0 +1,1051 @@
1
+ // cspell:ignore retargeted
2
+
3
+ import {
4
+ MERGE_QUEUED_LABEL,
5
+ SKIP_CI_LABEL,
6
+ stackDescendants,
7
+ type Claim,
8
+ } from 'pr-report-core';
9
+ import { Arr, Result } from 'ts-data-forge';
10
+ import { currentEpochMs, log, type ApiContext } from '../lib/index.mjs';
11
+ import { autoFix } from './auto-fix.mjs';
12
+ import { standingClaims } from './claims.mjs';
13
+ import { CONFIG_PATH, readConfig, type AutoFixConfig } from './config.mjs';
14
+ import { STALE_STATE_PAUSE_MS } from './constants.mjs';
15
+ import { afterWatch, followHead, pruneDemotions } from './demotions.mjs';
16
+ import {
17
+ checkPreflight,
18
+ readIssueOrder,
19
+ readOwnComments,
20
+ readSetAsideComments,
21
+ viewPullRequest,
22
+ writeSetAsideCommentOn,
23
+ } from './github.mjs';
24
+ import { isSkipCiLabelled, wantsAutoRebase } from './labels.mjs';
25
+ import type { Options } from './options.mjs';
26
+ import {
27
+ idleWaitSec,
28
+ initialQuiet,
29
+ observeSurvey,
30
+ surveyFingerprint,
31
+ } from './quiet.mjs';
32
+ import {
33
+ advance,
34
+ armOnPick,
35
+ restackBehind,
36
+ restackRetargeted,
37
+ } from './rebase.mjs';
38
+ import { firstInReleaseOrder, settleAfterRelease } from './release.mjs';
39
+ import {
40
+ describeResolvedBy,
41
+ resolvedCommentBody,
42
+ setAsideCommentBody,
43
+ } from './set-aside-comment.mjs';
44
+ import { describeFailedChecks } from './set-aside-detail.mjs';
45
+ import {
46
+ newSkips,
47
+ pruneSkips,
48
+ settleSkips,
49
+ unannouncedSkips,
50
+ withSkip,
51
+ } from './skips.mjs';
52
+ import { stackedOnAfter, stackParentsOf } from './stack.mjs';
53
+ import {
54
+ bypassPhrase,
55
+ describeAction,
56
+ reportTriage,
57
+ survey,
58
+ triage,
59
+ } from './triage.mjs';
60
+ import type {
61
+ CycleResult,
62
+ Demotions,
63
+ IssueOrder,
64
+ LoopState,
65
+ PullRequest,
66
+ SkipRecord,
67
+ SkipRecords,
68
+ Watched,
69
+ WatchOutcome,
70
+ } from './types.mjs';
71
+ import { installStopHandlers, pause, stopRequested } from './util.mjs';
72
+ import { watch } from './watch.mjs';
73
+
74
+ /**
75
+ * Keeps the pull requests labelled `merge-queued` moving, one at a time, by
76
+ * rebasing the one that is out-of-date with the default branch, arming its
77
+ * auto-merge, and waiting for GitHub to merge it.
78
+ *
79
+ * The `/unblock-prs` skill fixes what this script sets aside — a failing check
80
+ * that needs reading, a conflict — after claiming it, so that this script
81
+ * leaves it alone meanwhile (`claims.mts`). This script fixes only the one
82
+ * kind that needs no reading — a fixer's diff, see step 4 — and it never
83
+ * merges anything: auto-merge does that once the checks are green.
84
+ *
85
+ * One cycle:
86
+ *
87
+ * 1. List the open pull requests labelled `merge-queued` whose base is the
88
+ * default branch. Everything else is not this script's business: an
89
+ * unlabelled pull request is passed over in silence, and a labelled one
90
+ * that cannot be acted on — a draft, a base that is neither the default
91
+ * branch nor another open pull request's — is reported, because the label
92
+ * asked for something and the answer is no. A stacked one waits for the
93
+ * layer below it (`stack.mts`). Auto-merge is this script's to arm, when
94
+ * it picks a pull request (`armOnPick` in `rebase.mts`), whether or not
95
+ * it was armed before: the label is the only permission read. Drafts and
96
+ * anything a previous cycle gave up on are set aside, and so is one whose
97
+ * review holds its merge — a code owner has not approved it, or a
98
+ * conversation is unresolved — because no check reports that, and
99
+ * releasing it would only run a matrix to sit green. One whose waiting
100
+ * paths only its author owns is the exception, since no approval will
101
+ * ever come: it is released and watched with auto-merge left off, and
102
+ * once its checks are green it is set aside `awaiting-bypass` for a
103
+ * person to merge by a ruleset bypass (`pr-report-core`'s
104
+ * `review-hold.mts`). The one thing done without the label comes first:
105
+ * a layer GitHub moved onto the default branch when the one below it
106
+ * merged, still carrying that one's commits, is rebased off them and the
107
+ * list read again (`stack.mts`).
108
+ * 2. If one of them is already up to date and its checks are running, or it
109
+ * is clean and about to merge, watch that one instead of rebasing another:
110
+ * the merge will move `main` and put every other branch back to `BEHIND`,
111
+ * so a second rebase now would only run a CI matrix to throw it away.
112
+ * 3. Otherwise take the lowest-numbered pull request that is `BEHIND` —
113
+ * or, once those are exhausted, one GitHub calls `DIRTY`, then the version
114
+ * pull request, then one a watch saw sit green — rebase it onto
115
+ * `origin/<default branch>` in a throwaway worktree, and push with
116
+ * `--force-with-lease` against the head the survey saw, carrying the
117
+ * layers stacked on it along. A rebase that conflicts, or a push that is
118
+ * refused, drops that pull request for as long as its head and the base
119
+ * stay where they are, and the next
120
+ * candidate is tried in the same cycle. A branch that moved under the
121
+ * rebase is not one of those: someone else — the skill, or a person — is
122
+ * moving it, so nothing is recorded and the cycle surveys again rather
123
+ * than start a second matrix on the next candidate.
124
+ * When there is no candidate and nothing in flight, a pull request paused
125
+ * by `skip-ci` and labelled `auto-rebase` or `merge-queued` that is behind
126
+ * is rebased onto the tip instead, with its layers — under the label the
127
+ * push runs no checks, and it keeps the diff under review its own. Anything
128
+ * that moved sends the cycle back to the survey.
129
+ * 4. Poll the rebased pull request until it merges, or until something says
130
+ * it will not: a required check failed, auto-merge was switched off, the
131
+ * `skip-ci` label went on, the branch was pushed by someone else, or every
132
+ * required context reported
133
+ * green and it still did not merge. "Green" means the whole list the
134
+ * ruleset requires, not the part of it that has reported — a required
135
+ * context with no check run on the head commit is absent from the
136
+ * head's checks rather than pending in them, so reading its absence as
137
+ * success is what used to write a pull request off three minutes into a
138
+ * twenty-five minute matrix.
139
+ * When a required check failed and every job that failed under it is a
140
+ * `fix:` or `gen:` entry of the check matrices, run those commands, amend
141
+ * what they wrote onto the branch's one commit, push, and watch the new
142
+ * head instead — once, and only for the pull request this cycle is
143
+ * watching, never the others that are failing.
144
+ * 5. Remember the verdict against the head *and* the base it was reached on,
145
+ * so the pull request is left alone until someone pushes to it or the
146
+ * base moves, and say so in its one set-aside comment: a person ticks the
147
+ * retry box there to have it tried anyway, the comment is rewritten as
148
+ * resolved once the verdict ends, and a later run takes up a verdict it
149
+ * finds still standing (`settleSkips`). A pull request that sat green
150
+ * without merging is also picked last from then on, until someone else
151
+ * pushes to it: the base moving makes it `BEHIND` and a candidate again,
152
+ * but whatever held it is something triage could not read, and it would
153
+ * otherwise go first each time anything else merged.
154
+ * 6. Survey again, saying what became of the pull requests this run has
155
+ * touched. One that merges after the watch gave up simply stops appearing
156
+ * in the list, and this is the only place that gets recorded. When there
157
+ * is nothing to do the script sleeps before looking again —
158
+ * `--active-interval` seconds while the list keeps changing, so a pull
159
+ * request queued a moment ago is not left for minutes, and
160
+ * `--idle-interval` once `--idle-after` surveys in a row have seen the
161
+ * same list — and keeps going until it is interrupted or `--once` was
162
+ * given.
163
+ *
164
+ * The rebase happens in a `git worktree` under the OS temp directory, so the
165
+ * checkout this runs from is never touched — its working tree may be dirty,
166
+ * and the pull request's branch may even be checked out somewhere else.
167
+ *
168
+ * ## The order, and `skip-ci`
169
+ *
170
+ * Five things the pull requests themselves declare shape that loop.
171
+ *
172
+ * - **A base that is another open pull request's branch** stacks the pull
173
+ * request on that one: it is not picked while that one is open, exactly as
174
+ * if it had declared `Merge-After:` on it. `stack.mts` says what happens
175
+ * when the layer below moves, and when it merges.
176
+ * - **`Merge-After: #1234` in the body** adds an ordering constraint: the
177
+ * pull request is not *picked* while any pull request it names is still
178
+ * open. It constrains picking and nothing else — one that is already up to
179
+ * date and merging is watched as ever, because auto-merge is going to merge
180
+ * it whatever this script thinks, and passing it over would only send the
181
+ * cycle off to rebase a branch that merge is about to invalidate. Several
182
+ * numbers may be named, on one line or several, so the declarations form a
183
+ * graph rather than a chain; a cycle in it is reported by name and
184
+ * everything caught in it is left alone, because nothing here can move it.
185
+ * - **The `skip-ci` label** pauses a queued pull request rather than removing
186
+ * it: every check workflow skips while it is on and `no-skip-ci-label`
187
+ * holds the merge, so taking it off is the action, and it is taken off one
188
+ * pull request at a time. The rebase comes *first* and the label removal
189
+ * *second*: while the label is on, the push fires a `synchronize` whose
190
+ * every check is skipped, and taking the label off then fires `unlabeled`,
191
+ * which runs the matrix once, on the head that will actually be merged. The
192
+ * other order starts a full matrix on the pre-rebase head and has the push
193
+ * cancel it.
194
+ * - **The `blocks-release` label** says the next release must contain this
195
+ * pull request: while it is open the version pull request — the one
196
+ * `changesets/action` opens from `changeset-release/<base>` — is not
197
+ * picked. It exists because that is the one pull request whose body cannot
198
+ * declare anything: its title and body are overwritten on every push to the
199
+ * base, so a `Merge-After:` written there is wiped exactly while several
200
+ * queued pull requests made it matter. The same branch is rebuilt from the
201
+ * tip and force-pushed by the same workflow, so nothing here rebases it
202
+ * either: taking `skip-ci` off, once the release workflow has rebuilt it on
203
+ * the current tip, is all it is ever given, and it is picked last so that a
204
+ * queued change goes into the release rather than after it.
205
+ * - **The `auto-rebase` label** asks for a pull request paused by `skip-ci`
206
+ * to be kept on the tip while it waits, as a queued one is (step 3). It is
207
+ * opt-in so that a branch left to sleep is never moved for want of a label.
208
+ *
209
+ * Releasing one is as far as it goes. From there it is an ordinary queued
210
+ * pull request, and if its checks fail it is set aside like any other — with
211
+ * its `skip-ci` off. Everything that declared `Merge-After` on it then waits,
212
+ * because it has not merged, which is what a declared order is for.
213
+ *
214
+ * **One queued pull request is released at a time, from this side only.**
215
+ * Nothing is picked while one is in flight, and after releasing it looks
216
+ * again: when another the worker owns was released in the same moment and
217
+ * goes first, it gives back the `skip-ci` it has just taken off its own. It
218
+ * puts the label on no other pull request (`release.mts`).
219
+ *
220
+ * ## Where the rest of it is
221
+ *
222
+ * This file is the loop and the command line. Reading order, roughly outside
223
+ * in:
224
+ *
225
+ * - `triage.mts` — what one survey says about each pull request, and why.
226
+ * - `pr-report-core`'s `review-hold.mts` — whether a pull request's own review
227
+ * holds its merge.
228
+ * - `merge-after.mts` — the declared order: the trailer parser and the cycle
229
+ * detection.
230
+ * - `stack.mts` — stacked pull requests: when they wait, when they are armed,
231
+ * and what moves with them.
232
+ * - `version-pr.mts` — the version pull request, and what holds it back.
233
+ * - `rebase.mts` — moving a branch: the rebase in a throwaway worktree, and
234
+ * taking `skip-ci` off.
235
+ * - `release.mts` — holding the queue to one released pull request.
236
+ * - `watch.mts` — polling one pull request until it merges, or until it will
237
+ * not.
238
+ * - `auto-fix.mts` — running the fixer a failed check names, and pushing
239
+ * what it wrote.
240
+ * - `quiet.mts` — how long to sleep when there is nothing to do.
241
+ * - `checks.mts` — what the merge is waiting for, judged against the contexts
242
+ * the ruleset requires.
243
+ * - `github.mts` — everything that reads or writes GitHub or `git`, and
244
+ * nothing that decides. The API client, the runner and the quoting are
245
+ * `tools/scripts/lib/`'s.
246
+ * - `labels.mts`, `skips.mts`, `demotions.mts`, `options.mts`, `types.mts`,
247
+ * `constants.mts`, `util.mts` — the vocabulary.
248
+ */
249
+ export const unblockPrs = async (
250
+ options: Options,
251
+ ): Promise<Result<undefined, string>> => {
252
+ const preflight = await checkPreflight(options.repo);
253
+
254
+ if (Result.isErr(preflight)) {
255
+ return preflight;
256
+ }
257
+
258
+ const { api, defaultBranch } = preflight.value;
259
+
260
+ const config = await readConfig();
261
+
262
+ if (Result.isErr(config)) {
263
+ return config;
264
+ }
265
+
266
+ const autoFixConfig = options.autoFix ? config.value?.autoFix : undefined;
267
+
268
+ if (autoFixConfig === undefined && options.autoFix) {
269
+ log(
270
+ `No autoFix in ${CONFIG_PATH}: a failed check is reported, never fixed here.`,
271
+ );
272
+ }
273
+
274
+ log(
275
+ `Watching pull requests into ${defaultBranch}${options.dryRun ? ' (dry run)' : ''}.`,
276
+ );
277
+
278
+ installStopHandlers();
279
+
280
+ let mut_state: LoopState = {
281
+ skipped: new Map(),
282
+ demoted: new Map(),
283
+ tracked: new Set(),
284
+ baseSha: undefined,
285
+ quiet: initialQuiet,
286
+ stackedOn: undefined,
287
+ };
288
+
289
+ while (!stopRequested()) {
290
+ const cycle = await runCycle(
291
+ api,
292
+ defaultBranch,
293
+ mut_state,
294
+ options,
295
+ autoFixConfig,
296
+ );
297
+
298
+ if (!options.dryRun) {
299
+ await announceSetAside(
300
+ api,
301
+ newSkips(cycle.settled, cycle.state.skipped),
302
+ defaultBranch,
303
+ );
304
+ }
305
+
306
+ mut_state = cycle.state;
307
+
308
+ if (cycle.next === 'stop' || options.once || options.dryRun) {
309
+ break;
310
+ }
311
+
312
+ if (cycle.next !== 'idle') {
313
+ continue;
314
+ }
315
+
316
+ const waitSec = idleWaitSec(mut_state.quiet, options);
317
+
318
+ log(`Nothing to do. Checking again in ${waitSec}s.`);
319
+
320
+ await pause(waitSec * 1000);
321
+ }
322
+
323
+ return Result.ok(undefined);
324
+ };
325
+
326
+ /**
327
+ * Says on each newly set-aside pull request why, where GitHub keeps it, so
328
+ * that finding out does not mean finding the terminal this ran in: in its one
329
+ * set-aside comment, written over the one it has or posted as its first. A
330
+ * comment that could not be written is reported and nothing more: the job is
331
+ * to land pull requests, and this is not a reason to stop.
332
+ */
333
+ const announceSetAside = async (
334
+ api: ApiContext,
335
+ skips: readonly SkipRecord[],
336
+ defaultBranch: string,
337
+ ): Promise<void> => {
338
+ for (const skip of skips) {
339
+ const existing = await readSetAsideComments(api, [skip.number]);
340
+
341
+ const written = Result.isErr(existing)
342
+ ? existing
343
+ : await writeSetAsideCommentOn(
344
+ api,
345
+ skip.number,
346
+ existing.value.get(skip.number)?.databaseId,
347
+ setAsideCommentBody(skip, defaultBranch),
348
+ );
349
+
350
+ if (Result.isErr(written)) {
351
+ log(
352
+ `#${skip.number}: could not leave a comment saying why: ${written.value}`,
353
+ );
354
+ }
355
+ }
356
+ };
357
+
358
+ /**
359
+ * Reads this account's set-aside comments on the pull requests that are queued or
360
+ * ask for `auto-rebase`, and on
361
+ * any other this run holds a record for, and settles the records with them
362
+ * (`settleSkips`): a comment whose state has ended, or whose retry box a
363
+ * person ticked, is rewritten as resolved — not under `--dry-run` — and one
364
+ * an earlier run left standing becomes a record again. Comments that cannot
365
+ * be read leave the records as they were.
366
+ *
367
+ * The claims sessions wrote on the same pull requests are read with them,
368
+ * and those that still hold are returned beside the records (`claims.mts`).
369
+ * Comments that cannot be read hold nothing: every push is leased anyway.
370
+ */
371
+ const settleWithComments = async (
372
+ api: ApiContext,
373
+ skipped: SkipRecords,
374
+ pullRequests: readonly PullRequest[],
375
+ baseSha: string,
376
+ defaultBranch: string,
377
+ dryRun: boolean,
378
+ ): Promise<
379
+ Readonly<{ skipped: SkipRecords; claimed: ReadonlyMap<number, Claim> }>
380
+ > => {
381
+ const read = await readOwnComments(
382
+ api,
383
+ pullRequests
384
+ .filter((pr) => wantsAutoRebase(pr) || skipped.has(pr.number))
385
+ .map((pr) => pr.number),
386
+ );
387
+
388
+ if (Result.isErr(read)) {
389
+ log(`Cannot read the set-aside comments: ${read.value}`);
390
+
391
+ return { skipped, claimed: new Map() };
392
+ }
393
+
394
+ const settled = settleSkips(
395
+ skipped,
396
+ read.value.setAside,
397
+ pullRequests,
398
+ baseSha,
399
+ );
400
+
401
+ for (const { number, databaseId, setAside, resolvedBy } of settled.resolved) {
402
+ log(
403
+ `#${number}: no longer set aside (${setAside.reason}) — ${describeResolvedBy(resolvedBy, defaultBranch)}${resolvedBy === 'retry' ? '; trying it again' : ''}.`,
404
+ );
405
+
406
+ if (dryRun) {
407
+ continue;
408
+ }
409
+
410
+ const written = await writeSetAsideCommentOn(
411
+ api,
412
+ number,
413
+ databaseId,
414
+ resolvedCommentBody(
415
+ setAside,
416
+ resolvedBy,
417
+ defaultBranch,
418
+ currentEpochMs(),
419
+ ),
420
+ );
421
+
422
+ if (Result.isErr(written)) {
423
+ log(`#${number}: could not mark its comment resolved: ${written.value}`);
424
+ }
425
+ }
426
+
427
+ // A record whose comment never got written is one every other reader of
428
+ // the queue takes for the worker's while this run holds it aside.
429
+ const unannounced = unannouncedSkips(settled.skipped, read.value.setAside);
430
+
431
+ if (!dryRun && Arr.isNonEmpty(unannounced)) {
432
+ log(
433
+ `Writing the set-aside comment again on ${unannounced.map(({ number }) => `#${number}`).join(', ')}, which no comment says.`,
434
+ );
435
+
436
+ await announceSetAside(api, unannounced, defaultBranch);
437
+ }
438
+
439
+ return {
440
+ skipped: settled.skipped,
441
+ claimed: standingClaims(read.value.claims, pullRequests, currentEpochMs()),
442
+ };
443
+ };
444
+
445
+ const runCycle = async (
446
+ api: ApiContext,
447
+ defaultBranch: string,
448
+ before: LoopState,
449
+ options: Options,
450
+ autoFixConfig: AutoFixConfig | undefined,
451
+ ): Promise<CycleResult> => {
452
+ const listed = await survey(api, defaultBranch);
453
+
454
+ if (Result.isErr(listed)) {
455
+ log(`Survey failed: ${listed.value}`);
456
+
457
+ return { state: before, next: 'idle', settled: before.skipped };
458
+ }
459
+
460
+ // What the next cycle compares its bases with. From this survey rather
461
+ // than the one below, which a layer moving in between could skip.
462
+ const stackedOn = stackedOnAfter(
463
+ before.stackedOn,
464
+ listed.value.pullRequests,
465
+ stackParentsOf(listed.value.pullRequests, defaultBranch),
466
+ );
467
+
468
+ // A layer the one below merged from under goes first, labelled or not, and
469
+ // the heads read before it are then read again. See `stack.mts`.
470
+ const surveyed = (await restackRetargeted(
471
+ api,
472
+ before.stackedOn,
473
+ listed.value.pullRequests,
474
+ defaultBranch,
475
+ options.dryRun,
476
+ ))
477
+ ? await survey(api, defaultBranch)
478
+ : listed;
479
+
480
+ if (Result.isErr(surveyed)) {
481
+ log(`Survey failed: ${surveyed.value}`);
482
+
483
+ return { state: before, next: 'idle', settled: before.skipped };
484
+ }
485
+
486
+ const { pullRequests, baseSha, requiredContexts, reviewRequirements } =
487
+ surveyed.value;
488
+
489
+ if (before.baseSha !== undefined && before.baseSha !== baseSha) {
490
+ log(`${defaultBranch} moved to ${baseSha.slice(0, 10)}.`);
491
+ }
492
+
493
+ // A pull request this run acted on and can no longer see is one that ended
494
+ // while nothing was watching it — including one the watch had given up on,
495
+ // whose merge would otherwise go unrecorded.
496
+ const tracked = await reportDeparted(api, before.tracked, pullRequests);
497
+
498
+ const { skipped, claimed } = await settleWithComments(
499
+ api,
500
+ pruneSkips(before.skipped, pullRequests, baseSha),
501
+ pullRequests,
502
+ baseSha,
503
+ defaultBranch,
504
+ options.dryRun,
505
+ );
506
+
507
+ const demoted = pruneDemotions(before.demoted, pullRequests);
508
+
509
+ const triaged = await triage(api, pullRequests, {
510
+ defaultBranch,
511
+ baseSha,
512
+ skipped,
513
+ claimed,
514
+ demoted,
515
+ requiredContexts,
516
+ reviewRequirements,
517
+ issueOrder: await issueOrderFor(api, pullRequests),
518
+ });
519
+
520
+ reportTriage(triaged, pullRequests.length, { defaultBranch, demoted });
521
+
522
+ const quiet = observeSurvey(
523
+ before.quiet,
524
+ surveyFingerprint({ pullRequests, baseSha }, triaged.held),
525
+ );
526
+
527
+ const state = (
528
+ nextSkipped: SkipRecords,
529
+ nextDemoted: Demotions = demoted,
530
+ nextTracked: ReadonlySet<number> = tracked,
531
+ ): LoopState =>
532
+ ({
533
+ skipped: nextSkipped,
534
+ demoted: nextDemoted,
535
+ tracked: nextTracked,
536
+ baseSha,
537
+ quiet,
538
+ stackedOn,
539
+ }) as const;
540
+
541
+ // A pull request that is up to date and already failing gets remembered
542
+ // now, so that it is not rebased the moment `main` moves and put through
543
+ // the same failing matrix again.
544
+ let mut_skipped: SkipRecords = triaged.failing.reduce(
545
+ (acc, { pr, summary }) =>
546
+ withSkip(acc, {
547
+ number: pr.number,
548
+ headSha: pr.headRefOid,
549
+ baseSha,
550
+ reason: 'checks-failed',
551
+ ...describeFailedChecks(summary),
552
+ }),
553
+ skipped,
554
+ );
555
+
556
+ if (stopRequested()) {
557
+ return { state: state(mut_skipped), next: 'stop', settled: skipped };
558
+ }
559
+
560
+ // Normally the only one. Two are what a pull request opened already
561
+ // released looks like; the first is watched, and the other runs as it was
562
+ // released, since nothing here labels a pull request it did not pick.
563
+ const watched = firstInReleaseOrder(triaged.inFlight, defaultBranch);
564
+
565
+ if (watched !== undefined) {
566
+ const arm = triaged.toArm.has(watched.number);
567
+
568
+ const bypassReason = triaged.bypass.get(watched.number);
569
+
570
+ log(
571
+ `#${watched.number} is up to date (${watched.mergeStateStatus}); ${arm ? 'arming auto-merge and ' : ''}watching it rather than rebasing another${bypassPhrase(bypassReason)}.`,
572
+ );
573
+
574
+ if (options.dryRun) {
575
+ return { state: state(mut_skipped), next: 'stop', settled: skipped };
576
+ }
577
+
578
+ // Before the watch, which reads a pull request without auto-merge as one
579
+ // someone switched it off on.
580
+ if (arm) {
581
+ const armed = await armOnPick(
582
+ api,
583
+ watched,
584
+ watched.headRefOid,
585
+ defaultBranch,
586
+ );
587
+
588
+ if (Result.isErr(armed)) {
589
+ log(`#${watched.number}: ${armed.value.detail}`);
590
+
591
+ return {
592
+ state: state(
593
+ withSkip(mut_skipped, {
594
+ number: watched.number,
595
+ headSha: watched.headRefOid,
596
+ baseSha,
597
+ reason: armed.value.reason,
598
+ detail: armed.value.detail,
599
+ }),
600
+ ),
601
+ next: 'survey',
602
+ settled: skipped,
603
+ };
604
+ }
605
+
606
+ if (armed.value === 'moved') {
607
+ log(
608
+ `#${watched.number}: its branch moved before it was armed; surveying again.`,
609
+ );
610
+
611
+ return { state: state(mut_skipped), next: 'survey', settled: skipped };
612
+ }
613
+ }
614
+
615
+ const { ended, head } = await watchAndFix(
616
+ api,
617
+ watched,
618
+ watched.headRefOid,
619
+ { defaultBranch, requiredContexts, bypassReason, autoFixConfig },
620
+ options,
621
+ );
622
+
623
+ return {
624
+ state: state(
625
+ applyWatchOutcome(
626
+ mut_skipped,
627
+ { ...watched, headRefOid: head },
628
+ baseSha,
629
+ ended,
630
+ ),
631
+ afterWatch(demoted, watched.number, head, ended.outcome),
632
+ trackAfterWatch(tracked, watched.number, ended.outcome),
633
+ ),
634
+ next: ended.outcome === 'stopped' ? 'stop' : 'survey',
635
+ settled: skipped,
636
+ };
637
+ }
638
+
639
+ // Only when the queue has nothing to do: `main` is about to move again
640
+ // otherwise, and a push now would be undone by the next merge.
641
+ if (Arr.isEmpty(triaged.candidates)) {
642
+ const behind = await restackBehind(pullRequests, defaultBranch, {
643
+ baseSha,
644
+ skipped: mut_skipped,
645
+ owners: triaged.owners,
646
+ dryRun: options.dryRun,
647
+ });
648
+
649
+ mut_skipped = behind.skipped;
650
+
651
+ if (behind.moved) {
652
+ return { state: state(mut_skipped), next: 'survey', settled: skipped };
653
+ }
654
+ }
655
+
656
+ if (options.dryRun) {
657
+ if (Arr.isNonEmpty(triaged.candidates)) {
658
+ const first = triaged.candidates[0];
659
+
660
+ log(
661
+ `Would ${describeAction(first, defaultBranch, triaged)} for #${first.number}.`,
662
+ );
663
+ }
664
+
665
+ return { state: state(mut_skipped), next: 'stop', settled: skipped };
666
+ }
667
+
668
+ // What a release found afterwards can have happened in the same moment
669
+ // only for these; see `release.mts`.
670
+ const pausedAtSurvey: ReadonlySet<number> = new Set(
671
+ pullRequests.filter(isSkipCiLabelled).map((pr) => pr.number),
672
+ );
673
+
674
+ for (const target of triaged.candidates) {
675
+ if (stopRequested()) {
676
+ return { state: state(mut_skipped), next: 'stop', settled: skipped };
677
+ }
678
+
679
+ log(
680
+ `#${target.number} (${target.headRefName}) is next: ${describeAction(target, defaultBranch, triaged)}.`,
681
+ );
682
+
683
+ const advanced = await advance(api, target, defaultBranch, {
684
+ arm: triaged.toArm.has(target.number),
685
+ descendants: stackDescendants(
686
+ triaged.stackParents,
687
+ target.number,
688
+ ).flatMap((number) => pullRequests.filter((pr) => pr.number === number)),
689
+ });
690
+
691
+ if (Result.isErr(advanced)) {
692
+ log(`#${target.number}: ${advanced.value.detail}`);
693
+
694
+ mut_skipped = withSkip(mut_skipped, {
695
+ number: target.number,
696
+ headSha: target.headRefOid,
697
+ baseSha,
698
+ reason: advanced.value.reason,
699
+ detail: advanced.value.detail,
700
+ });
701
+
702
+ continue;
703
+ }
704
+
705
+ if (advanced.value.kind === 'moved') {
706
+ // Someone else is moving this one — the skill, or a person. Whatever
707
+ // they pushed is likely to start a matrix, so rather than start a
708
+ // second on the next candidate, survey again and take what is there.
709
+ log(
710
+ `#${target.number} moved while this run was moving it; leaving it to whoever pushed, and surveying again.`,
711
+ );
712
+
713
+ return { state: state(mut_skipped), next: 'survey', settled: skipped };
714
+ }
715
+
716
+ if (advanced.value.kind === 'stale-merge-state') {
717
+ // GitHub said BEHIND or DIRTY but the rebase changed nothing, and there
718
+ // was no label to take off either — the merge state was stale. The next
719
+ // survey will say what is actually there; wait first, because a state
720
+ // GitHub is still recomputing would otherwise send this straight back
721
+ // here, and each turn costs a fetch and a worktree.
722
+ log(
723
+ `#${target.number} was already on top of ${defaultBranch}; its ${target.mergeStateStatus} was stale.`,
724
+ );
725
+
726
+ await pause(STALE_STATE_PAUSE_MS);
727
+
728
+ return { state: state(mut_skipped), next: 'survey', settled: skipped };
729
+ }
730
+
731
+ const advancedHead = advanced.value.head;
732
+
733
+ // The rebase is this run's, not the author's, so a demotion goes with it.
734
+ const demotedNow = followHead(demoted, target.number, advancedHead);
735
+
736
+ if (
737
+ (await settleAfterRelease(
738
+ api,
739
+ target,
740
+ triaged.owners,
741
+ pausedAtSurvey,
742
+ defaultBranch,
743
+ )) === 'yielded'
744
+ ) {
745
+ log(
746
+ `#${target.number} was released at the same moment as another, which goes first; paused it again.`,
747
+ );
748
+
749
+ return {
750
+ state: state(mut_skipped, demotedNow),
751
+ next: 'survey',
752
+ settled: skipped,
753
+ };
754
+ }
755
+
756
+ log(
757
+ `#${target.number} is at ${advancedHead.slice(0, 10)}; waiting for it to merge.`,
758
+ );
759
+
760
+ const { ended, head } = await watchAndFix(
761
+ api,
762
+ target,
763
+ advancedHead,
764
+ {
765
+ defaultBranch,
766
+ requiredContexts,
767
+ bypassReason: triaged.bypass.get(target.number),
768
+ autoFixConfig,
769
+ },
770
+ options,
771
+ );
772
+
773
+ return {
774
+ state: state(
775
+ applyWatchOutcome(
776
+ mut_skipped,
777
+ { ...target, headRefOid: head },
778
+ baseSha,
779
+ ended,
780
+ ),
781
+ afterWatch(demotedNow, target.number, head, ended.outcome),
782
+ trackAfterWatch(tracked, target.number, ended.outcome),
783
+ ),
784
+ next: ended.outcome === 'stopped' ? 'stop' : 'survey',
785
+ settled: skipped,
786
+ };
787
+ }
788
+
789
+ // Nothing to act on, or every candidate failed to rebase or push.
790
+ return { state: state(mut_skipped), next: 'idle', settled: skipped };
791
+ };
792
+
793
+ /**
794
+ * Watches one pull request, and when its checks fail on nothing but a fixer's
795
+ * diff, has `autoFix` push the fix and watches the new head in its place.
796
+ * Resolves to the outcome and the head it was reached on, which is the one a
797
+ * skip record has to name.
798
+ *
799
+ * Once per watch: a head `autoFix` wrote that fails again is a fixer that
800
+ * does not agree with CI, and running it again would only push the same diff.
801
+ * The pull request is set aside as any other failure is, and nothing else
802
+ * failing in the queue is touched — the one being watched is the one this
803
+ * cycle picked.
804
+ */
805
+ const watchAndFix = async (
806
+ api: ApiContext,
807
+ pr: PullRequest,
808
+ expectedHead: string,
809
+ context: Readonly<{
810
+ defaultBranch: string;
811
+ requiredContexts: readonly string[];
812
+ /** Set for one only a ruleset bypass merges; see `watch`. */
813
+ bypassReason: string | undefined;
814
+ /** `undefined` under `--no-auto-fix`, or when the repository has none. */
815
+ autoFixConfig: AutoFixConfig | undefined;
816
+ }>,
817
+ options: Options,
818
+ ): Promise<Readonly<{ ended: Watched; head: string }>> => {
819
+ const ended = await watch(
820
+ api,
821
+ pr,
822
+ expectedHead,
823
+ context.requiredContexts,
824
+ options,
825
+ context.bypassReason,
826
+ );
827
+
828
+ if (
829
+ ended.outcome !== 'checks-failed' ||
830
+ context.autoFixConfig === undefined ||
831
+ stopRequested()
832
+ ) {
833
+ return { ended, head: expectedHead };
834
+ }
835
+
836
+ const fixed = await autoFix(
837
+ api,
838
+ pr,
839
+ expectedHead,
840
+ context.defaultBranch,
841
+ context.autoFixConfig,
842
+ );
843
+
844
+ switch (fixed.kind) {
845
+ case 'declined':
846
+ log(`#${pr.number}: not fixing it here: ${fixed.detail}`);
847
+
848
+ return { ended, head: expectedHead };
849
+
850
+ case 'moved':
851
+ return { ended: { outcome: 'head-moved' }, head: expectedHead };
852
+
853
+ case 'pushed':
854
+ log(
855
+ `#${pr.number}: pushed what ${fixed.commands.join(', ')} wrote as ${fixed.head.slice(0, 10)}; watching it again.`,
856
+ );
857
+
858
+ return {
859
+ ended: await watch(
860
+ api,
861
+ pr,
862
+ fixed.head,
863
+ context.requiredContexts,
864
+ options,
865
+ context.bypassReason,
866
+ ),
867
+ head: fixed.head,
868
+ };
869
+ }
870
+ };
871
+
872
+ /**
873
+ * A pull request the run has acted on is worth remembering while it might
874
+ * still end without anything watching it; once it has ended there is nothing
875
+ * left to report.
876
+ */
877
+ const trackAfterWatch = (
878
+ tracked: ReadonlySet<number>,
879
+ prNumber: number,
880
+ outcome: WatchOutcome,
881
+ ): ReadonlySet<number> =>
882
+ outcome === 'merged' || outcome === 'closed'
883
+ ? new Set(Array.from(tracked).filter((number) => number !== prNumber))
884
+ : new Set([...tracked, prNumber]);
885
+
886
+ /**
887
+ * Says what became of the pull requests this run acted on that have left the
888
+ * open list, and returns the ones that are still on it.
889
+ *
890
+ * Nothing else notices: a pull request the watch gave up on merges minutes
891
+ * later, and the only trace is that it stops being listed. Without this the
892
+ * run that pushed the merge commit never mentions it.
893
+ */
894
+ const reportDeparted = async (
895
+ api: ApiContext,
896
+ tracked: ReadonlySet<number>,
897
+ pullRequests: readonly PullRequest[],
898
+ ): Promise<ReadonlySet<number>> => {
899
+ const isOpen = (prNumber: number): boolean =>
900
+ pullRequests.some((pr) => pr.number === prNumber);
901
+
902
+ const departed = Array.from(tracked).filter((prNumber) => !isOpen(prNumber));
903
+
904
+ const viewed = await Promise.all(
905
+ departed.map(async (prNumber) => ({
906
+ prNumber,
907
+ result: await viewPullRequest(api, prNumber),
908
+ })),
909
+ );
910
+
911
+ for (const { prNumber, result } of viewed) {
912
+ if (Result.isErr(result)) {
913
+ const detail =
914
+ `left the open list; cannot read it: ${result.value}` as const;
915
+
916
+ log(`#${prNumber} has ${detail}`);
917
+ } else if (result.value.state === 'MERGED') {
918
+ log(`#${prNumber} merged — ${result.value.title}`);
919
+ } else {
920
+ const detail = `no longer open (${result.value.state})` as const;
921
+
922
+ log(`#${prNumber} is ${detail} — ${result.value.title}`);
923
+ }
924
+ }
925
+
926
+ return new Set(Array.from(tracked).filter(isOpen));
927
+ };
928
+
929
+ /**
930
+ * What the issues the open pull requests close add to the merge order. A
931
+ * read that fails holds nothing extra: the declared order and the stacks
932
+ * still stand, and an order inherited from an issue is the one a person can
933
+ * also write into the body.
934
+ */
935
+ const issueOrderFor = async (
936
+ api: ApiContext,
937
+ pullRequests: readonly PullRequest[],
938
+ ): Promise<IssueOrder> => {
939
+ const read = await readIssueOrder(
940
+ api,
941
+ pullRequests.map(({ number }) => number),
942
+ );
943
+
944
+ if (Result.isErr(read)) {
945
+ log(
946
+ `Cannot read the issues the pull requests close; inheriting no Start-After this cycle. (${read.value})`,
947
+ );
948
+
949
+ return { closes: new Map(), startAfter: new Map() };
950
+ }
951
+
952
+ return read.value;
953
+ };
954
+
955
+ const applyWatchOutcome = (
956
+ skipped: SkipRecords,
957
+ pr: PullRequest,
958
+ baseSha: string,
959
+ ended: Watched,
960
+ ): SkipRecords => {
961
+ switch (ended.outcome) {
962
+ case 'merged':
963
+ log(`#${pr.number} merged.`);
964
+
965
+ return skipped;
966
+
967
+ case 'closed':
968
+ log(`#${pr.number} was closed without merging.`);
969
+
970
+ return skipped;
971
+
972
+ case 'auto-merge-disabled':
973
+ log(
974
+ `#${pr.number}: auto-merge was switched off; it is armed again when next picked, unless ${MERGE_QUEUED_LABEL} comes off.`,
975
+ );
976
+
977
+ return skipped;
978
+
979
+ case 'skip-ci-labelled':
980
+ log(
981
+ `#${pr.number}: ${SKIP_CI_LABEL} went on while waiting; leaving it alone.`,
982
+ );
983
+
984
+ return skipped;
985
+
986
+ case 'head-moved':
987
+ log(`#${pr.number}: someone else pushed to the branch; surveying again.`);
988
+
989
+ return skipped;
990
+
991
+ case 'behind-again':
992
+ log(`#${pr.number}: the base moved while waiting; surveying again.`);
993
+
994
+ return skipped;
995
+
996
+ case 'checks-failed':
997
+ return withSkip(skipped, {
998
+ number: pr.number,
999
+ headSha: pr.headRefOid,
1000
+ baseSha,
1001
+ reason: 'checks-failed',
1002
+ detail: ended.detail ?? 'a required check failed',
1003
+ failedChecks: ended.failedChecks,
1004
+ });
1005
+
1006
+ case 'awaiting-bypass':
1007
+ log(
1008
+ `#${pr.number}: green, and left for a ruleset bypass merge; nothing here takes it further until the branch is pushed again or a retry is asked for.`,
1009
+ );
1010
+
1011
+ return withSkip(skipped, {
1012
+ number: pr.number,
1013
+ headSha: pr.headRefOid,
1014
+ baseSha,
1015
+ reason: 'awaiting-bypass',
1016
+ detail: ended.detail ?? 'every required check is green',
1017
+ });
1018
+
1019
+ case 'not-merging':
1020
+ log(
1021
+ `#${pr.number}: every required check reported green and it is still open; something other than the checks holds it that its review does not show — auto-merge armed by someone who may not merge, or a rule this script does not read. It goes last from now on, until someone pushes to it.`,
1022
+ );
1023
+
1024
+ return withSkip(skipped, {
1025
+ number: pr.number,
1026
+ headSha: pr.headRefOid,
1027
+ baseSha,
1028
+ reason: 'not-merging',
1029
+ detail: ended.detail ?? 'green but not merged',
1030
+ });
1031
+
1032
+ case 'timeout':
1033
+ log(`#${pr.number}: gave up waiting.`);
1034
+
1035
+ return withSkip(skipped, {
1036
+ number: pr.number,
1037
+ headSha: pr.headRefOid,
1038
+ baseSha,
1039
+ reason: 'watch-timeout',
1040
+ detail: ended.detail ?? 'checks did not finish in time',
1041
+ });
1042
+
1043
+ case 'error':
1044
+ log(`#${pr.number}: polling kept failing; surveying again later.`);
1045
+
1046
+ return skipped;
1047
+
1048
+ case 'stopped':
1049
+ return skipped;
1050
+ }
1051
+ };