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,149 @@
1
+ /**
2
+ * Keeping to one released pull request without touching any other.
3
+ *
4
+ * A pull request labelled `merge-queued` without `skip-ci` is released: a push
5
+ * to it, or the label coming off, runs the full matrix. Only one of them can
6
+ * merge before `main` moves and puts the rest back to `BEHIND`, so a second
7
+ * released pull request is a matrix run to be thrown away.
8
+ *
9
+ * **This script puts `skip-ci` on a pull request only to give back the label
10
+ * it has just taken off**, yielding to another released in the same moment,
11
+ * and on no other pull request. It used to pause every other released one before releasing its own, which
12
+ * labelled pull requests nobody had handed it — the dependency update the bot
13
+ * opens released, one a person had just released on purpose. Now it keeps the
14
+ * rule from its own side only: triage watches one already in flight rather
15
+ * than picking another, and once its own is released it looks again, and when
16
+ * another of its pull requests was released in the same moment and goes
17
+ * first, it pauses its own ({@link yieldsTo}). A pull request someone else
18
+ * releases runs, and what that costs is a matrix, not a wrong merge.
19
+ *
20
+ * "In the same moment" is the whole of it: only one the survey saw paused by
21
+ * `skip-ci` is yielded to. One already released when the survey picked
22
+ * `target` and not in flight — waiting on its declared order, held by its
23
+ * review, conflicting, released by hand — is released on the next survey
24
+ * too, which picks `target` again, so yielding to it took `skip-ci` off and
25
+ * put it back on `target` for as long as the script ran.
26
+ */
27
+
28
+ import { SKIP_CI_LABEL, type QueueOwner } from 'pr-report-core';
29
+ import { Arr, Result } from 'ts-data-forge';
30
+ import { log, type ApiContext } from '../lib/index.mjs';
31
+ import { addSkipCiLabel, listPullRequests } from './github.mjs';
32
+ import { isMergeQueued, isSkipCiLabelled, priorityRank } from './labels.mjs';
33
+ import type { PullRequest } from './types.mjs';
34
+ import { isVersionPullRequest } from './version-pr.mjs';
35
+
36
+ /**
37
+ * Looks again once `target` has been released, and when another pull request
38
+ * the worker owns was released in the same moment and goes first, pauses
39
+ * `target` — the one pull request this may label. `pausedAtSurvey` is the
40
+ * pull requests the survey that picked `target` saw labelled `skip-ci`.
41
+ * Resolves to whether `target` keeps its release.
42
+ */
43
+ export const settleAfterRelease = async (
44
+ api: ApiContext,
45
+ target: PullRequest,
46
+ owners: ReadonlyMap<number, QueueOwner>,
47
+ pausedAtSurvey: ReadonlySet<number>,
48
+ defaultBranch: string,
49
+ ): Promise<'kept' | 'yielded'> => {
50
+ const listed = await listPullRequests(api);
51
+
52
+ if (Result.isErr(listed)) {
53
+ log(`Cannot list pull requests to check the release: ${listed.value}`);
54
+
55
+ return 'kept';
56
+ }
57
+
58
+ const first = yieldsTo(
59
+ target,
60
+ listed.value,
61
+ owners,
62
+ pausedAtSurvey,
63
+ defaultBranch,
64
+ );
65
+
66
+ if (first === undefined) {
67
+ return 'kept';
68
+ }
69
+
70
+ const added = await addSkipCiLabel(api, target.number);
71
+
72
+ log(
73
+ Result.isErr(added)
74
+ ? `#${target.number}: cannot put ${SKIP_CI_LABEL} back on it to let #${first.number} go first: ${added.value}`
75
+ : `#${target.number}: put ${SKIP_CI_LABEL} back on it; #${first.number} was released in the same moment and goes first.`,
76
+ );
77
+
78
+ return Result.isErr(added) ? 'kept' : 'yielded';
79
+ };
80
+
81
+ /**
82
+ * The pull request `target` should step back for, if any: another one the
83
+ * worker owns, paused when the survey ran (`pausedAtSurvey`) and released
84
+ * since, that goes first in {@link firstInReleaseOrder}. One set aside or
85
+ * claimed is a skill's or a person's and runs nothing until its fix is
86
+ * pushed, so it holds nothing back; counting it would stall the queue behind
87
+ * a pull request nobody is about to run. One released before the survey is
88
+ * not counted either: see the top of this file.
89
+ */
90
+ export const yieldsTo = (
91
+ target: PullRequest,
92
+ pullRequests: readonly PullRequest[],
93
+ owners: ReadonlyMap<number, QueueOwner>,
94
+ pausedAtSurvey: ReadonlySet<number>,
95
+ defaultBranch: string,
96
+ ): PullRequest | undefined => {
97
+ const others = releasedExcept(pullRequests, target.number).filter(
98
+ (pr) =>
99
+ owners.get(pr.number)?.owner === 'worker' &&
100
+ pausedAtSurvey.has(pr.number),
101
+ );
102
+
103
+ const first = firstInReleaseOrder(
104
+ Arr.toUnshifted(target)(others),
105
+ defaultBranch,
106
+ );
107
+
108
+ return first === undefined || first.number === target.number
109
+ ? undefined
110
+ : first;
111
+ };
112
+
113
+ /**
114
+ * The open pull requests labelled `merge-queued` without `skip-ci`, other
115
+ * than `keep`. A draft, one without auto-merge and one on another base count
116
+ * as well: each runs a matrix when pushed to, and whether it could merge
117
+ * afterwards is a different question.
118
+ */
119
+ export const releasedExcept = (
120
+ pullRequests: readonly PullRequest[],
121
+ keep: number | undefined,
122
+ ): readonly PullRequest[] =>
123
+ pullRequests.filter(
124
+ (pr) =>
125
+ pr.number !== keep &&
126
+ pr.state === 'OPEN' &&
127
+ isMergeQueued(pr) &&
128
+ !isSkipCiLabelled(pr),
129
+ );
130
+
131
+ /**
132
+ * Which of several released pull requests keeps its release: `priority:high`
133
+ * first and `priority:low` last, then the lowest number with the version pull
134
+ * request last — the order candidates are picked in, so that the skill,
135
+ * reading the same rule, settles it the same way.
136
+ */
137
+ export const firstInReleaseOrder = (
138
+ released: readonly PullRequest[],
139
+ defaultBranch: string,
140
+ ): PullRequest | undefined => {
141
+ const rank = (pr: PullRequest): number =>
142
+ priorityRank(pr) * 2 + (isVersionPullRequest(pr, defaultBranch) ? 1 : 0);
143
+
144
+ const sorted = released.toSorted((a, b) =>
145
+ rank(a) === rank(b) ? a.number - b.number : rank(a) - rank(b),
146
+ );
147
+
148
+ return Arr.isNonEmpty(sorted) ? sorted[0] : undefined;
149
+ };
@@ -0,0 +1,209 @@
1
+ import type { QueueOwner } from 'pr-report-core';
2
+ import { Arr } from 'ts-data-forge';
3
+ import { firstInReleaseOrder, releasedExcept, yieldsTo } from './release.mjs';
4
+ import type { PullRequest } from './types.mjs';
5
+
6
+ describe(releasedExcept, () => {
7
+ test('is every other open queued pull request without skip-ci', () => {
8
+ const released = pullRequest({ number: 1, labels: [queued] });
9
+
10
+ const kept = pullRequest({ number: 2, labels: [queued] });
11
+
12
+ assert.deepStrictEqual(
13
+ releasedExcept(
14
+ [
15
+ released,
16
+ kept,
17
+ pullRequest({ number: 3, labels: [queued, skipCi] }),
18
+ pullRequest({ number: 4, labels: [] }),
19
+ pullRequest({ number: 5, labels: [queued], state: 'MERGED' }),
20
+ ],
21
+ kept.number,
22
+ ),
23
+ [released],
24
+ );
25
+ });
26
+
27
+ test('counts a draft, a pull request without auto-merge and one on another base', () => {
28
+ // Each of them runs a matrix when pushed to, which is what the rule is
29
+ // about; whether it could merge afterwards is a different question.
30
+ const others = [
31
+ pullRequest({ number: 1, labels: [queued], isDraft: true }),
32
+ pullRequest({ number: 2, labels: [queued], autoMergeRequest: null }),
33
+ pullRequest({ number: 3, labels: [queued], baseRefName: 'feature' }),
34
+ ] as const;
35
+
36
+ assert.deepStrictEqual(releasedExcept(others, 9), others);
37
+ });
38
+ });
39
+
40
+ describe(firstInReleaseOrder, () => {
41
+ test('is the lowest number', () => {
42
+ assert.strictEqual(
43
+ firstInReleaseOrder(
44
+ [pullRequest({ number: 12 }), pullRequest({ number: 7 })],
45
+ 'main',
46
+ )?.number,
47
+ 7,
48
+ );
49
+ });
50
+
51
+ test('puts the version pull request last', () => {
52
+ assert.strictEqual(
53
+ firstInReleaseOrder(
54
+ [
55
+ pullRequest({ number: 3, headRefName: 'changeset-release/main' }),
56
+ pullRequest({ number: 12 }),
57
+ ],
58
+ 'main',
59
+ )?.number,
60
+ 12,
61
+ );
62
+ });
63
+
64
+ test('puts priority:high first and priority:low last, the version pull request included', () => {
65
+ const released = [
66
+ pullRequest({ number: 1, labels: [queued, low] }),
67
+ pullRequest({ number: 2 }),
68
+ pullRequest({
69
+ number: 3,
70
+ headRefName: 'changeset-release/main',
71
+ labels: [queued, high],
72
+ }),
73
+ ] as const;
74
+
75
+ assert.strictEqual(firstInReleaseOrder(released, 'main')?.number, 3);
76
+
77
+ assert.strictEqual(
78
+ firstInReleaseOrder(Arr.take(released, 2), 'main')?.number,
79
+ 2,
80
+ );
81
+ });
82
+
83
+ test('is undefined for nothing', () => {
84
+ assert.isUndefined(firstInReleaseOrder([], 'main'));
85
+ });
86
+ });
87
+
88
+ describe(yieldsTo, () => {
89
+ const target = pullRequest({ number: 7, labels: [queued] });
90
+
91
+ const worker: QueueOwner = {
92
+ owner: 'worker',
93
+ reason: 'in the queue',
94
+ } as const;
95
+
96
+ const skill: QueueOwner = {
97
+ owner: 'skill',
98
+ reason: 'set aside `checks-failed` at its head',
99
+ } as const;
100
+
101
+ test('yields to a worker’s pull request released in the same moment that goes first', () => {
102
+ const other = pullRequest({ number: 3, labels: [queued] });
103
+
104
+ assert.strictEqual(
105
+ yieldsTo(
106
+ target,
107
+ [target, other],
108
+ new Map([[3, worker]]),
109
+ new Set([3, 7]),
110
+ 'main',
111
+ )?.number,
112
+ 3,
113
+ );
114
+ });
115
+
116
+ test('keeps its release against one that goes after it', () => {
117
+ const other = pullRequest({ number: 12, labels: [queued] });
118
+
119
+ assert.isUndefined(
120
+ yieldsTo(
121
+ target,
122
+ [target, other],
123
+ new Map([[12, worker]]),
124
+ new Set([7, 12]),
125
+ 'main',
126
+ ),
127
+ );
128
+ });
129
+
130
+ test('keeps its release against one that was already released when the survey picked it', () => {
131
+ // Released before the survey — a person took `skip-ci` off, or labelled
132
+ // `merge-queued` one that never had it — and not in flight, or the survey
133
+ // would have watched it: waiting on its declared order, held by its
134
+ // review, conflicting. It is still released on the next survey, so
135
+ // yielding to it would release and pause the same pull request forever.
136
+ const other = pullRequest({ number: 3, labels: [queued, high] });
137
+
138
+ assert.isUndefined(
139
+ yieldsTo(
140
+ target,
141
+ [target, other],
142
+ new Map([[3, worker]]),
143
+ new Set([7]),
144
+ 'main',
145
+ ),
146
+ );
147
+ });
148
+
149
+ test('does not count one set aside with its label off, which runs nothing', () => {
150
+ // A pull request whose checks failed stays released; it is the skill's
151
+ // until its fix is pushed, and holding the queue for it would stall it.
152
+ const other = pullRequest({ number: 3, labels: [queued] });
153
+
154
+ assert.isUndefined(
155
+ yieldsTo(
156
+ target,
157
+ [target, other],
158
+ new Map([[3, skill]]),
159
+ new Set([3, 7]),
160
+ 'main',
161
+ ),
162
+ );
163
+
164
+ assert.isUndefined(
165
+ yieldsTo(target, [target, other], new Map(), new Set([3, 7]), 'main'),
166
+ );
167
+ });
168
+
169
+ test('does not count one paused by skip-ci', () => {
170
+ const other = pullRequest({ number: 3, labels: [queued, skipCi] });
171
+
172
+ assert.isUndefined(
173
+ yieldsTo(
174
+ target,
175
+ [target, other],
176
+ new Map([[3, worker]]),
177
+ new Set([3, 7]),
178
+ 'main',
179
+ ),
180
+ );
181
+ });
182
+ });
183
+
184
+ const queued = { name: 'merge-queued' } as const;
185
+
186
+ const skipCi = { name: 'skip-ci' } as const;
187
+
188
+ const high = { name: 'priority:high' } as const;
189
+
190
+ const low = { name: 'priority:low' } as const;
191
+
192
+ const pullRequest = (
193
+ fields: Partial<PullRequest> & Readonly<{ number: number }>,
194
+ ): PullRequest =>
195
+ ({
196
+ id: `PR_${fields.number}`,
197
+ title: `pull request #${fields.number}`,
198
+ body: '',
199
+ state: 'OPEN',
200
+ headRefName: `feature/${fields.number}`,
201
+ headRefOid: 'a'.repeat(40),
202
+ baseRefName: 'main',
203
+ isCrossRepository: false,
204
+ isDraft: false,
205
+ mergeStateStatus: 'BLOCKED',
206
+ autoMergeRequest: {},
207
+ labels: [],
208
+ ...fields,
209
+ }) as const;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The prose of the comment a pull request gets when this script sets it
3
+ * aside, and of the one line it is rewritten to once that is over.
4
+ * `pr-report-core`'s `set-aside.mts` wraps it in the record the script and
5
+ * the page read back; nothing reads the prose.
6
+ */
7
+
8
+ import {
9
+ writeResolvedComment,
10
+ writeSetAsideComment,
11
+ type SetAside,
12
+ } from 'pr-report-core';
13
+ import { Arr } from 'ts-data-forge';
14
+ import { isoSeconds } from '../lib/index.mjs';
15
+ import type { SkipRecord } from './types.mjs';
16
+ import { lastLines } from './util.mjs';
17
+
18
+ /**
19
+ * What a reader needs without the terminal this ran in: the reason and its
20
+ * sentence, each failed check with its run, what the command printed —
21
+ * folded, and cut to its end — and what will take it off.
22
+ */
23
+ export const setAsideCommentBody = (
24
+ skip: SkipRecord,
25
+ defaultBranch: string,
26
+ ): string => {
27
+ const failedChecks = skip.failedChecks ?? [];
28
+
29
+ const output = skip.output?.trim() ?? '';
30
+
31
+ return writeSetAsideComment(
32
+ { reason: skip.reason, headSha: skip.headSha, baseSha: skip.baseSha },
33
+ [
34
+ `**\`unblock-prs\` set this pull request aside** (\`${skip.reason}\`): ${escapeMarkdown(skip.detail.replaceAll(/\s+/gu, ' ').trim())}`,
35
+ ...(Arr.isNonEmpty(failedChecks)
36
+ ? [
37
+ '',
38
+ 'Failed required checks:',
39
+ '',
40
+ ...failedChecks.map(({ name, link }) =>
41
+ link === ''
42
+ ? `- ${escapeMarkdown(name)}`
43
+ : `- [${escapeMarkdown(name)}](${link})`,
44
+ ),
45
+ ]
46
+ : []),
47
+ ...(output === ''
48
+ ? []
49
+ : [
50
+ '',
51
+ '<details><summary>What the command printed</summary>',
52
+ '',
53
+ fenced(tailOf(output)),
54
+ '',
55
+ '</details>',
56
+ ]),
57
+ ...(skip.reason === 'awaiting-bypass'
58
+ ? [
59
+ '',
60
+ "Nothing but a ruleset bypass merges it, which is a person's to do; auto-merge was left off.",
61
+ ]
62
+ : []),
63
+ '',
64
+ `Set aside at \`${short(skip.headSha)}\`, with \`${defaultBranch}\` at \`${short(skip.baseSha)}\`. ${stays(skip.reason, defaultBranch)}`,
65
+ ].join('\n'),
66
+ );
67
+ };
68
+
69
+ /** What takes the set-aside off, as the comment's last sentence. */
70
+ const stays = (reason: SkipRecord['reason'], defaultBranch: string): string =>
71
+ reason === 'checks-failed'
72
+ ? 'It stays aside until the branch is pushed again: rebasing it would only put the same failing checks through again.'
73
+ : reason === 'awaiting-bypass'
74
+ ? (`It stays aside until the branch is pushed again: rebasing it would only put the same checks through again. Tick the retry box to have it rebased and checked on the current \`${defaultBranch}\`.` as const)
75
+ : (`It stays aside until the branch is pushed again or \`${defaultBranch}\` moves.` as const);
76
+
77
+ /** The one line a comment is rewritten to: what it was, what ended it, when. */
78
+ export const resolvedCommentBody = (
79
+ setAside: SetAside,
80
+ resolvedBy: ResolvedBy,
81
+ defaultBranch: string,
82
+ atEpochMs: number,
83
+ ): string =>
84
+ writeResolvedComment(
85
+ `Resolved at ${isoSeconds(atEpochMs)}: no longer set aside (\`${setAside.reason}\` at \`${short(setAside.headSha)}\`) — ${describeResolvedBy(resolvedBy, defaultBranch)}.`,
86
+ );
87
+
88
+ export const describeResolvedBy = (
89
+ resolvedBy: ResolvedBy,
90
+ defaultBranch: string,
91
+ ): string => {
92
+ switch (resolvedBy) {
93
+ case 'base-moved':
94
+ return `\`${defaultBranch}\` moved`;
95
+
96
+ case 'pushed':
97
+ return 'the branch was pushed';
98
+
99
+ case 'retry':
100
+ return 'a retry was asked for';
101
+ }
102
+ };
103
+
104
+ /** What ended a set-aside, as the resolved line says it. */
105
+ export type ResolvedBy = 'base-moved' | 'pushed' | 'retry';
106
+
107
+ /**
108
+ * How much of what a command printed the comment keeps, from its end, where
109
+ * git says what stopped it. GitHub takes a comment of up to 65,536
110
+ * characters.
111
+ */
112
+ const OUTPUT_LINES = 60;
113
+
114
+ const OUTPUT_CHARS = 20_000;
115
+
116
+ const tailOf = (output: string): string => {
117
+ const kept = lastLines(output, OUTPUT_LINES);
118
+
119
+ const cut = kept.length > OUTPUT_CHARS ? kept.slice(-OUTPUT_CHARS) : kept;
120
+
121
+ return cut === output ? cut : `…\n${cut}`;
122
+ };
123
+
124
+ /** A fence longer than any run of backticks inside it, so none closes it. */
125
+ const fenced = (text: string): string => {
126
+ const longest = Math.max(
127
+ 0,
128
+ ...Array.from(text.matchAll(/`+/gu), ([run]) => run.length),
129
+ );
130
+
131
+ const fence = '`'.repeat(Math.max(3, longest + 1));
132
+
133
+ return [`${fence}text`, text, fence].join('\n');
134
+ };
135
+
136
+ /**
137
+ * A sentence built from what git and GitHub said, as literal text: file
138
+ * names carry underscores, and nothing in it should mention anyone.
139
+ */
140
+ const escapeMarkdown = (text: string): string =>
141
+ text.replaceAll(/[\\`*_[\]<>|~@]/gu, String.raw`\$&`);
142
+
143
+ const short = (sha: string): string => sha.slice(0, 10);
@@ -0,0 +1,148 @@
1
+ import { parseSetAsideComment } from 'pr-report-core';
2
+ import {
3
+ resolvedCommentBody,
4
+ setAsideCommentBody,
5
+ } from './set-aside-comment.mjs';
6
+ import type { SkipRecord } from './types.mjs';
7
+
8
+ describe(setAsideCommentBody, () => {
9
+ test('writes a record the script and the page read back', () => {
10
+ assert.deepStrictEqual(
11
+ parseSetAsideComment(setAsideCommentBody(skip(), 'main')),
12
+ {
13
+ kind: 'standing',
14
+ setAside: { reason: 'rebase-failed', headSha: HEAD, baseSha: BASE },
15
+ retry: false,
16
+ },
17
+ );
18
+ });
19
+
20
+ test('says why, and what takes it off', () => {
21
+ const body = setAsideCommentBody(skip(), 'main');
22
+
23
+ assert.include(
24
+ body,
25
+ '(`rebase-failed`): conflicts with origin/main in a\\_b.mts',
26
+ );
27
+
28
+ assert.include(
29
+ body,
30
+ 'It stays aside until the branch is pushed again or `main` moves.',
31
+ );
32
+
33
+ assert.notInclude(body, '<details>');
34
+ });
35
+
36
+ test('links each failed check, and says a moved base does not clear it', () => {
37
+ const body = setAsideCommentBody(
38
+ skip({
39
+ reason: 'checks-failed',
40
+ detail: 'failed: code-check-result / result, no-skip-ci-label',
41
+ failedChecks: [
42
+ {
43
+ name: 'code-check-result / result',
44
+ link: 'https://example.test/run/1',
45
+ },
46
+ { name: 'no-skip-ci-label', link: '' },
47
+ ],
48
+ }),
49
+ 'main',
50
+ );
51
+
52
+ assert.include(
53
+ body,
54
+ '- [code-check-result / result](https://example.test/run/1)\n- no-skip-ci-label',
55
+ );
56
+
57
+ assert.include(body, 'until the branch is pushed again: rebasing it');
58
+ });
59
+
60
+ test('asks for a bypass merge of a green one, and says a moved base does not clear it', () => {
61
+ const body = setAsideCommentBody(
62
+ skip({
63
+ reason: 'awaiting-bypass',
64
+ detail:
65
+ 'every required check is green; @owner is the only owner of .github/workflows/ci.yml and its author, who may not approve it',
66
+ }),
67
+ 'main',
68
+ );
69
+
70
+ assert.include(body, '(`awaiting-bypass`): every required check is green');
71
+
72
+ assert.include(
73
+ body,
74
+ "Nothing but a ruleset bypass merges it, which is a person's to do; auto-merge was left off.",
75
+ );
76
+
77
+ assert.include(
78
+ body,
79
+ 'It stays aside until the branch is pushed again: rebasing it would only put the same checks through again. Tick the retry box to have it rebased and checked on the current `main`.',
80
+ );
81
+ });
82
+
83
+ test('folds what the command printed away, in a fence nothing inside closes', () => {
84
+ const body = setAsideCommentBody(
85
+ skip({ output: 'CONFLICT (content): ```\nhint: @someone' }),
86
+ 'main',
87
+ );
88
+
89
+ assert.include(
90
+ body,
91
+ '<details><summary>What the command printed</summary>\n\n````text\nCONFLICT (content): ```\nhint: @someone\n````\n\n</details>',
92
+ );
93
+ });
94
+
95
+ test('keeps the end of a long output', () => {
96
+ const body = setAsideCommentBody(
97
+ skip({
98
+ output: Array.from({ length: 100 }, (_, i) => `line ${i}`).join('\n'),
99
+ }),
100
+ 'main',
101
+ );
102
+
103
+ assert.include(body, '```text\n…\nline 40\n');
104
+
105
+ assert.include(body, 'line 99\n```');
106
+
107
+ assert.notInclude(body, 'line 39\n');
108
+ });
109
+
110
+ test('keeps a mention in the detail from mentioning anyone', () => {
111
+ assert.include(
112
+ setAsideCommentBody(skip({ detail: 'refused by @someone' }), 'main'),
113
+ String.raw`refused by \@someone`,
114
+ );
115
+ });
116
+ });
117
+
118
+ describe(resolvedCommentBody, () => {
119
+ test('is one line saying what ended it and when, read back as resolved', () => {
120
+ const body = resolvedCommentBody(
121
+ { reason: 'rebase-failed', headSha: HEAD, baseSha: BASE },
122
+ 'base-moved',
123
+ 'main',
124
+ Date.parse('2026-09-29T01:02:03.456Z'),
125
+ );
126
+
127
+ assert.deepStrictEqual(parseSetAsideComment(body), { kind: 'resolved' });
128
+
129
+ assert.strictEqual(
130
+ body.split('\n').at(-1),
131
+ 'Resolved at 2026-09-29T01:02:03Z: no longer set aside (`rebase-failed` at `cccccccccc`) — `main` moved.',
132
+ );
133
+ });
134
+ });
135
+
136
+ const HEAD = 'c'.repeat(40);
137
+
138
+ const BASE = 'a'.repeat(40);
139
+
140
+ const skip = (overrides: Partial<SkipRecord> = {}): SkipRecord =>
141
+ ({
142
+ number: 7,
143
+ headSha: HEAD,
144
+ baseSha: BASE,
145
+ reason: 'rebase-failed',
146
+ detail: 'conflicts with origin/main in a_b.mts',
147
+ ...overrides,
148
+ }) as const;