pr-report-core 0.2.0 → 0.3.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 (107) hide show
  1. package/README.md +5 -2
  2. package/dist/branch-rules.d.mts +30 -0
  3. package/dist/branch-rules.d.mts.map +1 -0
  4. package/dist/branch-rules.mjs +71 -0
  5. package/dist/branch-rules.mjs.map +1 -0
  6. package/dist/checks.d.mts +61 -62
  7. package/dist/checks.d.mts.map +1 -1
  8. package/dist/checks.mjs +0 -1
  9. package/dist/checks.mjs.map +1 -1
  10. package/dist/claim.d.mts +21 -15
  11. package/dist/claim.d.mts.map +1 -1
  12. package/dist/claim.mjs +1 -1
  13. package/dist/claim.mjs.map +1 -1
  14. package/dist/closing-issues.d.mts +18 -19
  15. package/dist/closing-issues.d.mts.map +1 -1
  16. package/dist/closing-issues.mjs +0 -1
  17. package/dist/closing-issues.mjs.map +1 -1
  18. package/dist/code-owners.d.mts +33 -34
  19. package/dist/code-owners.d.mts.map +1 -1
  20. package/dist/code-owners.mjs +0 -2
  21. package/dist/code-owners.mjs.map +1 -1
  22. package/dist/index.d.mts +3 -0
  23. package/dist/index.d.mts.map +1 -1
  24. package/dist/index.mjs +3 -0
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/issue-claim.d.mts +56 -0
  27. package/dist/issue-claim.d.mts.map +1 -0
  28. package/dist/issue-claim.mjs +87 -0
  29. package/dist/issue-claim.mjs.map +1 -0
  30. package/dist/issue-priority.d.mts +44 -0
  31. package/dist/issue-priority.d.mts.map +1 -0
  32. package/dist/issue-priority.mjs +91 -0
  33. package/dist/issue-priority.mjs.map +1 -0
  34. package/dist/labels.d.mts +43 -42
  35. package/dist/labels.d.mts.map +1 -1
  36. package/dist/labels.mjs +2 -1
  37. package/dist/labels.mjs.map +1 -1
  38. package/dist/merge-after.d.mts +22 -16
  39. package/dist/merge-after.d.mts.map +1 -1
  40. package/dist/merge-after.mjs +87 -24
  41. package/dist/merge-after.mjs.map +1 -1
  42. package/dist/queue-comments.d.mts +2 -2
  43. package/dist/queue-order.d.mts +32 -32
  44. package/dist/queue-order.mjs +0 -2
  45. package/dist/queue-order.mjs.map +1 -1
  46. package/dist/queue-owner.d.mts +14 -14
  47. package/dist/queue-owner.d.mts.map +1 -1
  48. package/dist/queue-owner.mjs +18 -0
  49. package/dist/queue-owner.mjs.map +1 -1
  50. package/dist/report.d.mts +4 -4
  51. package/dist/review-hold.d.mts +14 -14
  52. package/dist/ruleset.d.mts +12 -13
  53. package/dist/ruleset.d.mts.map +1 -1
  54. package/dist/ruleset.mjs +0 -1
  55. package/dist/ruleset.mjs.map +1 -1
  56. package/dist/set-aside.d.mts +29 -28
  57. package/dist/set-aside.d.mts.map +1 -1
  58. package/dist/set-aside.mjs +2 -0
  59. package/dist/set-aside.mjs.map +1 -1
  60. package/dist/stack.d.mts +14 -15
  61. package/dist/stack.d.mts.map +1 -1
  62. package/dist/stack.mjs +0 -1
  63. package/dist/stack.mjs.map +1 -1
  64. package/dist/start-after.d.mts +20 -21
  65. package/dist/start-after.d.mts.map +1 -1
  66. package/dist/start-after.mjs +0 -1
  67. package/dist/start-after.mjs.map +1 -1
  68. package/dist/summarize.d.mts +2 -2
  69. package/dist/timestamp.d.mts +10 -10
  70. package/dist/tree.d.mts +5 -5
  71. package/dist/types.d.mts +32 -33
  72. package/dist/types.d.mts.map +1 -1
  73. package/package.json +10 -10
  74. package/src/branch-rules.mts +105 -0
  75. package/src/branch-rules.test.mts +221 -0
  76. package/src/checks.mts +64 -65
  77. package/src/checks.test.mts +30 -31
  78. package/src/claim.mts +16 -16
  79. package/src/closing-issues.mts +18 -19
  80. package/src/closing-issues.test.mts +2 -2
  81. package/src/code-owners.mts +39 -41
  82. package/src/index.mts +3 -0
  83. package/src/issue-claim.mts +138 -0
  84. package/src/issue-claim.test.mts +149 -0
  85. package/src/issue-priority.mts +116 -0
  86. package/src/issue-priority.test.mts +71 -0
  87. package/src/labels.mts +43 -42
  88. package/src/merge-after.mts +142 -49
  89. package/src/merge-after.test.mts +80 -3
  90. package/src/queue-comments.mts +2 -2
  91. package/src/queue-order.mts +60 -62
  92. package/src/queue-order.test.mts +6 -6
  93. package/src/queue-owner.mts +46 -25
  94. package/src/queue-owner.test.mts +4 -2
  95. package/src/report.mts +9 -9
  96. package/src/review-hold.mts +14 -14
  97. package/src/review-hold.test.mts +3 -3
  98. package/src/ruleset.mts +12 -13
  99. package/src/set-aside.mts +30 -28
  100. package/src/set-aside.test.mts +22 -0
  101. package/src/stack.mts +14 -15
  102. package/src/stack.test.mts +4 -4
  103. package/src/start-after.mts +20 -21
  104. package/src/summarize.mts +2 -2
  105. package/src/timestamp.mts +14 -14
  106. package/src/tree.mts +5 -5
  107. package/src/types.mts +32 -33
