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.
- package/README.md +5 -2
- package/dist/branch-rules.d.mts +30 -0
- package/dist/branch-rules.d.mts.map +1 -0
- package/dist/branch-rules.mjs +71 -0
- package/dist/branch-rules.mjs.map +1 -0
- package/dist/checks.d.mts +61 -62
- package/dist/checks.d.mts.map +1 -1
- package/dist/checks.mjs +0 -1
- package/dist/checks.mjs.map +1 -1
- package/dist/claim.d.mts +21 -15
- package/dist/claim.d.mts.map +1 -1
- package/dist/claim.mjs +1 -1
- package/dist/claim.mjs.map +1 -1
- package/dist/closing-issues.d.mts +18 -19
- package/dist/closing-issues.d.mts.map +1 -1
- package/dist/closing-issues.mjs +0 -1
- package/dist/closing-issues.mjs.map +1 -1
- package/dist/code-owners.d.mts +33 -34
- package/dist/code-owners.d.mts.map +1 -1
- package/dist/code-owners.mjs +0 -2
- package/dist/code-owners.mjs.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/dist/issue-claim.d.mts +56 -0
- package/dist/issue-claim.d.mts.map +1 -0
- package/dist/issue-claim.mjs +87 -0
- package/dist/issue-claim.mjs.map +1 -0
- package/dist/issue-priority.d.mts +44 -0
- package/dist/issue-priority.d.mts.map +1 -0
- package/dist/issue-priority.mjs +91 -0
- package/dist/issue-priority.mjs.map +1 -0
- package/dist/labels.d.mts +43 -42
- package/dist/labels.d.mts.map +1 -1
- package/dist/labels.mjs +2 -1
- package/dist/labels.mjs.map +1 -1
- package/dist/merge-after.d.mts +22 -16
- package/dist/merge-after.d.mts.map +1 -1
- package/dist/merge-after.mjs +87 -24
- package/dist/merge-after.mjs.map +1 -1
- package/dist/queue-comments.d.mts +2 -2
- package/dist/queue-order.d.mts +32 -32
- package/dist/queue-order.mjs +0 -2
- package/dist/queue-order.mjs.map +1 -1
- package/dist/queue-owner.d.mts +14 -14
- package/dist/queue-owner.d.mts.map +1 -1
- package/dist/queue-owner.mjs +18 -0
- package/dist/queue-owner.mjs.map +1 -1
- package/dist/report.d.mts +4 -4
- package/dist/review-hold.d.mts +14 -14
- package/dist/ruleset.d.mts +12 -13
- package/dist/ruleset.d.mts.map +1 -1
- package/dist/ruleset.mjs +0 -1
- package/dist/ruleset.mjs.map +1 -1
- package/dist/set-aside.d.mts +29 -28
- package/dist/set-aside.d.mts.map +1 -1
- package/dist/set-aside.mjs +2 -0
- package/dist/set-aside.mjs.map +1 -1
- package/dist/stack.d.mts +14 -15
- package/dist/stack.d.mts.map +1 -1
- package/dist/stack.mjs +0 -1
- package/dist/stack.mjs.map +1 -1
- package/dist/start-after.d.mts +20 -21
- package/dist/start-after.d.mts.map +1 -1
- package/dist/start-after.mjs +0 -1
- package/dist/start-after.mjs.map +1 -1
- package/dist/summarize.d.mts +2 -2
- package/dist/timestamp.d.mts +10 -10
- package/dist/tree.d.mts +5 -5
- package/dist/types.d.mts +32 -33
- package/dist/types.d.mts.map +1 -1
- package/package.json +10 -10
- package/src/branch-rules.mts +105 -0
- package/src/branch-rules.test.mts +221 -0
- package/src/checks.mts +64 -65
- package/src/checks.test.mts +30 -31
- package/src/claim.mts +16 -16
- package/src/closing-issues.mts +18 -19
- package/src/closing-issues.test.mts +2 -2
- package/src/code-owners.mts +39 -41
- package/src/index.mts +3 -0
- package/src/issue-claim.mts +138 -0
- package/src/issue-claim.test.mts +149 -0
- package/src/issue-priority.mts +116 -0
- package/src/issue-priority.test.mts +71 -0
- package/src/labels.mts +43 -42
- package/src/merge-after.mts +142 -49
- package/src/merge-after.test.mts +80 -3
- package/src/queue-comments.mts +2 -2
- package/src/queue-order.mts +60 -62
- package/src/queue-order.test.mts +6 -6
- package/src/queue-owner.mts +46 -25
- package/src/queue-owner.test.mts +4 -2
- package/src/report.mts +9 -9
- package/src/review-hold.mts +14 -14
- package/src/review-hold.test.mts +3 -3
- package/src/ruleset.mts +12 -13
- package/src/set-aside.mts +30 -28
- package/src/set-aside.test.mts +22 -0
- package/src/stack.mts +14 -15
- package/src/stack.test.mts +4 -4
- package/src/start-after.mts +20 -21
- package/src/summarize.mts +2 -2
- package/src/timestamp.mts +14 -14
- package/src/tree.mts +5 -5
- 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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
106
|
-
*
|
|
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
|
-
*
|
|
119
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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
|
-
//
|
|
170
|
-
//
|
|
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
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
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
|
-
*
|
|
243
|
-
*
|
|
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
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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,
|
package/src/checks.test.mts
CHANGED
|
@@ -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
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
-
//
|
|
119
|
-
//
|
|
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
|
-
//
|
|
212
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* `
|
|
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
|
-
*
|
|
612
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
742
|
-
//
|
|
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
|
|
7
|
-
* base does, when it tries the rebase again. A
|
|
8
|
-
* request is then racing the script, and a second
|
|
9
|
-
* opened the pull request and is woken by the
|
|
10
|
-
* fixing the same failure. The leases on every push
|
|
11
|
-
* overwriting another's work, but not from doing it
|
|
12
|
-
* on one says so first in a comment whose first line
|
|
13
|
-
* module writes and reads, and everyone else — the
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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
|
|
package/src/closing-issues.mts
CHANGED
|
@@ -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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
64
|
-
* `closingIssuesReferences` does not: it links a keyword only on a pull
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
package/src/code-owners.mts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Pull Requests Manager page, to show the wait, and by `unblock-prs`, to
|
|
11
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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';
|