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,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Arr, Result, unknownToString } from 'ts-data-forge';
4
+ import {
5
+ REPORT_UNOWNED_PRS_HELP,
6
+ reportUnownedPrs,
7
+ } from '../report-unowned-prs/index.mjs';
8
+
9
+ const args = Arr.skip(process.argv, 2).filter((arg) => arg !== '--');
10
+
11
+ if (args.includes('--help') || args.includes('-h')) {
12
+ console.info(REPORT_UNOWNED_PRS_HELP);
13
+
14
+ process.exit(0);
15
+ }
16
+
17
+ const unknown = args.filter((arg) => arg !== '--dry-run');
18
+
19
+ if (Arr.isNonEmpty(unknown)) {
20
+ console.error(
21
+ `unexpected argument: ${unknown.join(' ')}\n\n${REPORT_UNOWNED_PRS_HELP}`,
22
+ );
23
+
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = await reportUnownedPrs(args.includes('--dry-run')).catch(
28
+ (error: unknown) => Result.err(unknownToString(error)),
29
+ );
30
+
31
+ if (Result.isErr(result)) {
32
+ console.error(result.value);
33
+
34
+ process.exit(1);
35
+ }
36
+
37
+ console.info(result.value);
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Arr, Result } from 'ts-data-forge';
4
+ import { HELP, parseOptions, unblockPrs } from '../unblock-prs/index.mjs';
5
+
6
+ const options = parseOptions(Arr.skip(process.argv, 2));
7
+
8
+ if (Result.isErr(options)) {
9
+ console.error(`${options.value}\n\n${HELP}`);
10
+
11
+ process.exit(2);
12
+ }
13
+
14
+ if (options.value === 'help') {
15
+ console.info(HELP);
16
+ } else {
17
+ const result = await unblockPrs(options.value);
18
+
19
+ if (Result.isErr(result)) {
20
+ console.error(result.value);
21
+
22
+ process.exit(1);
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ export * from './lib/index.mjs';
2
+
3
+ export { readQueueState, type QueueState } from './pr-report/index.mjs';
@@ -0,0 +1,86 @@
1
+ /** Reading a command's arguments, and the values more than one command takes. */
2
+
3
+ import * as util from 'node:util';
4
+ import { Num, Result, unknownToString } from 'ts-data-forge';
5
+
6
+ /**
7
+ * `util.parseArgs` over `args`, answering `'help'` when `-h` / `--help` was
8
+ * given and the values of `options` otherwise. `options` does not declare
9
+ * `help`; this does. Nothing throws: an unknown flag or a missing value is
10
+ * the error.
11
+ */
12
+ export const parseCommandArgs = <const O extends util.ParseArgsOptionsConfig>(
13
+ args: readonly string[],
14
+ options: O,
15
+ ): Result<
16
+ | ReturnType<
17
+ typeof util.parseArgs<Readonly<{ args: readonly string[]; options: O }>>
18
+ >['values']
19
+ | 'help',
20
+ string
21
+ > => {
22
+ // `pnpm run <command> -- --flag` forwards the `--` as well, and
23
+ // `parseArgs` would read everything after it as positionals.
24
+ const [first, ...rest] = args;
25
+
26
+ const argv: readonly string[] = Array.from(first === '--' ? rest : args);
27
+
28
+ // Asked first and on its own, leniently, so that `--help` answers even
29
+ // beside a flag the command would refuse.
30
+ const help = Result.fromThrowable(
31
+ () =>
32
+ util.parseArgs({
33
+ args: argv,
34
+ options: { help: { type: 'boolean', short: 'h' } },
35
+ strict: false,
36
+ }).values.help,
37
+ );
38
+
39
+ if (Result.isOk(help) && help.value === true) {
40
+ return Result.ok('help');
41
+ }
42
+
43
+ const parsed = Result.fromThrowable(() =>
44
+ util.parseArgs({ args: argv, options }),
45
+ );
46
+
47
+ return Result.isErr(parsed)
48
+ ? Result.err(unknownToString(parsed.value))
49
+ : Result.ok(parsed.value.values);
50
+ };
51
+
52
+ /**
53
+ * `raw` as a positive whole number, or `fallback` when the flag was not
54
+ * given. Rejected rather than clamped otherwise: `-3` or `two` is a typo, and
55
+ * a command that quietly used the default instead would do something other
56
+ * than what was asked for.
57
+ */
58
+ export const positiveInteger = (
59
+ flag: string,
60
+ raw: string | undefined,
61
+ fallback: number,
62
+ ): Result<number, string> => {
63
+ if (raw === undefined) {
64
+ return Result.ok(fallback);
65
+ }
66
+
67
+ const parsed = /^[1-9]\d*$/u.test(raw)
68
+ ? Result.unwrapOkOr(Num.safeParseInt(raw), undefined)
69
+ : undefined;
70
+
71
+ return parsed !== undefined && Number.isSafeInteger(parsed)
72
+ ? Result.ok(parsed)
73
+ : Result.err(
74
+ `--${flag} must be a positive whole number, got ${JSON.stringify(raw)}`,
75
+ );
76
+ };
77
+
78
+ /**
79
+ * The pull request `raw` names, or `undefined` when it names none. `#1901`
80
+ * and `1901` both name the same one, and a person copying from GitHub will
81
+ * paste the first.
82
+ */
83
+ export const pullRequestNumber = (raw: string): number | undefined =>
84
+ /^#?[1-9]\d*$/u.test(raw)
85
+ ? Result.unwrapOkOr(Num.safeParseInt(raw.replace(/^#/u, '')), undefined)
86
+ : undefined;
@@ -0,0 +1,92 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import {
3
+ parseCommandArgs,
4
+ positiveInteger,
5
+ pullRequestNumber,
6
+ } from './cli-args.mjs';
7
+
8
+ describe(parseCommandArgs, () => {
9
+ const options = {
10
+ title: { type: 'string' },
11
+ 'dry-run': { type: 'boolean', default: false },
12
+ } as const;
13
+
14
+ test('reads the flags the command declares', () => {
15
+ assert.deepStrictEqual(
16
+ parseCommandArgs(['--title', 'x', '--dry-run'], options),
17
+ Result.ok({ title: 'x', 'dry-run': true }),
18
+ );
19
+ });
20
+
21
+ test('drops the `--` that `pnpm run <command> --` forwards', () => {
22
+ assert.deepStrictEqual(
23
+ parseCommandArgs(['--', '--dry-run'], options),
24
+ Result.ok({ 'dry-run': true }),
25
+ );
26
+ });
27
+
28
+ test('answers help for `--help` and `-h`, even beside a flag it would refuse', () => {
29
+ assert.deepStrictEqual(
30
+ parseCommandArgs(['--help'], options),
31
+ Result.ok('help'),
32
+ );
33
+
34
+ assert.deepStrictEqual(
35
+ parseCommandArgs(['--', '-h'], options),
36
+ Result.ok('help'),
37
+ );
38
+
39
+ assert.deepStrictEqual(
40
+ parseCommandArgs(['--nope', '--help'], options),
41
+ Result.ok('help'),
42
+ );
43
+ });
44
+
45
+ test('says what was wrong with an unknown flag, rather than throwing', () => {
46
+ const parsed = parseCommandArgs(['--nope'], options);
47
+
48
+ assert.isTrue(Result.isErr(parsed));
49
+
50
+ assert.include(parsed.value, '--nope');
51
+ });
52
+ });
53
+
54
+ describe(positiveInteger, () => {
55
+ test('falls back when the flag was not given', () => {
56
+ assert.deepStrictEqual(
57
+ positiveInteger('limit', undefined, 7),
58
+ Result.ok(7),
59
+ );
60
+ });
61
+
62
+ test('reads a positive whole number', () => {
63
+ assert.deepStrictEqual(positiveInteger('limit', '12', 7), Result.ok(12));
64
+ });
65
+
66
+ test.each(['0', '-3', '1.5', '12abc', 'two', ''])(
67
+ 'refuses %j rather than falling back',
68
+ (raw) => {
69
+ assert.deepStrictEqual(
70
+ positiveInteger('limit', raw, 7),
71
+ Result.err(
72
+ `--limit must be a positive whole number, got ${JSON.stringify(raw)}`,
73
+ ),
74
+ );
75
+ },
76
+ );
77
+ });
78
+
79
+ describe(pullRequestNumber, () => {
80
+ test('reads a number with or without the `#`', () => {
81
+ assert.strictEqual(pullRequestNumber('1901'), 1901);
82
+
83
+ assert.strictEqual(pullRequestNumber('#1901'), 1901);
84
+ });
85
+
86
+ test.each(['0', '#', '##1', '-1', '19o1', ''])(
87
+ 'names nothing by %j',
88
+ (raw) => {
89
+ assert.isUndefined(pullRequestNumber(raw));
90
+ },
91
+ );
92
+ });
@@ -0,0 +1,463 @@
1
+ /**
2
+ * The GitHub API over `fetch`, and the credential it needs, for every command.
3
+ *
4
+ * `open-pr` and `unblock-prs` used to shell out to `gh`. They do not any
5
+ * more, because the places they are most useful — a cloud container, a
6
+ * runner, a repository other than this one — are the places `gh` is least
7
+ * likely to be installed, while a token is already there. The
8
+ * API is the same one `gh` calls; only where the credential comes from
9
+ * differs, so `gh auth token` stays as the last resort for a machine set up
10
+ * that way with nothing exported.
11
+ *
12
+ * `mature-updates.mts` keeps a client of its own: it runs before
13
+ * `pnpm install`, when nothing this imports exists yet.
14
+ */
15
+
16
+ import * as nodePath from 'node:path';
17
+ import type { RepoRef } from 'pr-report-core';
18
+ import { Arr, Json, Result, unknownToString } from 'ts-data-forge';
19
+ import * as t from 'ts-fortress';
20
+ import type { ReadonlyRecord } from 'ts-type-forge';
21
+ import { parseJson, readJsonFile } from './json.mjs';
22
+ import { git, repoRoot } from './shell.mjs';
23
+
24
+ /** What every call here needs: who to talk to, and as whom. */
25
+ export type ApiContext = Readonly<{ repo: RepoRef; token: string }>;
26
+
27
+ /**
28
+ * The token the environment holds, if any: `GH_TOKEN`, then `GITHUB_TOKEN`.
29
+ *
30
+ * That is the order `gh` reads them in, so that with both set every command
31
+ * here acts as the same account as `gh` would — the commands trust a comment
32
+ * by whose account wrote it, and `unblock-prs`'s set-aside comments were
33
+ * written through `gh` before it moved here. A workflow sets either only by passing it; Actions
34
+ * exports neither by itself. `github-settings-as-code` reads a list of its
35
+ * own, without `GITHUB_TOKEN`, and says why in `resolveAuthToken`.
36
+ */
37
+ export const tokenFromEnv = (): string | undefined => {
38
+ const token = process.env['GH_TOKEN'] ?? process.env['GITHUB_TOKEN'] ?? '';
39
+
40
+ return token === '' ? undefined : token;
41
+ };
42
+
43
+ /**
44
+ * The token to authenticate with: the environment first, `gh` only if it has
45
+ * to be. `GH_TOKEN` and `GITHUB_TOKEN` are what a runner and a container
46
+ * already set, and asking `gh` first would fail on exactly the machines this
47
+ * is for.
48
+ */
49
+ export const resolveToken = async (): Promise<Result<string, string>> => {
50
+ const fromEnv = tokenFromEnv();
51
+
52
+ if (fromEnv !== undefined) {
53
+ return Result.ok(fromEnv);
54
+ }
55
+
56
+ const fromGh = await git('gh auth token');
57
+
58
+ if (Result.isErr(fromGh)) {
59
+ return Result.err(
60
+ 'no credential: set GH_TOKEN or GITHUB_TOKEN, or authenticate `gh`',
61
+ );
62
+ }
63
+
64
+ const trimmed = fromGh.value.trim();
65
+
66
+ return trimmed === ''
67
+ ? Result.err(
68
+ 'no credential: set GH_TOKEN or GITHUB_TOKEN, or authenticate `gh`',
69
+ )
70
+ : Result.ok(trimmed);
71
+ };
72
+
73
+ /**
74
+ * The repository a command acts on, for every command here: `repoOption`
75
+ * (`--repo owner/name`) when given, else `origin`, else the root
76
+ * `package.json`.
77
+ *
78
+ * `origin` before the manifest because a fork's checkout names the fork
79
+ * there and the upstream in the manifest, and a command that reads the queue
80
+ * of one and pushes or comments on the other is worse than either. The
81
+ * manifest is for a checkout whose `origin` names no GitHub repository at
82
+ * all — a proxy remote in a container, a tree without `.git` — where it is
83
+ * the only thing left that says.
84
+ */
85
+ export const resolveRepo = async (
86
+ repoOption?: string,
87
+ ): Promise<Result<RepoRef, string>> => {
88
+ if (repoOption !== undefined) {
89
+ return parseRepoRef(repoOption);
90
+ }
91
+
92
+ const origin = await git('git remote get-url origin');
93
+
94
+ const fromOrigin = Result.isOk(origin)
95
+ ? repoOfUrl(origin.value.trim())
96
+ : undefined;
97
+
98
+ if (fromOrigin !== undefined) {
99
+ return Result.ok(fromOrigin);
100
+ }
101
+
102
+ const manifest = await readJsonFile(
103
+ nodePath.join(await repoRoot(), 'package.json'),
104
+ PackageJsonSchema,
105
+ );
106
+
107
+ const fromManifest = Result.isOk(manifest)
108
+ ? repoOfUrl(manifest.value.repository.url)
109
+ : undefined;
110
+
111
+ return fromManifest === undefined
112
+ ? Result.err(
113
+ 'cannot tell which repository this is: origin is not a GitHub remote, and the root package.json names none; pass --repo owner/name',
114
+ )
115
+ : Result.ok(fromManifest);
116
+ };
117
+
118
+ /** `owner/name`, as it is written everywhere else. */
119
+ export const parseRepoRef = (raw: string): Result<RepoRef, string> => {
120
+ const parts = raw.split('/');
121
+
122
+ if (!Arr.isFixedLengthArray(2, parts)) {
123
+ return Result.err(`expected owner/name, got ${JSON.stringify(raw)}`);
124
+ }
125
+
126
+ const [owner, name] = parts;
127
+
128
+ return owner === '' || name === ''
129
+ ? Result.err(`expected owner/name, got ${JSON.stringify(raw)}`)
130
+ : Result.ok({ owner, name });
131
+ };
132
+
133
+ /**
134
+ * The GitHub repository a remote or `repository.url` names, in the forms a
135
+ * checkout here is cloned with or a manifest declares:
136
+ * `https://github.com/owner/name(.git)`, `git+https://…`, and
137
+ * `git@github.com:owner/name(.git)`.
138
+ */
139
+ export const repoOfUrl = (url: string): RepoRef | undefined => {
140
+ const match =
141
+ /github\.com[/:](?<owner>[\w.-]+)\/(?<name>[\w.-]+?)(?:\.git)?$/u.exec(url);
142
+
143
+ const owner = match?.groups?.['owner'];
144
+
145
+ const name = match?.groups?.['name'];
146
+
147
+ return owner === undefined || name === undefined
148
+ ? undefined
149
+ : { owner, name };
150
+ };
151
+
152
+ const PackageJsonSchema = t.record({
153
+ repository: t.record({ url: t.string() }),
154
+ });
155
+
156
+ /**
157
+ * The token and the repository, for a command that calls {@link rest}.
158
+ * `repoOption` is a `--repo owner/name` the command was given, if any
159
+ * ({@link resolveRepo}).
160
+ */
161
+ export const resolveApiContext = async (
162
+ repoOption?: string,
163
+ ): Promise<Result<ApiContext, string>> => {
164
+ const token = await resolveToken();
165
+
166
+ if (Result.isErr(token)) {
167
+ return token;
168
+ }
169
+
170
+ const repo = await resolveRepo(repoOption);
171
+
172
+ return Result.isErr(repo)
173
+ ? repo
174
+ : Result.ok({ repo: repo.value, token: token.value });
175
+ };
176
+
177
+ /**
178
+ * One REST call, resolving to the parsed body. A non-2xx becomes an error
179
+ * naming the status and what GitHub said about it, because "403" on its own
180
+ * sends the reader to the wrong place.
181
+ */
182
+ export const rest = async <A,>(
183
+ context: ApiContext,
184
+ method: HttpMethod,
185
+ path: string,
186
+ schema: t.Type<A>,
187
+ { query, body, withBodyHtml = false }: RestOptions = {},
188
+ ): Promise<Result<A, string>> => {
189
+ // `URLSearchParams` rather than hand-assembled pairs: a branch name carries
190
+ // slashes and the one qualified parameter here carries a colon.
191
+ const parameters =
192
+ query === undefined ? undefined : new URLSearchParams({ ...query });
193
+
194
+ const search =
195
+ parameters === undefined ? '' : (`?${parameters.toString()}` as const);
196
+
197
+ const response = await send({
198
+ url: `https://api.github.com/repos/${context.repo.owner}/${context.repo.name}${path}${search}`,
199
+ method,
200
+ token: context.token,
201
+ accept: withBodyHtml
202
+ ? 'application/vnd.github.full+json'
203
+ : 'application/vnd.github+json',
204
+ body: body === undefined ? undefined : JSON.stringify(body),
205
+ });
206
+
207
+ return Result.isErr(response)
208
+ ? Result.err(response.value.message)
209
+ : validate(response.value, schema);
210
+ };
211
+
212
+ /**
213
+ * The login the token authenticates as. Outside `rest` because the route is
214
+ * not under the repository.
215
+ */
216
+ export const viewerLogin = async (
217
+ token: string,
218
+ ): Promise<Result<string, string>> => {
219
+ const response = await send({
220
+ url: 'https://api.github.com/user',
221
+ method: 'GET',
222
+ token,
223
+ accept: 'application/vnd.github+json',
224
+ body: undefined,
225
+ });
226
+
227
+ return Result.isErr(response)
228
+ ? Result.err(response.value.message)
229
+ : Result.map(
230
+ validate(response.value, t.record({ login: t.string() })),
231
+ ({ login }) => login,
232
+ );
233
+ };
234
+
235
+ /** What a REST call may carry besides its path. */
236
+ export type RestOptions = Readonly<{
237
+ query?: ReadonlyRecord<string, string>;
238
+ body?: unknown;
239
+ /**
240
+ * Ask for `body_html` beside `body`: GitHub's rendering, which marks the
241
+ * closing keywords it recognized (`pr-report-core`'s
242
+ * `closingKeywordIssuesIn`).
243
+ */
244
+ withBodyHtml?: boolean;
245
+ }>;
246
+
247
+ /**
248
+ * One GraphQL mutation. Two of `open-pr`'s four writes have no REST
249
+ * equivalent — GitHub exposes neither "mark ready for review" nor "enable
250
+ * auto-merge" there — so those go through here rather than through a
251
+ * different tool.
252
+ */
253
+ export const graphql = async (
254
+ context: ApiContext,
255
+ query: string,
256
+ variables: GraphqlVariables,
257
+ ): Promise<Result<undefined, string>> =>
258
+ Result.map(
259
+ await graphqlQuery(context, query, variables, t.unknown()),
260
+ () => undefined,
261
+ );
262
+
263
+ /**
264
+ * One GraphQL request, resolving to its `data` validated by `schema`.
265
+ *
266
+ * GraphQL answers 200 to a failed request and puts the reason in `errors`,
267
+ * so that is checked rather than the status alone. A partial answer — `data`
268
+ * beside `errors` — is still a failure: what is missing from it is exactly
269
+ * what the caller asked for.
270
+ */
271
+ export const graphqlQuery = async <A,>(
272
+ context: ApiContext,
273
+ query: string,
274
+ variables: GraphqlVariables,
275
+ schema: t.Type<A>,
276
+ ): Promise<Result<A, string>> => {
277
+ const response = await send({
278
+ url: 'https://api.github.com/graphql',
279
+ method: 'POST',
280
+ token: context.token,
281
+ accept: 'application/json',
282
+ body: JSON.stringify({ query, variables }),
283
+ });
284
+
285
+ if (Result.isErr(response)) {
286
+ return Result.err(response.value.message);
287
+ }
288
+
289
+ const parsed = validate(response.value, GraphqlResponseSchema);
290
+
291
+ if (Result.isErr(parsed)) {
292
+ return parsed;
293
+ }
294
+
295
+ const errors = parsed.value.errors ?? [];
296
+
297
+ if (Arr.isNonEmpty(errors)) {
298
+ return Result.err(errors.map((error) => error.message).join('; '));
299
+ }
300
+
301
+ const data = schema.validate(parsed.value.data);
302
+
303
+ return Result.isErr(data)
304
+ ? Result.err(t.validationErrorsToMessages(data.value).join('\n'))
305
+ : Result.ok(data.value);
306
+ };
307
+
308
+ /** What a GraphQL request's variables may hold: names, numbers and ids. */
309
+ export type GraphqlVariables = ReadonlyRecord<string, number | string>;
310
+
311
+ const GraphqlResponseSchema = t.record({
312
+ data: t.optional(t.unknown()),
313
+ errors: t.optional(t.array(t.record({ message: t.string() }))),
314
+ });
315
+
316
+ /** The methods the commands call the REST API with. */
317
+ export type HttpMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST';
318
+
319
+ /**
320
+ * Adds `label` to a pull request. Labels hang off the issue, which is the
321
+ * same object as the pull request.
322
+ */
323
+ export const addLabel = async (
324
+ context: ApiContext,
325
+ prNumber: number,
326
+ label: string,
327
+ ): Promise<Result<undefined, string>> =>
328
+ Result.map(
329
+ await rest(context, 'POST', `/issues/${prNumber}/labels`, t.unknown(), {
330
+ body: { labels: [label] },
331
+ }),
332
+ () => undefined,
333
+ );
334
+
335
+ /**
336
+ * Takes `label` off a pull request. A label already off is not an error, as
337
+ * it is not to `gh pr edit --remove-label`: GitHub answers 404, and what the
338
+ * caller wanted is true.
339
+ */
340
+ export const removeLabel = async (
341
+ context: ApiContext,
342
+ prNumber: number,
343
+ label: string,
344
+ ): Promise<Result<undefined, string>> => {
345
+ // One path segment, whatever the name holds. The rule's premise, building
346
+ // a query string, is not what this is.
347
+ // eslint-disable-next-line no-restricted-globals
348
+ const segment = encodeURIComponent(label);
349
+
350
+ const removed = await rest(
351
+ context,
352
+ 'DELETE',
353
+ `/issues/${prNumber}/labels/${segment}`,
354
+ t.unknown(),
355
+ );
356
+
357
+ return Result.isOk(removed) || removed.value.startsWith('404 ')
358
+ ? Result.ok(undefined)
359
+ : removed;
360
+ };
361
+
362
+ /** One request to the API, before anything is made of the answer. */
363
+ export type GitHubRequest = Readonly<{
364
+ url: string;
365
+ method: HttpMethod;
366
+ /** Absent for an anonymous read, which a public repository allows. */
367
+ token: string | undefined;
368
+ accept: string;
369
+ body: string | undefined;
370
+ }>;
371
+
372
+ /**
373
+ * Why a request came to nothing: the transport failing, or GitHub refusing
374
+ * with a status and what it said about it.
375
+ */
376
+ export type GitHubRefusal = Readonly<{
377
+ message: string;
378
+ /**
379
+ * Whether the refusal is the rate limit being spent — the one failure a
380
+ * reader can do something about without reading the body of the answer.
381
+ */
382
+ rateLimited: boolean;
383
+ }>;
384
+
385
+ /**
386
+ * The request itself, resolving to the response body as text. A transport
387
+ * failure and a refusal both become a {@link GitHubRefusal}; nothing here
388
+ * throws.
389
+ *
390
+ * The headers are assembled here rather than passed in, so that every call
391
+ * the commands make carries the same ones and there is one place to read.
392
+ */
393
+ export const send = async ({
394
+ url,
395
+ method,
396
+ token,
397
+ accept,
398
+ body,
399
+ }: GitHubRequest): Promise<Result<string, GitHubRefusal>> => {
400
+ const response = await Result.fromPromise(
401
+ fetch(url, {
402
+ method,
403
+ headers: {
404
+ ...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
405
+ Accept: accept,
406
+ 'X-GitHub-Api-Version': '2022-11-28',
407
+ // GitHub rejects an API request without one.
408
+ 'User-Agent': 'noshiro-pf-mono-tools',
409
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
410
+ },
411
+ ...(body === undefined ? {} : { body }),
412
+ }),
413
+ );
414
+
415
+ if (Result.isErr(response)) {
416
+ return Result.err({
417
+ message: `request failed: ${unknownToString(response.value)}`,
418
+ rateLimited: false,
419
+ });
420
+ }
421
+
422
+ const read = await Result.fromPromise(response.value.text());
423
+
424
+ const payload = Result.isOk(read) ? read.value : '';
425
+
426
+ return response.value.ok
427
+ ? Result.ok(payload)
428
+ : Result.err({
429
+ message: `${response.value.status} ${response.value.statusText}: ${messageFrom(payload)}`,
430
+ rateLimited:
431
+ response.value.headers.get('x-ratelimit-remaining') === '0',
432
+ });
433
+ };
434
+
435
+ /** What GitHub says went wrong, or the raw body when it says nothing. */
436
+ const messageFrom = (body: string): string => {
437
+ const parsed = Json.parse(body);
438
+
439
+ if (Result.isErr(parsed)) {
440
+ return body.slice(0, 200);
441
+ }
442
+
443
+ const validated = t
444
+ .record({ message: t.optional(t.string()) })
445
+ .validate(parsed.value);
446
+
447
+ return Result.isOk(validated) && validated.value.message !== undefined
448
+ ? validated.value.message
449
+ : body.slice(0, 200);
450
+ };
451
+
452
+ const validate = <A,>(body: string, schema: t.Type<A>): Result<A, string> => {
453
+ // `204 No Content`, which a workflow dispatch answers, is no JSON at all.
454
+ if (body === '') {
455
+ const validated = schema.validate(undefined);
456
+
457
+ return Result.isErr(validated)
458
+ ? Result.err(t.validationErrorsToMessages(validated.value).join('\n'))
459
+ : Result.ok(validated.value);
460
+ }
461
+
462
+ return parseJson(body, schema, 'the answer from GitHub');
463
+ };