package/src/checks.mts CHANGED
@@ -6,36 +6,36 @@ import type { ChecksSummary, ContextState } from './types.mjs';
6
6
  /**
7
7
  * What every context reported, from a head commit's check runs.
8
8
  *
9
- * A name appears more than once whenever two check suites reported it, and on
10
- * a queued pull request that is the normal case rather than the odd one: the
11
- * run the `opened` event starts is cancelled by the `labeled` event's, and
12
- * both stay on the commit — the cancelled one leaving its `*-result`
13
- * aggregate concluded `failure`, because an `if: always()` job that asserts
14
- * on cancelled `needs` fails.
9
+ * A name appears more than once whenever two check suites reported it, and on a
10
+ * queued pull request that is the normal case rather than the odd one: the run
11
+ * the `opened` event starts is cancelled by the `labeled` event's, and both
12
+ * stay on the commit — the cancelled one leaving its `*-result` aggregate
13
+ * concluded `failure`, because an `if: always()` job that asserts on cancelled
14
+ * `needs` fails.
15
15
  *
16
- * **The newest check suite wins, and that is not the same as the newest
17
- * run.** GitHub resolves a required context to the run in the suite with the
18
- * greatest id, so where the cancelled suite happens to have been created
19
- * after the one that superseded it, the stale red is what holds the merge —
20
- * however much later the green one finished. Every suite of one push is
21
- * created in the same second, so which of them got the higher id is not
22
- * something the repository decides.
16
+ * **The newest check suite wins, and that is not the same as the newest run.**
17
+ * GitHub resolves a required context to the run in the suite with the greatest
18
+ * id, so where the cancelled suite happens to have been created after the one
19
+ * that superseded it, the stale red is what holds the merge — however much
20
+ * later the green one finished. Every suite of one push is created in the same
21
+ * second, so which of them got the higher id is not something the repository
22
+ * decides.
23
23
  *
24
24
  * This is why the rule is not "the newest `started_at`", which reads more
25
25
  * natural and would call such a pull request green while GitHub blocks it.
26
- * A report that says a blocked pull request will merge is worse than one
27
- * that says a green one will not: `CLAUDE.md`'s "Commits and pull requests"
28
- * says to ignore the red the cancelled run leaves, and this is the case where
26
+ * A report that says a blocked pull request will merge is worse than one that
27
+ * says a green one will not: `CLAUDE.md`'s "Commits and pull requests" says to
28
+ * ignore the red the cancelled run leaves, and this is the case where
29
29
  * GitHub does not.
30
30
  *
31
31
  * **A run whose workflow has a newer round still going is pending.** An
32
- * aggregate has no run in a round until the jobs it waits on are done, so
33
- * until then the only run of its name is the previous round's — on a pull
34
- * request whose label events started two rounds, the red the second left on
35
- * the first (#2191). That run is what GitHub shows, but not what it will
36
- * answer with: the newer round is in the suite with the greater id, and its
37
- * aggregate will be the newest run of the name. Where the newer round has
38
- * finished without one, the older run stands, as it does on GitHub.
32
+ * aggregate has no run in a round until the jobs it waits on are done, so until
33
+ * then the only run of its name is the previous round's — on a pull request
34
+ * whose label events started two rounds, the red the second left on the first
35
+ * (#2191). That run is what GitHub shows, but not what it will answer with: the
36
+ * newer round is in the suite with the greater id, and its aggregate will be
37
+ * the newest run of the name. Where the newer round has finished without one,
38
+ * the older run stands, as it does on GitHub.
39
39
  */
