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,327 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { AUTO_REBASE_LABEL, SKIP_CI_LABEL } from 'pr-report-core';
3
+ import { Arr, Result, unknownToString } from 'ts-data-forge';
4
+ import { addLabel, log, type ApiContext } from '../lib/index.mjs';
5
+ import {
6
+ containsBase,
7
+ createPullRequest,
8
+ findOpenPullRequest,
9
+ findStackParent,
10
+ lastCommitSubject,
11
+ markReady,
12
+ missingInheritedOrder,
13
+ preflight,
14
+ pushBranch,
15
+ updateBody,
16
+ viewPullRequest,
17
+ } from './github.mjs';
18
+ import type { Options } from './options.mjs';
19
+ import {
20
+ mergeAfterTrailer,
21
+ withInheritedMergeAfter,
22
+ withStackParent,
23
+ } from './steps.mjs';
24
+
25
+ /**
26
+ * Opens the pull request for the current branch the way this repository wants
27
+ * one opened: push, create it ready for review, add `skip-ci`, then
28
+ * `auto-rebase`.
29
+ *
30
+ * **It never arms auto-merge.** `unblock-prs` does, when it picks the pull
31
+ * request — once the author has queued it with `merge-queued`, and once it is
32
+ * onto the default branch. Armed any earlier, it would merge the moment its
33
+ * checks went green, reviewed or not; and a stacked one, onto a branch no
34
+ * ruleset covers, the moment nothing held it, into the layer below. So
35
+ * `skip-ci` holds the checks and nothing is armed until the queue says so.
36
+ *
37
+ * Every step is skipped when it is already done, so a run that failed part
38
+ * way through is finished by running it again rather than unpicked.
39
+ *
40
+ * **`auto-rebase` goes on every layer, stacked or not.** `unblock-prs` keeps
41
+ * only a pull request based on the default branch on its tip, so on a layer
42
+ * above it does nothing until the layer below merges and GitHub moves this
43
+ * one onto the default branch — the moment it is needed, and one at which
44
+ * nothing would add it. It goes on after `skip-ci`, so that its own `labeled`
45
+ * run finds the checks held.
46
+ *
47
+ * **A `--base` other than the default branch stacks it** on the open pull
48
+ * request that branch is from, which has to exist and which the branch has to
49
+ * contain, and declares that one with `Merge-After:` as well.
50
+ *
51
+ * **What the issues it closes start after is declared too**: the open pull
52
+ * requests their `Start-After:` comes to (`docs/issue-start-after.md`),
53
+ * written into the body's `Merge-After:` once GitHub has read which issues
54
+ * the body closes. The readers inherit it without, so the body is saying
55
+ * what they already derive.
56
+ */
57
+ export const openPullRequest = async (
58
+ options: Options,
59
+ ): Promise<Result<string, string>> => {
60
+ const context = await preflight();
61
+
62
+ if (Result.isErr(context)) {
63
+ return context;
64
+ }
65
+
66
+ const { api, branch, defaultBranch } = context.value;
67
+
68
+ const base = options.base ?? defaultBranch;
69
+
70
+ const parent = base === defaultBranch ? undefined : await stackOn(api, base);
71
+
72
+ if (parent !== undefined && Result.isErr(parent)) {
73
+ return parent;
74
+ }
75
+
76
+ const mergeAfter = withStackParent(options.mergeAfter, parent?.value);
77
+
78
+ if (options.dryRun) {
79
+ return dryRun(api, branch, base, options, parent?.value, mergeAfter);
80
+ }
81
+
82
+ const pushed = await pushBranch(branch);
83
+
84
+ if (Result.isErr(pushed)) {
85
+ return Result.err(`cannot push: ${pushed.value}`);
86
+ }
87
+
88
+ log(`pushed ${branch}`);
89
+
90
+ const existing = await findOpenPullRequest(api, branch);
91
+
92
+ if (Result.isErr(existing)) {
93
+ return existing;
94
+ }
95
+
96
+ const prNumber =
97
+ existing.value === undefined
98
+ ? await open(api, branch, base, options, mergeAfter)
99
+ : Result.ok(existing.value.number);
100
+
101
+ if (Result.isErr(prNumber)) {
102
+ return prNumber;
103
+ }
104
+
105
+ // Re-read rather than reuse what the listing said: between then and now
106
+ // this run has created it.
107
+ const created = await viewPullRequest(api, prNumber.value);
108
+
109
+ if (Result.isErr(created)) {
110
+ return created;
111
+ }
112
+
113
+ if (created.value.isDraft) {
114
+ const ready = await markReady(api, created.value.nodeId);
115
+
116
+ if (Result.isErr(ready)) {
117
+ return Result.err(`cannot mark it ready for review: ${ready.value}`);
118
+ }
119
+
120
+ log(`#${prNumber.value}: marked ready for review`);
121
+ }
122
+
123
+ const carried = await carryStartAfter(api, prNumber.value, mergeAfter);
124
+
125
+ if (Result.isErr(carried)) {
126
+ return carried;
127
+ }
128
+
129
+ const labelled = await addLabel(api, prNumber.value, SKIP_CI_LABEL);
130
+
131
+ if (Result.isErr(labelled)) {
132
+ return Result.err(`cannot add ${SKIP_CI_LABEL}: ${labelled.value}`);
133
+ }
134
+
135
+ log(`#${prNumber.value}: ${SKIP_CI_LABEL} is on`);
136
+
137
+ const autoRebase = await addLabel(api, prNumber.value, AUTO_REBASE_LABEL);
138
+
139
+ if (Result.isErr(autoRebase)) {
140
+ return Result.err(`cannot add ${AUTO_REBASE_LABEL}: ${autoRebase.value}`);
141
+ }
142
+
143
+ log(`#${prNumber.value}: ${AUTO_REBASE_LABEL} is on`);
144
+
145
+ return Result.ok(
146
+ [
147
+ `#${prNumber.value}: opened, ${SKIP_CI_LABEL} and ${AUTO_REBASE_LABEL} on, auto-merge left unarmed.`,
148
+ `Once it is reviewed, add merge-queued: unblock-prs arms auto-merge when it picks it${parent === undefined ? '' : `, which is once #${parent.value} has merged`}.`,
149
+ ].join('\n'),
150
+ );
151
+ };
152
+
153
+ /**
154
+ * Writes into the body, as `Merge-After:`, the open pull requests the issues
155
+ * it closes start after (`docs/issue-start-after.md`), once the body exists
156
+ * for GitHub to say which issues those are. A run on a body that says them
157
+ * already writes nothing.
158
+ */
159
+ const carryStartAfter = async (
160
+ api: ApiContext,
161
+ prNumber: number,
162
+ declared: readonly number[],
163
+ ): Promise<Result<undefined, string>> => {
164
+ const order = await missingInheritedOrder(api, prNumber, declared);
165
+
166
+ if (Result.isErr(order)) {
167
+ return Result.err(`cannot read the issues it closes: ${order.value}`);
168
+ }
169
+
170
+ const { body, missing } = order.value;
171
+
172
+ if (!Arr.isNonEmpty(missing)) {
173
+ return Result.ok(undefined);
174
+ }
175
+
176
+ const updated = await updateBody(
177
+ api,
178
+ prNumber,
179
+ withInheritedMergeAfter(body, missing),
180
+ );
181
+
182
+ if (Result.isErr(updated)) {
183
+ return Result.err(`cannot write the inherited order: ${updated.value}`);
184
+ }
185
+
186
+ log(
187
+ `#${prNumber}: ${mergeAfterTrailer(missing) ?? ''}, from the Start-After of the issues it closes`,
188
+ );
189
+
190
+ return Result.ok(undefined);
191
+ };
192
+
193
+ /**
194
+ * The pull request a new one onto `base` is stacked on, having checked that
195
+ * the branch is on its tip. Asked before anything is pushed, so that a base
196
+ * nothing will recognize as a stack is refused rather than opened.
197
+ */
198
+ const stackOn = async (
199
+ api: ApiContext,
200
+ base: string,
201
+ ): Promise<Result<number, string>> => {
202
+ const parent = await findStackParent(api, base);
203
+
204
+ if (Result.isErr(parent)) {
205
+ return parent;
206
+ }
207
+
208
+ const contained = await containsBase(base);
209
+
210
+ if (Result.isErr(contained)) {
211
+ return contained;
212
+ }
213
+
214
+ log(`stacking on #${parent.value} (${base})`);
215
+
216
+ return parent;
217
+ };
218
+
219
+ const open = async (
220
+ api: ApiContext,
221
+ branch: string,
222
+ base: string,
223
+ options: Options,
224
+ mergeAfter: readonly number[],
225
+ ): Promise<Result<number, string>> => {
226
+ const title = await resolveTitle(options);
227
+
228
+ if (Result.isErr(title)) {
229
+ return title;
230
+ }
231
+
232
+ const body = await resolveBody(options, mergeAfter);
233
+
234
+ if (Result.isErr(body)) {
235
+ return body;
236
+ }
237
+
238
+ const created = await createPullRequest({
239
+ api,
240
+ branch,
241
+ base,
242
+ title: title.value,
243
+ body: body.value,
244
+ });
245
+
246
+ if (Result.isErr(created)) {
247
+ return Result.err(`cannot create the pull request: ${created.value}`);
248
+ }
249
+
250
+ log(`#${created.value}: opened against ${base}`);
251
+
252
+ return created;
253
+ };
254
+
255
+ const resolveTitle = async (
256
+ options: Options,
257
+ ): Promise<Result<string, string>> =>
258
+ options.title === undefined ? lastCommitSubject() : Result.ok(options.title);
259
+
260
+ const resolveBody = async (
261
+ options: Options,
262
+ mergeAfter: readonly number[],
263
+ ): Promise<Result<string, string>> => {
264
+ const trailer = mergeAfterTrailer(mergeAfter);
265
+
266
+ if (options.bodyFile === undefined) {
267
+ return Result.ok(trailer === undefined ? '' : `${trailer}\n`);
268
+ }
269
+
270
+ const read = await Result.fromPromise(
271
+ // The path is one the operator typed; there is no literal to use here.
272
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
273
+ fs.readFile(options.bodyFile, 'utf8'),
274
+ );
275
+
276
+ if (Result.isErr(read)) {
277
+ return Result.err(
278
+ `cannot read ${options.bodyFile}: ${unknownToString(read.value)}`,
279
+ );
280
+ }
281
+
282
+ // The trailer goes first, where `unblock-prs` reads it and a reader sees it
283
+ // before the prose.
284
+ return Result.ok(
285
+ trailer === undefined ? read.value : `${trailer}\n\n${read.value}`,
286
+ );
287
+ };
288
+
289
+ const dryRun = async (
290
+ api: ApiContext,
291
+ branch: string,
292
+ base: string,
293
+ options: Options,
294
+ parent: number | undefined,
295
+ mergeAfter: readonly number[],
296
+ ): Promise<Result<string, string>> => {
297
+ const existing = await findOpenPullRequest(api, branch);
298
+
299
+ if (Result.isErr(existing)) {
300
+ return existing;
301
+ }
302
+
303
+ const title = await resolveTitle(options);
304
+
305
+ const trailer = mergeAfterTrailer(mergeAfter);
306
+
307
+ const steps: readonly string[] = [
308
+ `push ${branch} to origin`,
309
+ existing.value === undefined
310
+ ? `open a pull request against ${base} titled ${JSON.stringify(Result.isOk(title) ? title.value : '(unreadable)')}`
311
+ : `#${existing.value.number} is open already; leave it as it is`,
312
+ existing.value?.isDraft === true
313
+ ? `#${existing.value.number}: mark it ready for review`
314
+ : undefined,
315
+ `add ${SKIP_CI_LABEL}`,
316
+ `add ${AUTO_REBASE_LABEL}`,
317
+ parent === undefined
318
+ ? 'leave auto-merge unarmed: unblock-prs arms it when it picks it'
319
+ : `leave auto-merge unarmed: it is stacked on #${parent}, and unblock-prs arms it when it picks it, after #${parent} merges`,
320
+ trailer === undefined ? undefined : `declare ${trailer}`,
321
+ 'add to Merge-After the open pull requests the issues it closes start after, if the body does not name them',
322
+ ].filter((step) => step !== undefined);
323
+
324
+ return Result.ok(
325
+ Arr.toUnshifted('Would:')(steps.map((step) => ` - ${step}`)).join('\n'),
326
+ );
327
+ };
@@ -0,0 +1,101 @@
1
+ /** What the command line says, and how it is read. */
2
+
3
+ import { SKIP_CI_LABEL } from 'pr-report-core';
4
+ import { Result } from 'ts-data-forge';
5
+ import { parseCommandArgs, pullRequestNumber } from '../lib/index.mjs';
6
+
7
+ export type Options = Readonly<{
8
+ /** The pull request title; the branch's last commit subject when absent. */
9
+ title: string | undefined;
10
+ /** A file to take the body from; only the trailer, or nothing, when absent. */
11
+ bodyFile: string | undefined;
12
+ /**
13
+ * What to target; the repository's default branch when absent. Another
14
+ * open pull request's branch stacks it on that pull request.
15
+ */
16
+ base: string | undefined;
17
+ /** Pull requests to declare with `Merge-After:`, in the order given. */
18
+ mergeAfter: readonly number[];
19
+ /** Say what would be done, and touch nothing. */
20
+ dryRun: boolean;
21
+ }>;
22
+
23
+ export const HELP = [
24
+ 'Usage: open-pr [options]',
25
+ '',
26
+ 'Opens the pull request for the current branch the way this repository',
27
+ 'wants one opened: push, create it ready for review, and add the',
28
+ `${SKIP_CI_LABEL} label. It never arms auto-merge: unblock-prs does, when it`,
29
+ 'picks the pull request once it is labelled merge-queued.',
30
+ '',
31
+ 'Re-running is safe: each step is skipped when it is already done, so a run',
32
+ 'that failed part way through is finished by running it again.',
33
+ '',
34
+ 'The description is yours to write — say in it which checks you ran, since',
35
+ `while ${SKIP_CI_LABEL} is on they are the only checks the branch gets.`,
36
+ '',
37
+ 'Options:',
38
+ ' --title <text> pull request title (default: the last commit subject)',
39
+ ' --body-file <path> file to take the body from (default: the trailer alone)',
40
+ ' --base <branch> what to target (default: the repository default branch);',
41
+ ' the branch of an open pull request stacks it on that one',
42
+ ' --merge-after <number> declare a predecessor; repeat for several',
43
+ ' --dry-run say what would be done, and touch nothing',
44
+ ' -h, --help show this help',
45
+ ].join('\n');
46
+
47
+ export const parseOptions = (
48
+ args: readonly string[],
49
+ ): Result<Options | 'help', string> => {
50
+ const parsed = parseCommandArgs(args, {
51
+ title: { type: 'string' },
52
+ 'body-file': { type: 'string' },
53
+ base: { type: 'string' },
54
+ 'merge-after': { type: 'string', multiple: true },
55
+ 'dry-run': { type: 'boolean', default: false },
56
+ });
57
+
58
+ if (Result.isErr(parsed)) {
59
+ return parsed;
60
+ }
61
+
62
+ if (parsed.value === 'help') {
63
+ return Result.ok('help');
64
+ }
65
+
66
+ const values = parsed.value;
67
+
68
+ const mergeAfter = parseMergeAfter(values['merge-after'] ?? []);
69
+
70
+ if (Result.isErr(mergeAfter)) {
71
+ return mergeAfter;
72
+ }
73
+
74
+ return Result.ok({
75
+ title: values.title,
76
+ bodyFile: values['body-file'],
77
+ base: values.base,
78
+ mergeAfter: mergeAfter.value,
79
+ dryRun: values['dry-run'],
80
+ });
81
+ };
82
+
83
+ const parseMergeAfter = (
84
+ raw: readonly string[],
85
+ ): Result<readonly number[], string> => {
86
+ const mut_numbers: number[] = [];
87
+
88
+ for (const value of raw) {
89
+ const number = pullRequestNumber(value);
90
+
91
+ if (number === undefined) {
92
+ return Result.err(
93
+ `--merge-after must be a pull request number, got ${JSON.stringify(value)}`,
94
+ );
95
+ }
96
+
97
+ mut_numbers.push(number);
98
+ }
99
+
100
+ return Result.ok(mut_numbers);
101
+ };
@@ -0,0 +1,67 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import { parseOptions, type Options } from './options.mjs';
3
+
4
+ const parsed = (...args: readonly string[]): ReturnType<typeof parseOptions> =>
5
+ parseOptions(args);
6
+
7
+ /** Every field, so that a new one cannot be added without a decision here. */
8
+ const defaults: Options = {
9
+ title: undefined,
10
+ bodyFile: undefined,
11
+ base: undefined,
12
+ mergeAfter: [],
13
+ dryRun: false,
14
+ } as const;
15
+
16
+ describe(parseOptions, () => {
17
+ test('defaults to acting, on no declared predecessor', () => {
18
+ assert.deepStrictEqual(Result.unwrapOk(parsed()), defaults);
19
+ });
20
+
21
+ // `pnpm run open-pr -- --dry-run` forwards the `--` as well.
22
+ test('drops the separator pnpm forwards', () => {
23
+ assert.deepStrictEqual(Result.unwrapOk(parsed('--', '--dry-run')), {
24
+ ...defaults,
25
+ dryRun: true,
26
+ });
27
+ });
28
+
29
+ test('reads a title and a body file', () => {
30
+ assert.deepStrictEqual(
31
+ Result.unwrapOk(
32
+ parsed('--title', 'fix: a thing', '--body-file', './b.md'),
33
+ ),
34
+ { ...defaults, title: 'fix: a thing', bodyFile: './b.md' },
35
+ );
36
+ });
37
+
38
+ test('reads the base branch', () => {
39
+ assert.deepStrictEqual(Result.unwrapOk(parsed('--base', 'release')), {
40
+ ...defaults,
41
+ base: 'release',
42
+ });
43
+ });
44
+
45
+ test('accepts `--merge-after` once per predecessor, with or without a `#`', () => {
46
+ assert.deepStrictEqual(
47
+ Result.unwrapOk(
48
+ parsed('--merge-after', '#1901', '--merge-after', '1903'),
49
+ ),
50
+ { ...defaults, mergeAfter: [1901, 1903] },
51
+ );
52
+ });
53
+
54
+ test('refuses a `--merge-after` that is not a pull request number', () => {
55
+ assert.isTrue(Result.isErr(parsed('--merge-after', 'later')));
56
+ });
57
+
58
+ // `unblock-prs` passes a draft over, so the option that would ask for one
59
+ // is not quietly ignored.
60
+ test('refuses an unknown option rather than ignoring it', () => {
61
+ assert.isTrue(Result.isErr(parsed('--draft')));
62
+ });
63
+
64
+ test('asks for help', () => {
65
+ assert.strictEqual(Result.unwrapOk(parsed('--help')), 'help');
66
+ });
67
+ });
@@ -0,0 +1,57 @@
1
+ /** What the observed state allows, and nothing that talks to GitHub. */
2
+
3
+ import { Arr } from 'ts-data-forge';
4
+
5
+ /**
6
+ * The `Merge-After:` line for a chained pull request, or `undefined` when
7
+ * nothing was declared. One line, in the order given, which is the form
8
+ * `unblock-prs` reads.
9
+ */
10
+ export const mergeAfterTrailer = (
11
+ numbers: readonly number[],
12
+ ): string | undefined =>
13
+ Arr.isNonEmpty(numbers)
14
+ ? (`Merge-After: ${numbers.map((n) => `#${n}`).join(', ')}` as const)
15
+ : undefined;
16
+
17
+ /**
18
+ * What to declare with `Merge-After:`: what was asked for, and — for a pull
19
+ * request stacked on another — that one first. The base already says it, and
20
+ * `unblock-prs` and the reports read the base alone; the trailer is written
21
+ * as well so that the order is stated in the body like every other, and
22
+ * survives the base being moved by hand.
23
+ */
24
+ export const withStackParent = (
25
+ mergeAfter: readonly number[],
26
+ parent: number | undefined,
27
+ ): readonly number[] =>
28
+ parent === undefined
29
+ ? mergeAfter
30
+ : Arr.uniq(Arr.toUnshifted(parent)(mergeAfter));
31
+
32
+ /**
33
+ * `body` with `numbers` declared in it as well: added to the `Merge-After:`
34
+ * line it opens with, which is where this command writes one, or in a line
35
+ * of their own before it. The numbers are what the issues it closes start
36
+ * after and the body does not yet say (`openPullRequest`).
37
+ */
38
+ export const withInheritedMergeAfter = (
39
+ body: string,
40
+ numbers: readonly number[],
41
+ ): string => {
42
+ const trailer = mergeAfterTrailer(numbers);
43
+
44
+ if (trailer === undefined) {
45
+ return body;
46
+ }
47
+
48
+ const [first = '', ...rest] = body.split('\n');
49
+
50
+ if (/^merge-after\s*:/iu.test(first)) {
51
+ return Arr.toUnshifted(
52
+ `${first.trimEnd()}, ${numbers.map((n) => `#${n}`).join(', ')}`,
53
+ )(rest).join('\n');
54
+ }
55
+
56
+ return body === '' ? `${trailer}\n` : `${trailer}\n\n${body}`;
57
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ mergeAfterTrailer,
3
+ withInheritedMergeAfter,
4
+ withStackParent,
5
+ } from './steps.mjs';
6
+
7
+ describe(mergeAfterTrailer, () => {
8
+ test('is nothing when no pull request was named', () => {
9
+ assert.isUndefined(mergeAfterTrailer([]));
10
+ });
11
+
12
+ test('writes the numbers on one line, in the declared order', () => {
13
+ assert.strictEqual(
14
+ mergeAfterTrailer([1901, 1903]),
15
+ 'Merge-After: #1901, #1903',
16
+ );
17
+ });
18
+ });
19
+
20
+ describe(withStackParent, () => {
21
+ test('declares the layer below first when it is stacked', () => {
22
+ assert.deepStrictEqual(withStackParent([1903], 1901), [1901, 1903]);
23
+ });
24
+
25
+ test('does not name the layer below twice', () => {
26
+ assert.deepStrictEqual(withStackParent([1903, 1901], 1901), [1901, 1903]);
27
+ });
28
+
29
+ test('leaves the declaration alone when it is not stacked', () => {
30
+ assert.deepStrictEqual(withStackParent([1903], undefined), [1903]);
31
+ });
32
+ });
33
+
34
+ describe(withInheritedMergeAfter, () => {
35
+ test('adds the numbers to the trailer the body opens with', () => {
36
+ assert.strictEqual(
37
+ withInheritedMergeAfter('Merge-After: #1901\n\nProse.', [1905]),
38
+ 'Merge-After: #1901, #1905\n\nProse.',
39
+ );
40
+ });
41
+
42
+ test('puts a trailer first when the body opens with prose', () => {
43
+ assert.strictEqual(
44
+ withInheritedMergeAfter('Prose.\n', [1905, 1907]),
45
+ 'Merge-After: #1905, #1907\n\nProse.\n',
46
+ );
47
+ });
48
+
49
+ test('writes the trailer alone into an empty body', () => {
50
+ assert.strictEqual(
51
+ withInheritedMergeAfter('', [1905]),
52
+ 'Merge-After: #1905\n',
53
+ );
54
+ });
55
+
56
+ test('leaves the body as it is when there is nothing to add', () => {
57
+ assert.strictEqual(withInheritedMergeAfter('Prose.', []), 'Prose.');
58
+ });
59
+ });
@@ -0,0 +1,42 @@
1
+ /** The shapes this command passes around, and what the API answers with. */
2
+
3
+ import * as t from 'ts-fortress';
4
+
5
+ /**
6
+ * The fields the run needs, and no more. Deliberately smaller than
7
+ * `unblock-prs`'s pull request: this command decides two things — whether
8
+ * the pull request exists, and whether it is a draft — and asking for less
9
+ * keeps what a failure can be about small.
10
+ *
11
+ * `nodeId` is here because taking a pull request out of draft is a GraphQL
12
+ * mutation, which addresses it by node id rather than by number. It is
13
+ * carried on the read that already happens rather than fetched again.
14
+ */
15
+ export type PullRequest = Readonly<{
16
+ number: number;
17
+ nodeId: string;
18
+ isDraft: boolean;
19
+ }>;
20
+
21
+ /**
22
+ * What `GET /repos/{owner}/{repo}/pulls[/{number}]` answers with, in the
23
+ * REST API's own names. Only the fields above are declared; everything else
24
+ * in that response is ignored.
25
+ */
26
+ export const PullRequestResponseSchema = t.record({
27
+ number: t.number(),
28
+ node_id: t.string(),
29
+ draft: t.boolean(),
30
+ });
31
+
32
+ export const PullRequestListResponseSchema = t.array(PullRequestResponseSchema);
33
+
34
+ export type PullRequestResponse = t.TypeOf<typeof PullRequestResponseSchema>;
35
+
36
+ /** The REST response in this command's own terms. */
37
+ export const toPullRequest = (response: PullRequestResponse): PullRequest =>
38
+ ({
39
+ number: response.number,
40
+ nodeId: response.node_id,
41
+ isDraft: response.draft,
42
+ }) as const;