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,669 @@
1
+ /** What one survey says about each pull request, and why. */
2
+
3
+ import {
4
+ asksReview,
5
+ MERGE_QUEUED_LABEL,
6
+ orderCandidates as orderQueueCandidates,
7
+ parseCodeOwners,
8
+ PRIORITY_HIGH_LABEL,
9
+ PRIORITY_LOW_LABEL,
10
+ queueGraph,
11
+ queueStanding,
12
+ reviewHold,
13
+ SKIP_CI_LABEL,
14
+ stackDescendants,
15
+ withReviewHold,
16
+ type QueueFacts,
17
+ type QueueOwner,
18
+ type QueueReads,
19
+ type QueueStanding,
20
+ type ReviewHold,
21
+ type RulesetRequirements,
22
+ } from 'pr-report-core';
23
+ import { Arr, isRecord, Result } from 'ts-data-forge';
24
+ import type { StrictPick } from 'ts-type-forge';
25
+ import { currentEpochMs, log, type ApiContext } from '../lib/index.mjs';
26
+ import {
27
+ listRequiredChecks,
28
+ listRunningChecks,
29
+ summarizeChecks,
30
+ } from './checks.mjs';
31
+ import { UNKNOWN_STATE_RETRIES, UNKNOWN_STATE_RETRY_MS } from './constants.mjs';
32
+ import { isDemoted } from './demotions.mjs';
33
+ import {
34
+ listPullRequests,
35
+ readBranchRules,
36
+ readNativeStack,
37
+ readReviewStates,
38
+ remoteSha,
39
+ } from './github.mjs';
40
+ import { isSkipCiLabelled, priorityOf } from './labels.mjs';
41
+ import { waitingOnNote } from './merge-after.mjs';
42
+ import { ownerNote, ownerOf } from './owner.mjs';
43
+ import { nativeStackNote, stackedNote } from './stack.mjs';
44
+ import type {
45
+ ChecksSummary,
46
+ Classification,
47
+ Demotions,
48
+ PullRequest,
49
+ Survey,
50
+ Triage,
51
+ TriageBase,
52
+ TriageContext,
53
+ } from './types.mjs';
54
+ import { lastLines, pause, stopRequested } from './util.mjs';
55
+ import {
56
+ isRebuiltOnBaseTip,
57
+ isVersionPullRequest,
58
+ releaseBlockedNote,
59
+ releaseStaleNote,
60
+ } from './version-pr.mjs';
61
+
62
+ /**
63
+ * Lists the open pull requests, re-listing a few times while GitHub is still
64
+ * computing a merge state (`UNKNOWN`) for one that matters.
65
+ */
66
+ export const survey = async (
67
+ api: ApiContext,
68
+ defaultBranch: string,
69
+ ): Promise<Result<Survey, string>> => {
70
+ let mut_attempt = 0;
71
+
72
+ let mut_listed = await listPullRequests(api);
73
+
74
+ while (
75
+ Result.isOk(mut_listed) &&
76
+ mut_attempt < UNKNOWN_STATE_RETRIES &&
77
+ mut_listed.value.some(
78
+ (pr) =>
79
+ pr.mergeStateStatus === 'UNKNOWN' && isRecord(pr.autoMergeRequest),
80
+ ) &&
81
+ !stopRequested()
82
+ ) {
83
+ mut_attempt += 1;
84
+
85
+ log(
86
+ `GitHub is still computing a merge state; re-listing (${mut_attempt}/${UNKNOWN_STATE_RETRIES}).`,
87
+ );
88
+
89
+ await pause(UNKNOWN_STATE_RETRY_MS);
90
+
91
+ mut_listed = await listPullRequests(api);
92
+ }
93
+
94
+ if (Result.isErr(mut_listed)) {
95
+ return mut_listed;
96
+ }
97
+
98
+ const baseSha = await remoteSha(defaultBranch);
99
+
100
+ if (Result.isErr(baseSha)) {
101
+ return baseSha;
102
+ }
103
+
104
+ const rules = await readRules(api, defaultBranch);
105
+
106
+ return Result.ok({
107
+ pullRequests: mut_listed.value,
108
+ baseSha: baseSha.value,
109
+ requiredContexts: rules.requiredContexts,
110
+ reviewRequirements: {
111
+ requireCodeOwnerReview: rules.requireCodeOwnerReview,
112
+ requireConversationResolution: rules.requireConversationResolution,
113
+ },
114
+ });
115
+ };
116
+
117
+ /**
118
+ * What the rules on the default branch require, as GitHub applies them.
119
+ *
120
+ * The required contexts are what let "green" mean the whole list rather than
121
+ * the part of it that has reported. A required context with no check run on the
122
+ * head commit is not pending in the head's checks — it is absent from them — so a
123
+ * pull request three minutes into a twenty-five minute matrix looks finished
124
+ * without them. The review rules are what `pr-report-core`'s `review-hold.mts`
125
+ * asks about.
126
+ *
127
+ * Rules that cannot be read require nothing: the watch falls back to judging
128
+ * by what has reported, on a longer leash, and no review holds anything.
129
+ */
130
+ const readRules = async (
131
+ api: ApiContext,
132
+ branch: string,
133
+ ): Promise<RulesetRequirements> => {
134
+ const rules = await readBranchRules(api, branch);
135
+
136
+ if (Result.isErr(rules)) {
137
+ log(
138
+ `Cannot read the rules for ${branch}; judging by the checks that have reported, and holding nothing for review. (${lastLines(rules.value, 2)})`,
139
+ );
140
+
141
+ return {
142
+ requiredContexts: [],
143
+ requireCodeOwnerReview: false,
144
+ requireConversationResolution: false,
145
+ };
146
+ }
147
+
148
+ return rules.value;
149
+ };
150
+
151
+ export const triage = async (
152
+ api: ApiContext,
153
+ pullRequests: readonly PullRequest[],
154
+ base: TriageBase,
155
+ ): Promise<Triage> => {
156
+ const sorted = pullRequests.toSorted((a, b) => a.number - b.number);
157
+
158
+ // The stacks, the declared order and what holds the release, as every
159
+ // reader of the queue reads them (`pr-report-core`'s `queue-order.mts`).
160
+ const graph = queueGraph(
161
+ sorted.map((pr) => ({
162
+ number: pr.number,
163
+ body: pr.body,
164
+ isDraft: pr.isDraft,
165
+ labels: pr.labels.map(({ name }) => name),
166
+ headRef: pr.headRefName,
167
+ baseRef: pr.baseRefName,
168
+ fromFork: pr.isCrossRepository,
169
+ closes: base.issueOrder.closes.get(pr.number) ?? [],
170
+ })),
171
+ base.defaultBranch,
172
+ base.issueOrder.startAfter,
173
+ );
174
+
175
+ // Who moves each one next, by the rule every reader of the queue shares
176
+ // (`owner.mts`). Only the worker's are triaged further.
177
+ const nowEpochMs = currentEpochMs();
178
+
179
+ const owners: ReadonlyMap<number, QueueOwner> = new Map(
180
+ Arr.generate(function* () {
181
+ for (const pr of sorted) {
182
+ const owner = ownerOf(pr, {
183
+ defaultBranch: base.defaultBranch,
184
+ skipped: base.skipped,
185
+ claimed: base.claimed,
186
+ stackParents: graph.stackParents,
187
+ cyclic: graph.cyclic,
188
+ nowEpochMs,
189
+ });
190
+
191
+ if (owner !== undefined) {
192
+ yield [pr.number, owner] as const;
193
+ }
194
+ }
195
+ }),
196
+ );
197
+
198
+ const context: TriageContext = { ...base, ...graph, owners } as const;
199
+
200
+ const beforeReview = await Promise.all(
201
+ sorted.map(async (pr) => ({ pr, ...(await settle(api, pr, context)) })),
202
+ );
203
+
204
+ const holds = await readReviewHolds(
205
+ api,
206
+ beforeReview.filter(({ pr, standing }) =>
207
+ asksReview(standing, pr.mergeStateStatus),
208
+ ),
209
+ context,
210
+ );
211
+
212
+ const classified = beforeReview.map(({ pr, standing, unread, summary }) => ({
213
+ pr,
214
+ result:
215
+ unread ??
216
+ classification(
217
+ pr,
218
+ withReviewHold(standing, pr.mergeStateStatus, holds.get(pr.number)),
219
+ summary,
220
+ context,
221
+ ),
222
+ }));
223
+
224
+ // Released and watched as ever, but never armed: auto-merge would wait for
225
+ // an approval nobody may give. See `review-hold.mts` in `pr-report-core`.
226
+ const bypass: ReadonlyMap<number, string> = new Map(
227
+ Arr.generate(function* () {
228
+ for (const [prNumber, hold] of Array.from(holds)) {
229
+ if (hold.kind === 'bypass') {
230
+ yield [prNumber, hold.reason] as const;
231
+ }
232
+ }
233
+ }),
234
+ );
235
+
236
+ return {
237
+ cycles: graph.cycles,
238
+ owners,
239
+ inScope: classified.filter(({ result }) => result.kind !== 'ignore').length,
240
+ candidates: orderCandidates(
241
+ classified
242
+ .filter(({ result }) => result.kind === 'candidate')
243
+ .map(({ pr }) => pr),
244
+ base,
245
+ ),
246
+ // Anything this cycle may pick — a candidate, or one in flight — that
247
+ // has no auto-merge yet, however it came to have none: `merge-queued`
248
+ // is the one thing that says whether a pull request may merge.
249
+ toArm: new Set(
250
+ Arr.generate(function* () {
251
+ for (const { pr, result } of classified) {
252
+ if (
253
+ (result.kind === 'candidate' || result.kind === 'in-flight') &&
254
+ !isRecord(pr.autoMergeRequest) &&
255
+ !bypass.has(pr.number)
256
+ ) {
257
+ yield pr.number;
258
+ }
259
+ }
260
+ }),
261
+ ),
262
+ bypass,
263
+ stackParents: graph.stackParents,
264
+ inFlight: classified
265
+ .filter(({ result }) => result.kind === 'in-flight')
266
+ .map(({ pr }) => pr),
267
+ held: Arr.generate(function* () {
268
+ for (const { pr, result } of classified) {
269
+ if (result.kind === 'held') {
270
+ yield `#${pr.number}: held by its review — ${result.reason}; nothing is run until that changes`;
271
+ }
272
+ }
273
+ }),
274
+ failing: Arr.generate(function* () {
275
+ for (const { pr, result } of classified) {
276
+ if (result.kind === 'failing') {
277
+ yield { pr, summary: result.summary };
278
+ }
279
+ }
280
+ }),
281
+ notes: Arr.generate(function* () {
282
+ for (const { result } of classified) {
283
+ if (result.kind === 'note') {
284
+ yield result.note;
285
+ }
286
+ }
287
+ }),
288
+ };
289
+ };
290
+
291
+ /** What `queueStanding` decides from, as GitHub returned it. */
292
+ const factsOf = (pr: PullRequest): QueueFacts =>
293
+ ({
294
+ number: pr.number,
295
+ state: pr.state,
296
+ labels: pr.labels.map(({ name }) => name),
297
+ isDraft: pr.isDraft,
298
+ headRef: pr.headRefName,
299
+ baseRef: pr.baseRefName,
300
+ mergeState: pr.mergeStateStatus,
301
+ autoMerge: isRecord(pr.autoMergeRequest),
302
+ }) as const;
303
+
304
+ /** Where a pull request stands, with what was read on the way to it. */
305
+ type Settled = Readonly<{
306
+ standing: QueueStanding;
307
+ /** The note to report instead, when something it needed could not be read. */
308
+ unread: Classification | undefined;
309
+ /** The checks, when they were read. */
310
+ summary: ChecksSummary | undefined;
311
+ }>;
312
+
313
+ /**
314
+ * Asks `queueStanding` where `pr` stands, reading what it asks for — and
315
+ * only that, since each read is a request — until it can say.
316
+ */
317
+ const settle = async (
318
+ api: ApiContext,
319
+ pr: PullRequest,
320
+ context: TriageContext,
321
+ reads: QueueReads = {},
322
+ summary?: ChecksSummary,
323
+ ): Promise<Settled> => {
324
+ const standing = queueStanding(factsOf(pr), context, reads);
325
+
326
+ if (standing.kind !== 'needs') {
327
+ return { standing, unread: undefined, summary };
328
+ }
329
+
330
+ switch (standing.read) {
331
+ case 'nativeStack':
332
+ return settle(
333
+ api,
334
+ pr,
335
+ context,
336
+ { ...reads, nativeStack: (await inNativeStack(api, pr)) ?? 'none' },
337
+ summary,
338
+ );
339
+
340
+ case 'rebuiltOnTip': {
341
+ const rebuilt = await isRebuiltOnBaseTip(pr, context);
342
+
343
+ return Result.isErr(rebuilt)
344
+ ? { standing, unread: rebuilt.value, summary }
345
+ : settle(
346
+ api,
347
+ pr,
348
+ context,
349
+ { ...reads, rebuiltOnTip: rebuilt.value },
350
+ summary,
351
+ );
352
+ }
353
+
354
+ case 'checksFailed': {
355
+ const checks = await readChecks(api, pr, context);
356
+
357
+ return Result.isErr(checks)
358
+ ? { standing, unread: checks.value, summary }
359
+ : settle(
360
+ api,
361
+ pr,
362
+ context,
363
+ { ...reads, checksFailed: checks.value.verdict === 'failing' },
364
+ checks.value,
365
+ );
366
+ }
367
+ }
368
+ };
369
+
370
+ /** What triage reports for a standing, in the words this script logs. */
371
+ const classification = (
372
+ pr: PullRequest,
373
+ standing: QueueStanding,
374
+ summary: ChecksSummary | undefined,
375
+ context: TriageContext,
376
+ ): Classification => {
377
+ switch (standing.kind) {
378
+ case 'ignore':
379
+ case 'candidate':
380
+ case 'in-flight':
381
+ return { kind: standing.kind };
382
+
383
+ case 'held':
384
+ return { kind: 'held', reason: standing.reason };
385
+
386
+ case 'unactionable':
387
+ return { kind: 'note', note: `#${pr.number}: ${standing.reason}` };
388
+
389
+ case 'owned':
390
+ return (
391
+ ownerNote(pr, standing.owner, context.skipped) ?? {
392
+ kind: 'note',
393
+ note: `#${pr.number}: ${standing.owner.reason}`,
394
+ }
395
+ );
396
+
397
+ case 'stacked':
398
+ return stackedNote(pr, standing.on, context);
399
+
400
+ case 'native-stack':
401
+ return nativeStackNote(pr, standing.entry);
402
+
403
+ case 'waiting':
404
+ return waitingOnNote(
405
+ pr,
406
+ standing.on,
407
+ standing.cyclic,
408
+ context.inherited.get(pr.number),
409
+ );
410
+
411
+ case 'release-blocked':
412
+ return releaseBlockedNote(pr, standing.by);
413
+
414
+ case 'release-stale':
415
+ return releaseStaleNote(pr, context);
416
+
417
+ case 'merge-state':
418
+ return {
419
+ kind: 'note',
420
+ note: `#${pr.number}: merge state ${standing.state}`,
421
+ };
422
+
423
+ case 'failing':
424
+ return summary === undefined
425
+ ? { kind: 'note', note: `#${pr.number}: failing` }
426
+ : { kind: 'failing', summary };
427
+
428
+ case 'needs':
429
+ return {
430
+ kind: 'note',
431
+ note: `#${pr.number}: could not read ${standing.read}`,
432
+ };
433
+ }
434
+ };
435
+
436
+ /**
437
+ * The pull request's place in one of GitHub's native stacks (`stack.mts`),
438
+ * if it is in one. One whose stack cannot be read is taken to be in none,
439
+ * and GitHub's refusal to arm it, should it come, sets it aside.
440
+ */
441
+ const inNativeStack = async (
442
+ api: ApiContext,
443
+ pr: PullRequest,
444
+ ): Promise<QueueReads['nativeStack']> => {
445
+ const entry = await readNativeStack(api, pr.number);
446
+
447
+ if (Result.isErr(entry)) {
448
+ log(
449
+ `#${pr.number}: cannot read whether it is in a native stack; treating it as in none. (${lastLines(entry.value, 2)})`,
450
+ );
451
+
452
+ return undefined;
453
+ }
454
+
455
+ return entry.value;
456
+ };
457
+
458
+ /**
459
+ * What each pull request's review does to it, by number, for those it does
460
+ * anything to — `review-hold.mts` in `pr-report-core` says what is asked and
461
+ * why. One request covers all of them. A request that fails holds nothing,
462
+ * which is how this script behaved before it asked.
463
+ */
464
+ const readReviewHolds = async (
465
+ api: ApiContext,
466
+ entries: readonly Readonly<{ pr: PullRequest }>[],
467
+ context: TriageContext,
468
+ ): Promise<ReadonlyMap<number, ReviewHold>> => {
469
+ const { requireCodeOwnerReview, requireConversationResolution } =
470
+ context.reviewRequirements;
471
+
472
+ if (
473
+ !Arr.isNonEmpty(entries) ||
474
+ (!requireCodeOwnerReview && !requireConversationResolution)
475
+ ) {
476
+ return new Map();
477
+ }
478
+
479
+ const read = await readReviewStates(
480
+ api,
481
+ entries.map(({ pr }) => pr.number),
482
+ context.defaultBranch,
483
+ );
484
+
485
+ if (Result.isErr(read)) {
486
+ log(
487
+ `Cannot read the reviews; holding nothing for them. (${lastLines(read.value, 2)})`,
488
+ );
489
+
490
+ return new Map();
491
+ }
492
+
493
+ const rules = {
494
+ requireCodeOwnerReview,
495
+ requireConversationResolution,
496
+ codeOwners:
497
+ read.value.codeOwners === undefined
498
+ ? ([] as const)
499
+ : parseCodeOwners(read.value.codeOwners),
500
+ } as const;
501
+
502
+ return new Map(
503
+ Arr.generate(function* () {
504
+ for (const [prNumber, state] of Array.from(read.value.states)) {
505
+ const hold = reviewHold(state, rules);
506
+
507
+ if (hold !== undefined) {
508
+ yield [prNumber, hold] as const;
509
+ }
510
+ }
511
+ }),
512
+ );
513
+ };
514
+
515
+ /**
516
+ * The required checks of a pull request nothing here can move — waiting on
517
+ * its checks, or held by one that failed — or the note to report when they
518
+ * cannot be read.
519
+ */
520
+ const readChecks = async (
521
+ api: ApiContext,
522
+ pr: PullRequest,
523
+ context: TriageContext,
524
+ ): Promise<Result<ChecksSummary, Classification>> => {
525
+ const checks = await listRequiredChecks(api, pr.number);
526
+
527
+ if (Result.isErr(checks)) {
528
+ return Result.err({
529
+ kind: 'note',
530
+ note: `#${pr.number}: could not read checks: ${checks.value}`,
531
+ });
532
+ }
533
+
534
+ // Pending and passed are both in flight here, but a red left by the round
535
+ // before is not a failure while this round runs, and the checks still
536
+ // running are what tell the two apart.
537
+ const running = await listRunningChecks(api, pr.number);
538
+
539
+ if (Result.isErr(running)) {
540
+ return Result.err({
541
+ kind: 'note',
542
+ note: `#${pr.number}: could not read checks: ${running.value}`,
543
+ });
544
+ }
545
+
546
+ return Result.ok(
547
+ summarizeChecks(checks.value, context.requiredContexts, running.value),
548
+ );
549
+ };
550
+
551
+ /**
552
+ * Whether GitHub says merging this pull request into the base conflicts. It
553
+ * is a candidate all the same — see `queueStanding` — but a less promising
554
+ * one, so it is reported and ordered apart from the rest.
555
+ */
556
+ const isConflicting = (pr: PullRequest): boolean =>
557
+ pr.mergeStateStatus === 'DIRTY';
558
+
559
+ /**
560
+ * The cycle's order: `pr-report-core`'s `orderCandidates`, which says why,
561
+ * with what this run has demoted (`demotions.mts`).
562
+ */
563
+ export const orderCandidates = (
564
+ candidates: readonly PullRequest[],
565
+ context: Readonly<{ defaultBranch: string; demoted: Demotions }>,
566
+ ): readonly PullRequest[] =>
567
+ orderQueueCandidates(candidates, factsOf, {
568
+ defaultBranch: context.defaultBranch,
569
+ demoted: (pr) => isDemoted(context.demoted, pr),
570
+ });
571
+
572
+ export const reportTriage = (
573
+ triaged: Triage,
574
+ total: number,
575
+ context: Readonly<{ defaultBranch: string; demoted: Demotions }>,
576
+ ): void => {
577
+ const { defaultBranch } = context;
578
+
579
+ const paused = triaged.candidates.filter(isSkipCiLabelled).length;
580
+
581
+ log(
582
+ `${total} open pull request(s), ${triaged.inScope} labelled ${MERGE_QUEUED_LABEL}: ${triaged.candidates.length} to act on (${paused} paused by ${SKIP_CI_LABEL}), ${triaged.inFlight.length} in flight, ${triaged.failing.length} failing, ${triaged.held.length} held by review.`,
583
+ );
584
+
585
+ for (const cycle of triaged.cycles) {
586
+ if (!Arr.isNonEmpty(cycle)) {
587
+ continue;
588
+ }
589
+
590
+ log(
591
+ ` Merge-After cycle: ${[...cycle, cycle[0]].map((number) => `#${number}`).join(' → ')}`,
592
+ );
593
+ }
594
+
595
+ for (const held of triaged.held) {
596
+ log(` ${held}`);
597
+ }
598
+
599
+ for (const [prNumber, reason] of triaged.bypass) {
600
+ log(
601
+ ` #${prNumber}: only a ruleset bypass merges it — ${reason}; its checks are run, and auto-merge is left off`,
602
+ );
603
+ }
604
+
605
+ for (const note of triaged.notes) {
606
+ log(` ${note}`);
607
+ }
608
+
609
+ for (const { pr, summary } of triaged.failing) {
610
+ log(
611
+ ` #${pr.number}: up to date but failing: ${summary.failed.join(', ')}`,
612
+ );
613
+ }
614
+
615
+ for (const [position, pr] of triaged.candidates.entries()) {
616
+ log(
617
+ ` #${pr.number}: ${position === 0 ? 'next' : `${position} ahead of it`} — ${describeAction(pr, defaultBranch, triaged)}${isDemoted(context.demoted, pr) ? ' — last, because it sat green without merging' : ''}${priorityPhrase(pr)} — ${pr.title}`,
618
+ );
619
+ }
620
+ };
621
+
622
+ /** What this cycle is about to do to a pull request, as a phrase. */
623
+ export const describeAction = (
624
+ pr: PullRequest,
625
+ defaultBranch: string,
626
+ triaged: StrictPick<Triage, 'bypass' | 'stackParents' | 'toArm'>,
627
+ ): string => {
628
+ if (isVersionPullRequest(pr, defaultBranch)) {
629
+ return `take ${SKIP_CI_LABEL} off it — the release workflow owns the branch`;
630
+ }
631
+
632
+ const above = stackDescendants(triaged.stackParents, pr.number);
633
+
634
+ return [
635
+ `rebase onto ${defaultBranch}`,
636
+ isConflicting(pr)
637
+ ? ', which GitHub says conflicts — a rebase will say'
638
+ : '',
639
+ Arr.isNonEmpty(above)
640
+ ? `, carry ${above.map((n) => `#${n}`).join(', ')} stacked on it along`
641
+ : '',
642
+ triaged.toArm.has(pr.number) ? ', arm auto-merge' : '',
643
+ bypassPhrase(triaged.bypass.get(pr.number)),
644
+ isSkipCiLabelled(pr) ? `, then take ${SKIP_CI_LABEL} off it` : '',
645
+ ].join('');
646
+ };
647
+
648
+ /** What is done instead of arming one only a bypass merges, as a phrase. */
649
+ export const bypassPhrase = (reason: string | undefined): string =>
650
+ reason === undefined
651
+ ? ''
652
+ : (`, leave auto-merge off (${reason}, so it needs a bypass merge)` as const);
653
+
654
+ /** What the pull request's priority label did to its place, as a phrase. */
655
+ const priorityPhrase = (pr: PullRequest): string => {
656
+ switch (priorityOf(pr)) {
657
+ case 'high':
658
+ return ` — ahead of the rest, by ${PRIORITY_HIGH_LABEL}`;
659
+
660
+ case 'low':
661
+ return ` — behind the rest, by ${PRIORITY_LOW_LABEL}`;
662
+
663
+ case 'both':
664
+ return ` — labelled both ${PRIORITY_HIGH_LABEL} and ${PRIORITY_LOW_LABEL}, so neither counts`;
665
+
666
+ case 'none':
667
+ return '';
668
+ }
669
+ };