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.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -0
  3. package/bin/api.d.mts +19 -3
  4. package/bin/api.mjs +57 -8
  5. package/bin/classify/apply.d.mts +2 -0
  6. package/bin/classify/apply.mjs +1 -1
  7. package/bin/cli/args.mjs +1 -0
  8. package/bin/cli/default-poll.mjs +2 -0
  9. package/bin/cli/help-command-pages.d.mts +1 -1
  10. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  11. package/bin/cli/help-iterate-poll-pages.mjs +16 -10
  12. package/bin/cli/help-top-page.d.mts +1 -1
  13. package/bin/cli/help-top-page.mjs +10 -7
  14. package/bin/cli/help.d.mts +2 -2
  15. package/bin/cli/poll-handler.mjs +40 -11
  16. package/bin/cli/poll-summary-emitter.d.mts +4 -0
  17. package/bin/cli/poll-summary-emitter.mjs +22 -0
  18. package/bin/cli/poll-summary-formatter.d.mts +2 -0
  19. package/bin/cli/poll-summary-formatter.mjs +96 -0
  20. package/bin/cli/poll-targets.d.mts +15 -0
  21. package/bin/cli/poll-targets.mjs +114 -0
  22. package/bin/cli/validate-default-args.mjs +1 -4
  23. package/bin/commands/iterate/api-usage.d.mts +1 -1
  24. package/bin/commands/iterate/api-usage.mjs +6 -2
  25. package/bin/commands/iterate/run.mjs +1 -1
  26. package/bin/commands/poll-run.d.mts +1 -1
  27. package/bin/commands/poll-run.mjs +2 -2
  28. package/bin/commands/poll-summary.d.mts +10 -0
  29. package/bin/commands/poll-summary.mjs +163 -0
  30. package/bin/commands/poll.mjs +2 -1
  31. package/bin/commands/ready-delay.d.mts +3 -1
  32. package/bin/commands/ready-delay.mjs +3 -2
  33. package/bin/config/load.d.mts +7 -0
  34. package/bin/config/load.mjs +70 -19
  35. package/bin/config.json +9 -3
  36. package/bin/github/gql/poll-stack-summary.gql +33 -0
  37. package/bin/github/gql/poll-summary-fragment.gql +198 -0
  38. package/bin/github/poll-summary-checks.d.mts +3 -0
  39. package/bin/github/poll-summary-checks.mjs +61 -0
  40. package/bin/github/poll-summary-projector.d.mts +4 -0
  41. package/bin/github/poll-summary-projector.mjs +81 -0
  42. package/bin/github/poll-summary-raw.d.mts +134 -0
  43. package/bin/github/poll-summary-raw.mjs +1 -0
  44. package/bin/github/poll-summary-review.d.mts +4 -0
  45. package/bin/github/poll-summary-review.mjs +85 -0
  46. package/bin/github/poll-summary-route.d.mts +4 -0
  47. package/bin/github/poll-summary-route.mjs +47 -0
  48. package/bin/github/poll-summary.d.mts +7 -0
  49. package/bin/github/poll-summary.mjs +110 -0
  50. package/bin/github/queries.d.mts +4 -0
  51. package/bin/github/queries.mjs +4 -0
  52. package/bin/mcp/server.mjs +39 -6
  53. package/bin/pr-reference.d.mts +2 -0
  54. package/bin/pr-reference.mjs +4 -0
  55. package/bin/quota-warning.mjs +1 -1
  56. package/bin/types/iterate.d.mts +3 -3
  57. package/bin/types/poll-summary.d.mts +82 -0
  58. package/bin/types/poll-summary.mjs +1 -0
  59. package/bin/types.d.mts +1 -0
  60. package/bin/types.mjs +1 -0
  61. package/package.json +1 -1
  62. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  63. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  64. package/plugins/pr-shepherd/.mcp.json +1 -1
  65. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
@@ -137,7 +137,30 @@ function parseMergeCommandArgs(value) {
137
137
  }
138
138
  return strategies.length === 0 ? [...value, "--merge"] : [...value];
139
139
  }
140
- function parseGraphqlQuotaWarnings(value) {
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 (typeof pollIntervalMinutes !== "number" ||
159
- !Number.isFinite(pollIntervalMinutes) ||
160
- pollIntervalMinutes <= 0) {
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
- return { remainingPercent, pollIntervalMinutes };
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 defaults = builtins;
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(defaults), overlay);
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
- const fallback = { ...defaults };
306
- configCache.set(cwd, fallback);
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, "pollIntervalMinutes": 2 },
29
- { "remainingPercent": 20, "pollIntervalMinutes": 5 },
30
- { "remainingPercent": 10, "pollIntervalMinutes": 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,3 @@
1
+ import type { PollSummaryChecks } from "../types.mts";
2
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
3
+ export declare function summarizePollSummaryChecks(raw: RawSummaryPr): PollSummaryChecks;
@@ -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 {};