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,379 @@
1
+ // cspell:ignore ededed
2
+
3
+ import {
4
+ buildReport,
5
+ type Label,
6
+ type OpenIssue,
7
+ type PrReport,
8
+ type PullRequestFacts,
9
+ } from 'pr-report-core';
10
+ import { renderMarkdown, renderTerminal } from './render.mjs';
11
+
12
+ /** Labels by name alone, for the tests that do not care what colour they are. */
13
+ const labelled = (...names: readonly string[]): readonly Label[] =>
14
+ names.map((name) => ({ name, color: 'ededed', description: '' }));
15
+
16
+ const facts = (
17
+ overrides: Partial<PullRequestFacts> & Readonly<{ number: number }>,
18
+ ): PullRequestFacts =>
19
+ ({
20
+ title: `pull request ${overrides.number}`,
21
+ body: '',
22
+ author: 'noshiro-pf',
23
+ isDraft: false,
24
+ labels: [],
25
+ headRef: `branch-${overrides.number}`,
26
+ headSha: '0'.repeat(40),
27
+ baseRef: 'main',
28
+ fromFork: false,
29
+ url: `https://github.com/noshiro-pf/mono/pull/${overrides.number}`,
30
+ updatedAt: '2026-09-18T00:00:00Z',
31
+ headCommittedAt: '2026-09-17T00:00:00Z',
32
+ comparison: { aheadBy: 1, behindBy: 0 },
33
+ autoMerge: false,
34
+ reported: new Map(),
35
+ checksRunning: false,
36
+ linkedIssues: [],
37
+ ...overrides,
38
+ }) as const;
39
+
40
+ const report = (
41
+ pulls: readonly PullRequestFacts[],
42
+ issues: readonly OpenIssue[] = [],
43
+ ): PrReport =>
44
+ buildReport({
45
+ repo: { owner: 'noshiro-pf', name: 'mono' },
46
+ defaultBranch: 'main',
47
+ generatedAt: '2026-09-18T09:00:00Z',
48
+ required: ['code-check-result / result', 'no-skip-ci-label'],
49
+ merged: [],
50
+ mergedWithinDays: 7,
51
+ issues,
52
+ issuesLimit: 3,
53
+ authenticated: false,
54
+ pulls,
55
+ });
56
+
57
+ const issue = (number: number): OpenIssue =>
58
+ ({
59
+ number,
60
+ title: `issue ${number}`,
61
+ author: 'noshiro-pf',
62
+ url: `https://github.com/noshiro-pf/mono/issues/${number}`,
63
+ labels: labelled('bug'),
64
+ createdAt: '2026-09-10T00:00:00Z',
65
+ updatedAt: '2026-09-17T00:00:00Z',
66
+ comments: 2,
67
+ startAfter: [],
68
+ misplacedMergeAfter: [],
69
+ }) as const;
70
+
71
+ describe(renderMarkdown, () => {
72
+ test('says so plainly when there is nothing open', () => {
73
+ const rendered = renderMarkdown(report([]));
74
+
75
+ assert.isTrue(rendered.includes('No open pull requests.'));
76
+ });
77
+
78
+ test('nests a pull request under the one it declares it merges after', () => {
79
+ const rendered = renderMarkdown(
80
+ report([
81
+ facts({ number: 1901 }),
82
+ facts({ number: 1903, body: 'Merge-After: #1901' }),
83
+ ]),
84
+ );
85
+
86
+ const lines = rendered.split('\n');
87
+
88
+ const parent = lines.findIndex((l) => l.includes('#1901'));
89
+
90
+ const child = lines.findIndex((l) => l.includes('#1903'));
91
+
92
+ assert.isTrue(parent !== -1 && child === parent + 1);
93
+
94
+ // Two spaces of indent is one level of a Markdown list.
95
+ assert.isTrue(lines[child]?.startsWith(' - ') === true);
96
+
97
+ assert.isTrue(lines[parent]?.startsWith('- ') === true);
98
+ });
99
+
100
+ test('nests a stacked pull request under the one it is on, and says so', () => {
101
+ const rendered = renderMarkdown(
102
+ report([
103
+ facts({ number: 1901 }),
104
+ facts({ number: 1903, baseRef: 'branch-1901' }),
105
+ ]),
106
+ );
107
+
108
+ const lines = rendered.split('\n');
109
+
110
+ const child = lines.findIndex((l) => l.includes('#1903'));
111
+
112
+ assert.isTrue(lines[child]?.startsWith(' - ') === true);
113
+
114
+ assert.include(lines[child] ?? '', 'stacked on #1901');
115
+ });
116
+
117
+ test('says what the issues it closes add to the order', () => {
118
+ const rendered = renderMarkdown(
119
+ buildReport({
120
+ repo: { owner: 'noshiro-pf', name: 'mono' },
121
+ defaultBranch: 'main',
122
+ generatedAt: '2026-09-18T09:00:00Z',
123
+ required: [],
124
+ merged: [],
125
+ mergedWithinDays: 7,
126
+ issues: [],
127
+ issuesLimit: 3,
128
+ authenticated: false,
129
+ pulls: [
130
+ facts({
131
+ number: 1903,
132
+ linkedIssues: [
133
+ {
134
+ number: 1800,
135
+ title: '',
136
+ url: 'https://github.com/noshiro-pf/mono/issues/1800',
137
+ state: 'open',
138
+ },
139
+ ],
140
+ }),
141
+ facts({ number: 1901 }),
142
+ ],
143
+ issueDeclarations: {
144
+ startAfter: new Map([[1800, [1901, 1700]]]),
145
+ open: new Set([1700]),
146
+ },
147
+ }),
148
+ );
149
+
150
+ const lines = rendered.split('\n');
151
+
152
+ const child = lines.findIndex((l) => l.includes('#1903'));
153
+
154
+ // Drawn under the one it inherited, as a declared one would be.
155
+ assert.isTrue(lines[child]?.startsWith(' - ') === true);
156
+
157
+ assert.include(lines[child] ?? '', 'after #1901 (Start-After)');
158
+
159
+ assert.include(lines[child] ?? '', '#1700 (issue) not done');
160
+ });
161
+
162
+ test('links the pull request and every issue it closes', () => {
163
+ const rendered = renderMarkdown(
164
+ report([
165
+ facts({
166
+ number: 1901,
167
+ linkedIssues: [
168
+ {
169
+ number: 1880,
170
+ title: 'the bug',
171
+ url: 'https://github.com/noshiro-pf/mono/issues/1880',
172
+ state: 'open',
173
+ },
174
+ ],
175
+ }),
176
+ ]),
177
+ );
178
+
179
+ assert.isTrue(
180
+ rendered.includes(
181
+ '[#1901](https://github.com/noshiro-pf/mono/pull/1901)',
182
+ ),
183
+ );
184
+
185
+ assert.isTrue(
186
+ rendered.includes(
187
+ '[#1880](https://github.com/noshiro-pf/mono/issues/1880)',
188
+ ),
189
+ );
190
+ });
191
+
192
+ test('reports labels, the commit counts and a branch that is behind', () => {
193
+ const rendered = renderMarkdown(
194
+ report([
195
+ facts({
196
+ number: 1901,
197
+ labels: labelled('skip-ci', 'merge-queued'),
198
+ comparison: { aheadBy: 3, behindBy: 12 },
199
+ }),
200
+ ]),
201
+ );
202
+
203
+ assert.isTrue(rendered.includes('+3'));
204
+
205
+ assert.isTrue(rendered.includes('-12'));
206
+
207
+ assert.isTrue(rendered.includes('skip-ci'));
208
+
209
+ assert.isTrue(rendered.includes('merge-queued'));
210
+ });
211
+
212
+ test('says a queued pull request has auto-merge armed', () => {
213
+ const rendered = renderMarkdown(
214
+ report([
215
+ facts({
216
+ number: 1901,
217
+ labels: labelled('merge-queued'),
218
+ autoMerge: true,
219
+ }),
220
+ ]),
221
+ );
222
+
223
+ assert.isTrue(rendered.includes('auto-merge'));
224
+
225
+ assert.isFalse(rendered.includes('no auto-merge'));
226
+ });
227
+
228
+ // The ordinary state of a queued pull request: `unblock-prs` arms it when
229
+ // it picks it, so its absence until then is not news.
230
+ test('says nothing about a queued pull request not yet armed', () => {
231
+ const rendered = renderMarkdown(
232
+ report([
233
+ facts({
234
+ number: 1901,
235
+ labels: labelled('merge-queued'),
236
+ autoMerge: false,
237
+ }),
238
+ ]),
239
+ );
240
+
241
+ assert.isFalse(rendered.includes('auto-merge'));
242
+ });
243
+
244
+ test('says nothing about auto-merge on a pull request that is not queued', () => {
245
+ const rendered = renderMarkdown(
246
+ report([facts({ number: 1901, autoMerge: false })]),
247
+ );
248
+
249
+ assert.isFalse(rendered.includes('auto-merge'));
250
+ });
251
+
252
+ test('names the pull requests on a `Merge-After` cycle', () => {
253
+ const rendered = renderMarkdown(
254
+ report([
255
+ facts({ number: 1, body: 'Merge-After: #2' }),
256
+ facts({ number: 2, body: 'Merge-After: #1' }),
257
+ ]),
258
+ );
259
+
260
+ assert.isTrue(rendered.includes('Merge-After cycle'));
261
+
262
+ assert.isTrue(rendered.includes('#1'));
263
+
264
+ assert.isTrue(rendered.includes('#2'));
265
+ });
266
+ });
267
+
268
+ describe('the open issues section', () => {
269
+ test('lists every open issue with a link, its labels and its comments', () => {
270
+ const rendered = renderMarkdown(
271
+ report([facts({ number: 1901 })], [issue(2036)]),
272
+ );
273
+
274
+ assert.isTrue(rendered.includes('## Open issues (1)'));
275
+
276
+ assert.isTrue(
277
+ rendered.includes(
278
+ '- [#2036](https://github.com/noshiro-pf/mono/issues/2036) **issue 2036**',
279
+ ),
280
+ );
281
+
282
+ assert.isTrue(rendered.includes('`bug`'));
283
+
284
+ assert.isTrue(rendered.includes('2 comments'));
285
+ });
286
+
287
+ test('says how each issue stands for being started, and what it waits for', () => {
288
+ const rendered = renderMarkdown(
289
+ buildReport({
290
+ repo: { owner: 'noshiro-pf', name: 'mono' },
291
+ defaultBranch: 'main',
292
+ generatedAt: '2026-09-18T09:00:00Z',
293
+ required: [],
294
+ merged: [],
295
+ mergedWithinDays: 7,
296
+ issues: [
297
+ { ...issue(2001), startAfter: [1901, 1700] },
298
+ { ...issue(2002), startAfter: [1700] },
299
+ { ...issue(2003), misplacedMergeAfter: [1901] },
300
+ issue(2004),
301
+ ],
302
+ issuesLimit: 30,
303
+ authenticated: false,
304
+ pulls: [facts({ number: 1901 })],
305
+ references: new Map([[1700, 'done']]),
306
+ }),
307
+ );
308
+
309
+ const line = (number: number): string =>
310
+ rendered.split('\n').find((each) => each.includes(`#${number}]`)) ?? '';
311
+
312
+ assert.include(line(2001), 'waiting · after #1901 (open), #1700 (done)');
313
+
314
+ assert.include(line(2002), 'ready · after #1700 (done)');
315
+
316
+ assert.include(
317
+ line(2003),
318
+ 'Merge-After: #1901 is not read on an issue; use Start-After:',
319
+ );
320
+
321
+ // Free is the ordinary case, and is not said.
322
+ assert.notInclude(line(2004), 'free');
323
+ });
324
+
325
+ test('says when the list is full, so that it may not be all of them', () => {
326
+ const rendered = renderMarkdown(
327
+ report([facts({ number: 1901 })], [issue(1), issue(2), issue(3)]),
328
+ );
329
+
330
+ assert.isTrue(rendered.includes('## Open issues (3+)'));
331
+ });
332
+
333
+ test('is left out when no issue is open', () => {
334
+ const rendered = renderMarkdown(report([facts({ number: 1901 })]));
335
+
336
+ assert.isFalse(rendered.includes('Open issues'));
337
+ });
338
+
339
+ test('is there even when no pull request is open', () => {
340
+ const rendered = renderMarkdown(report([], [issue(2036)]));
341
+
342
+ assert.isTrue(rendered.includes('No open pull requests.'));
343
+
344
+ assert.isTrue(rendered.includes('#2036'));
345
+ });
346
+
347
+ test('drops the Markdown in a terminal', () => {
348
+ const rendered = renderTerminal(
349
+ report([facts({ number: 1901 })], [issue(2036)]),
350
+ );
351
+
352
+ assert.isTrue(rendered.includes('Open issues (1)'));
353
+
354
+ assert.isFalse(rendered.includes('## Open issues'));
355
+
356
+ assert.isTrue(rendered.includes('#2036 issue 2036'));
357
+
358
+ assert.isTrue(rendered.includes('[bug]'));
359
+ });
360
+ });
361
+
362
+ describe(renderTerminal, () => {
363
+ test('draws the same nesting without Markdown', () => {
364
+ const rendered = renderTerminal(
365
+ report([
366
+ facts({ number: 1901 }),
367
+ facts({ number: 1903, body: 'Merge-After: #1901' }),
368
+ ]),
369
+ );
370
+
371
+ assert.isFalse(rendered.includes(']('));
372
+
373
+ assert.isTrue(rendered.includes('#1901'));
374
+
375
+ assert.isTrue(rendered.includes('#1903'));
376
+
377
+ assert.isTrue(/[└├]/u.test(rendered));
378
+ });
379
+ });
@@ -0,0 +1,34 @@
1
+ /** The contexts a pull request has to satisfy, from where the repository says. */
2
+
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+ import { MAIN_RULESET_PATH, parseRuleset, type RepoRef } from 'pr-report-core';
6
+ import { Result } from 'ts-data-forge';
7
+ import { repoRoot } from '../lib/index.mjs';
8
+ import type { Client } from './github.mjs';
9
+
10
+ /**
11
+ * The required contexts: from the checkout's
12
+ * `repo-settings/rulesets/main.json` where the repository declares its
13
+ * ruleset there (`parseRuleset` says why the declaration rather than
14
+ * GitHub), and otherwise from what GitHub enforces on the default branch.
15
+ */
16
+ export const readRequiredContexts = async (
17
+ client: Client,
18
+ repo: RepoRef,
19
+ defaultBranch: string,
20
+ ): Promise<Result<readonly string[], string>> => {
21
+ const file = path.resolve(await repoRoot(), MAIN_RULESET_PATH);
22
+
23
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
24
+ const text = await Result.fromPromise(fs.readFile(file, 'utf8'));
25
+
26
+ if (Result.isErr(text)) {
27
+ return client.requiredContexts(repo, defaultBranch);
28
+ }
29
+
30
+ return Result.map(
31
+ parseRuleset(text.value, file),
32
+ ({ requiredContexts }) => requiredContexts,
33
+ );
34
+ };
@@ -0,0 +1 @@
1
+ export * from './push-fixed-pr.mjs';
@@ -0,0 +1,276 @@
1
+ import {
2
+ PNPM_UPDATE_BRANCH,
3
+ SKIP_CI_LABEL,
4
+ stackDescendants,
5
+ type QueueComments,
6
+ type QueueOwner,
7
+ type ReportEntry,
8
+ } from 'pr-report-core';
9
+ import { Arr, Result } from 'ts-data-forge';
10
+ import {
11
+ addLabel,
12
+ git,
13
+ pullRequestNumber,
14
+ resolveApiContext,
15
+ sh,
16
+ } from '../lib/index.mjs';
17
+ import { readQueueState } from '../pr-report/index.mjs';
18
+ import { pushLeased, restack, type BranchOf } from '../unblock-prs/index.mjs';
19
+
20
+ /** What `push-fixed-pr --help` prints. */
21
+ export const PUSH_FIXED_PR_HELP = [
22
+ 'Usage: push-fixed-pr <number> --by <claimant> --head <commit>',
23
+ '',
24
+ 'Hands a fixed pull request back to unblock-prs: pushes <commit> onto the',
25
+ 'branch --by claimed, under skip-ci, and restacks the pull requests stacked',
26
+ "on it. It refuses unless the claim on it is --by's.",
27
+ '',
28
+ 'Options:',
29
+ ' --by <claimant> the claimant, as given to claim-pr',
30
+ " --head <commit> the fix, already in this repository's objects",
31
+ ' -h, --help show this help',
32
+ ].join('\n');
33
+
34
+ /**
35
+ * Hands a fixed pull request back to the worker: pushes the commit a session
36
+ * made onto the branch it claimed, under `skip-ci`, and carries the layers
37
+ * stacked on it along — with the worker's own code (`pushLeased`, `restack`
38
+ * in `unblock-prs/rebase.mts`), so that a session's push is the push the
39
+ * worker would have made.
40
+ *
41
+ * ```sh
42
+ * push-fixed-pr 2267 --by <session URL> --head <commit>
43
+ * ```
44
+ *
45
+ * It refuses unless the pull request is a skill's (`pr-report-core`'s
46
+ * `queue-owner.mts`, read as every other command reads it) and the claim on
47
+ * it is `--by`'s: the claim is what kept the worker off it, and its head is
48
+ * the lease. `<commit>` is the fix, already in this repository's objects —
49
+ * any worktree of it will do. The push moves the head, which ends the claim
50
+ * and resolves the set-aside: from then on the pull request is the worker's.
51
+ */
52
+ export const pushFixedPr = async (
53
+ args: PushArgs,
54
+ ): Promise<Result<string, string>> => {
55
+ const commit = await git(
56
+ `git rev-parse --verify --quiet ${sh(`${args.head}^0`)}`,
57
+ );
58
+
59
+ if (Result.isErr(commit)) {
60
+ return Result.err(`${args.head} is not a commit here.`);
61
+ }
62
+
63
+ const state = await readQueueState();
64
+
65
+ if (Result.isErr(state)) {
66
+ return Result.err(`cannot read the queue: ${state.value}`);
67
+ }
68
+
69
+ const { decided, queue, owners } = state.value;
70
+
71
+ const decision = decidePush({
72
+ number: args.number,
73
+ by: args.by,
74
+ head: commit.value.trim(),
75
+ entries: decided.entries,
76
+ owner: owners.get(args.number),
77
+ comments: queue.get(args.number),
78
+ });
79
+
80
+ if (decision.kind === 'refuse') {
81
+ return Result.err(`#${args.number} not pushed: ${decision.reason}.`);
82
+ }
83
+
84
+ if (decision.addSkipCi) {
85
+ const api = await resolveApiContext();
86
+
87
+ const added = Result.isErr(api)
88
+ ? api
89
+ : await addLabel(api.value, args.number, SKIP_CI_LABEL);
90
+
91
+ if (Result.isErr(added)) {
92
+ return Result.err(
93
+ `#${args.number} not pushed: cannot put ${SKIP_CI_LABEL} on it first, and without it the push runs the matrix: ${added.value}`,
94
+ );
95
+ }
96
+ }
97
+
98
+ const pushed = await pushLeased(
99
+ decision.pr.headRefName,
100
+ decision.head,
101
+ decision.pr.headRefOid,
102
+ );
103
+
104
+ if (Result.isErr(pushed)) {
105
+ return Result.err(`#${args.number}: push refused: ${pushed.value}`);
106
+ }
107
+
108
+ if (pushed.value === 'moved') {
109
+ return Result.err(
110
+ `#${args.number}: someone pushed to ${decision.pr.headRefName} since it was claimed; throw the work away and survey again.`,
111
+ );
112
+ }
113
+
114
+ await restack(decision.pr, decision.head, decision.layers);
115
+
116
+ return Result.ok(
117
+ `#${args.number}: pushed ${decision.head.slice(0, 10)} under ${SKIP_CI_LABEL}${Arr.isNonEmpty(decision.layers) ? `, and carried ${decision.layers.map(({ number }) => `#${number}`).join(', ')} along` : ''}; it is the worker's again.`,
118
+ );
119
+ };
120
+
121
+ /**
122
+ * Whether `by` may push `head` to pull request `number`, and what that
123
+ * involves: the lease, whether `skip-ci` has to go on first, and the layers
124
+ * to carry along, lowest first.
125
+ */
126
+ export const decidePush = ({
127
+ number,
128
+ by,
129
+ head,
130
+ entries,
131
+ owner,
132
+ comments,
133
+ }: Readonly<{
134
+ number: number;
135
+ by: string;
136
+ head: string;
137
+ entries: readonly ReportEntry[];
138
+ owner: QueueOwner | undefined;
139
+ comments: QueueComments | undefined;
140
+ }>): Readonly<
141
+ | {
142
+ kind: 'push';
143
+ pr: BranchOf;
144
+ head: string;
145
+ addSkipCi: boolean;
146
+ layers: readonly BranchOf[];
147
+ }
148
+ | { kind: 'refuse'; reason: string }
149
+ > => {
150
+ const entry = entries.find((e) => e.number === number);
151
+
152
+ if (entry === undefined) {
153
+ return { kind: 'refuse', reason: 'it is not open' };
154
+ }
155
+
156
+ if (owner?.owner !== 'skill') {
157
+ return {
158
+ kind: 'refuse',
159
+ reason:
160
+ owner === undefined
161
+ ? 'who owns it could not be read'
162
+ : `it is the ${owner.owner}'s (${owner.reason}), not a skill's`,
163
+ };
164
+ }
165
+
166
+ const claim = comments?.claim;
167
+
168
+ if (
169
+ claim?.applies !== true ||
170
+ claim.says.kind !== 'standing' ||
171
+ claim.says.claim.by !== by
172
+ ) {
173
+ return {
174
+ kind: 'refuse',
175
+ reason: `${by} holds no claim on it at its head; claim it first (claim-pr)`,
176
+ };
177
+ }
178
+
179
+ if (head === entry.headSha) {
180
+ return { kind: 'refuse', reason: `${head} is already its head` };
181
+ }
182
+
183
+ // A generated branch, lockfile and all: its conflict is resolved by
184
+ // generating it again from the tip, not by a rebase resolved by hand.
185
+ if (
186
+ entry.headRef === PNPM_UPDATE_BRANCH &&
187
+ comments?.setAside?.says.kind === 'standing' &&
188
+ comments.setAside.says.setAside.reason === 'rebase-failed'
189
+ ) {
190
+ return {
191
+ kind: 'refuse',
192
+ reason:
193
+ 'a conflict on the dependency update is resolved by building it again: pnpm run rebuild-pnpm-update',
194
+ };
195
+ }
196
+
197
+ const parents: ReadonlyMap<number, number> = new Map(
198
+ Arr.generate(function* () {
199
+ for (const e of entries) {
200
+ if (e.stackedOn !== undefined) {
201
+ yield [e.number, e.stackedOn] as const;
202
+ }
203
+ }
204
+ }),
205
+ );
206
+
207
+ return {
208
+ kind: 'push',
209
+ pr: branchOf(entry),
210
+ head,
211
+ addSkipCi: entry.labels.every(({ name }) => name !== SKIP_CI_LABEL),
212
+ layers: stackDescendants(parents, number).flatMap((n) =>
213
+ entries.filter((e) => e.number === n).map(branchOf),
214
+ ),
215
+ };
216
+ };
217
+
218
+ /** The command line, or what is wrong with it. */
219
+ export const parsePushArgs = (args: readonly string[]): PushArgs | string => {
220
+ const flagValue = (flag: string): string | undefined => {
221
+ const at = args.indexOf(flag);
222
+
223
+ return at === -1 ? undefined : args[at + 1];
224
+ };
225
+
226
+ const by = flagValue('--by');
227
+
228
+ const head = flagValue('--head');
229
+
230
+ const flagged = new Set(
231
+ Arr.generate(function* () {
232
+ for (const flag of ['--by', '--head']) {
233
+ const at = args.indexOf(flag);
234
+
235
+ if (at === -1) {
236
+ continue;
237
+ }
238
+
239
+ yield at;
240
+
241
+ yield at + 1;
242
+ }
243
+ }),
244
+ );
245
+
246
+ const positional = args.filter(
247
+ (arg, index) => arg !== '--' && !flagged.has(index),
248
+ );
249
+
250
+ const [first] = positional;
251
+
252
+ const number = first === undefined ? undefined : pullRequestNumber(first);
253
+
254
+ return positional.length > 1
255
+ ? `unexpected argument: ${Arr.tail(positional).join(' ')}`
256
+ : number === undefined
257
+ ? 'name the pull request by its number'
258
+ : by === undefined || by.startsWith('--')
259
+ ? '--by names the claimant, as it claimed the pull request'
260
+ : head === undefined || !/^[0-9a-f]{7,40}$/u.test(head)
261
+ ? '--head names the commit to push, by its SHA'
262
+ : { number, by, head };
263
+ };
264
+
265
+ export type PushArgs = Readonly<{ number: number; by: string; head: string }>;
266
+
267
+ const branchOf = (entry: ReportEntry): BranchOf =>
268
+ ({
269
+ number: entry.number,
270
+ // Every entry is an open pull request.
271
+ state: 'OPEN',
272
+ headRefName: entry.headRef,
273
+ headRefOid: entry.headSha,
274
+ baseRefName: entry.baseRef,
275
+ isCrossRepository: entry.fromFork,
276
+ }) as const;