pr-report-core 0.2.0 → 0.2.1
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/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 +13 -14
- package/dist/claim.d.mts.map +1 -1
- package/dist/claim.mjs +0 -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/labels.d.mts +38 -40
- package/dist/labels.d.mts.map +1 -1
- package/dist/labels.mjs +0 -2
- 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 +13 -13
- 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 +10 -11
- 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 +28 -28
- 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 +8 -8
- package/src/checks.mts +64 -65
- package/src/checks.test.mts +30 -31
- package/src/claim.mts +13 -14
- package/src/closing-issues.mts +18 -19
- package/src/closing-issues.test.mts +2 -2
- package/src/code-owners.mts +39 -41
- package/src/labels.mts +38 -40
- 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 +25 -25
- package/src/queue-owner.test.mts +2 -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 +10 -11
- package/src/set-aside.mts +28 -28
- 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/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
|
);
|
package/src/queue-order.test.mts
CHANGED
|
@@ -253,8 +253,8 @@ describe(queueStanding, () => {
|
|
|
253
253
|
);
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
-
// Otherwise it would hold itself for as long as it existed, with nothing
|
|
257
|
-
//
|
|
256
|
+
// Otherwise it would hold itself for as long as it existed, with nothing able
|
|
257
|
+
// to take the label off — a `Merge-After` cycle by another name.
|
|
258
258
|
test('never holds the release by its own label', () => {
|
|
259
259
|
const release = facts({
|
|
260
260
|
number: 1,
|
|
@@ -338,8 +338,8 @@ describe(orderCandidates, () => {
|
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
// Priority comes before every rank but the demotion, the release included:
|
|
341
|
-
// a release can be declared urgent, and a change declared able to wait
|
|
342
|
-
//
|
|
341
|
+
// a release can be declared urgent, and a change declared able to wait goes
|
|
342
|
+
// out in the release after.
|
|
343
343
|
test('takes priority:high first and priority:low last, before what sat green', () => {
|
|
344
344
|
assert.deepStrictEqual(
|
|
345
345
|
order(
|
|
@@ -384,8 +384,8 @@ describe(orderCandidates, () => {
|
|
|
384
384
|
);
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
// Picked early, it is released, goes green and is set aside for a person,
|
|
388
|
-
//
|
|
387
|
+
// Picked early, it is released, goes green and is set aside for a person, and
|
|
388
|
+
// the cycle it took went to nothing that could merge on its own.
|
|
389
389
|
test('puts one only a bypass merges with priority:low, unless labelled priority:high', () => {
|
|
390
390
|
assert.deepStrictEqual(
|
|
391
391
|
order(
|
package/src/queue-owner.mts
CHANGED
|
@@ -4,26 +4,26 @@
|
|
|
4
4
|
* Who is to move each pull request in the queue next: the worker
|
|
5
5
|
* (`pnpm run unblock-prs`), a skill, a person, or — which is a bug — nobody.
|
|
6
6
|
*
|
|
7
|
-
* The worker and the `/unblock-prs` skill move the same queue side by side
|
|
8
|
-
*
|
|
7
|
+
* The worker and the `/unblock-prs` skill move the same queue side by side and
|
|
8
|
+
* cannot see each other. Each used to decide for itself whether a
|
|
9
9
|
* set-aside still stood, from its own reading of the tip of the default
|
|
10
|
-
* branch, and when the two readings differed each left the pull request to
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* branch, and when the two readings differed each left the pull request to the
|
|
11
|
+
* other (#2267). So the answer is decided here, once, and every reader asks
|
|
12
|
+
* this function: the worker before it acts, `pr-report` for the skill,
|
|
13
13
|
* `claim-pr` before it lets a claim be written, and `report-unowned-prs` to
|
|
14
14
|
* turn `none` into an issue.
|
|
15
15
|
*
|
|
16
16
|
* **It reads the comments and the pull request, never the tip of the base.**
|
|
17
|
-
* The set-aside comment is the hand-off: it stands until the worker rewrites
|
|
18
|
-
*
|
|
17
|
+
* The set-aside comment is the hand-off: it stands until the worker rewrites it
|
|
18
|
+
* as resolved — when the head moves, when a retry is asked for, or, for a
|
|
19
19
|
* reason tied to the base, when the base moves and the worker takes the pull
|
|
20
|
-
* request back to try again. Until then it is the skill's or a person's, and
|
|
21
|
-
*
|
|
20
|
+
* request back to try again. Until then it is the skill's or a person's, and a
|
|
21
|
+
* worker that has stopped running leaves it so rather than nobody's.
|
|
22
22
|
*
|
|
23
23
|
* **Every reason the worker sets aside for has an owner**
|
|
24
24
|
* ({@link setAsideOwner}, exhaustive over {@link SET_ASIDE_REASONS}), so `none`
|
|
25
|
-
* is only ever a combination nothing here expected: a reason this module
|
|
26
|
-
*
|
|
25
|
+
* is only ever a combination nothing here expected: a reason this module does
|
|
26
|
+
* not know, written by a worker newer than the reader.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { claimApplies, type ClaimComment } from './claim.mjs';
|
|
@@ -40,8 +40,8 @@ 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 — a branch the repository generates included, which
|
|
44
|
-
*
|
|
43
|
+
* the worker sets aside — a branch the repository generates included, which it
|
|
44
|
+
* builds again rather than rebases (`generatedBranches` in
|
|
45
45
|
* `.github/unblock-prs.json`).
|
|
46
46
|
*/
|
|
47
47
|
export type QueueOwner = Readonly<{
|
|
@@ -96,8 +96,8 @@ export const queueOwner = (pr: QueueOwnerInput): QueueOwner | undefined => {
|
|
|
96
96
|
return { owner: 'human', reason: unmovable };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// A layer waits for the one below whatever it says, and the worker carries
|
|
100
|
-
//
|
|
99
|
+
// A layer waits for the one below whatever it says, and the worker carries it
|
|
100
|
+
// along when that one moves.
|
|
101
101
|
if (pr.stackedOn !== undefined) {
|
|
102
102
|
return {
|
|
103
103
|
owner: 'worker',
|
|
@@ -173,12 +173,12 @@ export const isSetAsideReason = (reason: string): reason is SetAsideReason =>
|
|
|
173
173
|
KNOWN_REASONS.has(reason);
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
* Whose a pull request set aside for `reason` is, while the comment stands
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
176
|
+
* Whose a pull request set aside for `reason` is, while the comment stands at
|
|
177
|
+
* its head. A skill's reason is one a session can fix by pushing: a conflict to
|
|
178
|
+
* resolve, a failure to reproduce. A person's is one only a setting, a
|
|
179
|
+
* permission, a review or a merge by hand clears, and the comment says which. A
|
|
180
|
+
* `switch` with no default, so a reason added to {@link SET_ASIDE_REASONS}
|
|
181
|
+
* without an owner fails to compile.
|
|
182
182
|
*/
|
|
183
183
|
const setAsideOwner = (
|
|
184
184
|
reason: SetAsideReason,
|
|
@@ -254,8 +254,8 @@ export type QueueOwnerInput = Readonly<{
|
|
|
254
254
|
}>;
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* The set-aside `comment` says, if it stands at `headSha` with no retry
|
|
258
|
-
*
|
|
257
|
+
* The set-aside `comment` says, if it stands at `headSha` with no retry asked
|
|
258
|
+
* for. The base is not read: see the module comment.
|
|
259
259
|
*/
|
|
260
260
|
const standingAt = (
|
|
261
261
|
comment: SetAsideComment | undefined,
|
|
@@ -270,8 +270,8 @@ const standingAt = (
|
|
|
270
270
|
const KNOWN_REASONS: ReadonlySet<string> = new Set(SET_ASIDE_REASONS);
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
* Why nothing in the queue can move `pr` until a person does something, if
|
|
274
|
-
*
|
|
273
|
+
* Why nothing in the queue can move `pr` until a person does something, if so:
|
|
274
|
+
* the worker passes these over with a note, and no skill takes them up.
|
|
275
275
|
*/
|
|
276
276
|
const unmovableReason = (pr: QueueOwnerInput): string | undefined =>
|
|
277
277
|
pr.isDraft
|
package/src/queue-owner.test.mts
CHANGED
|
@@ -188,8 +188,8 @@ describe(queueOwner, () => {
|
|
|
188
188
|
},
|
|
189
189
|
);
|
|
190
190
|
|
|
191
|
-
// The same as for any other branch: which branches are generated, and
|
|
192
|
-
//
|
|
191
|
+
// The same as for any other branch: which branches are generated, and how,
|
|
192
|
+
// is the repository's configuration, which the skill reads.
|
|
193
193
|
assert.deepStrictEqual(
|
|
194
194
|
queueOwner({ ...update, setAside: standing('rebase-failed') }),
|
|
195
195
|
{
|
package/src/report.mts
CHANGED
|
@@ -29,8 +29,8 @@ import type {
|
|
|
29
29
|
/**
|
|
30
30
|
* Reads the declared merge order and the check verdicts out of the facts.
|
|
31
31
|
*
|
|
32
|
-
* Nothing here talks to GitHub, which is what lets the whole shape of a
|
|
33
|
-
*
|
|
32
|
+
* Nothing here talks to GitHub, which is what lets the whole shape of a report
|
|
33
|
+
* be tested against a handful of literals.
|
|
34
34
|
*/
|
|
35
35
|
export const buildReport = ({
|
|
36
36
|
repo,
|
|
@@ -96,8 +96,8 @@ export const buildEntries = ({
|
|
|
96
96
|
}: Readonly<{
|
|
97
97
|
/**
|
|
98
98
|
* The contexts the ruleset requires, or `undefined` for a repository that
|
|
99
|
-
* declares none: each pull request is then judged over every context
|
|
100
|
-
*
|
|
99
|
+
* declares none: each pull request is then judged over every context reported
|
|
100
|
+
* on its head.
|
|
101
101
|
*/
|
|
102
102
|
required: readonly string[] | undefined;
|
|
103
103
|
defaultBranch: string;
|
|
@@ -195,9 +195,9 @@ export const buildEntries = ({
|
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
197
|
* Each open issue with how it stands for being started
|
|
198
|
-
* (`docs/issue-start-after.md`): its start state, or how far someone has
|
|
199
|
-
*
|
|
200
|
-
*
|
|
198
|
+
* (`docs/issue-start-after.md`): its start state, or how far someone has got
|
|
199
|
+
* with it when it is startable (`progressOf`), and where each thing it names
|
|
200
|
+
* stands.
|
|
201
201
|
*/
|
|
202
202
|
const reportIssues = (
|
|
203
203
|
issues: readonly OpenIssue[],
|
|
@@ -243,8 +243,8 @@ const NO_DECLARATIONS: IssueDeclarations = {
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* The issues a pull request closes that may still declare an order: any not
|
|
246
|
-
* known to be closed. One read from the body without a token has an
|
|
247
|
-
*
|
|
246
|
+
* known to be closed. One read from the body without a token has an `unknown`
|
|
247
|
+
* state, and is given the benefit of the doubt.
|
|
248
248
|
*/
|
|
249
249
|
const notKnownClosed = (
|
|
250
250
|
linkedIssues: PullRequestFacts['linkedIssues'],
|
package/src/review-hold.mts
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
* `main`'s ruleset asks for two things no check reports: an approval from a
|
|
6
6
|
* code owner of every owned path the pull request touches, and every review
|
|
7
7
|
* conversation resolved. A queued pull request missing either goes green and
|
|
8
|
-
* sits there, and a rebase cannot change that. Released anyway, it costs a
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* sits there, and a rebase cannot change that. Released anyway, it costs a full
|
|
9
|
+
* matrix and holds the queue for the length of a watch; and because `main`
|
|
10
|
+
* moving is what puts it back in the running, it would be rebased and run again
|
|
11
|
+
* every time anything else merged. So triage reads both before picking, and
|
|
12
|
+
* leaves a pull request they hold in the queue with a note, until an approval
|
|
13
|
+
* or a resolved conversation changes the answer.
|
|
14
14
|
*
|
|
15
15
|
* Except when nothing can change it: GitHub does not let an author approve
|
|
16
16
|
* their own pull request, so one whose waiting paths only its author owns
|
|
17
|
-
* merges by a ruleset bypass or not at all. Holding that one would hold it
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* merges by a ruleset bypass or not at all. Holding that one would hold it for
|
|
18
|
+
* ever, with `skip-ci` on and no verdict for whoever makes the bypass merge to
|
|
19
|
+
* go by. So it is released and watched like any other, with
|
|
20
20
|
* auto-merge left off — it could never fire — until its checks report, and
|
|
21
21
|
* then left for that person (`awaiting-bypass`).
|
|
22
22
|
*
|
|
@@ -50,11 +50,11 @@ export type ReviewRules = Readonly<{
|
|
|
50
50
|
}>;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* What the review does to the pull request: `held` waits for an approval or
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* What the review does to the pull request: `held` waits for an approval or a
|
|
54
|
+
* resolved conversation, `bypass` waits for nobody — only its author owns what
|
|
55
|
+
* is waiting — and `undefined` holds nothing. What cannot be told for sure —
|
|
56
|
+
* changed files past what could be read — holds nothing: the watch still
|
|
57
|
+
* catches a pull request that goes green and does not merge, and
|
|
58
58
|
* `demotions.mts` is what follows.
|
|
59
59
|
*/
|
|
60
60
|
export type ReviewHold = Readonly<{ kind: 'bypass' | 'held'; reason: string }>;
|
package/src/review-hold.test.mts
CHANGED
|
@@ -123,9 +123,9 @@ describe(reviewHold, () => {
|
|
|
123
123
|
);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
// A changed file past what could be read might be owned, but "might" is not
|
|
127
|
-
//
|
|
128
|
-
//
|
|
126
|
+
// A changed file past what could be read might be owned, but "might" is not a
|
|
127
|
+
// reason to hold the queue: the watch still catches a pull request that goes
|
|
128
|
+
// green and does not merge.
|
|
129
129
|
test('holds nothing it cannot be sure of', () => {
|
|
130
130
|
assert.isUndefined(
|
|
131
131
|
reviewHold(
|
package/src/ruleset.mts
CHANGED
|
@@ -10,8 +10,8 @@ export type RulesetRequirements = Readonly<{
|
|
|
10
10
|
/** The contexts a pull request has to satisfy. */
|
|
11
11
|
requiredContexts: readonly string[];
|
|
12
12
|
/**
|
|
13
|
-
* Whether a change to a path `.github/CODEOWNERS` lists waits for one of
|
|
14
|
-
*
|
|
13
|
+
* Whether a change to a path `.github/CODEOWNERS` lists waits for one of its
|
|
14
|
+
* owners to approve it.
|
|
15
15
|
*/
|
|
16
16
|
requireCodeOwnerReview: boolean;
|
|
17
17
|
/** Whether every review conversation has to be resolved first. */
|
|
@@ -23,11 +23,10 @@ export type RulesetRequirements = Readonly<{
|
|
|
23
23
|
* `pnpm run pr-report`, which reads the checkout. The file is the desired
|
|
24
24
|
* state; what GitHub enforces is {@link requirementsOfRules} over
|
|
25
25
|
* `rules/branches/{branch}`, which `unblock-prs` and the Pull Requests
|
|
26
|
-
* Manager read and which wants no more than read access. The gap is that a
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* thing for a report to say out loud.
|
|
26
|
+
* Manager read and which wants no more than read access. The gap is that a file
|
|
27
|
+
* under `repo-settings/` changes nothing until `pnpm run repo-settings:apply`
|
|
28
|
+
* runs, so a context added here and not applied is reported as missing on every
|
|
29
|
+
* pull request — which is a fair thing for a report to say out loud.
|
|
31
30
|
*
|
|
32
31
|
* `source` names the file in the messages.
|
|
33
32
|
*/
|
|
@@ -98,8 +97,8 @@ const RulesetSchema = t.record({ rules: t.array(t.unknown()) });
|
|
|
98
97
|
|
|
99
98
|
/**
|
|
100
99
|
* One rule of the several kinds a ruleset holds. The others carry entirely
|
|
101
|
-
* different parameters, so each rule is tried against this and the ones that
|
|
102
|
-
*
|
|
100
|
+
* different parameters, so each rule is tried against this and the ones that do
|
|
101
|
+
* not match are simply not the rule being looked for.
|
|
103
102
|
*
|
|
104
103
|
* Written here rather than taken from `octokit-safe-types`, whose
|
|
105
104
|
* `RepositoryRule` is the whole union: validating against it would refuse a
|
|
@@ -126,8 +125,8 @@ const ConversationResolutionRuleSchema = t.record({
|
|
|
126
125
|
|
|
127
126
|
/**
|
|
128
127
|
* The rules as read: `ruleset.test.mts` checks each against GitHub's API
|
|
129
|
-
* description, so that a field read here that GitHub renames or retypes
|
|
130
|
-
*
|
|
128
|
+
* description, so that a field read here that GitHub renames or retypes fails
|
|
129
|
+
* the type check.
|
|
131
130
|
*/
|
|
132
131
|
export type RequiredStatusChecksRule = t.TypeOf<
|
|
133
132
|
typeof RequiredStatusChecksRuleSchema
|