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,40 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import { parseRepoRef, repoOfUrl } from './github-api.mjs';
3
+
4
+ describe(parseRepoRef, () => {
5
+ test('reads `owner/name`', () => {
6
+ assert.deepStrictEqual(
7
+ parseRepoRef('noshiro-pf/mono'),
8
+ Result.ok({ owner: 'noshiro-pf', name: 'mono' }),
9
+ );
10
+ });
11
+
12
+ test.each(['mono', 'noshiro-pf/', '/mono', 'a/b/c'])('refuses %j', (raw) => {
13
+ assert.deepStrictEqual(
14
+ parseRepoRef(raw),
15
+ Result.err(`expected owner/name, got ${JSON.stringify(raw)}`),
16
+ );
17
+ });
18
+ });
19
+
20
+ describe(repoOfUrl, () => {
21
+ test.each([
22
+ 'https://github.com/noshiro-pf/mono',
23
+ 'https://github.com/noshiro-pf/mono.git',
24
+ 'git+https://github.com/noshiro-pf/mono.git',
25
+ 'git@github.com:noshiro-pf/mono.git',
26
+ ])('reads the repository out of %j', (url) => {
27
+ assert.deepStrictEqual(repoOfUrl(url), {
28
+ owner: 'noshiro-pf',
29
+ name: 'mono',
30
+ });
31
+ });
32
+
33
+ test.each([
34
+ 'http://local_proxy@127.0.0.1:41234/git/noshiro-pf/mono',
35
+ 'https://gitlab.com/noshiro-pf/mono.git',
36
+ '',
37
+ ])('names no GitHub repository in %j', (url) => {
38
+ assert.isUndefined(repoOfUrl(url));
39
+ });
40
+ });
@@ -0,0 +1,6 @@
1
+ export * from './cli-args.mjs';
2
+ export * from './github-api.mjs';
3
+ export * from './json.mjs';
4
+ export * from './log.mjs';
5
+ export * from './shell.mjs';
6
+ export * from './time.mjs';
@@ -0,0 +1,43 @@
1
+ /** Reading JSON into a schema's type, from text or from a file. */
2
+
3
+ import * as fs from 'node:fs/promises';
4
+ import { Json, Result } from 'ts-data-forge';
5
+ import * as t from 'ts-fortress';
6
+
7
+ /**
8
+ * `text` parsed and validated against `schema`. `source` names where the text
9
+ * came from — a file, an API route, a command — so that the message says
10
+ * which answer was wrong, not only how.
11
+ */
12
+ export const parseJson = <A,>(
13
+ text: string,
14
+ schema: t.Type<A>,
15
+ source: string,
16
+ ): Result<A, string> => {
17
+ const parsed = Json.parse(text);
18
+
19
+ if (Result.isErr(parsed)) {
20
+ return Result.err(`${source} is not valid JSON: ${parsed.value}`);
21
+ }
22
+
23
+ const validated = schema.validate(parsed.value);
24
+
25
+ return Result.isErr(validated)
26
+ ? Result.err(
27
+ `${source} has an unexpected shape:\n${t.validationErrorsToMessages(validated.value).join('\n')}`,
28
+ )
29
+ : Result.ok(validated.value);
30
+ };
31
+
32
+ /** {@link parseJson} of a file's contents, the file naming itself. */
33
+ export const readJsonFile = async <A,>(
34
+ file: string,
35
+ schema: t.Type<A>,
36
+ ): Promise<Result<A, string>> => {
37
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
38
+ const text = await Result.fromPromise(fs.readFile(file, 'utf8'));
39
+
40
+ return Result.isErr(text)
41
+ ? Result.err(`cannot read ${file}`)
42
+ : parseJson(text.value, schema, file);
43
+ };
@@ -0,0 +1,34 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import * as t from 'ts-fortress';
3
+ import { parseJson } from './json.mjs';
4
+
5
+ describe(parseJson, () => {
6
+ const Schema = t.record({ number: t.number() });
7
+
8
+ test('answers the value when the text is JSON of the shape', () => {
9
+ assert.deepStrictEqual(
10
+ parseJson('{"number":1}', Schema, 'gh pr view 1'),
11
+ Result.ok({ number: 1 }),
12
+ );
13
+ });
14
+
15
+ test('names the source when the text is not JSON', () => {
16
+ const parsed = parseJson('not json', Schema, 'gh pr view 1');
17
+
18
+ assert.isTrue(Result.isErr(parsed));
19
+
20
+ assert.isTrue(parsed.value.startsWith('gh pr view 1 is not valid JSON: '));
21
+ });
22
+
23
+ test('names the source and the field when the shape is wrong', () => {
24
+ const parsed = parseJson('{"number":"1"}', Schema, 'gh pr view 1');
25
+
26
+ assert.isTrue(Result.isErr(parsed));
27
+
28
+ assert.isTrue(
29
+ parsed.value.startsWith('gh pr view 1 has an unexpected shape:\n'),
30
+ );
31
+
32
+ assert.include(parsed.value, 'number');
33
+ });
34
+ });
@@ -0,0 +1,19 @@
1
+ /** The log line every long-running command writes. */
2
+
3
+ import { currentEpochMs, localTimestamp } from './time.mjs';
4
+
5
+ export const log = (message: string): void => {
6
+ console.info(logLine(message));
7
+ };
8
+
9
+ /**
10
+ * `message` stamped with the time in the machine's time zone (`TZ`), because
11
+ * the person reading the log reads it against their own clock. The offset
12
+ * stays in the stamp, so a line pasted elsewhere still names one instant.
13
+ * `offsetMinutes` (east of UTC) is the machine's unless given.
14
+ */
15
+ export const logLine = (
16
+ message: string,
17
+ epochMs: number = currentEpochMs(),
18
+ offsetMinutes?: number,
19
+ ): string => `[${localTimestamp(epochMs, offsetMinutes)}] ${message}` as const;
@@ -0,0 +1,45 @@
1
+ // cspell:ignore unstub
2
+
3
+ import { logLine } from './log.mjs';
4
+
5
+ describe(logLine, () => {
6
+ test('stamps the line with the local time and its offset from UTC', () => {
7
+ assert.strictEqual(
8
+ logLine('surveying', Date.parse('2026-09-27T12:03:04.500Z'), 9 * 60),
9
+ '[2026-09-27T21:03:04.500+09:00] surveying',
10
+ );
11
+ });
12
+
13
+ test('writes the offset of a zone west of UTC, and of UTC itself', () => {
14
+ assert.strictEqual(
15
+ logLine('surveying', Date.parse('2026-09-27T12:03:04Z'), -4 * 60),
16
+ '[2026-09-27T08:03:04.000-04:00] surveying',
17
+ );
18
+
19
+ assert.strictEqual(
20
+ logLine('surveying', Date.parse('2026-09-27T12:03:04Z'), 0),
21
+ '[2026-09-27T12:03:04.000+00:00] surveying',
22
+ );
23
+ });
24
+
25
+ describe('without a time given', () => {
26
+ test.each([
27
+ [
28
+ 'Asia/Tokyo',
29
+ /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+09:00\] surveying$/u,
30
+ ],
31
+ [
32
+ 'UTC',
33
+ /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+00:00\] surveying$/u,
34
+ ],
35
+ ] as const)('uses the machine time zone (TZ=%s)', (tz, expected) => {
36
+ vi.stubEnv('TZ', tz);
37
+
38
+ try {
39
+ assert.match(logLine('surveying'), expected);
40
+ } finally {
41
+ vi.unstubAllEnvs();
42
+ }
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,52 @@
1
+ /** Running `git`, `gh` and `pnpm` from a command, and quoting what they take. */
2
+
3
+ import { Result } from 'ts-data-forge';
4
+ import { $ } from 'ts-repo-utils';
5
+
6
+ /**
7
+ * Runs a git or gh command silently and resolves to its stdout, or to a
8
+ * message that includes what it printed to stderr. `unblock-prs` runs its
9
+ * `pnpm` commands through it too. It runs where the command was started
10
+ * unless told otherwise; git finds the repository from any directory in it.
11
+ */
12
+ export const git = async (
13
+ command: string,
14
+ cwd: string = process.cwd(),
15
+ ): Promise<Result<string, string>> => {
16
+ const result = await $(command, {
17
+ cwd,
18
+ silent: true,
19
+ env: childEnv,
20
+ maxBuffer: 64 * 1024 * 1024,
21
+ });
22
+
23
+ return Result.isOk(result)
24
+ ? Result.ok(result.value.stdout)
25
+ : Result.err(result.value.message.trim());
26
+ };
27
+
28
+ /**
29
+ * The root of the repository the command runs in, where its configuration
30
+ * files are read from: the working tree holding the current directory, or
31
+ * the current directory itself where git finds none (a tree without `.git`).
32
+ */
33
+ export const repoRoot = async (): Promise<string> => {
34
+ const top = await git('git rev-parse --show-toplevel');
35
+
36
+ return Result.isOk(top) ? top.value.trim() : process.cwd();
37
+ };
38
+
39
+ /** Single-quotes a string for a POSIX shell. */
40
+ export const sh = (value: string): string =>
41
+ `'${value.replaceAll("'", String.raw`'\''`)}'` as const;
42
+
43
+ /**
44
+ * Passed to every git and gh invocation. `GIT_TERMINAL_PROMPT=0` turns a
45
+ * missing credential into a failure instead of a hang; `GIT_EDITOR=true`
46
+ * keeps any editor git might want out of the way.
47
+ */
48
+ const childEnv: NodeJS.ProcessEnv = {
49
+ ...process.env,
50
+ GIT_TERMINAL_PROMPT: '0',
51
+ GIT_EDITOR: 'true',
52
+ } as const;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The clock, and instants written as the commands write them — through
3
+ * `Date` rather than `Temporal`, which the oldest Node.js this package's
4
+ * `engines` names has only behind a flag. GitHub's
5
+ * timestamps are read with `pr-report-core`'s `epochMsOf`.
6
+ */
7
+
8
+ /** Milliseconds since the epoch, now. */
9
+ export const currentEpochMs = (): number => Date.now();
10
+
11
+ /** An instant in UTC to the second, as GitHub writes one: `2026-09-29T01:02:03Z`. */
12
+ export const isoSeconds = (epochMs: number): string =>
13
+ `${isoMillis(epochMs).slice(0, 19)}Z` as const;
14
+
15
+ /**
16
+ * An instant in local time to the millisecond, with its offset from UTC:
17
+ * `2026-09-27T21:03:04.500+09:00`. The offset is the machine's (`TZ`) at
18
+ * that instant unless one is given, in minutes east of UTC.
19
+ */
20
+ export const localTimestamp = (
21
+ epochMs: number,
22
+ offsetMinutes: number = machineOffsetMinutes(epochMs),
23
+ ): string => {
24
+ const local = isoMillis(epochMs + offsetMinutes * 60_000).slice(0, 23);
25
+
26
+ const sign = offsetMinutes < 0 ? '-' : '+';
27
+
28
+ const hours = Math.floor(Math.abs(offsetMinutes) / 60)
29
+ .toString()
30
+ .padStart(2, '0');
31
+
32
+ const minutes = (Math.abs(offsetMinutes) % 60).toString().padStart(2, '0');
33
+
34
+ return `${local}${sign}${hours}:${minutes}` as const;
35
+ };
36
+
37
+ /** `2026-09-27T12:03:04.500Z`. */
38
+ const isoMillis = (epochMs: number): string => {
39
+ const date = new Date(epochMs);
40
+
41
+ return date.toISOString();
42
+ };
43
+
44
+ /** The machine's (`TZ`) offset from UTC at an instant, in minutes east. */
45
+ const machineOffsetMinutes = (epochMs: number): number => {
46
+ const date = new Date(epochMs);
47
+
48
+ return -date.getTimezoneOffset();
49
+ };
@@ -0,0 +1,237 @@
1
+ <!-- cspell:ignore unreviewed -->
2
+
3
+ # `open-pr`
4
+
5
+ `pnpm run open-pr` — opens the pull request for the current branch the way
6
+ this repository wants one opened: held by `skip-ci`, and never armed.
7
+
8
+ - [日本語](#日本語)
9
+ - [English](#english)
10
+
11
+ The conventions this follows are `CLAUDE.md`'s: "Commits and pull requests"
12
+ and "CI". This file describes what the script does with them.
13
+
14
+ ## 日本語
15
+
16
+ ### 何をするスクリプトか
17
+
18
+ 現在のブランチを push し、PR を **ready for review** で作り、**`skip-ci`** と
19
+ **`auto-rebase`** を付けます。することはこれだけです。マージもせず、
20
+ `merge-queued` も付けず(それはレビュー後に人が行うことです)、**auto-merge も
21
+ 張りません**。
22
+
23
+ ### auto-merge を張らない理由
24
+
25
+ `main` の ruleset は `required_approving_review_count: 0` なので、
26
+ `.github/CODEOWNERS` が挙げるパスの外では、auto-merge の張られた PR は緑に
27
+ なった瞬間にマージされます。レビューが済んでいるかどうかは関係ありません。
28
+ stack する PR(下記)ならなおさらで、base は ruleset の掛からないブランチなので、
29
+ 張った瞬間に下の層のブランチへマージされかねません。
30
+
31
+ そこで **auto-merge は `unblock-prs` が、`merge-queued` の付いた PR を pick した
32
+ ときに張ります**。キューに入れること( `merge-queued` )が「マージしてよい」の
33
+ 合図で、それより前には誰も張りません。`skip-ci` はチェックを止め、
34
+ `no-skip-ci-label` でマージも止めておくためのものです。
35
+
36
+ ### `auto-rebase` を付ける理由
37
+
38
+ `auto-rebase` は、レビューや順番を待っている間も `unblock-prs` に `main` へ追従
39
+ させるためのラベルです(`../unblock-prs/README.md`)。stack の
40
+ 場合も全層に付けます。`unblock-prs` が rebase するのは base が `main` の PR だけ
41
+ なので、上の層に付いていても、下の層がマージされるまでは何も起きません。その
42
+ 層が `main` に付け替えられたときに初めて効きますが、そのときに付ける仕組みは無
43
+ いので、最初から付けておきます。`skip-ci` の後に付けるのは、このラベルの
44
+ `labeled` で走る CI を `skip-ci` で止めるためです。
45
+
46
+ ### 認証情報
47
+
48
+ GitHub の API を直接叩きます(`gh` は要りません)。トークンは
49
+ **`GH_TOKEN` → `GITHUB_TOKEN` → `gh auth token`** の順に探します(前2つは `gh` 自身が読む順で、`unblock-prs` などほかのコマンドと同じアカウントになるように揃えています)。コンテナや
50
+ runner には前2つが既にあり、`gh` で設定しただけのマシンでは3つ目が拾われるので、
51
+ どちらでも何も書き出さずに動きます。どれも無ければその旨を言って止まります。
52
+
53
+ `gh` を捨てたのは、このコマンドが一番効く場所 — クラウドのコンテナや runner —
54
+ が `gh` の入っていない場所だからです。呼ぶ API は `gh` と同じものです。
55
+
56
+ 既存の PR が draft だったときに ready にする操作だけは REST に無いので、GraphQL
57
+ の mutation を使います。
58
+
59
+ **プロキシ配下でも動きます。** Node の `fetch` は `HTTPS_PROXY` を自動では見な
60
+ いため、スクリプト側で `NODE_USE_ENV_PROXY=1` を付けています。これが無いと
61
+ プロキシを迂回して `401 Bad credentials` になり、トークンの問題に見えます。
62
+
63
+ ### 途中で失敗したら、もう一度実行してください
64
+
65
+ 各ステップは既に済んでいればスキップされます。PR が既にあれば作らず、ラベルが
66
+ 既にあれば付け直しません。巻き戻す必要はありません。
67
+
68
+ ### 実行
69
+
70
+ ```bash
71
+ pnpm run open-pr # 既定
72
+ pnpm run open-pr -- --dry-run # 何をするかだけ表示
73
+ pnpm run open-pr -- --title 'fix: 説明' --body-file ./pr.md
74
+ pnpm run open-pr -- --merge-after '#1901' # 順序を宣言
75
+ pnpm run open-pr -- --base feat/lower-layer # その PR の上に積む
76
+ ```
77
+
78
+ | オプション | 既定 | 意味 |
79
+ | :------------------- | ------------------: | :---------------------------------------- |
80
+ | `--title <text>` | 直前のコミット件名 | PR タイトル(英語、Conventional Commits) |
81
+ | `--body-file <path>` | トレーラのみ / 空欄 | 本文を読むファイル |
82
+ | `--base <branch>` | リポジトリの既定値 | 対象ブランチ(PR のブランチなら stack) |
83
+ | `--merge-after <n>` | なし | `Merge-After:` を宣言(複数回可) |
84
+ | `--dry-run` | off | 何も触らずに手順だけ出す |
85
+
86
+ **本文は自分で書くものです。** `skip-ci` が付いている間はローカルで走らせた
87
+ チェックがそのブランチの唯一のチェックなので、どれを走らせたかを本文に書いて
88
+ ください。`--merge-after` を付けると `Merge-After:` 行が本文の先頭に入ります。
89
+
90
+ **閉じる issue の `Start-After:` も本文に書きます。** PR を作った後、GitHub が
91
+ 本文から読んだ closing keyword の issue を読み、その `Start-After:` が名指す
92
+ 開いた PR(開いた issue なら、それを閉じる開いた PR。GitHub の issue の依存関係
93
+ blocked by も同じに読みます)のうち本文にまだ無いものを、
94
+ 先頭の `Merge-After:` 行に足します(`docs/issue-start-after.md`)。`unblock-prs`
95
+ と各レポートは本文に無くても引き継ぐので、これは順序を本文にも書いておくための
96
+ ものです。サイドバーで手でリンクした issue は見ません。
97
+
98
+ ### stacked PR
99
+
100
+ `--base` に別の open な PR のブランチを渡すと、その PR の上に積んだ PR になり
101
+ ます。差分はこの層の分だけになり、`unblock-prs` と各レポートは下の層がマージ
102
+ されるまで待つものとして扱います。下の層の番号は `Merge-After:` として本文の
103
+ 先頭にも自動で入ります( `--merge-after` を併用すれば同じ行にまとめます)。
104
+ ツールは base だけでも stack を読むので、これは順序を本文にも書いておくための
105
+ ものです。
106
+
107
+ - その base を head とする open な PR がこのリポジトリにちょうど1本あること、
108
+ そして現在のブランチが `origin/<base>` の先端を含んでいることを、push の前に
109
+ 確かめます。どちらかが違えば何もせずに止まります。
110
+ - auto-merge はほかの PR と同じく張りません。`unblock-prs` は下の層がマージされ
111
+ て GitHub がこの PR を既定ブランチに付け替えるまで pick しないので、張られる
112
+ のもそのときです。
113
+ - **GitHub のネイティブ stack にはしないでください。** stack は base だけで
114
+ 作ります。GitHub はネイティブ stack に入った PR に auto-merge を張らせず、
115
+ 下の層がマージされて既定ブランチに付け替えられた後も stack から外さないので、
116
+ `unblock-prs` はその PR を pick せず、手でマージするよう報告します。
117
+
118
+ ### 拒否すること
119
+
120
+ - 既定ブランチの上にいるとき(先にブランチを切ってください)
121
+ - detached HEAD のとき
122
+ - 認証情報がどこからも取れないとき
123
+ - `--base` が open な PR のブランチでない、または現在のブランチがその先端を
124
+ 含まないとき
125
+
126
+ 既存の PR が draft だった場合は ready にします。`unblock-prs` は draft を pick
127
+ しないからです。
128
+
129
+ ### この後
130
+
131
+ レビューが終わったら **`merge-queued` を付けるだけ**です。順番が来ると
132
+ `unblock-prs` が rebase して auto-merge を張り、`skip-ci` を外し、緑になった
133
+ 時点で GitHub がマージします。**auto-merge を手で張らないでください** — それが
134
+ 未レビューのままマージさせる操作です。
135
+
136
+ ## English
137
+
138
+ `pnpm run open-pr` pushes the current branch, creates the pull request ready
139
+ for review, and adds `skip-ci` and `auto-rebase`. It merges nothing, never
140
+ adds `merge-queued`, which is a person's statement that the pull request has
141
+ been reviewed, and **never arms auto-merge**.
142
+
143
+ **Why it arms nothing.** The `main` ruleset asks for
144
+ `required_approving_review_count: 0`, so outside the paths `.github/CODEOWNERS`
145
+ lists an armed pull request merges the moment it goes green, reviewed or not.
146
+ A stacked one (below) is worse off: its base is a branch no ruleset covers, so
147
+ armed it could merge into the layer below at once. So **`unblock-prs` arms a
148
+ pull request when it picks it**, which it does only once it is labelled
149
+ `merge-queued` and onto the default branch: queueing is the signal that it may
150
+ merge, and nothing is armed before it. `skip-ci` holds the checks, and with
151
+ `no-skip-ci-label` the merge, until then.
152
+
153
+ **Why it adds `auto-rebase`.** The label asks `unblock-prs` to keep the pull
154
+ request on `main` while it waits for review or its turn
155
+ (`../unblock-prs/README.md`). It goes on every layer of a stack
156
+ too: `unblock-prs` rebases only a pull request based on `main`, so on a layer
157
+ above it does nothing until the layer below merges and GitHub moves this one
158
+ onto `main` — the moment it is needed, and one at which nothing would add it.
159
+ It goes on after `skip-ci`, so that the run its `labeled` event starts finds
160
+ the checks held.
161
+
162
+ It calls the GitHub API directly; `gh` is not required. The token is looked
163
+ for in **`GH_TOKEN`, then `GITHUB_TOKEN`, then `gh auth token`** — the order
164
+ `gh` itself reads the first two in, so that this acts as the account
165
+ `unblock-prs` does — the first
166
+ two are already set in a container or on a runner, and the third picks up a
167
+ machine set up with `gh` alone, so neither kind of machine has to export
168
+ anything. `gh` was dropped because the places this command is most useful are
169
+ the places `gh` is least likely to be installed; the API it calls is the same
170
+ one `gh` calls. Taking an existing pull request out of draft has no REST
171
+ equivalent and goes through GraphQL.
172
+
173
+ It works behind a proxy: Node's `fetch` does not read `HTTPS_PROXY` on its
174
+ own, so the script sets `NODE_USE_ENV_PROXY=1`. Without that it bypasses the
175
+ proxy and fails with `401 Bad credentials`, which reads like a token problem
176
+ and is not one.
177
+
178
+ Every step is skipped when it is already done, so a run that failed part way
179
+ through is finished by running it again rather than unpicked.
180
+
181
+ ```bash
182
+ pnpm run open-pr # the default
183
+ pnpm run open-pr -- --dry-run # say what would be done
184
+ pnpm run open-pr -- --title 'fix: a thing' --body-file ./pr.md
185
+ pnpm run open-pr -- --merge-after '#1901' # declare an order
186
+ pnpm run open-pr -- --base feat/lower-layer # stack it on that one
187
+ ```
188
+
189
+ | option | default | meaning |
190
+ | :------------------- | ---------------------: | :---------------------------------------- |
191
+ | `--title <text>` | last commit subject | the title, in English |
192
+ | `--body-file <path>` | the trailer, or empty | where to read the body from |
193
+ | `--base <branch>` | the repository default | what to target; a pull request's stacks |
194
+ | `--merge-after <n>` | none | declare a predecessor; repeat for several |
195
+ | `--dry-run` | off | print the steps and touch nothing |
196
+
197
+ The description is yours to write: while `skip-ci` is on, the checks you ran
198
+ locally are the only checks the branch gets, so say which ones. `--merge-after`
199
+ puts the `Merge-After:` line at the top of the body, where `unblock-prs` reads
200
+ it.
201
+
202
+ **What the issues it closes start after is written too.** Once the pull
203
+ request exists, the script reads the issues GitHub found closing keywords for
204
+ in the body, and adds to the `Merge-After:` line at the top the open pull
205
+ requests their `Start-After:` names — or, for an open issue named, the open
206
+ pull requests closing it; GitHub's "blocked by" counts the same — that the
207
+ body does not name yet
208
+ (`docs/issue-start-after.md`). `unblock-prs` and the reports inherit the same
209
+ without it; this is the body stating the order. An issue linked by hand in
210
+ the sidebar is not seen.
211
+
212
+ A `--base` that is another open pull request's branch stacks this one on it:
213
+ the diff is its own layer, and `unblock-prs` and the reports treat it as
214
+ waiting for the layer below. That layer is also written into the
215
+ `Merge-After:` line at the top of the body, first, alongside anything given
216
+ with `--merge-after`: the tools read the stack from the base alone, and the
217
+ trailer states the order in the body as every other declared order is. Before
218
+ pushing, the script checks that exactly one open pull request here is from
219
+ that branch and that the current branch contains the tip of `origin/<base>`,
220
+ and stops otherwise. `unblock-prs` does not pick it, and so does not arm it,
221
+ until the layer below has merged and GitHub has moved it onto the default
222
+ branch.
223
+
224
+ **Do not make it one of GitHub's native stacks**; the base alone is the
225
+ stack. GitHub refuses auto-merge on a pull request in a native stack, and
226
+ keeps it in the stack after the layer below has merged and it has been moved
227
+ onto the default branch, so `unblock-prs` does not pick it and reports it to
228
+ be merged by hand.
229
+
230
+ It refuses to run on the default branch, on a detached HEAD, and without a
231
+ credential. An existing pull request that is a draft is marked ready, because
232
+ `unblock-prs` does not pick a draft.
233
+
234
+ Afterwards, queueing it is one action: add `merge-queued` once it has been
235
+ reviewed. When its turn comes `unblock-prs` rebases it, arms auto-merge, takes
236
+ `skip-ci` off, and GitHub merges it once the checks are green. Arming it by
237
+ hand is the action that merges unreviewed work.