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/merge-after.mts
CHANGED
|
@@ -8,11 +8,10 @@ import { Arr, Num, Result } from 'ts-data-forge';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* `#N`, and only when nothing that could end a repository name comes right
|
|
11
|
-
* before the `#`: `other/repo#5` is another repository's #5, and treating it
|
|
12
|
-
*
|
|
13
|
-
* `noshiro-pf/mono#5` is not read either, though it means this repository —
|
|
14
|
-
*
|
|
15
|
-
* it.
|
|
11
|
+
* before the `#`: `other/repo#5` is another repository's #5, and treating it as
|
|
12
|
+
* this one's would wait for a pull request the author never named.
|
|
13
|
+
* `noshiro-pf/mono#5` is not read either, though it means this repository — the
|
|
14
|
+
* declaration takes `#N` and nothing else, so there is one way to write it.
|
|
16
15
|
*/
|
|
17
16
|
const NUMBER_REF = /(?<![\w./-])#(?<number>\d+)\b/gu;
|
|
18
17
|
|
|
@@ -21,53 +20,84 @@ const DECLARATION_LINE = {
|
|
|
21
20
|
'start-after': /^[^\S\n]*start-after[^\S\n]*:(?<refs>[^\n]*)$/gimu,
|
|
22
21
|
} as const;
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A line that can open or close a fenced code block: a run of three or more
|
|
25
|
+
* backticks or tildes, and what follows it on the line.
|
|
26
|
+
*/
|
|
27
|
+
const FENCE_LINE = /^[^\S\n]*(?<marker>`{3,}|~{3,})(?<rest>[^\n]*)$/u;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* The pull request numbers a body declares this one must merge after.
|
|
29
31
|
*
|
|
30
32
|
* Duplicates are dropped and the order within the declaration is not kept:
|
|
31
33
|
* what is being read is a set of things that have to have happened, not a
|
|
32
|
-
* sequence. A number that names no pull request, or one that is already
|
|
33
|
-
*
|
|
34
|
+
* sequence. A number that names no pull request, or one that is already closed,
|
|
35
|
+
* simply never appears in the open list and so constrains nothing.
|
|
34
36
|
*/
|
|
35
37
|
export const parseMergeAfter = (body: string): readonly number[] =>
|
|
36
38
|
parseDeclaration(body, 'merge-after');
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
* The numbers an issue body declares it waits for before it is started,
|
|
40
|
-
*
|
|
41
|
+
* The numbers an issue body declares it waits for before it is started, read
|
|
42
|
+
* exactly as {@link parseMergeAfter} reads its trailer
|
|
41
43
|
* (`docs/issue-start-after.md`).
|
|
42
44
|
*/
|
|
43
45
|
export const parseStartAfter = (body: string): readonly number[] =>
|
|
44
46
|
parseDeclaration(body, 'start-after');
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* The body with the contents of its fenced code blocks blanked out, line
|
|
48
|
-
*
|
|
49
|
+
* The body with the contents of its fenced code blocks blanked out, line count
|
|
50
|
+
* preserved.
|
|
49
51
|
*
|
|
50
52
|
* A declaration and an example of one are the same text, and the pull request
|
|
51
53
|
* that introduced this feature had to describe it: written in a fence, the
|
|
52
|
-
* example would have been read as a constraint on the pull request
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* example would have been read as a constraint on the pull request documenting
|
|
55
|
+
* it. A fence is how every document here already says "this is a sample, not
|
|
56
|
+
* the thing itself", so it is the line to draw.
|
|
55
57
|
*
|
|
56
|
-
* Exported because `pr-report` reads the closing keywords (`Closes #12`) out
|
|
57
|
-
*
|
|
58
|
+
* Exported because `pr-report` reads the closing keywords (`Closes #12`) out of
|
|
59
|
+
* the same bodies and has to draw the line in the same place — a document
|
|
58
60
|
* showing what a declaration looks like must not become one.
|
|
59
61
|
*/
|
|
60
62
|
export const outsideCodeFences = (body: string): string => {
|
|
61
|
-
|
|
63
|
+
// The marker that opened the fence the current line is in, if any.
|
|
64
|
+
let mut_openedBy: string | undefined = undefined;
|
|
62
65
|
|
|
63
66
|
return body
|
|
64
67
|
.split('\n')
|
|
65
68
|
.map((line) => {
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
const fence = FENCE_LINE.exec(line)?.groups;
|
|
70
|
+
|
|
71
|
+
const marker = fence?.['marker'];
|
|
72
|
+
|
|
73
|
+
const rest = fence?.['rest'] ?? '';
|
|
74
|
+
|
|
75
|
+
if (mut_openedBy === undefined) {
|
|
76
|
+
// A backtick fence's info string cannot hold a backtick (CommonMark),
|
|
77
|
+
// so ```` ```x``` ```` is inline code, not an opening line.
|
|
78
|
+
if (
|
|
79
|
+
marker === undefined ||
|
|
80
|
+
(marker.startsWith('`') && rest.includes('`'))
|
|
81
|
+
) {
|
|
82
|
+
return line;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mut_openedBy = marker;
|
|
86
|
+
|
|
87
|
+
return '';
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
// Only a run of the opening character, at least as long, with nothing
|
|
91
|
+
// after it, closes the fence (CommonMark); any other fence line is part
|
|
92
|
+
// of the code, so a ``` inside a ```` or ~~~ block does not end it.
|
|
93
|
+
if (
|
|
94
|
+
marker !== undefined &&
|
|
95
|
+
marker.at(0) === mut_openedBy.at(0) &&
|
|
96
|
+
marker.length >= mut_openedBy.length &&
|
|
97
|
+
rest.trim() === ''
|
|
98
|
+
) {
|
|
99
|
+
mut_openedBy = undefined;
|
|
100
|
+
}
|
|
71
101
|
|
|
72
102
|
return '';
|
|
73
103
|
})
|
|
@@ -75,52 +105,50 @@ export const outsideCodeFences = (body: string): string => {
|
|
|
75
105
|
};
|
|
76
106
|
|
|
77
107
|
/**
|
|
78
|
-
* The `Merge-After` cycles,
|
|
79
|
-
*
|
|
80
|
-
*
|
|
108
|
+
* The `Merge-After` cycles, each listing the numbers in the order the edges run
|
|
109
|
+
* and rotated to start at its lowest number so that the same cycle is always
|
|
110
|
+
* reported the same way.
|
|
111
|
+
*
|
|
112
|
+
* Every number that lies on any cycle appears in at least one entry — callers
|
|
113
|
+
* take `cycles.flat()` as the set of numbers on a cycle. Each entry is the
|
|
114
|
+
* shortest cycle through the first number, in key order, that no earlier entry
|
|
115
|
+
* covers, so a component with several loops yields one entry per loop needed to
|
|
116
|
+
* cover it rather than every loop it holds.
|
|
81
117
|
*
|
|
82
118
|
* A cycle is not a state anything here can resolve: every pull request on one
|
|
83
119
|
* waits for another that waits, in the end, for it. Naming it is the whole of
|
|
84
120
|
* what can be done about it — without this the members would each report
|
|
85
121
|
* "waiting on #N" for as long as the loop runs, with nothing saying why that
|
|
86
|
-
* never changes. A pull request that declares itself is a cycle of one, and
|
|
87
|
-
*
|
|
122
|
+
* never changes. A pull request that declares itself is a cycle of one, and is
|
|
123
|
+
* found by the same search.
|
|
88
124
|
*/
|
|
89
125
|
export const findMergeAfterCycles = (
|
|
90
126
|
dependencies: ReadonlyMap<number, readonly number[]>,
|
|
91
127
|
): readonly (readonly number[])[] => {
|
|
92
|
-
const
|
|
128
|
+
const mut_covered = new Set<number>();
|
|
93
129
|
|
|
94
130
|
const mut_cycles = new Map<string, readonly number[]>();
|
|
95
131
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (at !== -1) {
|
|
100
|
-
// A back edge into the path being walked: everything from there on is
|
|
101
|
-
// the cycle.
|
|
102
|
-
const cycle = rotateToLowest(walked.slice(at));
|
|
103
|
-
|
|
104
|
-
mut_cycles.set(cycle.join(','), cycle);
|
|
105
|
-
|
|
106
|
-
return;
|
|
132
|
+
for (const node of dependencies.keys()) {
|
|
133
|
+
if (mut_covered.has(node)) {
|
|
134
|
+
continue;
|
|
107
135
|
}
|
|
108
136
|
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
// A search per node rather than one depth-first walk: a walk that skips
|
|
138
|
+
// finished nodes misses a node whose only way back runs through one.
|
|
139
|
+
const cycle = shortestCycleThrough(node, dependencies);
|
|
140
|
+
|
|
141
|
+
if (cycle === undefined) {
|
|
142
|
+
continue;
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
for (const
|
|
114
|
-
|
|
145
|
+
for (const member of cycle) {
|
|
146
|
+
mut_covered.add(member);
|
|
115
147
|
}
|
|
116
148
|
|
|
117
|
-
|
|
118
|
-
// this node's subtree can be missed by the walk that comes next.
|
|
119
|
-
mut_finished.add(node);
|
|
120
|
-
};
|
|
149
|
+
const rotated = rotateToLowest(cycle);
|
|
121
150
|
|
|
122
|
-
|
|
123
|
-
visit(node, []);
|
|
151
|
+
mut_cycles.set(rotated.join(','), rotated);
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
return Array.from(mut_cycles.values());
|
|
@@ -149,6 +177,71 @@ const parseDeclaration = (
|
|
|
149
177
|
.toArray(),
|
|
150
178
|
);
|
|
151
179
|
|
|
180
|
+
/** `start`, …, `end`, following `reachedFrom` back from `end`. */
|
|
181
|
+
const pathFromStart = (
|
|
182
|
+
start: number,
|
|
183
|
+
end: number,
|
|
184
|
+
reachedFrom: ReadonlyMap<number, number>,
|
|
185
|
+
): readonly number[] => {
|
|
186
|
+
const mut_reversed: number[] = [end];
|
|
187
|
+
|
|
188
|
+
let mut_node = end;
|
|
189
|
+
|
|
190
|
+
while (mut_node !== start) {
|
|
191
|
+
const previous = reachedFrom.get(mut_node);
|
|
192
|
+
|
|
193
|
+
if (previous === undefined) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
mut_reversed.push(previous);
|
|
198
|
+
|
|
199
|
+
mut_node = previous;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return mut_reversed.toReversed();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The shortest cycle through `start`, from `start` along the edges back to the
|
|
207
|
+
* node whose edge returns to it, or `undefined` when no path leads back.
|
|
208
|
+
*/
|
|
209
|
+
const shortestCycleThrough = (
|
|
210
|
+
start: number,
|
|
211
|
+
dependencies: ReadonlyMap<number, readonly number[]>,
|
|
212
|
+
): readonly number[] | undefined => {
|
|
213
|
+
// Each reached node, with the node it was first reached from.
|
|
214
|
+
const mut_reachedFrom = new Map<number, number>();
|
|
215
|
+
|
|
216
|
+
// Level by level: every node in the frontier is one edge further from `start`
|
|
217
|
+
// than the last, so the first edge back to `start` closes a shortest cycle.
|
|
218
|
+
let mut_frontier: readonly number[] = [start];
|
|
219
|
+
|
|
220
|
+
while (Arr.isNonEmpty(mut_frontier)) {
|
|
221
|
+
const mut_nextFrontier: number[] = [];
|
|
222
|
+
|
|
223
|
+
for (const node of mut_frontier) {
|
|
224
|
+
for (const next of dependencies.get(node) ?? []) {
|
|
225
|
+
if (next === start) {
|
|
226
|
+
return pathFromStart(start, node, mut_reachedFrom);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (mut_reachedFrom.has(next)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
mut_reachedFrom.set(next, node);
|
|
234
|
+
|
|
235
|
+
mut_nextFrontier.push(next);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
mut_frontier = mut_nextFrontier;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return undefined;
|
|
243
|
+
};
|
|
244
|
+
|
|
152
245
|
const rotateToLowest = (cycle: readonly number[]): readonly number[] => {
|
|
153
246
|
if (!Arr.isNonEmpty(cycle)) {
|
|
154
247
|
return cycle;
|
package/src/merge-after.test.mts
CHANGED
|
@@ -51,9 +51,9 @@ describe(parseMergeAfter, () => {
|
|
|
51
51
|
);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
// The fence markers and the column they sit in are what these two are
|
|
55
|
-
//
|
|
56
|
-
//
|
|
54
|
+
// The fence markers and the column they sit in are what these two are about,
|
|
55
|
+
// so the fixtures are arrays of lines rather than a template literal: what
|
|
56
|
+
// `dedent` would remove is what is being tested.
|
|
57
57
|
test('ignores a trailer inside a fenced code block', () => {
|
|
58
58
|
assert.deepStrictEqual(
|
|
59
59
|
parseMergeAfter(
|
|
@@ -79,6 +79,64 @@ describe(parseMergeAfter, () => {
|
|
|
79
79
|
);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('ignores the trailers inside a longer fence that holds a shorter one', () => {
|
|
83
|
+
assert.deepStrictEqual(
|
|
84
|
+
parseMergeAfter(
|
|
85
|
+
[
|
|
86
|
+
'````markdown',
|
|
87
|
+
'```',
|
|
88
|
+
'Merge-After: #1901',
|
|
89
|
+
'```',
|
|
90
|
+
'Merge-After: #1902',
|
|
91
|
+
'````',
|
|
92
|
+
'Merge-After: #1903',
|
|
93
|
+
'',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
),
|
|
96
|
+
[1903],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('ignores a trailer after a backtick fence inside a tilde fence', () => {
|
|
101
|
+
assert.deepStrictEqual(
|
|
102
|
+
parseMergeAfter(
|
|
103
|
+
['~~~', '```', 'Merge-After: #1901', '~~~', ''].join('\n'),
|
|
104
|
+
),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('does not close a fence on a line with an info string', () => {
|
|
110
|
+
assert.deepStrictEqual(
|
|
111
|
+
parseMergeAfter(
|
|
112
|
+
['```', '```ts', 'Merge-After: #1901', '```', ''].join('\n'),
|
|
113
|
+
),
|
|
114
|
+
[],
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('closes a fence on a longer run of the same character', () => {
|
|
119
|
+
assert.deepStrictEqual(
|
|
120
|
+
parseMergeAfter(
|
|
121
|
+
['```', 'Merge-After: #1901', '`````', 'Merge-After: #1903', ''].join(
|
|
122
|
+
'\n',
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
[1903],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('closes a fence on a CRLF line', () => {
|
|
130
|
+
assert.deepStrictEqual(
|
|
131
|
+
parseMergeAfter(
|
|
132
|
+
['```', 'Merge-After: #1901', '```', 'Merge-After: #1903', ''].join(
|
|
133
|
+
'\r\n',
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
[1903],
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
82
140
|
test('reads a body with the CRLF line endings GitHub returns', () => {
|
|
83
141
|
assert.deepStrictEqual(
|
|
84
142
|
parseMergeAfter('Adds the parser.\r\n\r\nMerge-After: #1901\r\n'),
|
|
@@ -218,6 +276,25 @@ describe(findMergeAfterCycles, () => {
|
|
|
218
276
|
);
|
|
219
277
|
});
|
|
220
278
|
|
|
279
|
+
test('reports a member whose only way back runs through a finished node', () => {
|
|
280
|
+
// 3 → 2 → 1 → 3 is a cycle as well as 1 ⇄ 2; a walk from 1 finishes 2
|
|
281
|
+
// before it reaches 3, so 3's edge into 2 must still put 3 on a cycle.
|
|
282
|
+
const cycles = findMergeAfterCycles(
|
|
283
|
+
new Map<number, readonly number[]>([
|
|
284
|
+
[1, [2, 3]],
|
|
285
|
+
[2, [1]],
|
|
286
|
+
[3, [2]],
|
|
287
|
+
]),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
assert.deepStrictEqual(cycles, [
|
|
291
|
+
[1, 2],
|
|
292
|
+
[1, 3, 2],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
assert.deepStrictEqual(new Set(cycles.flat()), new Set<number>([1, 2, 3]));
|
|
296
|
+
});
|
|
297
|
+
|
|
221
298
|
test('follows an edge to a pull request that declared nothing', () => {
|
|
222
299
|
assert.deepStrictEqual(
|
|
223
300
|
findMergeAfterCycles(new Map<number, readonly number[]>([[1, [2]]])),
|
package/src/queue-comments.mts
CHANGED
|
@@ -41,8 +41,8 @@ export type QueueComment<A> = Readonly<{
|
|
|
41
41
|
}>;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Reads `comments`, oldest first, for the oldest comment of each kind: each
|
|
45
|
-
*
|
|
44
|
+
* Reads `comments`, oldest first, for the oldest comment of each kind: each is
|
|
45
|
+
* one comment edited in place, and of several the oldest is the one.
|
|
46
46
|
*/
|
|
47
47
|
export const readQueueComments = (
|
|
48
48
|
comments: readonly OwnComment[],
|
package/src/queue-order.mts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Where each pull request stands in `unblock-prs`'s queue, and the order the
|
|
3
3
|
* worker picks the ones it may pick.
|
|
4
4
|
*
|
|
5
|
-
* The worker decides on someone's machine, from what `gh` and `git` return,
|
|
6
|
-
*
|
|
5
|
+
* The worker decides on someone's machine, from what `gh` and `git` return, and
|
|
6
|
+
* the Pull Requests Manager page draws the same queue from what GraphQL
|
|
7
7
|
* returns. So the deciding is here, once, over facts either side can supply,
|
|
8
8
|
* and each side does only its own reading: the worker reads some facts only
|
|
9
9
|
* when the answer needs them — the checks of a `BLOCKED` pull request, the
|
|
10
|
-
* native stack of one without auto-merge — which is why
|
|
11
|
-
*
|
|
10
|
+
* native stack of one without auto-merge — which is why {@link queueStanding}
|
|
11
|
+
* asks for them rather than taking them all up front.
|
|
12
12
|
*
|
|
13
13
|
* What the worker alone knows is left out: which pull requests one run has
|
|
14
14
|
* demoted (`demotions.mts` in `unblock-prs`) lives in that run's memory, so
|
|
@@ -39,8 +39,8 @@ export type QueueFacts = Readonly<{
|
|
|
39
39
|
headRef: string;
|
|
40
40
|
baseRef: string;
|
|
41
41
|
/**
|
|
42
|
-
* GitHub's `mergeStateStatus` — `BEHIND`, `DIRTY`, `CLEAN`, `BLOCKED` and
|
|
43
|
-
*
|
|
42
|
+
* GitHub's `mergeStateStatus` — `BEHIND`, `DIRTY`, `CLEAN`, `BLOCKED` and so
|
|
43
|
+
* on — or as near to it as the reader can tell.
|
|
44
44
|
*/
|
|
45
45
|
mergeState: string;
|
|
46
46
|
autoMerge: boolean;
|
|
@@ -57,8 +57,8 @@ export type QueueGraphMember = Readonly<{
|
|
|
57
57
|
baseRef: string;
|
|
58
58
|
fromFork: boolean;
|
|
59
59
|
/**
|
|
60
|
-
* The open issues it closes, whose `Start-After:` it inherits. Absent for
|
|
61
|
-
*
|
|
60
|
+
* The open issues it closes, whose `Start-After:` it inherits. Absent for a
|
|
61
|
+
* reader that did not read them: nothing is inherited.
|
|
62
62
|
*/
|
|
63
63
|
closes?: readonly number[];
|
|
64
64
|
}>;
|
|
@@ -68,8 +68,8 @@ export type QueueGraph = Readonly<{
|
|
|
68
68
|
/** Which open pull request each stacked one is on: child → parent. */
|
|
69
69
|
stackParents: ReadonlyMap<number, number>;
|
|
70
70
|
/**
|
|
71
|
-
* What each pull request must merge after: what it declared, the one it
|
|
72
|
-
*
|
|
71
|
+
* What each pull request must merge after: what it declared, the one it is
|
|
72
|
+
* stacked on, and what it inherits.
|
|
73
73
|
*/
|
|
74
74
|
dependencies: ReadonlyMap<number, readonly number[]>;
|
|
75
75
|
/**
|
|
@@ -165,9 +165,9 @@ export const versionBranchName = (defaultBranch: string): string =>
|
|
|
165
165
|
`changeset-release/${defaultBranch}` as const;
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* Whether a pull request from `headRef` is the version pull request. The
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* Whether a pull request from `headRef` is the version pull request. The branch
|
|
169
|
+
* is the only stable handle on it: the title and body are rewritten on every
|
|
170
|
+
* run, and the number changes with every release.
|
|
171
171
|
*/
|
|
172
172
|
export const isVersionBranch = (
|
|
173
173
|
headRef: string,
|
|
@@ -191,8 +191,8 @@ export const isMergeableState = (mergeState: string): boolean =>
|
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* The stacks, the declared order and its cycles, and what holds the release,
|
|
194
|
-
* read off the open pull requests. `startAfterOf` is what the issues they
|
|
195
|
-
*
|
|
194
|
+
* read off the open pull requests. `startAfterOf` is what the issues they close
|
|
195
|
+
* declare, each issue's `Start-After:` by its number.
|
|
196
196
|
*/
|
|
197
197
|
export const queueGraph = (
|
|
198
198
|
pulls: readonly QueueGraphMember[],
|
|
@@ -219,12 +219,12 @@ export const queueGraph = (
|
|
|
219
219
|
|
|
220
220
|
return [
|
|
221
221
|
pr.number,
|
|
222
|
-
// The version pull request declares nothing: its body is rewritten
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
222
|
+
// The version pull request declares nothing: its body is rewritten on
|
|
223
|
+
// every push to the base, so a trailer written there would hold for a
|
|
224
|
+
// cycle or two and then silently stop. `blocks-release`, on the pull
|
|
225
|
+
// requests it is waiting for, is how an order is declared on it. Other
|
|
226
|
+
// pull requests may still name *it*. A stacked pull request waits for
|
|
227
|
+
// the layer below it whether or not it says so.
|
|
228
228
|
isVersionBranch(pr.headRef, defaultBranch)
|
|
229
229
|
? []
|
|
230
230
|
: Arr.uniq([
|
|
@@ -241,8 +241,8 @@ export const queueGraph = (
|
|
|
241
241
|
|
|
242
242
|
return [
|
|
243
243
|
number,
|
|
244
|
-
// The version pull request closes nothing it means to, and is held
|
|
245
|
-
//
|
|
244
|
+
// The version pull request closes nothing it means to, and is held by
|
|
245
|
+
// `blocks-release` alone.
|
|
246
246
|
isVersionBranch(headRef, defaultBranch)
|
|
247
247
|
? []
|
|
248
248
|
: (inheritedOrders.get(number)?.mergeAfter ?? []).filter(
|
|
@@ -288,15 +288,15 @@ export const queueStanding = (
|
|
|
288
288
|
if (outOfScope !== undefined) {
|
|
289
289
|
// `merge-queued` is an explicit request, so anything that answers it with
|
|
290
290
|
// "no" is worth saying out loud — a queued pull request left as a draft
|
|
291
|
-
// would otherwise sit in the queue forever with nothing saying why. A
|
|
292
|
-
//
|
|
291
|
+
// would otherwise sit in the queue forever with nothing saying why. A pull
|
|
292
|
+
// request that never asked is passed over in silence.
|
|
293
293
|
return hasLabel(pr, MERGE_QUEUED_LABEL)
|
|
294
294
|
? { kind: 'unactionable', reason: outOfScope }
|
|
295
295
|
: { kind: 'ignore' };
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
// A pull request set aside, or claimed, is a skill's or a person's until
|
|
299
|
-
//
|
|
298
|
+
// A pull request set aside, or claimed, is a skill's or a person's until its
|
|
299
|
+
// comment is resolved (`queue-owner.mts`).
|
|
300
300
|
const owner = context.owners.get(pr.number);
|
|
301
301
|
|
|
302
302
|
if (owner !== undefined && owner.owner !== 'worker') {
|
|
@@ -327,9 +327,9 @@ export const queueStanding = (
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
// The version pull request is rebuilt from the tip of the base by the
|
|
331
|
-
//
|
|
332
|
-
//
|
|
330
|
+
// The version pull request is rebuilt from the tip of the base by the release
|
|
331
|
+
// workflow, never rebased here; taking `skip-ci` off is the whole of what it
|
|
332
|
+
// can be given.
|
|
333
333
|
if (isVersionBranch(pr.headRef, context.defaultBranch)) {
|
|
334
334
|
return versionStanding(pr, context, reads);
|
|
335
335
|
}
|
|
@@ -342,9 +342,9 @@ export const queueStanding = (
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
switch (pr.mergeState) {
|
|
345
|
-
// `DIRTY` is a candidate too: it answers whether *merging* conflicts,
|
|
346
|
-
//
|
|
347
|
-
//
|
|
345
|
+
// `DIRTY` is a candidate too: it answers whether *merging* conflicts, from
|
|
346
|
+
// a cached computation, where the worker *rebases*, and the rebase is what
|
|
347
|
+
// says whether the conflict is real.
|
|
348
348
|
case 'BEHIND':
|
|
349
349
|
case 'DIRTY':
|
|
350
350
|
return waitingOn(pr, context) ?? { kind: 'candidate' };
|
|
@@ -363,10 +363,10 @@ export const queueStanding = (
|
|
|
363
363
|
};
|
|
364
364
|
|
|
365
365
|
/**
|
|
366
|
-
* Whether what {@link queueStanding} decided is worth asking the review
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
366
|
+
* Whether what {@link queueStanding} decided is worth asking the review about:
|
|
367
|
+
* one about to be picked, or one about to be watched while GitHub still calls
|
|
368
|
+
* it `BLOCKED`. A merge state GitHub already calls mergeable is its own answer
|
|
369
|
+
* that nothing but the checks is left.
|
|
370
370
|
*/
|
|
371
371
|
export const asksReview = (
|
|
372
372
|
standing: QueueStanding,
|
|
@@ -377,8 +377,8 @@ export const asksReview = (
|
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
379
|
* The standing once the review is known: a hold that waits for someone holds
|
|
380
|
-
* it; one that only a bypass merges, and none at all, change nothing here —
|
|
381
|
-
*
|
|
380
|
+
* it; one that only a bypass merges, and none at all, change nothing here — the
|
|
381
|
+
* worker leaves the first unarmed, which is not a question of order.
|
|
382
382
|
*/
|
|
383
383
|
export const withReviewHold = (
|
|
384
384
|
standing: QueueStanding,
|
|
@@ -392,9 +392,9 @@ export const withReviewHold = (
|
|
|
392
392
|
export type QueuePriority = 'high' | 'low' | 'none' | 'both';
|
|
393
393
|
|
|
394
394
|
/**
|
|
395
|
-
* Which priority label a pull request carries: `both` is the two at once,
|
|
396
|
-
*
|
|
397
|
-
*
|
|
395
|
+
* Which priority label a pull request carries: `both` is the two at once, which
|
|
396
|
+
* contradict each other and so count as neither. `priority:mid` is the same as
|
|
397
|
+
* neither here.
|
|
398
398
|
*/
|
|
399
399
|
export const priorityOfLabels = (labels: readonly string[]): QueuePriority => {
|
|
400
400
|
const high = labels.includes(PRIORITY_HIGH_LABEL);
|
|
@@ -427,9 +427,9 @@ export const priorityRankOfLabels = (labels: readonly string[]): 0 | 1 | 2 => {
|
|
|
427
427
|
* ruleset bypass merges ({@link ReviewHold}): that one ranks with
|
|
428
428
|
* `priority:low` unless labelled `priority:high` — alone, since the two
|
|
429
429
|
* together count as neither. Picked, it is released, goes green and is set
|
|
430
|
-
* aside for a person, so the cycle spent on it lands nothing; picked last,
|
|
431
|
-
*
|
|
432
|
-
*
|
|
430
|
+
* aside for a person, so the cycle spent on it lands nothing; picked last, it
|
|
431
|
+
* still has its checks run. `priority:high` is a person saying they want that
|
|
432
|
+
* verdict now, and is the one way to ask for it, so it still counts.
|
|
433
433
|
*/
|
|
434
434
|
export const pickPriorityRank = (
|
|
435
435
|
labels: readonly string[],
|
|
@@ -443,9 +443,9 @@ export const pickPriorityRank = (
|
|
|
443
443
|
};
|
|
444
444
|
|
|
445
445
|
/**
|
|
446
|
-
* The order the worker picks candidates in. The declared order has already
|
|
447
|
-
*
|
|
448
|
-
*
|
|
446
|
+
* The order the worker picks candidates in. The declared order has already been
|
|
447
|
+
* applied, as a gate rather than a sort, so this is lowest number first within
|
|
448
|
+
* these ranks, outermost first:
|
|
449
449
|
*
|
|
450
450
|
* 1. What a watch has seen green and still open (`demoted`) goes after
|
|
451
451
|
* everything else, the release and `priority:high` included: everything
|
|
@@ -463,8 +463,8 @@ export const pickPriorityRank = (
|
|
|
463
463
|
* release: it only says that when a queued change and the release are
|
|
464
464
|
* both ready, the change goes first.
|
|
465
465
|
*
|
|
466
|
-
* `toSorted` is stable, so each sort keeps the order of the one before it
|
|
467
|
-
*
|
|
466
|
+
* `toSorted` is stable, so each sort keeps the order of the one before it among
|
|
467
|
+
* equals, and the last one is the outermost.
|
|
468
468
|
*/
|
|
469
469
|
export const orderCandidates = <P,>(
|
|
470
470
|
candidates: readonly P[],
|
|
@@ -498,9 +498,8 @@ export const orderCandidates = <P,>(
|
|
|
498
498
|
};
|
|
499
499
|
|
|
500
500
|
/**
|
|
501
|
-
* Whether `pr` carries the label `name`. A function rather than `includes`
|
|
502
|
-
*
|
|
503
|
-
* no.
|
|
501
|
+
* Whether `pr` carries the label `name`. A function rather than `includes` in
|
|
502
|
+
* place, which narrows the label constant itself to `never` where it says no.
|
|
504
503
|
*/
|
|
505
504
|
const hasLabel = (
|
|
506
505
|
pr: Readonly<{ labels: readonly string[] }>,
|
|
@@ -522,10 +521,9 @@ const outOfScopeReason = (
|
|
|
522
521
|
return `not labelled ${MERGE_QUEUED_LABEL}`;
|
|
523
522
|
}
|
|
524
523
|
|
|
525
|
-
// A base that is neither the default branch nor another open pull
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
// moves it.
|
|
524
|
+
// A base that is neither the default branch nor another open pull request's
|
|
525
|
+
// branch is one nothing here knows how to land on. A stacked one whose parent
|
|
526
|
+
// has just merged reads this way for the moment before GitHub moves it.
|
|
529
527
|
return pr.isDraft
|
|
530
528
|
? 'draft'
|
|
531
529
|
: pr.baseRef !== context.defaultBranch &&
|
|
@@ -565,17 +563,17 @@ const byChecks = (reads: QueueReads): QueueStanding =>
|
|
|
565
563
|
: ({ kind: 'in-flight' } as const);
|
|
566
564
|
|
|
567
565
|
/**
|
|
568
|
-
* The version pull request: held while anything labelled `blocks-release`
|
|
569
|
-
*
|
|
570
|
-
*
|
|
566
|
+
* The version pull request: held while anything labelled `blocks-release` is
|
|
567
|
+
* open, and while the branch was built before the tip of the base, then a
|
|
568
|
+
* candidate if paused and otherwise judged by its checks.
|
|
571
569
|
*/
|
|
572
570
|
const versionStanding = (
|
|
573
571
|
pr: QueueFacts,
|
|
574
572
|
context: QueueContext,
|
|
575
573
|
reads: QueueReads,
|
|
576
574
|
): QueueStanding => {
|
|
577
|
-
// Never one of its own blockers: the label on it would otherwise hold it
|
|
578
|
-
//
|
|
575
|
+
// Never one of its own blockers: the label on it would otherwise hold it for
|
|
576
|
+
// as long as it existed.
|
|
579
577
|
const by = context.releaseBlockers.filter(
|
|
580
578
|
(blocker) => blocker.number !== pr.number,
|
|
581
579
|
);
|