pr-shepherd 0.47.0 → 0.49.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/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -0
- package/bin/api.d.mts +19 -3
- package/bin/api.mjs +57 -8
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +1 -1
- package/bin/cli/args.mjs +1 -0
- package/bin/cli/default-poll.mjs +2 -0
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +16 -10
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +10 -7
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/poll-handler.mjs +40 -11
- package/bin/cli/poll-summary-emitter.d.mts +4 -0
- package/bin/cli/poll-summary-emitter.mjs +22 -0
- package/bin/cli/poll-summary-formatter.d.mts +2 -0
- package/bin/cli/poll-summary-formatter.mjs +96 -0
- package/bin/cli/poll-targets.d.mts +15 -0
- package/bin/cli/poll-targets.mjs +114 -0
- package/bin/cli/validate-default-args.mjs +1 -4
- package/bin/commands/iterate/api-usage.d.mts +1 -1
- package/bin/commands/iterate/api-usage.mjs +6 -2
- package/bin/commands/iterate/run.mjs +1 -1
- package/bin/commands/poll-run.d.mts +1 -1
- package/bin/commands/poll-run.mjs +2 -2
- package/bin/commands/poll-summary.d.mts +10 -0
- package/bin/commands/poll-summary.mjs +163 -0
- package/bin/commands/poll.mjs +2 -1
- package/bin/commands/ready-delay.d.mts +3 -1
- package/bin/commands/ready-delay.mjs +3 -2
- package/bin/config/load.d.mts +7 -0
- package/bin/config/load.mjs +70 -19
- package/bin/config.json +9 -3
- package/bin/github/gql/poll-stack-summary.gql +33 -0
- package/bin/github/gql/poll-summary-fragment.gql +198 -0
- package/bin/github/poll-summary-checks.d.mts +3 -0
- package/bin/github/poll-summary-checks.mjs +61 -0
- package/bin/github/poll-summary-projector.d.mts +4 -0
- package/bin/github/poll-summary-projector.mjs +81 -0
- package/bin/github/poll-summary-raw.d.mts +134 -0
- package/bin/github/poll-summary-raw.mjs +1 -0
- package/bin/github/poll-summary-review.d.mts +4 -0
- package/bin/github/poll-summary-review.mjs +85 -0
- package/bin/github/poll-summary-route.d.mts +4 -0
- package/bin/github/poll-summary-route.mjs +47 -0
- package/bin/github/poll-summary.d.mts +7 -0
- package/bin/github/poll-summary.mjs +110 -0
- package/bin/github/queries.d.mts +4 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/mcp/server.mjs +39 -6
- package/bin/pr-reference.d.mts +2 -0
- package/bin/pr-reference.mjs +4 -0
- package/bin/quota-warning.mjs +1 -1
- package/bin/types/iterate.d.mts +3 -3
- package/bin/types/poll-summary.d.mts +82 -0
- package/bin/types/poll-summary.mjs +1 -0
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
package/bin/config/load.mjs
CHANGED
|
@@ -137,7 +137,30 @@ function parseMergeCommandArgs(value) {
|
|
|
137
137
|
}
|
|
138
138
|
return strategies.length === 0 ? [...value, "--merge"] : [...value];
|
|
139
139
|
}
|
|
140
|
-
function
|
|
140
|
+
function parsePollConfig(value) {
|
|
141
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
142
|
+
throw new Error("Invalid config: poll must be a plain object");
|
|
143
|
+
}
|
|
144
|
+
const record = value;
|
|
145
|
+
const intervalSeconds = parsePollDuration(record["intervalSeconds"], "intervalSeconds");
|
|
146
|
+
const timeoutSeconds = parsePollDuration(record["timeoutSeconds"], "timeoutSeconds");
|
|
147
|
+
const debounceSeconds = parsePollDuration(record["debounceSeconds"], "debounceSeconds", true);
|
|
148
|
+
const quietStatus = record["quietStatus"];
|
|
149
|
+
if (typeof quietStatus !== "boolean") {
|
|
150
|
+
throw new Error(`Invalid config: poll.quietStatus must be a boolean, got ${JSON.stringify(quietStatus)}`);
|
|
151
|
+
}
|
|
152
|
+
return { intervalSeconds, timeoutSeconds, debounceSeconds, quietStatus };
|
|
153
|
+
}
|
|
154
|
+
function parsePollDuration(value, key, allowZero = false) {
|
|
155
|
+
if (typeof value !== "number" ||
|
|
156
|
+
!Number.isFinite(value) ||
|
|
157
|
+
(allowZero ? value < 0 : value <= 0)) {
|
|
158
|
+
const range = allowZero ? "a non-negative" : "a positive";
|
|
159
|
+
throw new Error(`Invalid config: poll.${key} must be ${range} finite number, got ${JSON.stringify(value)}`);
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function parseGraphqlQuotaWarnings(value, pollIntervalSeconds) {
|
|
141
164
|
if (!Array.isArray(value)) {
|
|
142
165
|
throw new Error("Invalid config: watch.graphqlQuotaWarnings must be an array");
|
|
143
166
|
}
|
|
@@ -149,22 +172,42 @@ function parseGraphqlQuotaWarnings(value) {
|
|
|
149
172
|
const record = item;
|
|
150
173
|
const remainingPercent = record["remainingPercent"];
|
|
151
174
|
const pollIntervalMinutes = record["pollIntervalMinutes"];
|
|
175
|
+
const pollIntervalFactor = record["pollIntervalFactor"];
|
|
152
176
|
if (typeof remainingPercent !== "number" ||
|
|
153
177
|
!Number.isInteger(remainingPercent) ||
|
|
154
178
|
remainingPercent < 1 ||
|
|
155
179
|
remainingPercent > 100) {
|
|
156
180
|
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}].remainingPercent must be an integer from 1 to 100`);
|
|
157
181
|
}
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
if (pollIntervalMinutes === undefined && pollIntervalFactor === undefined) {
|
|
183
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}] must define pollIntervalMinutes, pollIntervalFactor, or both`);
|
|
184
|
+
}
|
|
185
|
+
if (pollIntervalMinutes !== undefined &&
|
|
186
|
+
(typeof pollIntervalMinutes !== "number" ||
|
|
187
|
+
!Number.isFinite(pollIntervalMinutes) ||
|
|
188
|
+
pollIntervalMinutes <= 0)) {
|
|
161
189
|
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}].pollIntervalMinutes must be a positive number`);
|
|
162
190
|
}
|
|
191
|
+
if (pollIntervalFactor !== undefined &&
|
|
192
|
+
(typeof pollIntervalFactor !== "number" ||
|
|
193
|
+
!Number.isFinite(pollIntervalFactor) ||
|
|
194
|
+
pollIntervalFactor < 1)) {
|
|
195
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}].pollIntervalFactor must be a number greater than or equal to 1`);
|
|
196
|
+
}
|
|
163
197
|
if (seen.has(remainingPercent)) {
|
|
164
198
|
throw new Error(`Invalid config: watch.graphqlQuotaWarnings has duplicate remainingPercent ${remainingPercent}`);
|
|
165
199
|
}
|
|
166
200
|
seen.add(remainingPercent);
|
|
167
|
-
|
|
201
|
+
const factorMinutes = typeof pollIntervalFactor === "number" ? (pollIntervalSeconds * pollIntervalFactor) / 60 : 0;
|
|
202
|
+
const absoluteMinutes = typeof pollIntervalMinutes === "number" ? pollIntervalMinutes : 0;
|
|
203
|
+
const resolvedPollIntervalMinutes = Math.max(absoluteMinutes, factorMinutes);
|
|
204
|
+
if (!Number.isFinite(resolvedPollIntervalMinutes)) {
|
|
205
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}] resolves to a non-finite poll interval`);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
remainingPercent,
|
|
209
|
+
pollIntervalMinutes: resolvedPollIntervalMinutes,
|
|
210
|
+
};
|
|
168
211
|
});
|
|
169
212
|
parsed.sort((left, right) => right.remainingPercent - left.remainingPercent);
|
|
170
213
|
for (let index = 1; index < parsed.length; index += 1) {
|
|
@@ -183,6 +226,7 @@ const KNOWN_CONFIG_KEYS = new Set([
|
|
|
183
226
|
"botUsernames",
|
|
184
227
|
"ignoreChecks",
|
|
185
228
|
"iterate",
|
|
229
|
+
"poll",
|
|
186
230
|
"watch",
|
|
187
231
|
"resolve",
|
|
188
232
|
"checks",
|
|
@@ -199,6 +243,7 @@ const KNOWN_NESTED_KEYS = {
|
|
|
199
243
|
"behindBaseHint",
|
|
200
244
|
"resolveOtherHumanThreads",
|
|
201
245
|
]),
|
|
246
|
+
poll: new Set(["intervalSeconds", "timeoutSeconds", "debounceSeconds", "quietStatus"]),
|
|
202
247
|
watch: new Set(["readyDelayMinutes", "graphqlQuotaWarnings"]),
|
|
203
248
|
resolve: new Set(["shaPoll"]),
|
|
204
249
|
checks: new Set(["ciTriggerEvents", "ignoreLogLines"]),
|
|
@@ -236,7 +281,23 @@ function warnUnknownConfigKeys(config) {
|
|
|
236
281
|
}
|
|
237
282
|
}
|
|
238
283
|
}
|
|
239
|
-
const
|
|
284
|
+
const rawDefaults = builtins;
|
|
285
|
+
function parseConfig(value, normalizeMergeArgs = true) {
|
|
286
|
+
const config = value;
|
|
287
|
+
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
288
|
+
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
289
|
+
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
290
|
+
if (config.merge && normalizeMergeArgs) {
|
|
291
|
+
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
292
|
+
}
|
|
293
|
+
config.poll = parsePollConfig(config.poll);
|
|
294
|
+
config.watch.graphqlQuotaWarnings = parseGraphqlQuotaWarnings(config.watch.graphqlQuotaWarnings, config.poll.intervalSeconds);
|
|
295
|
+
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
296
|
+
config.iterate.resolveOtherHumanThreads = parseResolveOtherHumanThreads(config.iterate.resolveOtherHumanThreads);
|
|
297
|
+
config.checks.ignoreLogLines = parseIgnoreLogLines(config.checks.ignoreLogLines);
|
|
298
|
+
return config;
|
|
299
|
+
}
|
|
300
|
+
const defaults = parseConfig(structuredClone(rawDefaults), false);
|
|
240
301
|
const configCache = new Map();
|
|
241
302
|
function stripDeprecatedActionKeys(parsed) {
|
|
242
303
|
const rawActions = parsed.actions;
|
|
@@ -287,24 +348,14 @@ export function loadConfig() {
|
|
|
287
348
|
configCache.set(cwd, defaults);
|
|
288
349
|
return defaults;
|
|
289
350
|
}
|
|
290
|
-
const config = deepMerge(structuredClone(
|
|
291
|
-
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
292
|
-
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
293
|
-
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
294
|
-
if (config.merge)
|
|
295
|
-
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
296
|
-
config.watch.graphqlQuotaWarnings = parseGraphqlQuotaWarnings(config.watch.graphqlQuotaWarnings);
|
|
297
|
-
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
298
|
-
config.iterate.resolveOtherHumanThreads = parseResolveOtherHumanThreads(config.iterate.resolveOtherHumanThreads);
|
|
299
|
-
config.checks.ignoreLogLines = parseIgnoreLogLines(config.checks.ignoreLogLines);
|
|
351
|
+
const config = parseConfig(deepMerge(structuredClone(rawDefaults), overlay));
|
|
300
352
|
configCache.set(cwd, config);
|
|
301
353
|
return config;
|
|
302
354
|
}
|
|
303
355
|
catch (err) {
|
|
304
356
|
process.stderr.write(`pr-shepherd: failed to parse ${rcPaths[0]}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return fallback;
|
|
357
|
+
configCache.set(cwd, defaults);
|
|
358
|
+
return defaults;
|
|
308
359
|
}
|
|
309
360
|
}
|
|
310
361
|
/** Reset the config cache — for use in tests that change directories. */
|
package/bin/config.json
CHANGED
|
@@ -22,12 +22,18 @@
|
|
|
22
22
|
"behindBaseHint": "",
|
|
23
23
|
"resolveOtherHumanThreads": "none"
|
|
24
24
|
},
|
|
25
|
+
"poll": {
|
|
26
|
+
"intervalSeconds": 60,
|
|
27
|
+
"timeoutSeconds": 270,
|
|
28
|
+
"debounceSeconds": 60,
|
|
29
|
+
"quietStatus": false
|
|
30
|
+
},
|
|
25
31
|
"watch": {
|
|
26
32
|
"readyDelayMinutes": 10,
|
|
27
33
|
"graphqlQuotaWarnings": [
|
|
28
|
-
{ "remainingPercent": 30, "
|
|
29
|
-
{ "remainingPercent": 20, "
|
|
30
|
-
{ "remainingPercent": 10, "
|
|
34
|
+
{ "remainingPercent": 30, "pollIntervalFactor": 2 },
|
|
35
|
+
{ "remainingPercent": 20, "pollIntervalFactor": 5 },
|
|
36
|
+
{ "remainingPercent": 10, "pollIntervalFactor": 10 }
|
|
31
37
|
]
|
|
32
38
|
},
|
|
33
39
|
"resolve": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
query PollStackSummary($owner: String!, $repo: String!, $anchor: Int!, $after: String) {
|
|
2
|
+
_shepherdRateLimit: rateLimit {
|
|
3
|
+
cost
|
|
4
|
+
limit
|
|
5
|
+
nodeCount
|
|
6
|
+
remaining
|
|
7
|
+
resetAt
|
|
8
|
+
used
|
|
9
|
+
}
|
|
10
|
+
repository(owner: $owner, name: $repo) {
|
|
11
|
+
viewerCanAdminister
|
|
12
|
+
pullRequest(number: $anchor) {
|
|
13
|
+
stack {
|
|
14
|
+
id
|
|
15
|
+
number
|
|
16
|
+
size
|
|
17
|
+
baseRefName
|
|
18
|
+
entries(first: 50, after: $after) {
|
|
19
|
+
pageInfo {
|
|
20
|
+
hasNextPage
|
|
21
|
+
endCursor
|
|
22
|
+
}
|
|
23
|
+
nodes {
|
|
24
|
+
position
|
|
25
|
+
pullRequest {
|
|
26
|
+
...PollSummaryPr
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
fragment PollSummaryPr on PullRequest {
|
|
2
|
+
number
|
|
3
|
+
title
|
|
4
|
+
url
|
|
5
|
+
state
|
|
6
|
+
isDraft
|
|
7
|
+
viewerCanUpdate
|
|
8
|
+
headRefName
|
|
9
|
+
headRefOid
|
|
10
|
+
baseRefName
|
|
11
|
+
mergeable
|
|
12
|
+
mergeStateStatus
|
|
13
|
+
reviewDecision
|
|
14
|
+
reviewRequests(last: 50) {
|
|
15
|
+
nodes {
|
|
16
|
+
requestedReviewer {
|
|
17
|
+
__typename
|
|
18
|
+
... on User {
|
|
19
|
+
login
|
|
20
|
+
}
|
|
21
|
+
... on Bot {
|
|
22
|
+
login
|
|
23
|
+
}
|
|
24
|
+
... on Mannequin {
|
|
25
|
+
login
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
latestReviews(last: 50) {
|
|
31
|
+
nodes {
|
|
32
|
+
state
|
|
33
|
+
author {
|
|
34
|
+
__typename
|
|
35
|
+
login
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
isInMergeQueue
|
|
40
|
+
mergeQueueEntry {
|
|
41
|
+
headCommit {
|
|
42
|
+
statusCheckRollup {
|
|
43
|
+
contexts(last: 100) {
|
|
44
|
+
totalCount
|
|
45
|
+
pageInfo {
|
|
46
|
+
hasPreviousPage
|
|
47
|
+
}
|
|
48
|
+
nodes {
|
|
49
|
+
__typename
|
|
50
|
+
... on CheckRun {
|
|
51
|
+
id
|
|
52
|
+
name
|
|
53
|
+
status
|
|
54
|
+
conclusion
|
|
55
|
+
detailsUrl
|
|
56
|
+
checkSuite {
|
|
57
|
+
workflowRun {
|
|
58
|
+
databaseId
|
|
59
|
+
event
|
|
60
|
+
workflow {
|
|
61
|
+
databaseId
|
|
62
|
+
name
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
... on StatusContext {
|
|
68
|
+
context
|
|
69
|
+
state
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
stack {
|
|
77
|
+
number
|
|
78
|
+
size
|
|
79
|
+
baseRefName
|
|
80
|
+
}
|
|
81
|
+
stackEntry {
|
|
82
|
+
position
|
|
83
|
+
}
|
|
84
|
+
comments(last: 100) {
|
|
85
|
+
totalCount
|
|
86
|
+
pageInfo {
|
|
87
|
+
hasPreviousPage
|
|
88
|
+
}
|
|
89
|
+
nodes {
|
|
90
|
+
id
|
|
91
|
+
body
|
|
92
|
+
isMinimized
|
|
93
|
+
url
|
|
94
|
+
authorAssociation
|
|
95
|
+
author {
|
|
96
|
+
__typename
|
|
97
|
+
login
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
reviews(last: 100) {
|
|
102
|
+
totalCount
|
|
103
|
+
pageInfo {
|
|
104
|
+
hasPreviousPage
|
|
105
|
+
}
|
|
106
|
+
nodes {
|
|
107
|
+
id
|
|
108
|
+
body
|
|
109
|
+
state
|
|
110
|
+
isMinimized
|
|
111
|
+
authorAssociation
|
|
112
|
+
author {
|
|
113
|
+
__typename
|
|
114
|
+
login
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
reviewThreads(last: 20) {
|
|
119
|
+
totalCount
|
|
120
|
+
pageInfo {
|
|
121
|
+
hasPreviousPage
|
|
122
|
+
}
|
|
123
|
+
nodes {
|
|
124
|
+
id
|
|
125
|
+
isResolved
|
|
126
|
+
isOutdated
|
|
127
|
+
path
|
|
128
|
+
rootComments: comments(first: 1) {
|
|
129
|
+
nodes {
|
|
130
|
+
id
|
|
131
|
+
body
|
|
132
|
+
url
|
|
133
|
+
viewerDidAuthor
|
|
134
|
+
authorAssociation
|
|
135
|
+
author {
|
|
136
|
+
__typename
|
|
137
|
+
login
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
comments(last: 5) {
|
|
142
|
+
totalCount
|
|
143
|
+
pageInfo {
|
|
144
|
+
hasPreviousPage
|
|
145
|
+
}
|
|
146
|
+
nodes {
|
|
147
|
+
id
|
|
148
|
+
body
|
|
149
|
+
url
|
|
150
|
+
authorAssociation
|
|
151
|
+
viewerDidAuthor
|
|
152
|
+
author {
|
|
153
|
+
__typename
|
|
154
|
+
login
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
commits(last: 1) {
|
|
161
|
+
nodes {
|
|
162
|
+
commit {
|
|
163
|
+
statusCheckRollup {
|
|
164
|
+
contexts(last: 100) {
|
|
165
|
+
totalCount
|
|
166
|
+
pageInfo {
|
|
167
|
+
hasPreviousPage
|
|
168
|
+
}
|
|
169
|
+
nodes {
|
|
170
|
+
__typename
|
|
171
|
+
... on CheckRun {
|
|
172
|
+
id
|
|
173
|
+
name
|
|
174
|
+
status
|
|
175
|
+
conclusion
|
|
176
|
+
detailsUrl
|
|
177
|
+
checkSuite {
|
|
178
|
+
workflowRun {
|
|
179
|
+
databaseId
|
|
180
|
+
event
|
|
181
|
+
workflow {
|
|
182
|
+
databaseId
|
|
183
|
+
name
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
... on StatusContext {
|
|
189
|
+
context
|
|
190
|
+
state
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { classifyChecks } from "../checks/classify.mjs";
|
|
2
|
+
export function summarizePollSummaryChecks(raw) {
|
|
3
|
+
const rollups = [
|
|
4
|
+
raw.commits.nodes[0]?.commit.statusCheckRollup,
|
|
5
|
+
raw.mergeQueueEntry?.headCommit?.statusCheckRollup,
|
|
6
|
+
].filter((rollup) => rollup !== null && rollup !== undefined);
|
|
7
|
+
const checks = rollups.flatMap((rollup, rollupIndex) => rollup.contexts.nodes.map((context) => {
|
|
8
|
+
if (context.__typename === "StatusContext") {
|
|
9
|
+
return {
|
|
10
|
+
name: context.context,
|
|
11
|
+
status: context.state === "PENDING" || context.state === "EXPECTED"
|
|
12
|
+
? "IN_PROGRESS"
|
|
13
|
+
: "COMPLETED",
|
|
14
|
+
conclusion: context.state === "SUCCESS"
|
|
15
|
+
? "SUCCESS"
|
|
16
|
+
: context.state === "FAILURE" || context.state === "ERROR"
|
|
17
|
+
? "FAILURE"
|
|
18
|
+
: null,
|
|
19
|
+
source: "status_context",
|
|
20
|
+
detailsUrl: "",
|
|
21
|
+
event: null,
|
|
22
|
+
runId: null,
|
|
23
|
+
...(rollupIndex === 1 && { scope: "merge_group" }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const run = context.checkSuite?.workflowRun;
|
|
27
|
+
return {
|
|
28
|
+
id: context.id,
|
|
29
|
+
name: context.name,
|
|
30
|
+
status: context.status,
|
|
31
|
+
conclusion: context.conclusion,
|
|
32
|
+
source: "check_run",
|
|
33
|
+
detailsUrl: context.detailsUrl ?? "",
|
|
34
|
+
event: run?.event ?? null,
|
|
35
|
+
runId: run?.databaseId != null ? String(run.databaseId) : null,
|
|
36
|
+
...(run?.workflow?.name && { workflowName: run.workflow.name }),
|
|
37
|
+
...(run?.workflow?.databaseId != null && {
|
|
38
|
+
workflowId: String(run.workflow.databaseId),
|
|
39
|
+
}),
|
|
40
|
+
...(rollupIndex === 1 && { scope: "merge_group" }),
|
|
41
|
+
};
|
|
42
|
+
}));
|
|
43
|
+
const counts = {};
|
|
44
|
+
for (const check of classifyChecks(checks, { additionalRelevantEvents: ["merge_group"] })) {
|
|
45
|
+
const key = {
|
|
46
|
+
passed: "passing",
|
|
47
|
+
failing: "failing",
|
|
48
|
+
in_progress: "inProgress",
|
|
49
|
+
skipped: "skipped",
|
|
50
|
+
filtered: "filtered",
|
|
51
|
+
ignored: "ignored",
|
|
52
|
+
superseded: "superseded",
|
|
53
|
+
}[check.category];
|
|
54
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
55
|
+
}
|
|
56
|
+
const summary = counts;
|
|
57
|
+
if (rollups.some((rollup) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
58
|
+
summary.incomplete = true;
|
|
59
|
+
}
|
|
60
|
+
return summary;
|
|
61
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
|
|
2
|
+
import type { RepoInfo } from "./client.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
+
export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean): Promise<PollSummaryItem>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
import { updateReadyDelay } from "../commands/ready-delay.mjs";
|
|
3
|
+
import { formatPrUrl } from "../pr-reference.mjs";
|
|
4
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
5
|
+
import { loadSeenMap } from "../state/seen-comments.mjs";
|
|
6
|
+
import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
|
|
7
|
+
import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
|
|
8
|
+
import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
|
|
9
|
+
export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
|
|
10
|
+
const repoName = `${repo.owner}/${repo.name}`;
|
|
11
|
+
const seen = await loadSeenMap({ owner: repo.owner, repo: repo.name, pr: raw.number });
|
|
12
|
+
const checks = summarizePollSummaryChecks(raw);
|
|
13
|
+
const review = await summarizePollSummaryReview(raw, seen, viewerCanAdminister);
|
|
14
|
+
const blockingReviewerInProgress = detectBlockingReviewer(raw);
|
|
15
|
+
let { action, reasons } = routePollSummary(raw, checks, review, opts);
|
|
16
|
+
let remainingSeconds;
|
|
17
|
+
if (raw.isDraft && blockingReviewerInProgress && action === "mark_ready") {
|
|
18
|
+
action = "wait";
|
|
19
|
+
reasons = ["blocking-reviewer-in-progress"];
|
|
20
|
+
}
|
|
21
|
+
const appearsReady = reasons.includes("appears-ready");
|
|
22
|
+
const readyDelaySeconds = opts.readyDelaySeconds ?? (loadConfig().watch?.readyDelayMinutes ?? 10) * 60;
|
|
23
|
+
const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { retainElapsed: true });
|
|
24
|
+
if (appearsReady && !readyState.shouldCancel) {
|
|
25
|
+
action = "wait";
|
|
26
|
+
reasons = ["ready-delay"];
|
|
27
|
+
remainingSeconds = readyState.remainingSeconds;
|
|
28
|
+
}
|
|
29
|
+
const stack = raw.stack
|
|
30
|
+
? {
|
|
31
|
+
number: raw.stack.number,
|
|
32
|
+
size: raw.stack.size,
|
|
33
|
+
position: raw.stackEntry?.position ?? 0,
|
|
34
|
+
baseRefName: raw.stack.baseRefName,
|
|
35
|
+
}
|
|
36
|
+
: undefined;
|
|
37
|
+
return {
|
|
38
|
+
pr: raw.number,
|
|
39
|
+
repo: repoName,
|
|
40
|
+
title: raw.title,
|
|
41
|
+
url: raw.url || formatPrUrl(repoName, raw.number),
|
|
42
|
+
action,
|
|
43
|
+
reasons,
|
|
44
|
+
state: normalizePollSummaryState(raw.state),
|
|
45
|
+
mergeable: raw.mergeable,
|
|
46
|
+
mergeStateStatus: raw.mergeStateStatus,
|
|
47
|
+
...(raw.reviewDecision && { reviewDecision: raw.reviewDecision }),
|
|
48
|
+
headRefName: raw.headRefName,
|
|
49
|
+
headRefOid: raw.headRefOid,
|
|
50
|
+
baseRefName: raw.baseRefName,
|
|
51
|
+
...(raw.isDraft && { isDraft: true }),
|
|
52
|
+
...(raw.isInMergeQueue && { isInMergeQueue: true }),
|
|
53
|
+
...(blockingReviewerInProgress && { blockingReviewerInProgress: true }),
|
|
54
|
+
...(remainingSeconds !== undefined && { remainingSeconds }),
|
|
55
|
+
...(Object.keys(checks).length > 0 && { checks }),
|
|
56
|
+
...(Object.keys(review).length > 0 && { review }),
|
|
57
|
+
...(stack && { stack }),
|
|
58
|
+
...(!["wait", "cancel"].includes(action) && {
|
|
59
|
+
pollCommand: buildPollCommand(repoName, raw.number, opts),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function detectBlockingReviewer(raw) {
|
|
64
|
+
const prefixes = (loadConfig().mergeStatus?.blockingReviewerLogins ?? []).map((login) => login.toLowerCase());
|
|
65
|
+
const matches = (author) => author !== null && prefixes.some((prefix) => author.login.toLowerCase().startsWith(prefix));
|
|
66
|
+
return ((raw.reviewRequests?.nodes ?? []).some((request) => matches(request.requestedReviewer)) ||
|
|
67
|
+
(raw.latestReviews?.nodes ?? []).some((review) => review.state === "PENDING" && matches(review.author)));
|
|
68
|
+
}
|
|
69
|
+
function buildPollCommand(repo, pr, opts) {
|
|
70
|
+
const args = [formatPrUrl(repo, pr), "--until-terminal"];
|
|
71
|
+
if (opts.merge)
|
|
72
|
+
args.push("--merge");
|
|
73
|
+
if (opts.readyDelaySeconds !== undefined)
|
|
74
|
+
args.push("--ready-delay", `${opts.readyDelaySeconds}s`);
|
|
75
|
+
if (opts.stallTimeoutSeconds !== undefined) {
|
|
76
|
+
args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
|
|
77
|
+
}
|
|
78
|
+
if (opts.noAutoMarkReady)
|
|
79
|
+
args.push("--no-auto-mark-ready");
|
|
80
|
+
return buildPrShepherdCommand(args).text;
|
|
81
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export interface RawAuthor {
|
|
2
|
+
__typename?: string;
|
|
3
|
+
login: string;
|
|
4
|
+
}
|
|
5
|
+
interface RawSummaryComment {
|
|
6
|
+
id: string;
|
|
7
|
+
body: string;
|
|
8
|
+
isMinimized: boolean;
|
|
9
|
+
viewerDidAuthor?: boolean;
|
|
10
|
+
authorAssociation?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
author: RawAuthor | null;
|
|
13
|
+
}
|
|
14
|
+
interface SummaryConnection<T> {
|
|
15
|
+
totalCount: number;
|
|
16
|
+
pageInfo: {
|
|
17
|
+
hasPreviousPage: boolean;
|
|
18
|
+
};
|
|
19
|
+
nodes: T[];
|
|
20
|
+
}
|
|
21
|
+
type RawCheckContext = {
|
|
22
|
+
__typename: "CheckRun";
|
|
23
|
+
id?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
status: string;
|
|
26
|
+
conclusion: string | null;
|
|
27
|
+
detailsUrl?: string;
|
|
28
|
+
checkSuite: {
|
|
29
|
+
workflowRun: {
|
|
30
|
+
databaseId?: string | number;
|
|
31
|
+
event: string;
|
|
32
|
+
workflow?: {
|
|
33
|
+
name: string;
|
|
34
|
+
databaseId: string | number;
|
|
35
|
+
} | null;
|
|
36
|
+
} | null;
|
|
37
|
+
} | null;
|
|
38
|
+
} | {
|
|
39
|
+
__typename: "StatusContext";
|
|
40
|
+
context: string;
|
|
41
|
+
state: string;
|
|
42
|
+
};
|
|
43
|
+
interface RawCheckRollup {
|
|
44
|
+
contexts: SummaryConnection<RawCheckContext>;
|
|
45
|
+
}
|
|
46
|
+
export interface RawSummaryPr {
|
|
47
|
+
number: number;
|
|
48
|
+
title: string;
|
|
49
|
+
url: string;
|
|
50
|
+
state: string;
|
|
51
|
+
isDraft: boolean;
|
|
52
|
+
viewerCanUpdate: boolean;
|
|
53
|
+
headRefName: string;
|
|
54
|
+
headRefOid: string;
|
|
55
|
+
baseRefName: string;
|
|
56
|
+
mergeable: string;
|
|
57
|
+
mergeStateStatus: string;
|
|
58
|
+
reviewDecision: string | null;
|
|
59
|
+
reviewRequests?: {
|
|
60
|
+
nodes: Array<{
|
|
61
|
+
requestedReviewer: RawAuthor | null;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
latestReviews?: {
|
|
65
|
+
nodes: Array<{
|
|
66
|
+
state: string;
|
|
67
|
+
author: RawAuthor | null;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
isInMergeQueue: boolean;
|
|
71
|
+
mergeQueueEntry: {
|
|
72
|
+
headCommit: {
|
|
73
|
+
statusCheckRollup: RawCheckRollup | null;
|
|
74
|
+
} | null;
|
|
75
|
+
} | null;
|
|
76
|
+
stack: {
|
|
77
|
+
number: number;
|
|
78
|
+
size: number;
|
|
79
|
+
baseRefName: string;
|
|
80
|
+
} | null;
|
|
81
|
+
stackEntry: {
|
|
82
|
+
position: number;
|
|
83
|
+
} | null;
|
|
84
|
+
comments: SummaryConnection<RawSummaryComment>;
|
|
85
|
+
reviews: SummaryConnection<RawSummaryComment & {
|
|
86
|
+
state: string;
|
|
87
|
+
}>;
|
|
88
|
+
reviewThreads: SummaryConnection<{
|
|
89
|
+
id: string;
|
|
90
|
+
isResolved: boolean;
|
|
91
|
+
isOutdated: boolean;
|
|
92
|
+
path: string | null;
|
|
93
|
+
rootComments?: {
|
|
94
|
+
nodes: RawSummaryComment[];
|
|
95
|
+
};
|
|
96
|
+
comments: SummaryConnection<RawSummaryComment>;
|
|
97
|
+
}>;
|
|
98
|
+
commits: {
|
|
99
|
+
nodes: Array<{
|
|
100
|
+
commit: {
|
|
101
|
+
statusCheckRollup: RawCheckRollup | null;
|
|
102
|
+
};
|
|
103
|
+
}>;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export interface RawExplicitResponse {
|
|
107
|
+
repository: ({
|
|
108
|
+
viewerCanAdminister: boolean;
|
|
109
|
+
} & Record<string, RawSummaryPr | boolean | null>) | null;
|
|
110
|
+
}
|
|
111
|
+
export interface RawStackResponse {
|
|
112
|
+
repository: {
|
|
113
|
+
viewerCanAdminister: boolean;
|
|
114
|
+
pullRequest: {
|
|
115
|
+
stack: {
|
|
116
|
+
id: string;
|
|
117
|
+
number: number;
|
|
118
|
+
size: number;
|
|
119
|
+
baseRefName: string;
|
|
120
|
+
entries: {
|
|
121
|
+
pageInfo: {
|
|
122
|
+
hasNextPage: boolean;
|
|
123
|
+
endCursor: string | null;
|
|
124
|
+
};
|
|
125
|
+
nodes: Array<{
|
|
126
|
+
position: number;
|
|
127
|
+
pullRequest: RawSummaryPr | null;
|
|
128
|
+
}>;
|
|
129
|
+
};
|
|
130
|
+
} | null;
|
|
131
|
+
} | null;
|
|
132
|
+
} | null;
|
|
133
|
+
}
|
|
134
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|