pr-report-core 0.0.5 → 0.1.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 +6 -6
- package/dist/checks.mjs.map +1 -1
- package/dist/labels.d.mts +18 -0
- package/dist/labels.d.mts.map +1 -1
- package/dist/labels.mjs +18 -0
- package/dist/labels.mjs.map +1 -1
- package/dist/queue-order.d.mts.map +1 -1
- package/dist/queue-order.mjs.map +1 -1
- package/dist/queue-owner.d.mts +3 -7
- package/dist/queue-owner.d.mts.map +1 -1
- package/dist/queue-owner.mjs +1 -12
- package/dist/queue-owner.mjs.map +1 -1
- package/dist/report.d.mts.map +1 -1
- package/dist/report.mjs +2 -2
- package/dist/report.mjs.map +1 -1
- package/dist/start-after.d.mts +32 -7
- package/dist/start-after.d.mts.map +1 -1
- package/dist/start-after.mjs +20 -1
- package/dist/start-after.mjs.map +1 -1
- package/package.json +9 -9
- package/src/checks.mts +1 -1
- package/src/checks.test.mts +20 -15
- package/src/labels.mts +20 -0
- package/src/merge-after.test.mts +13 -7
- package/src/queue-order.mts +5 -2
- package/src/queue-order.test.mts +19 -15
- package/src/queue-owner.mts +4 -18
- package/src/queue-owner.test.mts +4 -4
- package/src/report.mts +7 -6
- package/src/report.test.mts +57 -7
- package/src/stack.test.mts +2 -2
- package/src/start-after.mts +47 -8
- package/src/start-after.test.mts +79 -30
package/src/checks.test.mts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
statesFromCheckRuns,
|
|
9
9
|
summarizeChecks,
|
|
10
10
|
} from './checks.mjs';
|
|
11
|
+
import type { ContextState } from './types.mjs';
|
|
11
12
|
|
|
12
13
|
describe(classifyCheckRun, () => {
|
|
13
14
|
test('an unfinished run is pending whatever it last concluded', () => {
|
|
@@ -60,7 +61,7 @@ describe(summarizeChecks, () => {
|
|
|
60
61
|
test('every required context green is a pass', () => {
|
|
61
62
|
const summary = summarizeChecks({
|
|
62
63
|
required,
|
|
63
|
-
reported: new Map([
|
|
64
|
+
reported: new Map<string, ContextState>([
|
|
64
65
|
['code-check-result / result', 'passed'],
|
|
65
66
|
['no-skip-ci-label', 'passed'],
|
|
66
67
|
]),
|
|
@@ -79,7 +80,9 @@ describe(summarizeChecks, () => {
|
|
|
79
80
|
// absent from the check list rather than pending in it.
|
|
80
81
|
const summary = summarizeChecks({
|
|
81
82
|
required,
|
|
82
|
-
reported: new Map([
|
|
83
|
+
reported: new Map<string, ContextState>([
|
|
84
|
+
['code-check-result / result', 'passed'],
|
|
85
|
+
]),
|
|
83
86
|
paused: false,
|
|
84
87
|
running: false,
|
|
85
88
|
baseCovered: true,
|
|
@@ -95,7 +98,7 @@ describe(summarizeChecks, () => {
|
|
|
95
98
|
test('a failure outranks anything still running', () => {
|
|
96
99
|
const summary = summarizeChecks({
|
|
97
100
|
required,
|
|
98
|
-
reported: new Map([
|
|
101
|
+
reported: new Map<string, ContextState>([
|
|
99
102
|
['code-check-result / result', 'failed'],
|
|
100
103
|
['no-skip-ci-label', 'pending'],
|
|
101
104
|
]),
|
|
@@ -117,7 +120,7 @@ describe(summarizeChecks, () => {
|
|
|
117
120
|
// asks about.
|
|
118
121
|
const summary = summarizeChecks({
|
|
119
122
|
required,
|
|
120
|
-
reported: new Map([
|
|
123
|
+
reported: new Map<string, ContextState>([
|
|
121
124
|
['code-check-result / result', 'failed'],
|
|
122
125
|
['no-skip-ci-label', 'pending'],
|
|
123
126
|
]),
|
|
@@ -136,7 +139,7 @@ describe(summarizeChecks, () => {
|
|
|
136
139
|
test('a skipped context counts as met and is listed as skipped', () => {
|
|
137
140
|
const summary = summarizeChecks({
|
|
138
141
|
required,
|
|
139
|
-
reported: new Map([
|
|
142
|
+
reported: new Map<string, ContextState>([
|
|
140
143
|
['code-check-result / result', 'skipped'],
|
|
141
144
|
['no-skip-ci-label', 'passed'],
|
|
142
145
|
]),
|
|
@@ -155,7 +158,7 @@ describe(summarizeChecks, () => {
|
|
|
155
158
|
test('anything still running on the commit keeps the verdict pending', () => {
|
|
156
159
|
const summary = summarizeChecks({
|
|
157
160
|
required,
|
|
158
|
-
reported: new Map([
|
|
161
|
+
reported: new Map<string, ContextState>([
|
|
159
162
|
['code-check-result / result', 'passed'],
|
|
160
163
|
['no-skip-ci-label', 'passed'],
|
|
161
164
|
]),
|
|
@@ -173,7 +176,7 @@ describe(summarizeChecks, () => {
|
|
|
173
176
|
test('a failure is not hidden by something still running', () => {
|
|
174
177
|
const summary = summarizeChecks({
|
|
175
178
|
required,
|
|
176
|
-
reported: new Map([
|
|
179
|
+
reported: new Map<string, ContextState>([
|
|
177
180
|
['code-check-result / result', 'failed'],
|
|
178
181
|
['no-skip-ci-label', 'passed'],
|
|
179
182
|
]),
|
|
@@ -188,7 +191,7 @@ describe(summarizeChecks, () => {
|
|
|
188
191
|
test('a context nothing requires is not counted', () => {
|
|
189
192
|
const summary = summarizeChecks({
|
|
190
193
|
required,
|
|
191
|
-
reported: new Map([
|
|
194
|
+
reported: new Map<string, ContextState>([
|
|
192
195
|
['code-check-result / result', 'passed'],
|
|
193
196
|
['no-skip-ci-label', 'passed'],
|
|
194
197
|
['some-optional-job', 'failed'],
|
|
@@ -213,7 +216,7 @@ describe(summarizeChecks, () => {
|
|
|
213
216
|
test('a context that never reported is not waited for', () => {
|
|
214
217
|
const summary = summarizeChecks({
|
|
215
218
|
required: withLint,
|
|
216
|
-
reported: new Map([
|
|
219
|
+
reported: new Map<string, ContextState>([
|
|
217
220
|
['code-check-result / result', 'passed'],
|
|
218
221
|
['no-skip-ci-label', 'passed'],
|
|
219
222
|
]),
|
|
@@ -236,7 +239,7 @@ describe(summarizeChecks, () => {
|
|
|
236
239
|
// on it at all; that is not a pass over zero contexts.
|
|
237
240
|
const summary = summarizeChecks({
|
|
238
241
|
required: withLint,
|
|
239
|
-
reported: new Map(),
|
|
242
|
+
reported: new Map<string, ContextState>(),
|
|
240
243
|
paused: false,
|
|
241
244
|
running: false,
|
|
242
245
|
baseCovered: false,
|
|
@@ -252,7 +255,7 @@ describe(summarizeChecks, () => {
|
|
|
252
255
|
test('a context that reported is judged as anywhere else', () => {
|
|
253
256
|
const summary = summarizeChecks({
|
|
254
257
|
required: withLint,
|
|
255
|
-
reported: new Map([
|
|
258
|
+
reported: new Map<string, ContextState>([
|
|
256
259
|
['code-check-result / result', 'failed'],
|
|
257
260
|
['no-skip-ci-label', 'pending'],
|
|
258
261
|
]),
|
|
@@ -270,7 +273,9 @@ describe(summarizeChecks, () => {
|
|
|
270
273
|
test('on a base the ruleset covers, nothing is excused from reporting', () => {
|
|
271
274
|
const summary = summarizeChecks({
|
|
272
275
|
required,
|
|
273
|
-
reported: new Map([
|
|
276
|
+
reported: new Map<string, ContextState>([
|
|
277
|
+
['code-check-result / result', 'passed'],
|
|
278
|
+
]),
|
|
274
279
|
paused: false,
|
|
275
280
|
running: false,
|
|
276
281
|
baseCovered: true,
|
|
@@ -422,7 +427,7 @@ describe(reportedContexts, () => {
|
|
|
422
427
|
[completedRun('code-check-result / result', 'success')],
|
|
423
428
|
[{ context: 'no-skip-ci-label', state: 'pending' }],
|
|
424
429
|
),
|
|
425
|
-
new Map([
|
|
430
|
+
new Map<string, ContextState>([
|
|
426
431
|
['code-check-result / result', 'passed'],
|
|
427
432
|
['no-skip-ci-label', 'pending'],
|
|
428
433
|
]),
|
|
@@ -435,7 +440,7 @@ describe(reportedContexts, () => {
|
|
|
435
440
|
[completedRun('lint', 'failure')],
|
|
436
441
|
[{ context: 'lint', state: 'success' }],
|
|
437
442
|
),
|
|
438
|
-
new Map([['lint', 'failed']]),
|
|
443
|
+
new Map<string, ContextState>([['lint', 'failed']]),
|
|
439
444
|
);
|
|
440
445
|
|
|
441
446
|
assert.deepStrictEqual(
|
|
@@ -443,7 +448,7 @@ describe(reportedContexts, () => {
|
|
|
443
448
|
[completedRun('lint', 'success')],
|
|
444
449
|
[{ context: 'lint', state: 'pending' }],
|
|
445
450
|
),
|
|
446
|
-
new Map([['lint', 'pending']]),
|
|
451
|
+
new Map<string, ContextState>([['lint', 'pending']]),
|
|
447
452
|
);
|
|
448
453
|
});
|
|
449
454
|
});
|
package/src/labels.mts
CHANGED
|
@@ -77,3 +77,23 @@ export const PRIORITY_MID_LABEL = 'priority:mid';
|
|
|
77
77
|
* `unblock-prs`: what is held is simply not labelled `merge-queued`.
|
|
78
78
|
*/
|
|
79
79
|
export const PENDING_LABEL = 'pending';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The label that says someone has started an issue, before there is a pull
|
|
83
|
+
* request to say so. `start-issue` puts it on, and refuses an issue that
|
|
84
|
+
* already carries it, so that two sessions do not pick up the same one; the
|
|
85
|
+
* Pull Requests Manager page and `pr-report` show a startable issue carrying
|
|
86
|
+
* it as in progress, as they show one an open pull request closes
|
|
87
|
+
* (`start-after.mts`'s `progressOf`). Nothing takes it off when the work
|
|
88
|
+
* merges: the issue closes, and the label with it stops mattering.
|
|
89
|
+
*/
|
|
90
|
+
export const IN_PROGRESS_LABEL = 'in-progress';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The label that says someone means to start an issue next. It holds nothing
|
|
94
|
+
* back — `start-issue` takes it off as it puts {@link IN_PROGRESS_LABEL} on —
|
|
95
|
+
* and only tells whoever is choosing what to start that this one is spoken
|
|
96
|
+
* for; the page and `pr-report` show a startable issue carrying it as
|
|
97
|
+
* planned.
|
|
98
|
+
*/
|
|
99
|
+
export const PLANNED_LABEL = 'planned';
|
package/src/merge-after.test.mts
CHANGED
|
@@ -141,7 +141,7 @@ describe(findMergeAfterCycles, () => {
|
|
|
141
141
|
test('finds nothing in a chain', () => {
|
|
142
142
|
assert.deepStrictEqual(
|
|
143
143
|
findMergeAfterCycles(
|
|
144
|
-
new Map([
|
|
144
|
+
new Map<number, readonly number[]>([
|
|
145
145
|
[3, [2]],
|
|
146
146
|
[2, [1]],
|
|
147
147
|
[1, []],
|
|
@@ -154,7 +154,7 @@ describe(findMergeAfterCycles, () => {
|
|
|
154
154
|
test('finds nothing in a diamond', () => {
|
|
155
155
|
assert.deepStrictEqual(
|
|
156
156
|
findMergeAfterCycles(
|
|
157
|
-
new Map([
|
|
157
|
+
new Map<number, readonly number[]>([
|
|
158
158
|
[4, [2, 3]],
|
|
159
159
|
[3, [1]],
|
|
160
160
|
[2, [1]],
|
|
@@ -168,7 +168,7 @@ describe(findMergeAfterCycles, () => {
|
|
|
168
168
|
test('finds a two-node cycle, starting at its lowest number', () => {
|
|
169
169
|
assert.deepStrictEqual(
|
|
170
170
|
findMergeAfterCycles(
|
|
171
|
-
new Map([
|
|
171
|
+
new Map<number, readonly number[]>([
|
|
172
172
|
[2, [1]],
|
|
173
173
|
[1, [2]],
|
|
174
174
|
]),
|
|
@@ -178,7 +178,10 @@ describe(findMergeAfterCycles, () => {
|
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
test('finds a pull request that declares itself', () => {
|
|
181
|
-
assert.deepStrictEqual(
|
|
181
|
+
assert.deepStrictEqual(
|
|
182
|
+
findMergeAfterCycles(new Map<number, readonly number[]>([[7, [7]]])),
|
|
183
|
+
[[7]],
|
|
184
|
+
);
|
|
182
185
|
});
|
|
183
186
|
|
|
184
187
|
test('reports a cycle once however it is entered', () => {
|
|
@@ -186,7 +189,7 @@ describe(findMergeAfterCycles, () => {
|
|
|
186
189
|
// different node each time.
|
|
187
190
|
assert.deepStrictEqual(
|
|
188
191
|
findMergeAfterCycles(
|
|
189
|
-
new Map([
|
|
192
|
+
new Map<number, readonly number[]>([
|
|
190
193
|
[9, [1]],
|
|
191
194
|
[10, [2]],
|
|
192
195
|
[1, [2]],
|
|
@@ -201,7 +204,7 @@ describe(findMergeAfterCycles, () => {
|
|
|
201
204
|
test('finds two independent cycles', () => {
|
|
202
205
|
assert.deepStrictEqual(
|
|
203
206
|
findMergeAfterCycles(
|
|
204
|
-
new Map([
|
|
207
|
+
new Map<number, readonly number[]>([
|
|
205
208
|
[1, [2]],
|
|
206
209
|
[2, [1]],
|
|
207
210
|
[5, [6]],
|
|
@@ -216,6 +219,9 @@ describe(findMergeAfterCycles, () => {
|
|
|
216
219
|
});
|
|
217
220
|
|
|
218
221
|
test('follows an edge to a pull request that declared nothing', () => {
|
|
219
|
-
assert.deepStrictEqual(
|
|
222
|
+
assert.deepStrictEqual(
|
|
223
|
+
findMergeAfterCycles(new Map<number, readonly number[]>([[1, [2]]])),
|
|
224
|
+
[],
|
|
225
|
+
);
|
|
220
226
|
});
|
|
221
227
|
});
|
package/src/queue-order.mts
CHANGED
|
@@ -197,7 +197,10 @@ export const isMergeableState = (mergeState: string): boolean =>
|
|
|
197
197
|
export const queueGraph = (
|
|
198
198
|
pulls: readonly QueueGraphMember[],
|
|
199
199
|
defaultBranch: string,
|
|
200
|
-
startAfterOf: ReadonlyMap<number, readonly number[]> = new Map
|
|
200
|
+
startAfterOf: ReadonlyMap<number, readonly number[]> = new Map<
|
|
201
|
+
number,
|
|
202
|
+
readonly number[]
|
|
203
|
+
>(),
|
|
201
204
|
): QueueGraph => {
|
|
202
205
|
const sorted = pulls.toSorted((a, b) => a.number - b.number);
|
|
203
206
|
|
|
@@ -207,7 +210,7 @@ export const queueGraph = (
|
|
|
207
210
|
const inheritedOrders = inheritStartAfter({
|
|
208
211
|
pulls: sorted.map(({ number, closes = [] }) => ({ number, closes })),
|
|
209
212
|
startAfterOf,
|
|
210
|
-
openIssues: new Set(),
|
|
213
|
+
openIssues: new Set<number>(),
|
|
211
214
|
});
|
|
212
215
|
|
|
213
216
|
const declared: ReadonlyMap<number, readonly number[]> = new Map(
|
package/src/queue-order.test.mts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type QueueFacts,
|
|
15
15
|
type QueueGraphMember,
|
|
16
16
|
} from './queue-order.mjs';
|
|
17
|
+
import type { QueueOwner } from './queue-owner.mjs';
|
|
17
18
|
|
|
18
19
|
describe(queueGraph, () => {
|
|
19
20
|
test('reads the declared order and the stack together, and the cycles in them', () => {
|
|
@@ -26,11 +27,14 @@ describe(queueGraph, () => {
|
|
|
26
27
|
'main',
|
|
27
28
|
);
|
|
28
29
|
|
|
29
|
-
assert.deepStrictEqual(
|
|
30
|
+
assert.deepStrictEqual(
|
|
31
|
+
graph.stackParents,
|
|
32
|
+
new Map<number, number>([[2, 1]]),
|
|
33
|
+
);
|
|
30
34
|
|
|
31
35
|
assert.deepStrictEqual(
|
|
32
36
|
graph.dependencies,
|
|
33
|
-
new Map([
|
|
37
|
+
new Map<number, readonly number[]>([
|
|
34
38
|
[1, [3]],
|
|
35
39
|
[2, [1]],
|
|
36
40
|
[3, [1]],
|
|
@@ -65,7 +69,7 @@ describe(queueGraph, () => {
|
|
|
65
69
|
member({ number: 3, body: 'Merge-After: #1' }),
|
|
66
70
|
],
|
|
67
71
|
'main',
|
|
68
|
-
new Map([
|
|
72
|
+
new Map<number, readonly number[]>([
|
|
69
73
|
[11, [12]],
|
|
70
74
|
[12, [3]],
|
|
71
75
|
]),
|
|
@@ -73,7 +77,7 @@ describe(queueGraph, () => {
|
|
|
73
77
|
|
|
74
78
|
assert.deepStrictEqual(
|
|
75
79
|
graph.dependencies,
|
|
76
|
-
new Map([
|
|
80
|
+
new Map<number, readonly number[]>([
|
|
77
81
|
[1, [2]],
|
|
78
82
|
[2, [3]],
|
|
79
83
|
[3, [1]],
|
|
@@ -82,7 +86,7 @@ describe(queueGraph, () => {
|
|
|
82
86
|
|
|
83
87
|
assert.deepStrictEqual(
|
|
84
88
|
graph.inherited,
|
|
85
|
-
new Map([
|
|
89
|
+
new Map<number, readonly number[]>([
|
|
86
90
|
[1, [2]],
|
|
87
91
|
[2, [3]],
|
|
88
92
|
[3, []],
|
|
@@ -132,7 +136,7 @@ describe(queueStanding, () => {
|
|
|
132
136
|
assert.deepStrictEqual(
|
|
133
137
|
queueStanding(
|
|
134
138
|
facts({ number: 1 }),
|
|
135
|
-
context({ owners: new Map([[1, owner]]) }),
|
|
139
|
+
context({ owners: new Map<number, QueueOwner>([[1, owner]]) }),
|
|
136
140
|
),
|
|
137
141
|
{ kind: 'owned', owner },
|
|
138
142
|
);
|
|
@@ -142,7 +146,7 @@ describe(queueStanding, () => {
|
|
|
142
146
|
assert.deepStrictEqual(
|
|
143
147
|
queueStanding(
|
|
144
148
|
facts({ number: 2, baseRef: 'feature/1' }),
|
|
145
|
-
context({ stackParents: new Map([[2, 1]]) }),
|
|
149
|
+
context({ stackParents: new Map<number, number>([[2, 1]]) }),
|
|
146
150
|
),
|
|
147
151
|
{ kind: 'stacked', on: 1, cyclic: false },
|
|
148
152
|
);
|
|
@@ -184,8 +188,8 @@ describe(queueStanding, () => {
|
|
|
184
188
|
queueStanding(
|
|
185
189
|
pr,
|
|
186
190
|
context({
|
|
187
|
-
dependencies: new Map([[2, [1]]]),
|
|
188
|
-
openNumbers: new Set([1, 2]),
|
|
191
|
+
dependencies: new Map<number, readonly number[]>([[2, [1]]]),
|
|
192
|
+
openNumbers: new Set<number>([1, 2]),
|
|
189
193
|
}),
|
|
190
194
|
),
|
|
191
195
|
{ kind: 'waiting', on: [1], cyclic: false },
|
|
@@ -420,13 +424,13 @@ const member = (
|
|
|
420
424
|
const context = (fields: Partial<QueueContext> = {}): QueueContext =>
|
|
421
425
|
({
|
|
422
426
|
defaultBranch: 'main',
|
|
423
|
-
stackParents: new Map(),
|
|
424
|
-
dependencies: new Map(),
|
|
425
|
-
inherited: new Map(),
|
|
427
|
+
stackParents: new Map<number, number>(),
|
|
428
|
+
dependencies: new Map<number, readonly number[]>(),
|
|
429
|
+
inherited: new Map<number, readonly number[]>(),
|
|
426
430
|
cycles: [],
|
|
427
|
-
cyclic: new Set(),
|
|
428
|
-
openNumbers: new Set(),
|
|
431
|
+
cyclic: new Set<number>(),
|
|
432
|
+
openNumbers: new Set<number>(),
|
|
429
433
|
releaseBlockers: [],
|
|
430
|
-
owners: new Map(),
|
|
434
|
+
owners: new Map<number, QueueOwner>(),
|
|
431
435
|
...fields,
|
|
432
436
|
}) as const;
|
package/src/queue-owner.mts
CHANGED
|
@@ -40,8 +40,9 @@ export const QUEUE_OWNERS = ['worker', 'skill', 'human', 'none'] as const;
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* `skill` is the `/unblock-prs` skill, which fixes every queued pull request
|
|
43
|
-
* the worker sets aside — the
|
|
44
|
-
*
|
|
43
|
+
* the worker sets aside — a branch the repository generates included, which
|
|
44
|
+
* it builds again rather than rebases (`generatedBranches` in
|
|
45
|
+
* `.github/unblock-prs.json`).
|
|
45
46
|
*/
|
|
46
47
|
export type QueueOwner = Readonly<{
|
|
47
48
|
owner: 'human' | 'none' | 'skill' | 'worker';
|
|
@@ -214,13 +215,7 @@ export const queueOwner = (pr: QueueOwnerInput): QueueOwner | undefined => {
|
|
|
214
215
|
const handedOff = skillHandOff(pr, setAside.reason, queued);
|
|
215
216
|
|
|
216
217
|
return handedOff === undefined
|
|
217
|
-
? {
|
|
218
|
-
owner: 'skill',
|
|
219
|
-
reason:
|
|
220
|
-
setAside.reason === 'rebase-failed' && isPnpmUpdate(pr)
|
|
221
|
-
? `${said}; the skill has \`pnpm-update.yml\` rebuild it from main`
|
|
222
|
-
: `${said}; ${why}`,
|
|
223
|
-
}
|
|
218
|
+
? { owner: 'skill', reason: `${said}; ${why}` }
|
|
224
219
|
: { owner: 'human', reason: `${said}: ${handedOff}` };
|
|
225
220
|
};
|
|
226
221
|
|
|
@@ -274,15 +269,6 @@ export const isSetAsideReason = (reason: string): reason is SetAsideReason =>
|
|
|
274
269
|
|
|
275
270
|
const KNOWN_REASONS: ReadonlySet<string> = new Set(SET_ASIDE_REASONS);
|
|
276
271
|
|
|
277
|
-
/**
|
|
278
|
-
* The dependency update, which `pnpm-update.yml` builds from the default
|
|
279
|
-
* branch: a conflict on it is resolved by building it again, not by a rebase.
|
|
280
|
-
*/
|
|
281
|
-
export const PNPM_UPDATE_BRANCH = 'chore/pnpm-update';
|
|
282
|
-
|
|
283
|
-
const isPnpmUpdate = (pr: QueueOwnerInput): boolean =>
|
|
284
|
-
pr.headRef === PNPM_UPDATE_BRANCH;
|
|
285
|
-
|
|
286
272
|
/**
|
|
287
273
|
* Why nothing in the queue can move `pr` until a person does something, if
|
|
288
274
|
* so: the worker passes these over with a note, and no skill takes them up.
|
package/src/queue-owner.test.mts
CHANGED
|
@@ -177,7 +177,7 @@ describe(queueOwner, () => {
|
|
|
177
177
|
);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
test('gives
|
|
180
|
+
test('gives a generated branch’s failure and its conflict to the skill', () => {
|
|
181
181
|
const update = { ...queued, headRef: 'chore/pnpm-update' } as const;
|
|
182
182
|
|
|
183
183
|
assert.deepStrictEqual(
|
|
@@ -188,13 +188,13 @@ describe(queueOwner, () => {
|
|
|
188
188
|
},
|
|
189
189
|
);
|
|
190
190
|
|
|
191
|
-
//
|
|
191
|
+
// The same as for any other branch: which branches are generated, and
|
|
192
|
+
// how, is the repository's configuration, which the skill reads.
|
|
192
193
|
assert.deepStrictEqual(
|
|
193
194
|
queueOwner({ ...update, setAside: standing('rebase-failed') }),
|
|
194
195
|
{
|
|
195
196
|
owner: 'skill',
|
|
196
|
-
reason:
|
|
197
|
-
'set aside `rebase-failed` at its head; the skill has `pnpm-update.yml` rebuild it from main',
|
|
197
|
+
reason: 'set aside `rebase-failed` at its head; the skill fixes it',
|
|
198
198
|
},
|
|
199
199
|
);
|
|
200
200
|
});
|
package/src/report.mts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
decideStartStates,
|
|
10
10
|
inheritStartAfter,
|
|
11
11
|
issuesInProgress,
|
|
12
|
+
progressOf,
|
|
12
13
|
withProgress,
|
|
13
14
|
type ReferenceStatus,
|
|
14
15
|
} from './start-after.mjs';
|
|
@@ -43,7 +44,7 @@ export const buildReport = ({
|
|
|
43
44
|
issues,
|
|
44
45
|
issuesLimit,
|
|
45
46
|
issueDeclarations,
|
|
46
|
-
references = new Map(),
|
|
47
|
+
references = new Map<number, ReferenceStatus>(),
|
|
47
48
|
}: Readonly<{
|
|
48
49
|
repo: RepoRef;
|
|
49
50
|
defaultBranch: string;
|
|
@@ -83,8 +84,8 @@ export const buildReport = ({
|
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Each open issue with how it stands for being started
|
|
86
|
-
* (`docs/issue-start-after.md`): its start state, or
|
|
87
|
-
*
|
|
87
|
+
* (`docs/issue-start-after.md`): its start state, or how far someone has
|
|
88
|
+
* got with it when it is startable (`progressOf`), and where each thing it
|
|
88
89
|
* names stands.
|
|
89
90
|
*/
|
|
90
91
|
const reportIssues = (
|
|
@@ -109,7 +110,7 @@ const reportIssues = (
|
|
|
109
110
|
...issue,
|
|
110
111
|
start: withProgress(
|
|
111
112
|
states.get(issue.number) ?? 'free',
|
|
112
|
-
worked.has(issue.number),
|
|
113
|
+
progressOf(issue.labels, worked.has(issue.number)),
|
|
113
114
|
),
|
|
114
115
|
named: issue.startAfter.map((number) => ({
|
|
115
116
|
number,
|
|
@@ -236,8 +237,8 @@ export const buildEntries = ({
|
|
|
236
237
|
};
|
|
237
238
|
|
|
238
239
|
const NO_DECLARATIONS: IssueDeclarations = {
|
|
239
|
-
startAfter: new Map(),
|
|
240
|
-
open: new Set(),
|
|
240
|
+
startAfter: new Map<number, readonly number[]>(),
|
|
241
|
+
open: new Set<number>(),
|
|
241
242
|
} as const;
|
|
242
243
|
|
|
243
244
|
/**
|
package/src/report.test.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEntries, buildReport } from './report.mjs';
|
|
2
|
+
import type { ReferenceStatus } from './start-after.mjs';
|
|
2
3
|
import type {
|
|
3
4
|
ContextState,
|
|
4
5
|
LinkedIssue,
|
|
@@ -26,7 +27,7 @@ const facts = (
|
|
|
26
27
|
updatedAt: '2026-09-26T00:00:00Z',
|
|
27
28
|
headCommittedAt: undefined,
|
|
28
29
|
comparison: undefined,
|
|
29
|
-
reported: new Map(),
|
|
30
|
+
reported: new Map<string, ContextState>(),
|
|
30
31
|
checksRunning: false,
|
|
31
32
|
linkedIssues: [],
|
|
32
33
|
...overrides,
|
|
@@ -103,8 +104,8 @@ describe(buildEntries, () => {
|
|
|
103
104
|
issues: {
|
|
104
105
|
// #11 waits for #12, which #2 is closing, and for the open #3; #13
|
|
105
106
|
// is open and nobody has started it.
|
|
106
|
-
startAfter: new Map([[11, [12, 3, 13]]]),
|
|
107
|
-
open: new Set([11, 12, 13]),
|
|
107
|
+
startAfter: new Map<number, readonly number[]>([[11, [12, 3, 13]]]),
|
|
108
|
+
open: new Set<number>([11, 12, 13]),
|
|
108
109
|
},
|
|
109
110
|
});
|
|
110
111
|
|
|
@@ -153,7 +154,10 @@ describe(buildEntries, () => {
|
|
|
153
154
|
}),
|
|
154
155
|
facts(2),
|
|
155
156
|
],
|
|
156
|
-
issues: {
|
|
157
|
+
issues: {
|
|
158
|
+
startAfter: new Map<number, readonly number[]>([[11, [2]]]),
|
|
159
|
+
open: new Set<number>(),
|
|
160
|
+
},
|
|
157
161
|
});
|
|
158
162
|
|
|
159
163
|
assert.deepStrictEqual(entries[0]?.inheritedMergeAfter, []);
|
|
@@ -169,7 +173,10 @@ describe(buildEntries, () => {
|
|
|
169
173
|
facts(1, { linkedIssues: [linked(11, 'open')] }),
|
|
170
174
|
facts(2, { body: 'Merge-After: #1' }),
|
|
171
175
|
],
|
|
172
|
-
issues: {
|
|
176
|
+
issues: {
|
|
177
|
+
startAfter: new Map<number, readonly number[]>([[11, [2]]]),
|
|
178
|
+
open: new Set<number>(),
|
|
179
|
+
},
|
|
173
180
|
});
|
|
174
181
|
|
|
175
182
|
assert.deepStrictEqual(cycles, [[1, 2]]);
|
|
@@ -180,7 +187,10 @@ describe(buildEntries, () => {
|
|
|
180
187
|
required: [],
|
|
181
188
|
defaultBranch: 'main',
|
|
182
189
|
pulls: [facts(1, { linkedIssues: [linked(11, 'closed')] }), facts(2)],
|
|
183
|
-
issues: {
|
|
190
|
+
issues: {
|
|
191
|
+
startAfter: new Map<number, readonly number[]>([[11, [2]]]),
|
|
192
|
+
open: new Set<number>(),
|
|
193
|
+
},
|
|
184
194
|
});
|
|
185
195
|
|
|
186
196
|
assert.deepStrictEqual(entries[0]?.blockedBy, []);
|
|
@@ -309,7 +319,7 @@ describe(buildReport, () => {
|
|
|
309
319
|
issue(4, [100, 101], [7]),
|
|
310
320
|
],
|
|
311
321
|
issuesLimit: 30,
|
|
312
|
-
references: new Map([
|
|
322
|
+
references: new Map<number, ReferenceStatus>([
|
|
313
323
|
[100, 'done'],
|
|
314
324
|
[101, 'abandoned'],
|
|
315
325
|
]),
|
|
@@ -369,6 +379,36 @@ describe(buildReport, () => {
|
|
|
369
379
|
|
|
370
380
|
assert.strictEqual(report.issues[0]?.start, 'attention');
|
|
371
381
|
});
|
|
382
|
+
|
|
383
|
+
test('reads the in-progress and planned labels of a startable issue', () => {
|
|
384
|
+
const report = buildReport({
|
|
385
|
+
repo: { owner: 'o', name: 'r' },
|
|
386
|
+
defaultBranch: 'main',
|
|
387
|
+
generatedAt: '2026-10-24T00:00:00Z',
|
|
388
|
+
required: [],
|
|
389
|
+
authenticated: true,
|
|
390
|
+
pulls: [],
|
|
391
|
+
merged: [],
|
|
392
|
+
mergedWithinDays: 7,
|
|
393
|
+
issues: [
|
|
394
|
+
labelled(1, [], 'in-progress'),
|
|
395
|
+
labelled(2, [], 'planned'),
|
|
396
|
+
// Blocked says so, whatever is planned for it.
|
|
397
|
+
labelled(3, [1], 'planned'),
|
|
398
|
+
],
|
|
399
|
+
issuesLimit: 30,
|
|
400
|
+
references: new Map<number, ReferenceStatus>(),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
assert.deepStrictEqual(
|
|
404
|
+
report.issues.map(({ number, start }) => ({ number, start })),
|
|
405
|
+
[
|
|
406
|
+
{ number: 1, start: 'in-progress' },
|
|
407
|
+
{ number: 2, start: 'planned' },
|
|
408
|
+
{ number: 3, start: 'blocked' },
|
|
409
|
+
],
|
|
410
|
+
);
|
|
411
|
+
});
|
|
372
412
|
});
|
|
373
413
|
|
|
374
414
|
const issue = (
|
|
@@ -388,3 +428,13 @@ const issue = (
|
|
|
388
428
|
startAfter,
|
|
389
429
|
misplacedMergeAfter,
|
|
390
430
|
}) as const;
|
|
431
|
+
|
|
432
|
+
const labelled = (
|
|
433
|
+
number: number,
|
|
434
|
+
startAfter: readonly number[],
|
|
435
|
+
name: string,
|
|
436
|
+
): OpenIssue =>
|
|
437
|
+
({
|
|
438
|
+
...issue(number, startAfter),
|
|
439
|
+
labels: [{ name, color: 'cccccc', description: '' }],
|
|
440
|
+
}) as const;
|
package/src/stack.test.mts
CHANGED
|
@@ -96,7 +96,7 @@ describe(findStackParents, () => {
|
|
|
96
96
|
|
|
97
97
|
describe(stackDescendants, () => {
|
|
98
98
|
test('every layer above, each after the one it is on', () => {
|
|
99
|
-
const parents = new Map([
|
|
99
|
+
const parents = new Map<number, number>([
|
|
100
100
|
[2, 1],
|
|
101
101
|
[3, 2],
|
|
102
102
|
[4, 1],
|
|
@@ -114,7 +114,7 @@ describe(stackDescendants, () => {
|
|
|
114
114
|
// Two pull requests opened onto each other's branches. Nothing lands
|
|
115
115
|
// either, and the merge order reports it as a cycle; the walk only has to
|
|
116
116
|
// stop.
|
|
117
|
-
const parents = new Map([
|
|
117
|
+
const parents = new Map<number, number>([
|
|
118
118
|
[1, 2],
|
|
119
119
|
[2, 1],
|
|
120
120
|
]);
|
package/src/start-after.mts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Arr } from 'ts-data-forge';
|
|
8
|
+
import { IN_PROGRESS_LABEL, PLANNED_LABEL } from './labels.mjs';
|
|
8
9
|
import { findMergeAfterCycles, parseStartAfter } from './merge-after.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -48,19 +49,57 @@ export const waitsFor = (
|
|
|
48
49
|
): readonly number[] => Arr.uniq(parseStartAfter(body).concat(blockedBy));
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
|
-
* How an issue is shown: its {@link StartState}, or
|
|
52
|
-
*
|
|
53
|
-
* state in the rules (`docs/issue-start-after.md`): one that is blocked
|
|
54
|
-
* on a cycle says so whether or not someone has started it.
|
|
52
|
+
* How an issue is shown: its {@link StartState}, or how far someone has got
|
|
53
|
+
* with it when it is startable ({@link IssueProgress}). Kept apart from the
|
|
54
|
+
* start state in the rules (`docs/issue-start-after.md`): one that is blocked
|
|
55
|
+
* or on a cycle says so whether or not someone has started it.
|
|
55
56
|
*/
|
|
56
|
-
export type IssueStartState = StartState | 'in-progress';
|
|
57
|
+
export type IssueStartState = StartState | 'in-progress' | 'planned';
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* How far someone has got with an issue, whatever it waits for:
|
|
61
|
+
*
|
|
62
|
+
* - `in-progress` — an open pull request closes it, or it is labelled
|
|
63
|
+
* `in-progress` (`labels.mts`), which is how it says so before there is one.
|
|
64
|
+
* - `planned` — it is labelled `planned`: someone means to start it next.
|
|
65
|
+
* - `none` — neither.
|
|
66
|
+
*/
|
|
67
|
+
export type IssueProgress = 'in-progress' | 'none' | 'planned';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The {@link IssueProgress} of an issue carrying `labels`, given whether an
|
|
71
|
+
* open pull request closes it. Being worked on wins over being planned: one
|
|
72
|
+
* carrying both was started without the plan being taken off.
|
|
73
|
+
*/
|
|
74
|
+
export const progressOf = (
|
|
75
|
+
labels: readonly Readonly<{ name: string }>[],
|
|
76
|
+
closedByOpenPull: boolean,
|
|
77
|
+
): IssueProgress =>
|
|
78
|
+
closedByOpenPull || hasLabel(labels, IN_PROGRESS_LABEL)
|
|
79
|
+
? 'in-progress'
|
|
80
|
+
: hasLabel(labels, PLANNED_LABEL)
|
|
81
|
+
? 'planned'
|
|
82
|
+
: 'none';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether `labels` holds `name`, whatever its case. GitHub's label names
|
|
86
|
+
* are: adding `in-progress` to a repository that has `In-Progress` puts that
|
|
87
|
+
* one on, so an exact match would not see the label it just added.
|
|
88
|
+
*/
|
|
89
|
+
export const hasLabel = (
|
|
90
|
+
labels: readonly Readonly<{ name: string }>[],
|
|
91
|
+
name: string,
|
|
92
|
+
): boolean =>
|
|
93
|
+
labels.some((label) => label.name.toLowerCase() === name.toLowerCase());
|
|
94
|
+
|
|
95
|
+
/** The {@link IssueStartState} of an issue, given how far it has got. */
|
|
59
96
|
export const withProgress = (
|
|
60
97
|
state: StartState,
|
|
61
|
-
|
|
98
|
+
progress: IssueProgress,
|
|
62
99
|
): IssueStartState =>
|
|
63
|
-
|
|
100
|
+
progress !== 'none' && (state === 'free' || state === 'ready')
|
|
101
|
+
? progress
|
|
102
|
+
: state;
|
|
64
103
|
|
|
65
104
|
/** The issues an open pull request closes: the ones being worked on. */
|
|
66
105
|
export const issuesInProgress = (
|