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,538 @@
1
+ /**
2
+ * What the merge is waiting for, read from the head commit's checks and
3
+ * judged against the contexts the ruleset requires.
4
+ */
5
+
6
+ import {
7
+ combineContextStates,
8
+ epochMsOf,
9
+ summarizeChecks as summarizeContexts,
10
+ type ContextState,
11
+ } from 'pr-report-core';
12
+ import { Arr, Num, Result } from 'ts-data-forge';
13
+ import * as t from 'ts-fortress';
14
+ import { graphqlQuery, type ApiContext } from '../lib/index.mjs';
15
+ import type { ChecksSummary } from './types.mjs';
16
+
17
+ /**
18
+ * One check on the head, in the shape `gh pr checks --json` printed, which
19
+ * is what this script read until it called the API itself.
20
+ */
21
+ export type ReportedCheck = Readonly<{
22
+ name: string;
23
+ /** `pass` | `fail` | `pending` | `skipping` | `cancel` */
24
+ bucket: string;
25
+ link: string;
26
+ /** ISO 8601; GitHub's zero time before the check has started. */
27
+ startedAt: string;
28
+ /** ISO 8601; GitHub's zero time before the check has finished. */
29
+ completedAt: string;
30
+ /** The workflow the job belongs to; empty for a commit status. */
31
+ workflow: string;
32
+ }>;
33
+
34
+ /**
35
+ * A check on the head that has not finished, when it started, and the
36
+ * workflow run it is a job of.
37
+ */
38
+ export type RunningCheck = Readonly<{
39
+ name: string;
40
+ startedAt: string;
41
+ workflow: string;
42
+ link: string;
43
+ }>;
44
+
45
+ /**
46
+ * The checks that gate the merge, as GitHub reports them for the head commit.
47
+ * `isRequired` is GitHub's own answer for this pull request, so the list
48
+ * follows the ruleset without this script having to know it.
49
+ */
50
+ export const listRequiredChecks = async (
51
+ api: ApiContext,
52
+ prNumber: number,
53
+ ): Promise<Result<readonly ReportedCheck[], string>> =>
54
+ Result.map(await readCheckContexts(api, prNumber), (contexts) =>
55
+ reportedChecksOf(contexts, 'required'),
56
+ );
57
+
58
+ /**
59
+ * The checks on the head commit that have not finished, required or not.
60
+ *
61
+ * A required aggregate has no run in a new round until the jobs it waits on
62
+ * are done, so until then the required ones answer with the round before.
63
+ * When that round was the one `skip-ci` skipped, every aggregate reads
64
+ * `skipped` and the commit looks green halfway through its matrix. Anything
65
+ * still running is what says the round is not over, and when it started is
66
+ * what tells a failure of the round before from one of this round.
67
+ */
68
+ export const listRunningChecks = async (
69
+ api: ApiContext,
70
+ prNumber: number,
71
+ ): Promise<Result<readonly RunningCheck[], string>> =>
72
+ Result.map(await readCheckContexts(api, prNumber), (contexts) =>
73
+ reportedChecksOf(contexts, 'all')
74
+ .filter((check) => check.bucket === 'pending')
75
+ .map(({ name, startedAt, workflow, link }) => ({
76
+ name,
77
+ startedAt,
78
+ workflow,
79
+ link,
80
+ })),
81
+ );
82
+
83
+ /**
84
+ * What the head commit's checks say, as `gh pr checks` said it: each check
85
+ * run and commit status in a bucket, and of the runs of one job in one
86
+ * workflow and event only the latest — a re-run, or the round `skip-ci`
87
+ * cancelled under the one now running — so that a superseded failure does
88
+ * not speak for its context. `which` is `'required'` for only what GitHub
89
+ * says this pull request requires.
90
+ *
91
+ * Unlike `gh`, the survivors keep the order GitHub listed them in. A commit
92
+ * status's start and finish are both when it was set, the one time it has.
93
+ */
94
+ export const reportedChecksOf = (
95
+ contexts: readonly CheckContextNode[],
96
+ which: 'all' | 'required',
97
+ ): readonly ReportedCheck[] => {
98
+ const mut_latest = new Map<string, CheckContextNode>();
99
+
100
+ for (const context of contexts) {
101
+ const key = keyOf(context);
102
+
103
+ const kept = mut_latest.get(key);
104
+
105
+ if (kept === undefined || startedAtOf(context) > startedAtOf(kept)) {
106
+ mut_latest.set(key, context);
107
+ }
108
+ }
109
+
110
+ const survivors = new Set(mut_latest.values());
111
+
112
+ return contexts
113
+ .filter(
114
+ (context) =>
115
+ survivors.has(context) && (which === 'all' || context.isRequired),
116
+ )
117
+ .map(reportedCheckOf);
118
+ };
119
+
120
+ /** A context of the head commit's status check rollup, as read. */
121
+ export type CheckContextNode = t.TypeOf<typeof CheckContextNodeSchema>;
122
+
123
+ /**
124
+ * Every context of the pull request's head commit, page by page. A head with
125
+ * none reported yet — right after a push — is an empty list.
126
+ */
127
+ const readCheckContexts = async (
128
+ api: ApiContext,
129
+ prNumber: number,
130
+ ): Promise<Result<readonly CheckContextNode[], string>> => {
131
+ const mut_contexts: CheckContextNode[] = [];
132
+
133
+ let mut_after: string | undefined = undefined;
134
+
135
+ for (const _page of Arr.seq(MAX_PAGES)) {
136
+ const rollup = await readRollupPage(api, prNumber, mut_after);
137
+
138
+ if (Result.isErr(rollup)) {
139
+ return rollup;
140
+ }
141
+
142
+ if (rollup.value === null) {
143
+ return Result.ok(mut_contexts);
144
+ }
145
+
146
+ mut_contexts.push(...rollup.value.nodes);
147
+
148
+ const { hasNextPage, endCursor } = rollup.value.pageInfo;
149
+
150
+ if (!hasNextPage || endCursor === null) {
151
+ return Result.ok(mut_contexts);
152
+ }
153
+
154
+ mut_after = endCursor;
155
+ }
156
+
157
+ return Result.err(
158
+ `#${prNumber} has more than ${MAX_PAGES * PAGE_SIZE} checks on its head`,
159
+ );
160
+ };
161
+
162
+ /**
163
+ * One page of the head commit's contexts, from `after` on; `null` when the
164
+ * head has no rollup yet.
165
+ */
166
+ const readRollupPage = async (
167
+ api: ApiContext,
168
+ prNumber: number,
169
+ after: string | undefined,
170
+ ): Promise<Result<RollupPage | null, string>> =>
171
+ Result.map(
172
+ await graphqlQuery(
173
+ api,
174
+ CHECKS_QUERY,
175
+ {
176
+ owner: api.repo.owner,
177
+ name: api.repo.name,
178
+ number: prNumber,
179
+ ...(after === undefined ? {} : { after }),
180
+ },
181
+ ChecksAnswerSchema,
182
+ ),
183
+ (answer) =>
184
+ answer.repository.pullRequest.commits.nodes[0]?.commit.statusCheckRollup
185
+ ?.contexts ?? null,
186
+ );
187
+
188
+ type RollupPage = Readonly<{
189
+ pageInfo: Readonly<{ hasNextPage: boolean; endCursor: string | null }>;
190
+ nodes: readonly CheckContextNode[];
191
+ }>;
192
+
193
+ const PAGE_SIZE = 100;
194
+
195
+ const MAX_PAGES = 10;
196
+
197
+ const CHECKS_QUERY = [
198
+ 'query($owner: String!, $name: String!, $number: Int!, $after: String) {',
199
+ ' repository(owner: $owner, name: $name) {',
200
+ ' pullRequest(number: $number) {',
201
+ ' commits(last: 1) { nodes { commit { statusCheckRollup {',
202
+ ` contexts(first: ${PAGE_SIZE}, after: $after) {`,
203
+ ' pageInfo { hasNextPage endCursor }',
204
+ ' nodes {',
205
+ ' __typename',
206
+ ' ... on CheckRun {',
207
+ ' name status conclusion startedAt completedAt detailsUrl',
208
+ ' isRequired(pullRequestNumber: $number)',
209
+ ' checkSuite { workflowRun { event workflow { name } } }',
210
+ ' }',
211
+ ' ... on StatusContext {',
212
+ ' context state targetUrl createdAt',
213
+ ' isRequired(pullRequestNumber: $number)',
214
+ ' }',
215
+ ' }',
216
+ ' }',
217
+ ' } } }',
218
+ ' }',
219
+ ' }',
220
+ '}',
221
+ ].join('\n');
222
+
223
+ const CheckContextNodeSchema = t.union([
224
+ t.record({
225
+ __typename: t.literal('CheckRun'),
226
+ name: t.string(),
227
+ status: t.string(),
228
+ conclusion: t.union([t.string(), t.nullType]),
229
+ startedAt: t.union([t.string(), t.nullType]),
230
+ completedAt: t.union([t.string(), t.nullType]),
231
+ detailsUrl: t.union([t.string(), t.nullType]),
232
+ isRequired: t.boolean(),
233
+ checkSuite: t.union([
234
+ t.record({
235
+ workflowRun: t.union([
236
+ t.record({
237
+ event: t.string(),
238
+ workflow: t.record({ name: t.string() }),
239
+ }),
240
+ t.nullType,
241
+ ]),
242
+ }),
243
+ t.nullType,
244
+ ]),
245
+ }),
246
+ t.record({
247
+ __typename: t.literal('StatusContext'),
248
+ context: t.string(),
249
+ state: t.string(),
250
+ targetUrl: t.union([t.string(), t.nullType]),
251
+ createdAt: t.string(),
252
+ isRequired: t.boolean(),
253
+ }),
254
+ ]);
255
+
256
+ const ChecksAnswerSchema = t.record({
257
+ repository: t.record({
258
+ pullRequest: t.record({
259
+ commits: t.record({
260
+ nodes: t.array(
261
+ t.record({
262
+ commit: t.record({
263
+ statusCheckRollup: t.union([
264
+ t.record({
265
+ contexts: t.record({
266
+ pageInfo: t.record({
267
+ hasNextPage: t.boolean(),
268
+ endCursor: t.union([t.string(), t.nullType]),
269
+ }),
270
+ nodes: t.array(CheckContextNodeSchema),
271
+ }),
272
+ }),
273
+ t.nullType,
274
+ ]),
275
+ }),
276
+ }),
277
+ ),
278
+ }),
279
+ }),
280
+ }),
281
+ });
282
+
283
+ /** What `gh` wrote for a time a check has not reached. */
284
+ const ZERO_TIME = '0001-01-01T00:00:00Z';
285
+
286
+ /**
287
+ * Which runs supersede each other: a commit status by its context, a check
288
+ * run by its name, workflow and event, as `gh` told them apart.
289
+ */
290
+ const keyOf = (context: CheckContextNode): string =>
291
+ context.__typename === 'StatusContext'
292
+ ? (`status:${context.context}` as const)
293
+ : [
294
+ 'run',
295
+ context.name,
296
+ context.checkSuite?.workflowRun?.workflow.name ?? '',
297
+ context.checkSuite?.workflowRun?.event ?? '',
298
+ ].join(':');
299
+
300
+ const startedAtOf = (context: CheckContextNode): string =>
301
+ (context.__typename === 'StatusContext'
302
+ ? context.createdAt
303
+ : context.startedAt) ?? ZERO_TIME;
304
+
305
+ const reportedCheckOf = (context: CheckContextNode): ReportedCheck =>
306
+ context.__typename === 'StatusContext'
307
+ ? ({
308
+ name: context.context,
309
+ bucket: bucketOf(context.state),
310
+ link: context.targetUrl ?? '',
311
+ startedAt: context.createdAt,
312
+ completedAt: context.createdAt,
313
+ workflow: '',
314
+ } as const)
315
+ : ({
316
+ name: context.name,
317
+ bucket: bucketOf(
318
+ context.status === 'COMPLETED'
319
+ ? (context.conclusion ?? '')
320
+ : context.status,
321
+ ),
322
+ link: context.detailsUrl ?? '',
323
+ startedAt: context.startedAt ?? ZERO_TIME,
324
+ completedAt: context.completedAt ?? ZERO_TIME,
325
+ workflow: context.checkSuite?.workflowRun?.workflow.name ?? '',
326
+ } as const);
327
+
328
+ /**
329
+ * A check run's conclusion, or its status while it runs, or a commit
330
+ * status's state, in `gh pr checks`'s buckets. Anything else — `QUEUED`,
331
+ * `IN_PROGRESS`, `PENDING`, `EXPECTED`, `STALE` — is waited for.
332
+ */
333
+ const bucketOf = (state: string): string => {
334
+ switch (state) {
335
+ case 'SUCCESS':
336
+ return 'pass';
337
+
338
+ case 'SKIPPED':
339
+ case 'NEUTRAL':
340
+ return 'skipping';
341
+
342
+ case 'ERROR':
343
+ case 'FAILURE':
344
+ case 'TIMED_OUT':
345
+ case 'ACTION_REQUIRED':
346
+ case 'STARTUP_FAILURE':
347
+ return 'fail';
348
+
349
+ case 'CANCELLED':
350
+ return 'cancel';
351
+
352
+ default:
353
+ return 'pending';
354
+ }
355
+ };
356
+
357
+ /**
358
+ * `checks` is what {@link listRequiredChecks} reported for the head commit;
359
+ * `requiredContexts` is what the ruleset asks for. The difference between the
360
+ * two is the point: a context in the second and not the first has not
361
+ * reported yet, and GitHub will not merge until it does. `running` is every
362
+ * check on the head that has not finished ({@link listRunningChecks}); while
363
+ * it is not empty the required ones may be the round before speaking, so
364
+ * they cannot pass the pull request.
365
+ *
366
+ * Nor can they fail it when they finished before the round still running
367
+ * began: the round `skip-ci` cancelled, as `open-pr` opens every pull request
368
+ * under it, leaves its aggregates red, and reading that red as this round's
369
+ * set a pull request aside while its first real round was under way (#2246).
370
+ * Such a failure waits for the round to end, as a check not yet reported
371
+ * does. So does one that finished after the round began but whose workflow
372
+ * has a newer run still going: the round `skip-ci` was put on for can take
373
+ * long enough to be cancelled that its aggregate goes red after the round
374
+ * taking its place has started, and only the run that replaced it says it is
375
+ * the round before (#2463). A failure of this round still fails the pull
376
+ * request at once.
377
+ *
378
+ * The verdict itself is `pr-report-core`'s `summarizeChecks`, which
379
+ * `pr-report` and the page reach theirs through, so the worker and the report
380
+ * cannot disagree about one; what is here turns what GitHub reports into
381
+ * the states that reads.
382
+ */
383
+ export const summarizeChecks = (
384
+ checks: readonly ReportedCheck[],
385
+ requiredContexts: readonly string[],
386
+ running: readonly RunningCheck[],
387
+ ): ChecksSummary => {
388
+ const roundStartedMs = earliestStart(running);
389
+
390
+ const isOfRoundBefore = (check: ReportedCheck): boolean => {
391
+ const completedMs = epochMsOf(check.completedAt);
392
+
393
+ return (
394
+ (roundStartedMs !== undefined &&
395
+ completedMs !== undefined &&
396
+ completedMs < roundStartedMs) ||
397
+ isSuperseded(check, running)
398
+ );
399
+ };
400
+
401
+ const isFailureOfThisRound = (check: ReportedCheck): boolean =>
402
+ isFailure(check) && !isOfRoundBefore(check);
403
+
404
+ const mut_reported = new Map<string, ContextState>();
405
+
406
+ for (const check of checks) {
407
+ const state = stateOf(check, isOfRoundBefore(check));
408
+
409
+ const previous = mut_reported.get(check.name);
410
+
411
+ mut_reported.set(
412
+ check.name,
413
+ previous === undefined ? state : combineContextStates(previous, state),
414
+ );
415
+ }
416
+
417
+ const pendingNames = new Set(
418
+ checks
419
+ .filter((check) => mut_reported.get(check.name) === 'pending')
420
+ .map((check) => check.name),
421
+ );
422
+
423
+ const stillRunning = running
424
+ .map(({ name }) => name)
425
+ .filter((name) => !pendingNames.has(name));
426
+
427
+ const summary = summarizeContexts({
428
+ // What the ruleset asks for, and whatever `--required` reported besides:
429
+ // all of it, when the rules could not be read.
430
+ required: Arr.uniq([
431
+ ...requiredContexts,
432
+ ...checks.map((check) => check.name),
433
+ ]),
434
+ reported: mut_reported,
435
+ // `skip-ci` is the queue's to read (`queueStanding`), not the checks'.
436
+ paused: false,
437
+ // No checks at all means they have not been reported yet — which is
438
+ // also all that can be said when the ruleset could not be read.
439
+ running: Arr.isNonEmpty(stillRunning) || !Arr.isNonEmpty(checks),
440
+ // A required context that has not reported is waited for wherever the
441
+ // pull request points: the worker arms what it judges green, and an
442
+ // armed pull request stacked on a branch no ruleset covers merges into
443
+ // it at once.
444
+ baseCovered: true,
445
+ });
446
+
447
+ return {
448
+ ...summary,
449
+ failedRuns: checks
450
+ .filter(isFailureOfThisRound)
451
+ .map(({ name, link }) => ({ name, link })),
452
+ running: stillRunning,
453
+ };
454
+ };
455
+
456
+ /** Everything the merge is still waiting for, reported and unreported. */
457
+ export const describeWaitingOn = (summary: ChecksSummary): string => {
458
+ const waiting = [
459
+ ...summary.pending,
460
+ ...summary.missing.map((context) => `${context} (not reported yet)`),
461
+ ...summary.running.map((name) => `${name} (running)`),
462
+ ] as const;
463
+
464
+ return Arr.isNonEmpty(waiting) ? waiting.join(', ') : 'checks to be reported';
465
+ };
466
+
467
+ const isFailure = (check: ReportedCheck): boolean =>
468
+ check.bucket === 'fail' || check.bucket === 'cancel';
469
+
470
+ /**
471
+ * What one reported check says of its context. A failure of the round before
472
+ * waits for the round still running, as a check not yet reported does.
473
+ */
474
+ const stateOf = (check: ReportedCheck, ofRoundBefore: boolean): ContextState =>
475
+ isFailure(check)
476
+ ? ofRoundBefore
477
+ ? 'pending'
478
+ : 'failed'
479
+ : check.bucket === 'pending'
480
+ ? 'pending'
481
+ : check.bucket === 'skipping'
482
+ ? 'skipped'
483
+ : 'passed';
484
+
485
+ /**
486
+ * Whether a newer run of the check's own workflow is still running. Run ids
487
+ * only grow, so a higher one is a later run; a commit status has no workflow
488
+ * and no run, and is never superseded.
489
+ */
490
+ const isSuperseded = (
491
+ check: ReportedCheck,
492
+ running: readonly RunningCheck[],
493
+ ): boolean => {
494
+ const runId = workflowRunId(check.link);
495
+
496
+ return (
497
+ check.workflow !== '' &&
498
+ runId !== undefined &&
499
+ running.some((other) => {
500
+ const otherRunId = workflowRunId(other.link);
501
+
502
+ return (
503
+ other.workflow === check.workflow &&
504
+ otherRunId !== undefined &&
505
+ otherRunId > runId
506
+ );
507
+ })
508
+ );
509
+ };
510
+
511
+ /** The run id in a job's link (`…/actions/runs/<id>/job/<id>`). */
512
+ const workflowRunId = (link: string): number | undefined => {
513
+ const id = /\/actions\/runs\/(\d+)\//u.exec(link)?.[1];
514
+
515
+ return id === undefined
516
+ ? undefined
517
+ : Result.unwrapOkOr(Num.safeParseInt(id), undefined);
518
+ };
519
+
520
+ /**
521
+ * When the earliest check still running started, in milliseconds; `undefined`
522
+ * when nothing is running or no start time can be read.
523
+ */
524
+ const earliestStart = (
525
+ running: readonly RunningCheck[],
526
+ ): number | undefined => {
527
+ const starts = Arr.generate(function* () {
528
+ for (const { startedAt } of running) {
529
+ const ms = epochMsOf(startedAt);
530
+
531
+ if (ms !== undefined) {
532
+ yield ms;
533
+ }
534
+ }
535
+ });
536
+
537
+ return Arr.isNonEmpty(starts) ? Math.min(...starts) : undefined;
538
+ };