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,82 @@
1
+ /**
2
+ * The one sentence a set-aside record carries: what the log says, and what
3
+ * the comment on the pull request leads with.
4
+ *
5
+ * Each says what a reader would otherwise have to go and find — which check,
6
+ * which file, which hold — rather than restate the reason. The comment has
7
+ * room for more, and `set-aside-comment.mts` adds it: the failed checks with
8
+ * their runs, and what the command printed.
9
+ */
10
+
11
+ import { Arr } from 'ts-data-forge';
12
+ import { describeWaitingOn } from './checks.mjs';
13
+ import type { ChecksSummary } from './types.mjs';
14
+
15
+ /**
16
+ * The failed required checks by name, and each with its run, which the
17
+ * comment links so that the failure itself is one click away.
18
+ */
19
+ export const describeFailedChecks = (
20
+ summary: ChecksSummary,
21
+ ): Readonly<{ detail: string; failedChecks: ChecksSummary['failedRuns'] }> =>
22
+ ({
23
+ detail: `failed: ${summary.failed.join(', ')}`,
24
+ failedChecks: summary.failedRuns,
25
+ }) as const;
26
+
27
+ /**
28
+ * What holds a pull request whose required checks are all green.
29
+ * `reviewDecision` is GitHub's: `REVIEW_REQUIRED` when a review the
30
+ * rules ask for is missing, which with `required_approving_review_count: 0`
31
+ * leaves a code owner's; empty when the rules ask for none.
32
+ */
33
+ export const describeHold = (
34
+ mergeStateStatus: string,
35
+ reviewDecision: string | undefined,
36
+ ): string => {
37
+ const head = `green but ${mergeStateStatus}` as const;
38
+
39
+ return reviewDecision === 'REVIEW_REQUIRED'
40
+ ? `${head}: a required review is missing (a code owner?)`
41
+ : reviewDecision === 'CHANGES_REQUESTED'
42
+ ? `${head}: changes requested`
43
+ : `${head}: an unresolved conversation, or armed by who may not merge?`;
44
+ };
45
+
46
+ /** What the watch was still waiting on when it gave up. */
47
+ export const describeTimeout = (
48
+ summary: ChecksSummary,
49
+ timeoutMin: number,
50
+ ): string =>
51
+ `after ${timeoutMin} min, still waiting on ${describeWaitingOn(summary)}` as const;
52
+
53
+ /**
54
+ * The files a rebase stopped on, or when git named none — a rebase can fail
55
+ * before it reaches a merge — the end of what it printed.
56
+ */
57
+ export const describeConflict = (
58
+ conflicted: readonly string[],
59
+ output: string,
60
+ onto: string,
61
+ ): string =>
62
+ Arr.isNonEmpty(conflicted)
63
+ ? (`conflicts with ${onto} in ${conflicted.join(', ')}` as const)
64
+ : (`rebase onto ${onto} failed: ${output.replaceAll(/\s+/gu, ' ').trim()}` as const);
65
+
66
+ /**
67
+ * The end of what a failed command printed. Node's message starts with
68
+ * `Command failed: <the command>`, which says nothing the reason does not and
69
+ * crowds out what does: an arm that failed was once reported as its own
70
+ * GraphQL mutation, cut off before GitHub's answer. Only a command that
71
+ * printed nothing is left with that line.
72
+ */
73
+ export const describeCommandFailure = (message: string): string => {
74
+ const lines = message.trim().split('\n');
75
+
76
+ const printed =
77
+ lines[0]?.startsWith(COMMAND_FAILED) === true ? Arr.tail(lines) : lines;
78
+
79
+ return Arr.takeLast(Arr.isNonEmpty(printed) ? printed : lines, 2).join('\n');
80
+ };
81
+
82
+ const COMMAND_FAILED = 'Command failed: ';
@@ -0,0 +1,131 @@
1
+ import {
2
+ describeCommandFailure,
3
+ describeConflict,
4
+ describeFailedChecks,
5
+ describeHold,
6
+ describeTimeout,
7
+ } from './set-aside-detail.mjs';
8
+ import type { ChecksSummary } from './types.mjs';
9
+
10
+ describe(describeFailedChecks, () => {
11
+ test('names the failed checks and keeps each run', () => {
12
+ const failed = [
13
+ {
14
+ name: 'code-check-result / result',
15
+ link: 'https://example.test/1',
16
+ },
17
+ {
18
+ name: 'style-check-result / result',
19
+ link: 'https://example.test/2',
20
+ },
21
+ ] as const;
22
+
23
+ assert.deepStrictEqual(
24
+ describeFailedChecks(
25
+ summary({ failed: failed.map(({ name }) => name), failedRuns: failed }),
26
+ ),
27
+ {
28
+ detail:
29
+ 'failed: code-check-result / result, style-check-result / result',
30
+ failedChecks: failed,
31
+ },
32
+ );
33
+ });
34
+ });
35
+
36
+ describe(describeHold, () => {
37
+ test('says a review is missing', () => {
38
+ assert.strictEqual(
39
+ describeHold('BLOCKED', 'REVIEW_REQUIRED'),
40
+ 'green but BLOCKED: a required review is missing (a code owner?)',
41
+ );
42
+ });
43
+
44
+ test('says changes were requested', () => {
45
+ assert.strictEqual(
46
+ describeHold('BLOCKED', 'CHANGES_REQUESTED'),
47
+ 'green but BLOCKED: changes requested',
48
+ );
49
+ });
50
+
51
+ // #2069 was set aside as "green but not merged", which named nothing.
52
+ test('names what is left when no review is missing', () => {
53
+ for (const reviewDecision of ['APPROVED', '', undefined]) {
54
+ assert.strictEqual(
55
+ describeHold('CLEAN', reviewDecision),
56
+ 'green but CLEAN: an unresolved conversation, or armed by who may not merge?',
57
+ );
58
+ }
59
+ });
60
+ });
61
+
62
+ describe(describeTimeout, () => {
63
+ test('names what it was still waiting on', () => {
64
+ assert.strictEqual(
65
+ describeTimeout(summary({ running: ['code-check (ws:fix:lint)'] }), 90),
66
+ 'after 90 min, still waiting on code-check (ws:fix:lint) (running)',
67
+ );
68
+ });
69
+ });
70
+
71
+ describe(describeConflict, () => {
72
+ test('names the conflicting files', () => {
73
+ assert.strictEqual(
74
+ describeConflict(['a.mts', 'b/c.mts'], 'CONFLICT (content): …', 'main'),
75
+ 'conflicts with main in a.mts, b/c.mts',
76
+ );
77
+ });
78
+
79
+ test("falls back to git's own words when no file is named", () => {
80
+ assert.strictEqual(
81
+ describeConflict([], 'error: could not apply 1234abc\nhint: …', 'main'),
82
+ 'rebase onto main failed: error: could not apply 1234abc hint: …',
83
+ );
84
+ });
85
+ });
86
+
87
+ describe(describeCommandFailure, () => {
88
+ test('keeps what the command printed rather than the command', () => {
89
+ // The command line alone once filled a 140-character status, which is
90
+ // how GitHub's answer came to be cut off.
91
+ assert.strictEqual(
92
+ describeCommandFailure(
93
+ "Command failed: gh api graphql -f 'id=PR_1' -f 'query=mutation($id: ID!) { enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: SQUASH }) { clientMutationId } }'\ngh: Auto-merge is not supported for stacked pull requests.",
94
+ ),
95
+ 'gh: Auto-merge is not supported for stacked pull requests.',
96
+ );
97
+ });
98
+
99
+ test('keeps the last two lines of a longer answer', () => {
100
+ assert.strictEqual(
101
+ describeCommandFailure('Command failed: gh pr edit 1\none\ntwo\nthree'),
102
+ 'two\nthree',
103
+ );
104
+ });
105
+
106
+ test('keeps the command when it printed nothing', () => {
107
+ assert.strictEqual(
108
+ describeCommandFailure('Command failed: gh pr edit 1'),
109
+ 'Command failed: gh pr edit 1',
110
+ );
111
+ });
112
+
113
+ test('keeps the end of a message that names no command', () => {
114
+ assert.strictEqual(describeCommandFailure('one\ntwo\nthree'), 'two\nthree');
115
+ });
116
+ });
117
+
118
+ const summary = (overrides: Partial<ChecksSummary> = {}): ChecksSummary =>
119
+ ({
120
+ verdict: 'pending',
121
+ passed: [],
122
+ failed: [],
123
+ failedRuns: [],
124
+ pending: [],
125
+ skipped: [],
126
+ missing: [],
127
+ notRun: [],
128
+ running: [],
129
+ required: 9,
130
+ ...overrides,
131
+ }) as const;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * What the loop remembers about a pull request it has given up on, and for
3
+ * how long.
4
+ */
5
+
6
+ import {
7
+ isSetAsideReason,
8
+ setAsideApplies,
9
+ setAsideStillApplies,
10
+ type SetAside,
11
+ } from 'pr-report-core';
12
+ import { Arr } from 'ts-data-forge';
13
+ import type { ResolvedBy } from './set-aside-comment.mjs';
14
+ import type {
15
+ OwnSetAsideComment,
16
+ PullRequest,
17
+ SkipRecord,
18
+ SkipRecords,
19
+ } from './types.mjs';
20
+
21
+ /**
22
+ * A verdict lasts as long as the state it was reached in. Every reason is
23
+ * tied to the head, so a push to the branch clears it; all but
24
+ * `checks-failed` are tied to the base as well.
25
+ *
26
+ * The base is what makes the difference to a pull request that sat green
27
+ * without merging or ran out of watch time: once the base moves that pull
28
+ * request is `BEHIND`, which is the one thing this script knows how to fix,
29
+ * and a verdict that outlived its state is what left a ready pull request
30
+ * sitting still through cycle after cycle of "Nothing to do". One that sat
31
+ * green goes back in the running last, not first — `demotions.mts`. A rebase or
32
+ * push failure is tied to the base for the older reason: the commit it
33
+ * conflicted with may have gone with it.
34
+ *
35
+ * `checks-failed` is deliberately not tied to the base. Rebasing it would put
36
+ * the same failing matrix through again, and fixing the failure is the
37
+ * skill's job, not this script's — the push that carries the fix is what
38
+ * clears it. Nor is `awaiting-bypass`: the bypass merge it waits for does not
39
+ * wait for the base, and a rebase each time anything else merged would run
40
+ * the matrix again for nothing. Its comment's retry box is how a person asks
41
+ * for a run on the current tip.
42
+ *
43
+ * The rule is `pr-report-core`'s `setAsideApplies`, because the Pull Requests
44
+ * Manager page applies the same rule to the comment this script leaves on
45
+ * the pull request, and so does {@link settleSkips} here.
46
+ */
47
+ export const skipStillApplies = (
48
+ skip: SkipRecord,
49
+ pr: PullRequest,
50
+ baseSha: string,
51
+ ): boolean => setAsideApplies(skip, pr.headRefOid, baseSha);
52
+
53
+ /** Drops records for pull requests that are gone or have moved on. */
54
+ export const pruneSkips = (
55
+ skipped: SkipRecords,
56
+ pullRequests: readonly PullRequest[],
57
+ baseSha: string,
58
+ ): SkipRecords =>
59
+ new Map(
60
+ Array.from(skipped.values())
61
+ .filter((skip) => {
62
+ const pr = pullRequests.find((p) => p.number === skip.number);
63
+
64
+ return pr !== undefined && skipStillApplies(skip, pr, baseSha);
65
+ })
66
+ .map((skip) => [skip.number, skip] as const),
67
+ );
68
+
69
+ export const withSkip = (skipped: SkipRecords, skip: SkipRecord): SkipRecords =>
70
+ new Map([...skipped, [skip.number, skip]]);
71
+
72
+ /**
73
+ * The records in `after` that `before` did not have — a pull request newly
74
+ * set aside, or set aside again for a different reason or in a different
75
+ * state. What the loop writes to GitHub, once each, rather than on every
76
+ * survey that finds the same record standing.
77
+ *
78
+ * `before` is what the cycle acted from, after {@link settleSkips}, so that a
79
+ * record read back from a comment is not written over that comment again.
80
+ */
81
+ export const newSkips = (
82
+ before: SkipRecords,
83
+ after: SkipRecords,
84
+ ): readonly SkipRecord[] =>
85
+ Array.from(after.values()).filter((skip) => {
86
+ const previous = before.get(skip.number);
87
+
88
+ return (
89
+ previous?.headSha !== skip.headSha ||
90
+ previous.baseSha !== skip.baseSha ||
91
+ previous.reason !== skip.reason
92
+ );
93
+ });
94
+
95
+ /**
96
+ * The records no comment says: never written, because the write failed, or
97
+ * written over since. Until it is written the worker holds the pull request
98
+ * aside while every other reader of the queue (`owner.mts`) reads it as the
99
+ * worker's, and each leaves it to the other; so the loop writes these again.
100
+ */
101
+ export const unannouncedSkips = (
102
+ skipped: SkipRecords,
103
+ comments: ReadonlyMap<number, OwnSetAsideComment>,
104
+ ): readonly SkipRecord[] =>
105
+ Array.from(skipped.values()).filter((skip) => {
106
+ const says = comments.get(skip.number)?.says;
107
+
108
+ return (
109
+ says?.kind !== 'standing' ||
110
+ says.setAside.headSha !== skip.headSha ||
111
+ says.setAside.reason !== skip.reason
112
+ );
113
+ });
114
+
115
+ /**
116
+ * What this account's set-aside comments on the open pull requests say, laid
117
+ * over the records: the comments to rewrite as resolved, and the records to
118
+ * act from.
119
+ *
120
+ * - **Ended**: a comment set aside at a head that has moved, or against a
121
+ * base that has — by the same rule as the records, {@link skipStillApplies}
122
+ * — is rewritten as resolved.
123
+ * - **Retry**: a comment whose box a person ticked is rewritten as resolved,
124
+ * and the record goes with it, so this cycle's triage treats the pull
125
+ * request as if it had never been set aside. One that fails again is set
126
+ * aside again, and its comment says so again.
127
+ * - **Standing, and this run does not know it**: a record an earlier run
128
+ * reached, which is taken up as it stands rather than tried again — a
129
+ * rebase that conflicted would conflict the same way.
130
+ *
131
+ * A record this run holds is left as it is otherwise: the comment says what
132
+ * the record said when it was written.
133
+ */
134
+ export const settleSkips = (
135
+ skipped: SkipRecords,
136
+ comments: ReadonlyMap<number, OwnSetAsideComment>,
137
+ pullRequests: readonly PullRequest[],
138
+ baseSha: string,
139
+ ): Readonly<{ skipped: SkipRecords; resolved: readonly Resolution[] }> => {
140
+ const judged = Array.from(comments).flatMap(([number, comment]) => {
141
+ const pr = pullRequests.find((p) => p.number === number);
142
+
143
+ if (pr === undefined || comment.says.kind === 'resolved') {
144
+ return [];
145
+ }
146
+
147
+ const { setAside, retry } = comment.says;
148
+
149
+ const resolvedBy: ResolvedBy | undefined =
150
+ setAside.headSha !== pr.headRefOid
151
+ ? 'pushed'
152
+ : !setAsideStillApplies(setAside, baseSha)
153
+ ? 'base-moved'
154
+ : retry
155
+ ? 'retry'
156
+ : undefined;
157
+
158
+ return [
159
+ { number, databaseId: comment.databaseId, setAside, resolvedBy } as const,
160
+ ];
161
+ });
162
+
163
+ const retried = new Set(
164
+ Arr.generate(function* () {
165
+ for (const { number, resolvedBy } of judged) {
166
+ if (resolvedBy === 'retry') {
167
+ yield number;
168
+ }
169
+ }
170
+ }),
171
+ );
172
+
173
+ const taken = Arr.generate(function* () {
174
+ for (const { number, setAside, resolvedBy } of judged) {
175
+ if (
176
+ resolvedBy === undefined &&
177
+ !skipped.has(number) &&
178
+ isSetAsideReason(setAside.reason)
179
+ ) {
180
+ yield {
181
+ number,
182
+ headSha: setAside.headSha,
183
+ baseSha: setAside.baseSha,
184
+ reason: setAside.reason,
185
+ detail: TAKEN_UP_DETAIL,
186
+ } as const;
187
+ }
188
+ }
189
+ });
190
+
191
+ return {
192
+ skipped: taken.reduce(
193
+ withSkip,
194
+ new Map(Array.from(skipped).filter(([number]) => !retried.has(number))),
195
+ ),
196
+ resolved: Arr.generate(function* () {
197
+ for (const { number, databaseId, setAside, resolvedBy } of judged) {
198
+ if (resolvedBy !== undefined) {
199
+ yield { number, databaseId, setAside, resolvedBy };
200
+ }
201
+ }
202
+ }),
203
+ };
204
+ };
205
+
206
+ /** A set-aside comment to rewrite as resolved, and why. */
207
+ export type Resolution = Readonly<{
208
+ number: number;
209
+ databaseId: number;
210
+ setAside: SetAside;
211
+ resolvedBy: ResolvedBy;
212
+ }>;
213
+
214
+ /** What the log says about a record taken up from a comment. */
215
+ const TAKEN_UP_DETAIL = 'set aside by an earlier run, as its comment says';
@@ -0,0 +1,280 @@
1
+ import type { SetAside } from 'pr-report-core';
2
+ import { newSkips, settleSkips, unannouncedSkips, withSkip } from './skips.mjs';
3
+ import type {
4
+ OwnSetAsideComment,
5
+ PullRequest,
6
+ SkipRecord,
7
+ SkipRecords,
8
+ } from './types.mjs';
9
+
10
+ describe(newSkips, () => {
11
+ const none: SkipRecords = new Map();
12
+
13
+ test('reports a pull request set aside for the first time', () => {
14
+ assert.deepStrictEqual(newSkips(none, withSkip(none, skip())), [skip()]);
15
+ });
16
+
17
+ test('does not report a record that is still standing', () => {
18
+ const standing = withSkip(none, skip());
19
+
20
+ assert.deepStrictEqual(newSkips(standing, standing), []);
21
+
22
+ // The detail is prose, and prose that changed is not a new state.
23
+ assert.deepStrictEqual(
24
+ newSkips(standing, withSkip(none, skip({ detail: 'reworded' }))),
25
+ [],
26
+ );
27
+ });
28
+
29
+ test('reports it again once the state it was reached in has changed', () => {
30
+ const standing = withSkip(none, skip());
31
+
32
+ for (const changed of [
33
+ skip({ headSha: 'pushed' }),
34
+ skip({ baseSha: 'moved' }),
35
+ skip({ reason: 'push-failed' }),
36
+ ]) {
37
+ assert.deepStrictEqual(newSkips(standing, withSkip(none, changed)), [
38
+ changed,
39
+ ]);
40
+ }
41
+ });
42
+ });
43
+
44
+ describe(unannouncedSkips, () => {
45
+ const none: SkipRecords = new Map();
46
+
47
+ const record = skip({ headSha: HEAD, baseSha: BASE });
48
+
49
+ test('names a record whose comment was never written, or says otherwise', () => {
50
+ // The worker holds it aside while the comment the skill reads does not:
51
+ // each would leave it to the other until the comment is written.
52
+ assert.deepStrictEqual(
53
+ unannouncedSkips(withSkip(none, record), comments()),
54
+ [record],
55
+ );
56
+
57
+ assert.deepStrictEqual(
58
+ unannouncedSkips(
59
+ withSkip(none, record),
60
+ comments([7, { databaseId: 0, says: { kind: 'resolved' } }]),
61
+ ),
62
+ [record],
63
+ );
64
+
65
+ assert.deepStrictEqual(
66
+ unannouncedSkips(
67
+ withSkip(none, record),
68
+ comments([
69
+ 7,
70
+ standingComment({
71
+ setAside: { ...SET_ASIDE, reason: 'checks-failed' },
72
+ }),
73
+ ]),
74
+ ),
75
+ [record],
76
+ );
77
+ });
78
+
79
+ test('passes over a record its comment says', () => {
80
+ assert.deepStrictEqual(
81
+ unannouncedSkips(
82
+ withSkip(none, record),
83
+ comments([7, standingComment()]),
84
+ ),
85
+ [],
86
+ );
87
+ });
88
+ });
89
+
90
+ describe(settleSkips, () => {
91
+ const none: SkipRecords = new Map();
92
+
93
+ test('resolves a comment whose head has moved', () => {
94
+ assert.deepStrictEqual(
95
+ settleSkips(
96
+ none,
97
+ comments([7, standingComment()]),
98
+ [pullRequest(7, { headRefOid: PUSHED })],
99
+ BASE,
100
+ ),
101
+ {
102
+ skipped: none,
103
+ resolved: [
104
+ {
105
+ number: 7,
106
+ databaseId: 107,
107
+ setAside: SET_ASIDE,
108
+ resolvedBy: 'pushed',
109
+ },
110
+ ],
111
+ },
112
+ );
113
+ });
114
+
115
+ test('resolves a comment whose base has moved, except for failed checks', () => {
116
+ const settled = settleSkips(
117
+ none,
118
+ comments(
119
+ [7, standingComment()],
120
+ [
121
+ 8,
122
+ standingComment({
123
+ setAside: { ...SET_ASIDE, reason: 'checks-failed' },
124
+ }),
125
+ ],
126
+ ),
127
+ [pullRequest(7), pullRequest(8)],
128
+ MOVED,
129
+ );
130
+
131
+ assert.deepStrictEqual(
132
+ settled.resolved.map(({ number, resolvedBy }) => [number, resolvedBy]),
133
+ [[7, 'base-moved']],
134
+ );
135
+
136
+ // The failed checks stand, and are taken up.
137
+ assert.strictEqual(settled.skipped.get(8)?.reason, 'checks-failed');
138
+ });
139
+
140
+ test('drops the record of a pull request a person asked to retry', () => {
141
+ const held = withSkip(none, skip({ headSha: HEAD, baseSha: BASE }));
142
+
143
+ const settled = settleSkips(
144
+ held,
145
+ comments([7, standingComment({ retry: true })]),
146
+ [pullRequest(7)],
147
+ BASE,
148
+ );
149
+
150
+ assert.isFalse(settled.skipped.has(7));
151
+
152
+ assert.deepStrictEqual(
153
+ settled.resolved.map(({ number, resolvedBy }) => [number, resolvedBy]),
154
+ [[7, 'retry']],
155
+ );
156
+ });
157
+
158
+ test('takes up a record an earlier run left standing', () => {
159
+ const settled = settleSkips(
160
+ none,
161
+ comments([7, standingComment()]),
162
+ [pullRequest(7)],
163
+ BASE,
164
+ );
165
+
166
+ assert.deepStrictEqual(settled.resolved, []);
167
+
168
+ assert.deepStrictEqual(
169
+ [settled.skipped.get(7)?.reason, settled.skipped.get(7)?.headSha],
170
+ ['rebase-failed', HEAD],
171
+ );
172
+
173
+ // Not new: the comment already says it, so nothing writes over it.
174
+ assert.deepStrictEqual(newSkips(settled.skipped, settled.skipped), []);
175
+ });
176
+
177
+ test('keeps the record this run holds over the one its comment says', () => {
178
+ const held = withSkip(
179
+ none,
180
+ skip({ headSha: HEAD, baseSha: BASE, detail: 'conflicts in a.mts' }),
181
+ );
182
+
183
+ assert.deepStrictEqual(
184
+ settleSkips(
185
+ held,
186
+ comments([7, standingComment()]),
187
+ [pullRequest(7)],
188
+ BASE,
189
+ ),
190
+ { skipped: held, resolved: [] },
191
+ );
192
+ });
193
+
194
+ test('leaves alone what is resolved, closed, or a reason it does not know', () => {
195
+ assert.deepStrictEqual(
196
+ settleSkips(
197
+ none,
198
+ comments(
199
+ [7, { databaseId: 107, says: { kind: 'resolved' } }],
200
+ [8, standingComment()],
201
+ [
202
+ 9,
203
+ standingComment({ setAside: { ...SET_ASIDE, reason: 'renamed' } }),
204
+ ],
205
+ ),
206
+ [pullRequest(7), pullRequest(9)],
207
+ BASE,
208
+ ),
209
+ { skipped: none, resolved: [] },
210
+ );
211
+ });
212
+ });
213
+
214
+ const HEAD = 'c'.repeat(40);
215
+
216
+ const PUSHED = 'd'.repeat(40);
217
+
218
+ const BASE = 'a'.repeat(40);
219
+
220
+ const MOVED = 'b'.repeat(40);
221
+
222
+ const SET_ASIDE: SetAside = {
223
+ reason: 'rebase-failed',
224
+ headSha: HEAD,
225
+ baseSha: BASE,
226
+ } as const;
227
+
228
+ const standingComment = (
229
+ overrides: Partial<Readonly<{ setAside: SetAside; retry: boolean }>> = {},
230
+ ): OwnSetAsideComment =>
231
+ ({
232
+ databaseId: 0,
233
+ says: {
234
+ kind: 'standing',
235
+ setAside: overrides.setAside ?? SET_ASIDE,
236
+ retry: overrides.retry ?? false,
237
+ },
238
+ }) as const;
239
+
240
+ /** Each pull request's comment, with a database id of 100 more than its number. */
241
+ const comments = (
242
+ ...entries: readonly (readonly [number, OwnSetAsideComment])[]
243
+ ): ReadonlyMap<number, OwnSetAsideComment> =>
244
+ new Map(
245
+ entries.map(
246
+ ([number, comment]) =>
247
+ [number, { ...comment, databaseId: 100 + number }] as const,
248
+ ),
249
+ );
250
+
251
+ const pullRequest = (
252
+ number: number,
253
+ fields: Partial<PullRequest> = {},
254
+ ): PullRequest =>
255
+ ({
256
+ number,
257
+ id: `PR_${number}`,
258
+ title: `pull request #${number}`,
259
+ body: '',
260
+ state: 'OPEN',
261
+ headRefName: `feature/${number}`,
262
+ headRefOid: HEAD,
263
+ baseRefName: 'main',
264
+ isCrossRepository: false,
265
+ isDraft: false,
266
+ mergeStateStatus: 'BEHIND',
267
+ autoMergeRequest: {},
268
+ labels: [{ name: 'merge-queued' }],
269
+ ...fields,
270
+ }) as const;
271
+
272
+ const skip = (overrides: Partial<SkipRecord> = {}): SkipRecord =>
273
+ ({
274
+ number: 7,
275
+ headSha: 'head',
276
+ baseSha: 'base',
277
+ reason: 'rebase-failed',
278
+ detail: 'rebase conflicts',
279
+ ...overrides,
280
+ }) as const;