40
40
  export const statesFromCheckRuns = (
41
41
  runs: readonly CheckRunReport[],
@@ -101,9 +101,9 @@ export type CheckRunReport = Readonly<{
101
101
  /**
102
102
  * The workflow run its suite is, where it is one: the workflow, so that a
103
103
  * newer round of it can be told from an unrelated suite, and whether the
104
- * round has finished, which its runs alone do not say — an aggregate's run
105
- * is created only once every job before it has completed. `undefined` for
106
- * a run no workflow made, or where the workflow runs could not be read.
104
+ * round has finished, which its runs alone do not say — an aggregate's run is
105
+ * created only once every job before it has completed. `undefined` for a run
106
+ * no workflow made, or where the workflow runs could not be read.
107
107
  */
108
108
  round: Readonly<{ workflowId: number; completed: boolean }> | undefined;
109
109
  }>;
@@ -114,12 +114,12 @@ export type CheckRunReport = Readonly<{
114
114
  * A run that has not completed is pending whatever it concluded last — the
115
115
  * conclusion of a re-run is the previous verdict until the new one finishes.
116
116
  * A `skipped` conclusion is reported as `skipped` and counted as met by
117
- * {@link summarizeChecks}, because GitHub counts it as met: the gated jobs
118
- * skip on `skip-ci` and on a diff the workflow does not read, and reading
119
- * that as a failure would call every gated pull request red.
117
+ * {@link summarizeChecks}, because GitHub counts it as met: the gated jobs skip
118
+ * on `skip-ci` and on a diff the workflow does not read, and reading that as a
119
+ * failure would call every gated pull request red.
120
120
  * Everything else that finished — `cancelled`, `stale`, `timed_out`,
121
- * `action_required` — is something a reader has to act on, so it is a
122
- * failure here even where GitHub is vaguer about it.
121
+ * `action_required` — is something a reader has to act on, so it is a failure
122
+ * here even where GitHub is vaguer about it.
123
123
  */
124
124
  export const classifyCheckRun = (
125
125
  status: string,
@@ -141,12 +141,12 @@ const PASSING_CONCLUSIONS: ReadonlySet<string> = new Set<string>([
141
141
  /**
142
142
  * Whether any run on the head commit has not finished, or any round has not.
143
143
  *
144
- * Every run, not only the required ones: the aggregates are `if: always()`
145
- * and have no run in the newest round until the jobs they wait on are done,
146
- * so "every required context has reported" is true of a commit halfway
147
- * through a round — with the reports coming from the round before. And the
148
- * round, not only its runs: between the last job finishing and the aggregate
149
- * being created, every run there is has completed.
144
+ * Every run, not only the required ones: the aggregates are `if: always()` and
145
+ * have no run in the newest round until the jobs they wait on are done, so
146
+ * "every required context has reported" is true of a commit halfway through a
147
+ * round — with the reports coming from the round before. And the round, not
148
+ * only its runs: between the last job finishing and the aggregate being
149
+ * created, every run there is has completed.
150
150
  */
151
151
  export const anyRunInProgress = (runs: readonly CheckRunReport[]): boolean =>
152
152
  runs.some(
@@ -154,10 +154,10 @@ export const anyRunInProgress = (runs: readonly CheckRunReport[]): boolean =>
154
154
  );
155
155
 
156
156
  /**
157
- * What every context has reported on one commit, from both places GitHub
158
- * keeps them. The aggregate jobs are check runs; `no-skip-ci-label` is a
159
- * commit status, so reading only the first would report the context that
160
- * holds every labelled pull request as missing.
157
+ * What every context has reported on one commit, from both places GitHub keeps
158
+ * them. The aggregate jobs are check runs; `no-skip-ci-label` is a commit
159
+ * status, so reading only the first would report the context that holds every
160
+ * labelled pull request as missing.
161
161
  *
162
162
  * `state` is a commit status's as the REST API spells it, in lower case.
163
163
  */
@@ -165,9 +165,9 @@ export const reportedContexts = (
165
165
  runs: readonly CheckRunReport[],
166
166
  statuses: readonly Readonly<{ context: string; state: string }>[],
167
167
  ): ReadonlyMap<string, ContextState> => {
168
- // A commit status and a check run of the same name are two requirements,
169
- // not one reported twice: GitHub asks both to pass. So the stricter of
170
- // the two is kept rather than whichever was read second.
168
+ // A commit status and a check run of the same name are two requirements, not
169
+ // one reported twice: GitHub asks both to pass. So the stricter of the two is
170
+ // kept rather than whichever was read second.
171
171
  const mut_states = new Map<string, ContextState>(statesFromCheckRuns(runs));
172
172
 
173
173
  for (const { context, state } of statuses) {
@@ -189,12 +189,11 @@ export const reportedContexts = (
189
189
  /**
190
190
  * What a context reported that is both a check run and a commit status.
191
191
  *
192
- * Not "the status wins", which is what this used to assume. GitHub's own
193
- * answer is that neither does: "If a check and a commit status have the same
194
- * name, both must pass when that name is required." So the stricter of the
195
- * two is the verdict, and a green status cannot cover a red check run of the
196
- * same name. A skip beside a pass is a skip: both are met, but only one of
197
- * them ran.
192
+ * Not "the status wins", which is what this used to assume. GitHub's own answer
193
+ * is that neither does: "If a check and a commit status have the same name,
194
+ * both must pass when that name is required." So the stricter of the two is the
195
+ * verdict, and a green status cannot cover a red check run of the same name. A
196
+ * skip beside a pass is a skip: both are met, but only one of them ran.
198
197
  */
199
198
  export const combineContextStates = (
200
199
  a: ContextState,
@@ -210,8 +209,8 @@ export const combineContextStates = (
210
209
 
211
210
  /**
212
211
  * What one commit status has reported. `no-skip-ci-label` is one of these
213
- * rather than a check run, so a report that read only check runs would call
214
- * the context that holds every labelled pull request "missing".
212
+ * rather than a check run, so a report that read only check runs would call the
213
+ * context that holds every labelled pull request "missing".
215
214
  */
216
215
  export const classifyCommitStatus = (state: string): ContextState => {
217
216
  switch (state) {
@@ -231,24 +230,24 @@ export const classifyCommitStatus = (state: string): ContextState => {
231
230
  * requires and no others: a failing job nothing requires does not block a
232
231
  * merge, and listing it would bury the one that does.
233
232
  *
234
- * `paused` outranks the rest. While `skip-ci` is on, the checks a reader
235
- * sees are whatever survived from before the label went on — typically the
236
- * red left by the `opened` run that the `labeled` run cancelled. Calling
237
- * that "failing" would make every queued pull request look broken; the
238
- * failures are still listed, because they are what a reader asks about.
233
+ * `paused` outranks the rest. While `skip-ci` is on, the checks a reader sees
234
+ * are whatever survived from before the label went on — typically the red left
235
+ * by the `opened` run that the `labeled` run cancelled. Calling that "failing"
236
+ * would make every queued pull request look broken; the failures are still
237
+ * listed, because they are what a reader asks about.
239
238
  *
240
239
  * `running` is the third thing that makes a verdict provisional, beside a
241
- * required context that is pending and one that has not reported. Without it
242
- * a commit mid-round reads as settled out of the round before — which is how
243
- * a failing pull request (#2031) came to show a tick.
240
+ * required context that is pending and one that has not reported. Without it a
241
+ * commit mid-round reads as settled out of the round before — which is how a
242
+ * failing pull request (#2031) came to show a tick.
244
243
  *
245
244
  * **Off the covered base, a context that never reported is not awaited.**
246
245
  * The contexts are the ones the ruleset asks of `main`, and a workflow that
247
- * runs only on pull requests into `main` never reports on a stacked one —
248
- * which kept #2103, every check of it green, "running" until it was moved
249
- * onto `main`. Such a context is {@link ChecksSummary.notRun} instead, but
250
- * only once some required context has reported: before the first check
251
- * suite, nothing having come is not a pass over zero contexts.
246
+ * runs only on pull requests into `main` never reports on a stacked one — which
247
+ * kept #2103, every check of it green, "running" until it was moved onto
248
+ * `main`. Such a context is {@link ChecksSummary.notRun} instead, but only once
249
+ * some required context has reported: before the first check suite, nothing
250
+ * having come is not a pass over zero contexts.
252
251
  */
253
252
  export const summarizeChecks = ({
254
253
  required,
@@ -17,11 +17,11 @@ describe(classifyCheckRun, () => {
17
17
  assert.strictEqual(classifyCheckRun('in_progress', 'failure'), 'pending');
18
18
  });
19
19
 
20
- // The gated jobs skip on `skip-ci` and on a diff the workflow does not
21
- // read; GitHub counts that as met, so reading it as a failure would call
22
- // every gated pull request red. `summarizeChecks` still counts it as met —
23
- // but it is reported as what it was, because folding it into `passed` is
24
- // what let a failing pull request show a tick.
20
+ // The gated jobs skip on `skip-ci` and on a diff the workflow does not read;
21
+ // GitHub counts that as met, so reading it as a failure would call every
22
+ // gated pull request red. `summarizeChecks` still counts it as met — but it
23
+ // is reported as what it was, because folding it into `passed` is what let a
24
+ // failing pull request show a tick.
25
25
  test('a skipped job is reported as skipped, not as passed', () => {
26
26
  assert.strictEqual(classifyCheckRun('completed', 'skipped'), 'skipped');
27
27
 
@@ -114,10 +114,9 @@ describe(summarizeChecks, () => {
114
114
 
115
115
  test('`skip-ci` is a pause, and the detail is still reported', () => {
116
116
  // While the label is on, the gated jobs boot no runner and
117
- // `no-skip-ci-label` sits `pending` by design. Calling that "failing"
118
- // would make every queued pull request look broken; the red left by the
119
- // cancelled `opened` run is still listed, because it is what a reader
120
- // asks about.
117
+ // `no-skip-ci-label` sits `pending` by design. Calling that "failing" would
118
+ // make every queued pull request look broken; the red left by the cancelled
119
+ // `opened` run is still listed, because it is what a reader asks about.
121
120
  const summary = summarizeChecks({
122
121
  required,
123
122
  reported: new Map<string, ContextState>([
@@ -207,9 +206,9 @@ describe(summarizeChecks, () => {
207
206
  });
208
207
 
209
208
  // #2103, stacked on `chore/pnpm-update`: `lint-pull-request.yml` runs on
210
- // pull requests into `main` only, so its three contexts never came, and
211
- // the page kept calling a pull request whose checks had all passed
212
- // "checks running".
209
+ // pull requests into `main` only, so its three contexts never came, and the
210
+ // page kept calling a pull request whose checks had all passed "checks
211
+ // running".
213
212
  describe('on a base the ruleset does not cover', () => {
214
213
  const withLint = Arr.toPushed(required, 'Validate PR title');
215
214
 
@@ -235,8 +234,8 @@ describe(summarizeChecks, () => {
235
234
  });
236
235
 
237
236
  test('while nothing required has reported, everything is still coming', () => {
238
- // Between a push and the first check suite, a head commit has nothing
239
- // on it at all; that is not a pass over zero contexts.
237
+ // Between a push and the first check suite, a head commit has nothing on
238
+ // it at all; that is not a pass over zero contexts.
240
239
  const summary = summarizeChecks({
241
240
  required: withLint,
242
241
  reported: new Map<string, ContextState>(),
@@ -331,8 +330,8 @@ describe(statesFromCheckRuns, () => {
331
330
  // `test-node-versions-result`, the one context of the five that blocked
332
331
  // #2021: the cancelled suite happens to have the greater id, so GitHub
333
332
  // reports the failure however much later the green one finished. Reading
334
- // the newest `started_at` instead would call this pull request green
335
- // while GitHub blocks it.
333
+ // the newest `started_at` instead would call this pull request green while
334
+ // GitHub blocks it.
336
335
  const states = statesFromCheckRuns([
337
336
  run(
338
337
  'test-node-versions-result / result',
@@ -496,11 +495,11 @@ describe(anyRunInProgress, () => {
496
495
  * The state #2031 was in when the page showed it a tick.
497
496
  *
498
497
  * `skip-ci` came off, which started a new round. The old round's
499
- * `code-check-result` had concluded `skipped` and was the only run of that
500
- * name on the commit — the new round's aggregate does not exist until the
501
- * jobs it waits on are done. Folding `skipped` into `passed` then made the
502
- * pull request look settled and green, two minutes before
503
- * `code-check-result` concluded `failure`.
498
+ * `code-check-result` had concluded `skipped` and was the only run of that name
499
+ * on the commit — the new round's aggregate does not exist until the jobs it
500
+ * waits on are done. Folding `skipped` into `passed` then made the pull request
501
+ * look settled and green, two minutes before `code-check-result` concluded
502
+ * `failure`.
504
503
  */
505
504
  describe('a required context in a superseded round', () => {
506
505
  const required = [
@@ -607,9 +606,9 @@ describe('a required context in a superseded round', () => {
607
606
  * `skip-ci` came off, which started a round, and `merge-queued` went on a
608
607
  * minute later, which started another and cancelled the first — whose
609
608
  * `*-result` aggregates, `if: always()` over cancelled `needs`, concluded
610
- * `failure`. The newer round was still at its matrix, so its aggregates did
611
- * not exist yet, and the red of the cancelled one was the only run of each
612
- * name on the commit.
609
+ * `failure`. The newer round was still at its matrix, so its aggregates did not
610
+ * exist yet, and the red of the cancelled one was the only run of each name on
611
+ * the commit.
613
612
  */
614
613
  describe('a required context whose workflow has a newer round running', () => {
615
614
  const codeCheck = 359_266_435;
@@ -659,8 +658,8 @@ describe('a required context whose workflow has a newer round running', () => {
659
658
  conclusion: undefined,
660
659
  round: { workflowId: codeCheck, completed: false },
661
660
  },
662
- // Style Check has finished every job it has created, but the round is
663
- // not over: its aggregate is created only after that.
661
+ // Style Check has finished every job it has created, but the round is not
662
+ // over: its aggregate is created only after that.
664
663
  {
665
664
  id: 114_375_644_790,
666
665
  checkSuiteId: 103_247_840_622,
@@ -719,8 +718,8 @@ describe('a required context whose workflow has a newer round running', () => {
719
718
  });
720
719
 
721
720
  // The rule is not "an older round never counts": where the newer round
722
- // finished without a run of that name, GitHub answers with the older run,
723
- // and so does this.
721
+ // finished without a run of that name, GitHub answers with the older run, and
722
+ // so does this.
724
723
  test('a finished round that never ran the job does not hide the older run', () => {
725
724
  const states = statesFromCheckRuns([
726
725
  runs[0],
@@ -737,9 +736,9 @@ describe('a required context whose workflow has a newer round running', () => {
737
736
  assert.strictEqual(states.get('code-check-result / result'), 'failed');
738
737
  });
739
738
 
740
- // Nor "the round still running wins": GitHub resolves a context to the
741
- // suite with the greatest id, and where the cancelled one has it, its red
742
- // holds the merge however the other round goes.
739
+ // Nor "the round still running wins": GitHub resolves a context to the suite
740
+ // with the greatest id, and where the cancelled one has it, its red holds the
741
+ // merge however the other round goes.
743
742
  test('a cancelled round with the greater id still holds the merge', () => {
744
743
  const states = statesFromCheckRuns([
745
744
  {
package/src/claim.mts CHANGED
@@ -3,26 +3,26 @@
3
3
  * writer of the queue can read it.
4
4
  *
5
5
  * `unblock-prs` leaves a set-aside pull request alone until its head moves —
6
- * or, for every reason but `checks-failed` and `awaiting-bypass`, until the
7
- * base does, when it tries the rebase again. A session fixing that pull
8
- * request is then racing the script, and a second session, or the one that
9
- * opened the pull request and is woken by the set-aside comment, may be
10
- * fixing the same failure. The leases on every push keep any of them from
11
- * overwriting another's work, but not from doing it twice. So whoever starts
12
- * on one says so first in a comment whose first line is a hidden record this
13
- * module writes and reads, and everyone else — the script included — leaves
14
- * that pull request to it.
6
+ * or, for every reason but `checks-failed`, `awaiting-bypass` and
7
+ * `head-out-of-sync`, until the base does, when it tries the rebase again. A
8
+ * session fixing that pull request is then racing the script, and a second
9
+ * session, or the one that opened the pull request and is woken by the
10
+ * set-aside comment, may be fixing the same failure. The leases on every push
11
+ * keep any of them from overwriting another's work, but not from doing it
12
+ * twice. So whoever starts on one says so first in a comment whose first line
13
+ * is a hidden record this module writes and reads, and everyone else — the
14
+ * script included — leaves that pull request to it.
15
15
  *
16
16
  * **It lasts while the head is the one it claimed**, and for at most
17
17
  * {@link CLAIM_LIFETIME_MINUTES}. The push that carries the fix moves the head
18
- * and so ends the claim with nothing left to clean up; that push is what
19
- * hands the pull request back to the script, as it always was. The lifetime is
20
- * for a claimant that stopped without pushing. A claimant that needs longer
21
- * writes the claim again.
18
+ * and so ends the claim with nothing left to clean up; that push is what hands
19
+ * the pull request back to the script, as it always was. The lifetime is for a
20
+ * claimant that stopped without pushing. A claimant that needs longer writes
21
+ * the claim again.
22
22
  *
23
23
  * **One comment per pull request, edited in place**, like the set-aside one;
24
- * a claim that has ended is simply written over. Of several, the oldest is
25
- * the one, so two claimants writing at once both read back the same answer.
24
+ * a claim that has ended is simply written over. Of several, the oldest is the
25
+ * one, so two claimants writing at once both read back the same answer.
26
26
  *
27
27
  * **Only the comments the reader's own account wrote count**, as for the
28
28
  * set-aside comment (`set-aside.mts`): the repository is public. `by` is what
@@ -114,7 +114,7 @@ export const claimApplies = (
114
114
  * Further than that is a clock that would otherwise stretch the lifetime by
115
115
  * however far ahead it ran.
116
116
  */
117
- const CLOCK_SKEW_MINUTES = 5;
117
+ export const CLOCK_SKEW_MINUTES = 5;
118
118
 
119
119
  const MS_PER_MINUTE = 60_000;
120
120
 
@@ -19,15 +19,14 @@ export type ClosingIssueNode = Readonly<{
19
19
  /**
20
20
  * The closing issues that belong to `repo`, in GitHub's order.
21
21
  *
22
- * **Only this repository's.** A pull request can close an issue anywhere,
23
- * and a report lists each as `#N` — which for another repository's issue
24
- * names a different issue of this one, and for a private repository's may
25
- * put a title the report's reader cannot see into a report someone else
26
- * reads. The body parser `pr-report` falls back on without a token draws the
27
- * same line.
22
+ * **Only this repository's.** A pull request can close an issue anywhere, and a
23
+ * report lists each as `#N` — which for another repository's issue names a
24
+ * different issue of this one, and for a private repository's may put a title
25
+ * the report's reader cannot see into a report someone else reads. The body
26
+ * parser `pr-report` falls back on without a token draws the same line.
28
27
  *
29
- * `null` entries are skipped rather than rejected: one issue the token may
30
- * not see is no reason to lose the rest.
28
+ * `null` entries are skipped rather than rejected: one issue the token may not
29
+ * see is no reason to lose the rest.
31
30
  */
32
31
  export const closingIssuesIn = (
33
32
  repo: RepoRef,
@@ -55,21 +54,21 @@ export const closingIssuesIn = (
55
54
  };
56
55
 
57
56
  /**
58
- * The issues of `repo` a pull request body closes by keyword, as GitHub
59
- * itself read the body: first appearance order, each once.
57
+ * The issues of `repo` a pull request body closes by keyword, as GitHub itself
58
+ * read the body: first appearance order, each once.
60
59
  *
61
60
  * **Read from GitHub's rendering, not from the text.** GitHub wraps each
62
- * closing keyword it recognized in `<span class="issue-keyword">` and puts
63
- * the reference right after it, and it does so whatever the base branch.
64
- * `closingIssuesReferences` does not: it links a keyword only on a pull
65
- * request into the default branch, so a stacked one lists nothing until the
66
- * layers below it merge. Which spellings count, whether a reference inside
67
- * code or after a comma is closed, is GitHub's to decide, and this takes its
68
- * decision rather than copying the grammar. A body cannot forge the mark:
61
+ * closing keyword it recognized in `<span class="issue-keyword">` and puts the
62
+ * reference right after it, and it does so whatever the base branch.
63
+ * `closingIssuesReferences` does not: it links a keyword only on a pull request
64
+ * into the default branch, so a stacked one lists nothing until the layers
65
+ * below it merge. Which spellings count, whether a reference inside code or
66
+ * after a comma is closed, is GitHub's to decide, and this takes its decision
67
+ * rather than copying the grammar. A body cannot forge the mark:
69
68
  * GitHub strips `class` from HTML written in one.
70
69
  *
71
- * A reference to another repository's issue, or to a pull request, is left
72
- * out, for the reasons {@link closingIssuesIn} gives.
70
+ * A reference to another repository's issue, or to a pull request, is left out,
71
+ * for the reasons {@link closingIssuesIn} gives.
73
72
  */
74
73
  export const closingKeywordIssuesIn = (
75
74
  repo: RepoRef,
@@ -145,8 +145,8 @@ describe(closingKeywordIssuesIn, () => {
145
145
  });
146
146
 
147
147
  /**
148
- * A reference as GitHub renders it in a pull request's `bodyHTML`, with
149
- * the attributes in the order GitHub writes them.
148
+ * A reference as GitHub renders it in a pull request's `bodyHTML`, with the
149
+ * attributes in the order GitHub writes them.
150
150
  */
151
151
  const link = (
152
152
  nameWithOwner: string,
@@ -3,18 +3,18 @@
3
3
  *
4
4
  * Read because GitHub will not say. `main`'s ruleset has
5
5
  * `require_code_owner_review` on, so a pull request touching an owned path
6
- * waits for an owner to approve it — and nothing on a pull request reports
7
- * that wait: `reviewDecision` answers `null` for every pull request here,
8
- * owned paths or not, because it describes branch protection rather than a
9
- * ruleset. So it is worked out the way GitHub does, from the file — by the
10
- * Pull Requests Manager page, to show the wait, and by `unblock-prs`, to
11
- * leave a pull request that waits in the queue rather than run its checks.
6
+ * waits for an owner to approve it — and nothing on a pull request reports that
7
+ * wait: `reviewDecision` answers `null` for every pull request here, owned
8
+ * paths or not, because it describes branch protection rather than a ruleset.
9
+ * So it is worked out the way GitHub does, from the file — by the
10
+ * Pull Requests Manager page, to show the wait, and by `unblock-prs`, to leave
11
+ * a pull request that waits in the queue rather than run its checks.
12
12
  *
13
- * The patterns are the subset of `.gitignore` syntax GitHub documents for
14
- * this file: a leading `/` or a `/` in the middle anchors a pattern to the
15
- * root, a trailing `/` makes it a directory, `*` and `?` stay inside one path
16
- * segment and `**` crosses them. Negation and character ranges are not
17
- * supported by GitHub here either.
13
+ * The patterns are the subset of `.gitignore` syntax GitHub documents for this
14
+ * file: a leading `/` or a `/` in the middle anchors a pattern to the root, a
15
+ * trailing `/` makes it a directory, `*` and `?` stay inside one path segment
16
+ * and `**` crosses them. Negation and character ranges are not supported by
17
+ * GitHub here either.
18
18
  */
19
19
 
20
20
  import { Arr } from 'ts-data-forge';
@@ -22,8 +22,8 @@ import { Arr } from 'ts-data-forge';
22
22
  export type CodeOwnersRule = Readonly<{
23
23
  pattern: string;
24
24
  /**
25
- * Without the leading `@`. Empty for a pattern that clears the owners of
26
- * what an earlier line matched.
25
+ * Without the leading `@`. Empty for a pattern that clears the owners of what
26
+ * an earlier line matched.
27
27
  */
28
28
  owners: readonly string[];
29
29
  }>;
@@ -45,9 +45,9 @@ export const parseCodeOwners = (text: string): readonly CodeOwnersRule[] =>
45
45
  });
46
46
 
47
47
  /**
48
- * The owners of one path: those of the **last** pattern that matches it,
49
- * which is how GitHub reads the file — a later line overrides an earlier one
50
- * rather than adding to it.
48
+ * The owners of one path: those of the **last** pattern that matches it, which
49
+ * is how GitHub reads the file — a later line overrides an earlier one rather
50
+ * than adding to it.
51
51
  */
52
52
  export const ownersOf = (
53
53
  path: string,
@@ -57,19 +57,18 @@ export const ownersOf = (
57
57
 
58
58
  /**
59
59
  * What GitHub would say, worked out the way GitHub does: every changed path
60
- * that has owners needs an approval from one of them. An approval is the
61
- * latest review of an owner that says so; one dismissed by a later push —
62
- * the ruleset dismisses stale approvals — no longer counts, and GitHub has
63
- * already stopped calling it an approval by the time this reads it.
60
+ * that has owners needs an approval from one of them. An approval is the latest
61
+ * review of an owner that says so; one dismissed by a later push — the ruleset
62
+ * dismisses stale approvals — no longer counts, and GitHub has already stopped
63
+ * calling it an approval by the time this reads it.
64
64
  *
65
- * Owners are compared by login, case-insensitively, as GitHub compares
66
- * them. A team owner (`@org/team`) cannot be: telling who is on the team
67
- * takes `read:org`, which neither reader asks for. So a path a team owns
68
- * waits while nobody at all has approved, is approved by a person owning it
69
- * beside the team, and otherwise — someone approved, and might be on the
70
- * team — cannot be told, which makes the whole `unknown` unless another
71
- * path is plainly still waiting. `CODEOWNERS` in this repository names no
72
- * team.
65
+ * Owners are compared by login, case-insensitively, as GitHub compares them. A
66
+ * team owner (`@org/team`) cannot be: telling who is on the team takes
67
+ * `read:org`, which neither reader asks for. So a path a team owns waits while
68
+ * nobody at all has approved, is approved by a person owning it beside the
69
+ * team, and otherwise — someone approved, and might be on the team — cannot be
70
+ * told, which makes the whole `unknown` unless another path is plainly still
71
+ * waiting. `CODEOWNERS` in this repository names no team.
73
72
  */
74
73
  export const codeOwnerReview = ({
75
74
  required,
@@ -150,8 +149,8 @@ const stripComment = (line: string): string => {
150
149
  return at === -1 ? line : line.slice(0, at);
151
150
  };
152
151
 
153
- // Not `RegExp.escape`: it arrived in Node 24, and `ws:check:test` also runs
154
- // on the `engines.node` floor (`test-node-versions (minimum)`), which is 22.
152
+ // Not `RegExp.escape`: it arrived in Node 24, and `ws:check:test` also runs on
153
+ // the `engines.node` floor (`test-node-versions (minimum)`), which is 22.
155
154
  // Under the `u` flag only the syntax characters and `/` may be escaped, and
156
155
  // everything else is a literal as it stands.
157
156
  const escapeChar = (char: string): string =>
@@ -187,11 +186,10 @@ const toRegExp = (pattern: string): RegExp => {
187
186
 
188
187
  const body = globToRegExpSource(trimmed);
189
188
 
190
- // A directory owns what is under it and never matches as a file. A name
191
- // with no trailing slash may be either, so it matches itself and what is
192
- // under it — unless its last segment is a wildcard, which GitHub reads as
193
- // files at that level only: `docs/*` owns `docs/a.md` and not
194
- // `docs/build/a.md`.
189
+ // A directory owns what is under it and never matches as a file. A name with
190
+ // no trailing slash may be either, so it matches itself and what is under it
191
+ // — unless its last segment is a wildcard, which GitHub reads as files at
192
+ // that level only: `docs/*` owns `docs/a.md` and not `docs/build/a.md`.
195
193
  const tail = directory
196
194
  ? '/.+'
197
195
  : (trimmed.split('/').at(-1) ?? '').includes('*')
@@ -206,12 +204,12 @@ const toRegExp = (pattern: string): RegExp => {
206
204
  * Where a pull request stands with the code owners.
207
205
  *
208
206
  * `required` lists what is still waiting — the owned paths no owner has
209
- * approved yet, and who could approve them. `authorOwns` is the case that
210
- * waits for ever: GitHub does not let an author approve their own pull
211
- * request, so when the author is the only owner nothing but a ruleset bypass
212
- * will merge it. `unknown` is a pull request whose changed files were more
213
- * than could be read, where the unread ones might be owned — or, with
214
- * `teams`, one approved by someone these teams might have as a member.
207
+ * approved yet, and who could approve them. `authorOwns` is the case that waits
208
+ * for ever: GitHub does not let an author approve their own pull request, so
209
+ * when the author is the only owner nothing but a ruleset bypass will merge it.
210
+ * `unknown` is a pull request whose changed files were more than could be read,
211
+ * where the unread ones might be owned — or, with `teams`, one approved by
212
+ * someone these teams might have as a member.
215
213
  */
216
214
  export type CodeOwnerReview = Readonly<
217
215
  | { state: 'approved' }
package/src/index.mts CHANGED
@@ -1,7 +1,10 @@
1
+ export * from './branch-rules.mjs';
1
2
  export * from './checks.mjs';
2
3
  export * from './claim.mjs';
3
4
  export * from './closing-issues.mjs';
4
5
  export * from './code-owners.mjs';
6
+ export * from './issue-claim.mjs';
7
+ export * from './issue-priority.mjs';
5
8
  export * from './labels.mjs';
6
9
  export * from './merge-after.mjs';
7
10
  export * from './queue-comments.mjs';