pr-report-core 0.1.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 +8 -9
- 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 +48 -49
- package/dist/code-owners.d.mts.map +1 -1
- package/dist/code-owners.mjs +41 -43
- 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 +97 -34
- package/dist/merge-after.mjs.map +1 -1
- package/dist/queue-comments.d.mts +2 -2
- package/dist/queue-order.d.mts +43 -30
- package/dist/queue-order.d.mts.map +1 -1
- package/dist/queue-order.mjs +33 -18
- package/dist/queue-order.mjs.map +1 -1
- package/dist/queue-owner.d.mts +30 -30
- package/dist/queue-owner.d.mts.map +1 -1
- package/dist/queue-owner.mjs +51 -51
- package/dist/queue-owner.mjs.map +1 -1
- package/dist/report.d.mts +9 -9
- package/dist/report.d.mts.map +1 -1
- package/dist/report.mjs +23 -23
- package/dist/report.mjs.map +1 -1
- 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 +74 -75
- 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 +107 -109
- package/src/labels.mts +38 -40
- package/src/merge-after.mts +164 -71
- package/src/merge-after.test.mts +80 -3
- package/src/queue-comments.mts +2 -2
- package/src/queue-order.mts +103 -83
- package/src/queue-order.test.mts +41 -4
- package/src/queue-owner.mts +103 -103
- package/src/queue-owner.test.mts +2 -2
- package/src/report.mts +49 -49
- 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-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<{
|
|
@@ -64,87 +64,6 @@ export const SET_ASIDE_REASONS = [
|
|
|
64
64
|
|
|
65
65
|
export type SetAsideReason = (typeof SET_ASIDE_REASONS)[number];
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Whose a pull request set aside for `reason` is, while the comment stands
|
|
69
|
-
* at its head. A skill's reason is one a session can fix by pushing: a
|
|
70
|
-
* conflict to resolve, a failure to reproduce. A person's is one only a
|
|
71
|
-
* setting, a permission, a review or a merge by hand clears, and the comment
|
|
72
|
-
* says which. A `switch` with no default, so a reason added to
|
|
73
|
-
* {@link SET_ASIDE_REASONS} without an owner fails to compile.
|
|
74
|
-
*/
|
|
75
|
-
const setAsideOwner = (
|
|
76
|
-
reason: SetAsideReason,
|
|
77
|
-
): Readonly<{ owner: 'human' | 'skill'; why: string }> => {
|
|
78
|
-
switch (reason) {
|
|
79
|
-
case 'checks-failed':
|
|
80
|
-
case 'rebase-failed':
|
|
81
|
-
return { owner: 'skill', why: 'the skill fixes it' };
|
|
82
|
-
|
|
83
|
-
case 'already-in-base':
|
|
84
|
-
return {
|
|
85
|
-
owner: 'human',
|
|
86
|
-
why: 'every commit is already in the base; close it',
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
case 'arm-failed':
|
|
90
|
-
return {
|
|
91
|
-
owner: 'human',
|
|
92
|
-
why: 'auto-merge could not be armed; a setting or a permission holds it',
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
case 'awaiting-bypass':
|
|
96
|
-
return {
|
|
97
|
-
owner: 'human',
|
|
98
|
-
why: 'green, and only a ruleset bypass merges it',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
case 'not-merging':
|
|
102
|
-
return {
|
|
103
|
-
owner: 'human',
|
|
104
|
-
why: 'green but not merging; something the worker cannot read holds it',
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
case 'push-failed':
|
|
108
|
-
return {
|
|
109
|
-
owner: 'human',
|
|
110
|
-
why: 'the push was refused; a setting or a permission holds it',
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
case 'unlabel-failed':
|
|
114
|
-
return {
|
|
115
|
-
owner: 'human',
|
|
116
|
-
why: '`skip-ci` could not be taken off; a permission holds it',
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
case 'watch-timeout':
|
|
120
|
-
return {
|
|
121
|
-
owner: 'human',
|
|
122
|
-
why: 'its checks ran past the watch; look at the run',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/** What {@link queueOwner} reads about one pull request. */
|
|
128
|
-
export type QueueOwnerInput = Readonly<{
|
|
129
|
-
number: number;
|
|
130
|
-
labels: readonly string[];
|
|
131
|
-
isDraft: boolean;
|
|
132
|
-
fromFork: boolean;
|
|
133
|
-
headRef: string;
|
|
134
|
-
headSha: string;
|
|
135
|
-
baseRef: string;
|
|
136
|
-
defaultBranch: string;
|
|
137
|
-
/** The open pull request whose branch this one is on, if any. */
|
|
138
|
-
stackedOn: number | undefined;
|
|
139
|
-
/** Whether it sits on a `Merge-After` cycle. */
|
|
140
|
-
inCycle: boolean;
|
|
141
|
-
/** This account's set-aside comment on it (`set-aside.mts`). */
|
|
142
|
-
setAside: SetAsideComment | undefined;
|
|
143
|
-
/** This account's claim comment on it (`claim.mts`). */
|
|
144
|
-
claim: ClaimComment | undefined;
|
|
145
|
-
nowEpochMs: number;
|
|
146
|
-
}>;
|
|
147
|
-
|
|
148
67
|
/**
|
|
149
68
|
* Who moves `pr` next, or `undefined` for a pull request the queue does not
|
|
150
69
|
* cover: neither `merge-queued` nor `auto-rebase` under `skip-ci`.
|
|
@@ -177,8 +96,8 @@ export const queueOwner = (pr: QueueOwnerInput): QueueOwner | undefined => {
|
|
|
177
96
|
return { owner: 'human', reason: unmovable };
|
|
178
97
|
}
|
|
179
98
|
|
|
180
|
-
// A layer waits for the one below whatever it says, and the worker carries
|
|
181
|
-
//
|
|
99
|
+
// A layer waits for the one below whatever it says, and the worker carries it
|
|
100
|
+
// along when that one moves.
|
|
182
101
|
if (pr.stackedOn !== undefined) {
|
|
183
102
|
return {
|
|
184
103
|
owner: 'worker',
|
|
@@ -250,9 +169,93 @@ export const queueOwnerOfEntry = (
|
|
|
250
169
|
nowEpochMs: context.nowEpochMs,
|
|
251
170
|
});
|
|
252
171
|
|
|
172
|
+
export const isSetAsideReason = (reason: string): reason is SetAsideReason =>
|
|
173
|
+
KNOWN_REASONS.has(reason);
|
|
174
|
+
|
|
253
175
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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
|
+
*/
|
|
183
|
+
const setAsideOwner = (
|
|
184
|
+
reason: SetAsideReason,
|
|
185
|
+
): Readonly<{ owner: 'human' | 'skill'; why: string }> => {
|
|
186
|
+
switch (reason) {
|
|
187
|
+
case 'checks-failed':
|
|
188
|
+
case 'rebase-failed':
|
|
189
|
+
return { owner: 'skill', why: 'the skill fixes it' };
|
|
190
|
+
|
|
191
|
+
case 'already-in-base':
|
|
192
|
+
return {
|
|
193
|
+
owner: 'human',
|
|
194
|
+
why: 'every commit is already in the base; close it',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
case 'arm-failed':
|
|
198
|
+
return {
|
|
199
|
+
owner: 'human',
|
|
200
|
+
why: 'auto-merge could not be armed; a setting or a permission holds it',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
case 'awaiting-bypass':
|
|
204
|
+
return {
|
|
205
|
+
owner: 'human',
|
|
206
|
+
why: 'green, and only a ruleset bypass merges it',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
case 'not-merging':
|
|
210
|
+
return {
|
|
211
|
+
owner: 'human',
|
|
212
|
+
why: 'green but not merging; something the worker cannot read holds it',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
case 'push-failed':
|
|
216
|
+
return {
|
|
217
|
+
owner: 'human',
|
|
218
|
+
why: 'the push was refused; a setting or a permission holds it',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
case 'unlabel-failed':
|
|
222
|
+
return {
|
|
223
|
+
owner: 'human',
|
|
224
|
+
why: '`skip-ci` could not be taken off; a permission holds it',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
case 'watch-timeout':
|
|
228
|
+
return {
|
|
229
|
+
owner: 'human',
|
|
230
|
+
why: 'its checks ran past the watch; look at the run',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** What {@link queueOwner} reads about one pull request. */
|
|
236
|
+
export type QueueOwnerInput = Readonly<{
|
|
237
|
+
number: number;
|
|
238
|
+
labels: readonly string[];
|
|
239
|
+
isDraft: boolean;
|
|
240
|
+
fromFork: boolean;
|
|
241
|
+
headRef: string;
|
|
242
|
+
headSha: string;
|
|
243
|
+
baseRef: string;
|
|
244
|
+
defaultBranch: string;
|
|
245
|
+
/** The open pull request whose branch this one is on, if any. */
|
|
246
|
+
stackedOn: number | undefined;
|
|
247
|
+
/** Whether it sits on a `Merge-After` cycle. */
|
|
248
|
+
inCycle: boolean;
|
|
249
|
+
/** This account's set-aside comment on it (`set-aside.mts`). */
|
|
250
|
+
setAside: SetAsideComment | undefined;
|
|
251
|
+
/** This account's claim comment on it (`claim.mts`). */
|
|
252
|
+
claim: ClaimComment | undefined;
|
|
253
|
+
nowEpochMs: number;
|
|
254
|
+
}>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
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.
|
|
256
259
|
*/
|
|
257
260
|
const standingAt = (
|
|
258
261
|
comment: SetAsideComment | undefined,
|
|
@@ -264,14 +267,11 @@ const standingAt = (
|
|
|
264
267
|
? comment.setAside
|
|
265
268
|
: undefined;
|
|
266
269
|
|
|
267
|
-
export const isSetAsideReason = (reason: string): reason is SetAsideReason =>
|
|
268
|
-
KNOWN_REASONS.has(reason);
|
|
269
|
-
|
|
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,
|
|
@@ -82,49 +82,6 @@ export const buildReport = ({
|
|
|
82
82
|
issuesLimit,
|
|
83
83
|
}) as const;
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* Each open issue with how it stands for being started
|
|
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
|
|
89
|
-
* names stands.
|
|
90
|
-
*/
|
|
91
|
-
const reportIssues = (
|
|
92
|
-
issues: readonly OpenIssue[],
|
|
93
|
-
pulls: readonly PullRequestFacts[],
|
|
94
|
-
references: ReadonlyMap<number, ReferenceStatus>,
|
|
95
|
-
): readonly ReportIssue[] => {
|
|
96
|
-
const listed = new Set(issues.map(({ number }) => number));
|
|
97
|
-
|
|
98
|
-
const open = new Set(pulls.map(({ number }) => number));
|
|
99
|
-
|
|
100
|
-
const statusOf = (number: number): ReferenceStatus =>
|
|
101
|
-
listed.has(number) || open.has(number)
|
|
102
|
-
? 'waiting'
|
|
103
|
-
: (references.get(number) ?? 'unknown');
|
|
104
|
-
|
|
105
|
-
const states = decideStartStates(issues, statusOf);
|
|
106
|
-
|
|
107
|
-
const worked = issuesInProgress(pulls);
|
|
108
|
-
|
|
109
|
-
return issues.map((issue) => ({
|
|
110
|
-
...issue,
|
|
111
|
-
start: withProgress(
|
|
112
|
-
states.get(issue.number) ?? 'free',
|
|
113
|
-
progressOf(issue.labels, worked.has(issue.number)),
|
|
114
|
-
),
|
|
115
|
-
named: issue.startAfter.map((number) => ({
|
|
116
|
-
number,
|
|
117
|
-
status: statusOf(number),
|
|
118
|
-
})),
|
|
119
|
-
}));
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export type DecidedEntries = Readonly<{
|
|
123
|
-
entries: readonly ReportEntry[];
|
|
124
|
-
roots: readonly TreeNode[];
|
|
125
|
-
cycles: readonly (readonly number[])[];
|
|
126
|
-
}>;
|
|
127
|
-
|
|
128
85
|
/**
|
|
129
86
|
* The part of a report that is decided rather than read: each open pull
|
|
130
87
|
* request's declared order, the stack it is in, its check verdict, and the
|
|
@@ -139,8 +96,8 @@ export const buildEntries = ({
|
|
|
139
96
|
}: Readonly<{
|
|
140
97
|
/**
|
|
141
98
|
* The contexts the ruleset requires, or `undefined` for a repository that
|
|
142
|
-
* declares none: each pull request is then judged over every context
|
|
143
|
-
*
|
|
99
|
+
* declares none: each pull request is then judged over every context reported
|
|
100
|
+
* on its head.
|
|
144
101
|
*/
|
|
145
102
|
required: readonly string[] | undefined;
|
|
146
103
|
defaultBranch: string;
|
|
@@ -236,6 +193,49 @@ export const buildEntries = ({
|
|
|
236
193
|
return { entries, roots, cycles };
|
|
237
194
|
};
|
|
238
195
|
|
|
196
|
+
/**
|
|
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 got
|
|
199
|
+
* with it when it is startable (`progressOf`), and where each thing it names
|
|
200
|
+
* stands.
|
|
201
|
+
*/
|
|
202
|
+
const reportIssues = (
|
|
203
|
+
issues: readonly OpenIssue[],
|
|
204
|
+
pulls: readonly PullRequestFacts[],
|
|
205
|
+
references: ReadonlyMap<number, ReferenceStatus>,
|
|
206
|
+
): readonly ReportIssue[] => {
|
|
207
|
+
const listed = new Set(issues.map(({ number }) => number));
|
|
208
|
+
|
|
209
|
+
const open = new Set(pulls.map(({ number }) => number));
|
|
210
|
+
|
|
211
|
+
const statusOf = (number: number): ReferenceStatus =>
|
|
212
|
+
listed.has(number) || open.has(number)
|
|
213
|
+
? 'waiting'
|
|
214
|
+
: (references.get(number) ?? 'unknown');
|
|
215
|
+
|
|
216
|
+
const states = decideStartStates(issues, statusOf);
|
|
217
|
+
|
|
218
|
+
const worked = issuesInProgress(pulls);
|
|
219
|
+
|
|
220
|
+
return issues.map((issue) => ({
|
|
221
|
+
...issue,
|
|
222
|
+
start: withProgress(
|
|
223
|
+
states.get(issue.number) ?? 'free',
|
|
224
|
+
progressOf(issue.labels, worked.has(issue.number)),
|
|
225
|
+
),
|
|
226
|
+
named: issue.startAfter.map((number) => ({
|
|
227
|
+
number,
|
|
228
|
+
status: statusOf(number),
|
|
229
|
+
})),
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type DecidedEntries = Readonly<{
|
|
234
|
+
entries: readonly ReportEntry[];
|
|
235
|
+
roots: readonly TreeNode[];
|
|
236
|
+
cycles: readonly (readonly number[])[];
|
|
237
|
+
}>;
|
|
238
|
+
|
|
239
239
|
const NO_DECLARATIONS: IssueDeclarations = {
|
|
240
240
|
startAfter: new Map<number, readonly number[]>(),
|
|
241
241
|
open: new Set<number>(),
|
|
@@ -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
|
package/src/set-aside.mts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Why `unblock-prs` set a pull request aside, written where GitHub keeps it.
|
|
3
3
|
*
|
|
4
|
-
* The script decides on someone's machine and says so on standard output,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The script decides on someone's machine and says so on standard output, which
|
|
5
|
+
* is where nobody can see it afterwards. So each time it sets a pull request
|
|
6
|
+
* aside it also writes a comment on that pull request — one per pull request,
|
|
7
|
+
* edited in place from then on — whose first line is a hidden record this
|
|
8
|
+
* module writes and reads back, and whose last line is a task box a person
|
|
9
|
+
* ticks to have the script try again. What lies between is prose for whoever
|
|
10
|
+
* reads it, and nothing reads it back. The Pull Requests
|
|
11
11
|
* Manager page and the script itself read the record; anyone else reads the
|
|
12
12
|
* prose on the pull request.
|
|
13
13
|
*
|
|
14
14
|
* **A comment rather than a commit status**, which is what it used to be. A
|
|
15
|
-
* status has no state that means "for information": `failure` left a red ✗ on
|
|
16
|
-
*
|
|
15
|
+
* status has no state that means "for information": `failure` left a red ✗ on a
|
|
16
|
+
* context nothing requires, until the next push, with 140 characters to say
|
|
17
17
|
* why, and nobody could take it off or ask for another try. **Rather than the
|
|
18
18
|
* body**, because the author, a session and `open-pr` write the body too, the
|
|
19
19
|
* REST API has no conditional update to keep an edit made in between, and the
|
|
20
20
|
* version pull request's body is rewritten on every push to the base.
|
|
21
21
|
*
|
|
22
|
-
* **Only the comments the reader's own account wrote count.** The repository
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* **Only the comments the reader's own account wrote count.** The repository is
|
|
23
|
+
* public, so anyone can post a comment that starts with the marker; the script
|
|
24
|
+
* and the page both ask GitHub for `viewerDidAuthor` and read nothing else —
|
|
25
|
+
* the account that runs `unblock-prs` is the one that looks at the page. A task
|
|
26
|
+
* box in someone else's comment can be ticked only by those who can write to
|
|
27
|
+
* the repository, so the retry is not open to outsiders either.
|
|
28
28
|
*
|
|
29
29
|
* **Its own record rather than GitHub's `mergeable`.** That answers whether
|
|
30
30
|
* *merging* the branch conflicts, from a cached background computation; the
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
* parent was squash-merged and on a cache that has not caught up. Only the
|
|
33
33
|
* script knows what the rebase said.
|
|
34
34
|
*
|
|
35
|
-
* **It lasts exactly as long as the state it was reached in.** The record
|
|
36
|
-
*
|
|
35
|
+
* **It lasts exactly as long as the state it was reached in.** The record names
|
|
36
|
+
* the head and the base it was reached at; a push moves the head, and
|
|
37
37
|
* {@link setAsideApplies} is where a moved head and a moved base are judged,
|
|
38
38
|
* for the script and the page alike. Once it no longer applies, or a person
|
|
39
39
|
* asked for a retry, the script rewrites the comment to one resolved line
|
|
@@ -60,9 +60,9 @@ export type SetAside = Readonly<{
|
|
|
60
60
|
}>;
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* What one of the script's comments says: a pull request still set aside,
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* What one of the script's comments says: a pull request still set aside, and
|
|
64
|
+
* whether a person has ticked the retry box on it, or one that was and is not
|
|
65
|
+
* any more.
|
|
66
66
|
*/
|
|
67
67
|
export type SetAsideComment = Readonly<
|
|
68
68
|
| { kind: 'resolved' }
|
|
@@ -70,9 +70,9 @@ export type SetAsideComment = Readonly<
|
|
|
70
70
|
>;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* The record line, `prose`, and an empty retry box on the last line. The
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* The record line, `prose`, and an empty retry box on the last line. The whole
|
|
74
|
+
* SHAs rather than short ones, because what reads them back compares them with
|
|
75
|
+
* the head and the base.
|
|
76
76
|
*/
|
|
77
77
|
export const writeSetAsideComment = (
|
|
78
78
|
setAside: SetAside,
|
|
@@ -148,11 +148,11 @@ export const setAsideApplies = (
|
|
|
148
148
|
* judged the head.
|
|
149
149
|
*
|
|
150
150
|
* Every reason clears when the base moves, because a moved base is the one
|
|
151
|
-
* thing the script knows how to act on — a rebase that conflicted may not
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
151
|
+
* thing the script knows how to act on — a rebase that conflicted may not any
|
|
152
|
+
* more — except two whose head a rebase would only put through the same matrix
|
|
153
|
+
* again: `checks-failed`, which the push that carries the fix clears, and
|
|
154
|
+
* `awaiting-bypass`, green and waiting for a ruleset bypass merge, which does
|
|
155
|
+
* not wait for the base.
|
|
156
156
|
*/
|
|
157
157
|
export const setAsideStillApplies = (
|
|
158
158
|
setAside: Readonly<{ reason: string; baseSha: string }>,
|
package/src/stack.mts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* branch, so that its diff is its own layer and nothing below it.
|
|
4
4
|
*
|
|
5
5
|
* The stack is read from the bases alone, because GitHub already holds it: a
|
|
6
|
-
* pull request onto `a` is on top of whichever pull request is `a`'s. When
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* pull request onto `a` is on top of whichever pull request is `a`'s. When that
|
|
7
|
+
* one merges, GitHub moves the layer above it onto its base and rebases it, so
|
|
8
|
+
* the base chain is the merge order, and everything that orders pull requests
|
|
9
|
+
* here reads a parent as a `Merge-After:` on it. `open-pr --base` writes that
|
|
10
|
+
* trailer too, so that the body states the order as it does for every other; a
|
|
11
|
+
* stack made any other way, without one, counts all the same, and one naming
|
|
12
|
+
* the parent is not counted twice.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/** The fields a stack is read from. */
|
|
@@ -20,8 +20,8 @@ export type StackMember = Readonly<{
|
|
|
20
20
|
headRef: string;
|
|
21
21
|
baseRef: string;
|
|
22
22
|
/**
|
|
23
|
-
* Whether the head branch lives in a fork. A fork's branch is not a branch
|
|
24
|
-
*
|
|
23
|
+
* Whether the head branch lives in a fork. A fork's branch is not a branch of
|
|
24
|
+
* this repository, whatever its name, so it is never a parent.
|
|
25
25
|
*/
|
|
26
26
|
fromFork: boolean;
|
|
27
27
|
}>;
|
|
@@ -31,9 +31,9 @@ export type StackMember = Readonly<{
|
|
|
31
31
|
* A pull request that is not stacked has no entry.
|
|
32
32
|
*
|
|
33
33
|
* A base is a parent's head only when exactly one open pull request from this
|
|
34
|
-
* repository heads it: a branch opened against two bases is two pull
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* repository heads it: a branch opened against two bases is two pull requests,
|
|
35
|
+
* and which of them a layer above means cannot be told. Nothing onto the
|
|
36
|
+
* default branch is stacked, even if some odd pull request heads it.
|
|
37
37
|
*/
|
|
38
38
|
export const findStackParents = (
|
|
39
39
|
pulls: readonly StackMember[],
|
|
@@ -60,9 +60,8 @@ export const findStackParents = (
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Every layer above `number`, each after the one it is on — the order in
|
|
64
|
-
*
|
|
65
|
-
* first.
|
|
63
|
+
* Every layer above `number`, each after the one it is on — the order in which
|
|
64
|
+
* they are moved when `number` moves. Siblings keep the lowest number first.
|
|
66
65
|
*/
|
|
67
66
|
export const stackDescendants = (
|
|
68
67
|
parents: ReadonlyMap<number, number>,
|