pr-shepherd 0.48.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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -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/default-poll.mjs +1 -0
  8. package/bin/cli/help-command-pages.d.mts +1 -1
  9. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  10. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  11. package/bin/cli/help-top-page.d.mts +1 -1
  12. package/bin/cli/help-top-page.mjs +5 -3
  13. package/bin/cli/help.d.mts +2 -2
  14. package/bin/cli/poll-handler.mjs +25 -4
  15. package/bin/cli/poll-summary-emitter.d.mts +4 -0
  16. package/bin/cli/poll-summary-emitter.mjs +22 -0
  17. package/bin/cli/poll-summary-formatter.d.mts +2 -0
  18. package/bin/cli/poll-summary-formatter.mjs +96 -0
  19. package/bin/cli/poll-targets.d.mts +15 -0
  20. package/bin/cli/poll-targets.mjs +114 -0
  21. package/bin/cli/validate-default-args.mjs +1 -4
  22. package/bin/commands/poll-summary.d.mts +10 -0
  23. package/bin/commands/poll-summary.mjs +163 -0
  24. package/bin/commands/ready-delay.d.mts +3 -1
  25. package/bin/commands/ready-delay.mjs +3 -2
  26. package/bin/github/gql/poll-stack-summary.gql +33 -0
  27. package/bin/github/gql/poll-summary-fragment.gql +198 -0
  28. package/bin/github/poll-summary-checks.d.mts +3 -0
  29. package/bin/github/poll-summary-checks.mjs +61 -0
  30. package/bin/github/poll-summary-projector.d.mts +4 -0
  31. package/bin/github/poll-summary-projector.mjs +81 -0
  32. package/bin/github/poll-summary-raw.d.mts +134 -0
  33. package/bin/github/poll-summary-raw.mjs +1 -0
  34. package/bin/github/poll-summary-review.d.mts +4 -0
  35. package/bin/github/poll-summary-review.mjs +85 -0
  36. package/bin/github/poll-summary-route.d.mts +4 -0
  37. package/bin/github/poll-summary-route.mjs +47 -0
  38. package/bin/github/poll-summary.d.mts +7 -0
  39. package/bin/github/poll-summary.mjs +110 -0
  40. package/bin/github/queries.d.mts +4 -0
  41. package/bin/github/queries.mjs +4 -0
  42. package/bin/mcp/server.mjs +39 -6
  43. package/bin/pr-reference.d.mts +2 -0
  44. package/bin/pr-reference.mjs +4 -0
  45. package/bin/types/poll-summary.d.mts +82 -0
  46. package/bin/types/poll-summary.mjs +1 -0
  47. package/bin/types.d.mts +1 -0
  48. package/bin/types.mjs +1 -0
  49. package/package.json +1 -1
  50. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  51. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  52. package/plugins/pr-shepherd/.mcp.json +1 -1
  53. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
@@ -0,0 +1,114 @@
1
+ import { parseArgs } from "node:util";
2
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
3
+ import { getRepoInfo } from "../github/client.mjs";
4
+ import { parseCliPrReference, normalizeRepositoryIdentity, resolveParsedPrTarget, } from "../pr-reference.mjs";
5
+ const VALUE_FLAGS = new Set([
6
+ "--format",
7
+ "--ready-delay",
8
+ "--stall-timeout",
9
+ "--interval",
10
+ "--timeout",
11
+ "--debounce",
12
+ "--stack",
13
+ ]);
14
+ export function parsePollTargets(args) {
15
+ const { values } = parseArgs({
16
+ args,
17
+ strict: false,
18
+ allowPositionals: true,
19
+ options: { format: { type: "string" }, verbose: { type: "boolean" } },
20
+ });
21
+ const consumed = new Set();
22
+ const refs = [];
23
+ let stack;
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const arg = args[index];
26
+ if (VALUE_FLAGS.has(arg)) {
27
+ if (arg === "--format" || arg === "--stack") {
28
+ consumed.add(index);
29
+ if (index + 1 < args.length)
30
+ consumed.add(index + 1);
31
+ }
32
+ if (arg === "--stack") {
33
+ const parsed = parseCliPrReference(args[index + 1] ?? "");
34
+ if (!parsed)
35
+ return usage(`invalid --stack PR reference: ${args[index + 1] ?? "(missing)"}`);
36
+ if (stack)
37
+ return usage("--stack may only be specified once");
38
+ stack = parsed;
39
+ }
40
+ index += 1;
41
+ continue;
42
+ }
43
+ const equals = arg.indexOf("=");
44
+ const name = equals < 0 ? arg : arg.slice(0, equals);
45
+ if (VALUE_FLAGS.has(name)) {
46
+ if (name === "--format" || name === "--stack")
47
+ consumed.add(index);
48
+ if (name === "--stack") {
49
+ const parsed = parseCliPrReference(arg.slice(equals + 1));
50
+ if (!parsed)
51
+ return usage(`invalid --stack PR reference: ${arg.slice(equals + 1)}`);
52
+ if (stack)
53
+ return usage("--stack may only be specified once");
54
+ stack = parsed;
55
+ }
56
+ continue;
57
+ }
58
+ if (arg === "--verbose") {
59
+ consumed.add(index);
60
+ continue;
61
+ }
62
+ if (arg.startsWith("--"))
63
+ continue;
64
+ const parsed = parseCliPrReference(arg);
65
+ if (!parsed)
66
+ continue;
67
+ refs.push(parsed);
68
+ consumed.add(index);
69
+ }
70
+ if (stack && refs.length > 0)
71
+ return usage("--stack cannot be combined with explicit PRs");
72
+ const format = (values.format ?? "text");
73
+ return {
74
+ refs,
75
+ stack,
76
+ global: { format, verbose: values.verbose === true },
77
+ extra: args.filter((_, index) => !consumed.has(index)),
78
+ };
79
+ }
80
+ export async function resolvePollTargets(parsed) {
81
+ const all = parsed.stack ? [parsed.stack] : parsed.refs;
82
+ if (all.length === 0)
83
+ return { prNumbers: [] };
84
+ const checkoutRepo = all.some((ref) => ref.repository === undefined)
85
+ ? await getRepoInfo()
86
+ : undefined;
87
+ let selectedRepo;
88
+ const prNumbers = [];
89
+ for (const ref of all) {
90
+ const target = resolveParsedPrTarget(ref);
91
+ if (target.prNumber === undefined)
92
+ return usageThrow("PR number is required");
93
+ const repo = target.targetRepository ?? checkoutRepo;
94
+ if (selectedRepo &&
95
+ normalizeRepositoryIdentity(`${selectedRepo.owner}/${selectedRepo.name}`) !==
96
+ normalizeRepositoryIdentity(`${repo.owner}/${repo.name}`)) {
97
+ return usageThrow("aggregate poll only supports PRs from one repository");
98
+ }
99
+ selectedRepo = repo;
100
+ if (!prNumbers.includes(target.prNumber))
101
+ prNumbers.push(target.prNumber);
102
+ }
103
+ return parsed.stack
104
+ ? { prNumbers: [], stackPrNumber: prNumbers[0], targetRepository: selectedRepo }
105
+ : { prNumbers, targetRepository: selectedRepo };
106
+ }
107
+ function usage(message) {
108
+ process.stderr.write(`pr-shepherd: ${message}\n`);
109
+ process.exitCode = EXIT.USAGE;
110
+ return null;
111
+ }
112
+ function usageThrow(message) {
113
+ throw new ShepherdError(message, EXIT.USAGE);
114
+ }
@@ -5,7 +5,6 @@ import { parsePrNumber } from "./args.mjs";
5
5
  * offending arg so callers can print their own usage message.
6
6
  */
7
7
  export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError) {
8
- let sawPr = false;
9
8
  for (let i = 0; i < args.length; i += 1) {
10
9
  const arg = args[i];
11
10
  if (flagsWithValues.has(arg)) {
@@ -21,10 +20,8 @@ export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError
21
20
  continue;
22
21
  if (booleanFlags.has(arg))
23
22
  continue;
24
- if (parsePrNumber(arg) !== null && !sawPr) {
25
- sawPr = true;
23
+ if (parsePrNumber(arg) !== null)
26
24
  continue;
27
- }
28
25
  onError(arg);
29
26
  return false;
30
27
  }
@@ -0,0 +1,10 @@
1
+ import type { PollSummaryCommandOptions, PollSummaryResult } from "../types.mts";
2
+ export interface AggregatePollCommandOptions extends PollSummaryCommandOptions {
3
+ intervalSeconds: number;
4
+ timeoutSeconds: number;
5
+ debounceSeconds?: number;
6
+ quietStatus?: boolean;
7
+ untilTerminal?: boolean;
8
+ }
9
+ export declare function runPollSummary(opts: PollSummaryCommandOptions): Promise<PollSummaryResult>;
10
+ export declare function runAggregatePoll(opts: AggregatePollCommandOptions): Promise<PollSummaryResult>;
@@ -0,0 +1,163 @@
1
+ import { loadConfig } from "../config/load.mjs";
2
+ import { ShepherdError } from "../exit-codes.mjs";
3
+ import { getRepoInfo } from "../github/client.mjs";
4
+ import { withApiTelemetryScope, summarizeApiTelemetry } from "../github/api-telemetry.mjs";
5
+ import { fetchPollSummary } from "../github/poll-summary.mjs";
6
+ import { sleep } from "../util/sleep.mjs";
7
+ import { graphqlQuotaPollIntervalMs, pollGraphQlRetryAfterMs } from "./poll-quota.mjs";
8
+ import { evaluateWorktreeGraphqlQuotaWarning } from "../state/graphql-quota-warnings.mjs";
9
+ const MAX_TIMER_MS = 2 ** 31 - 1;
10
+ const TIMER_DRIFT_TOLERANCE_MS = 500;
11
+ export function runPollSummary(opts) {
12
+ return withApiTelemetryScope(async () => attachUsage(await runPollSummaryCore(opts)));
13
+ }
14
+ export function runAggregatePoll(opts) {
15
+ return withApiTelemetryScope(() => runAggregatePollCore(opts));
16
+ }
17
+ async function runPollSummaryCore(opts) {
18
+ const repo = opts.targetRepository ?? (await getRepoInfo());
19
+ const fetched = await fetchPollSummary(opts, repo);
20
+ const allTerminal = fetched.prs.every((item) => item.action === "cancel");
21
+ const actionable = fetched.prs.some((item) => item.action !== "wait" && item.action !== "cancel");
22
+ return {
23
+ mode: "summary",
24
+ repo: `${repo.owner}/${repo.name}`,
25
+ selection: fetched.selection,
26
+ reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
27
+ prs: fetched.prs,
28
+ };
29
+ }
30
+ async function runAggregatePollCore(opts) {
31
+ const intervalMs = Math.min(opts.intervalSeconds * 1000, MAX_TIMER_MS);
32
+ const timeoutMs = Math.min(opts.timeoutSeconds * 1000, MAX_TIMER_MS);
33
+ const debounceMs = Math.min((opts.debounceSeconds ?? 60) * 1000, MAX_TIMER_MS);
34
+ const quotaBands = loadConfig().watch.graphqlQuotaWarnings;
35
+ const start = Date.now();
36
+ let tick = 0;
37
+ let debounceUntil = null;
38
+ let last;
39
+ let lastStatusSignature = null;
40
+ let rateLimitRetries = 0;
41
+ let pendingQuotaWarning;
42
+ while (true) {
43
+ tick += 1;
44
+ try {
45
+ last = await runPollSummaryCore(opts);
46
+ rateLimitRetries = 0;
47
+ }
48
+ catch (error) {
49
+ if (last?.selection.kind === "stack" && isMissingStack(error)) {
50
+ const explicit = await runPollSummaryCore({
51
+ ...opts,
52
+ stackPrNumber: undefined,
53
+ prNumbers: last.prs.map((item) => item.pr),
54
+ });
55
+ if (explicit.prs.every((item) => item.action === "cancel")) {
56
+ const warning = await aggregateQuotaWarning(explicit, quotaBands, opts.intervalSeconds);
57
+ if (warning)
58
+ pendingQuotaWarning = warning;
59
+ return attachUsage({
60
+ ...explicit,
61
+ reason: "all_terminal",
62
+ ...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
63
+ });
64
+ }
65
+ }
66
+ const retryMs = opts.untilTerminal ? pollGraphQlRetryAfterMs(error) : null;
67
+ if (retryMs === null || rateLimitRetries >= 1)
68
+ throw error;
69
+ rateLimitRetries += 1;
70
+ process.stderr.write(`[aggregate poll tick ${tick} / +${Math.round((Date.now() - start) / 1000)}s] GraphQL rate limit — retrying in ${Math.round(retryMs / 1000)}s\n`);
71
+ await sleep(retryMs);
72
+ continue;
73
+ }
74
+ const allTerminal = last.prs.every((item) => item.action === "cancel");
75
+ const immediate = last.prs.some((item) => ["escalate", "merge", "mark_ready"].includes(item.action));
76
+ const hasFix = last.prs.some((item) => item.action === "fix_code");
77
+ const warning = await aggregateQuotaWarning(last, quotaBands, opts.intervalSeconds);
78
+ if (warning)
79
+ pendingQuotaWarning = warning;
80
+ if (allTerminal) {
81
+ return attachUsage({
82
+ ...last,
83
+ reason: "all_terminal",
84
+ ...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
85
+ });
86
+ }
87
+ if (immediate) {
88
+ return attachUsage({
89
+ ...last,
90
+ reason: "actionable",
91
+ ...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
92
+ });
93
+ }
94
+ if (hasFix) {
95
+ if (debounceMs === 0)
96
+ return attachUsage({
97
+ ...last,
98
+ reason: "actionable",
99
+ ...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
100
+ });
101
+ debounceUntil ??= Date.now() + debounceMs;
102
+ if (Date.now() >= debounceUntil)
103
+ return attachUsage({
104
+ ...last,
105
+ reason: "actionable",
106
+ ...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
107
+ });
108
+ }
109
+ else {
110
+ debounceUntil = null;
111
+ if (opts.untilTerminal && pendingQuotaWarning) {
112
+ return attachUsage({ ...last, quotaWarning: pendingQuotaWarning });
113
+ }
114
+ }
115
+ const elapsedMs = Date.now() - start;
116
+ const sleepMs = debounceUntil
117
+ ? Math.min(intervalMs, Math.max(debounceUntil - Date.now(), 0))
118
+ : graphqlQuotaPollIntervalMs(quotaBands, summarizeApiTelemetry()?.graphql, intervalMs, MAX_TIMER_MS);
119
+ if (!opts.untilTerminal && debounceUntil === null) {
120
+ const remainingMs = timeoutMs - elapsedMs;
121
+ if (remainingMs <= 0 || remainingMs + TIMER_DRIFT_TOLERANCE_MS < sleepMs) {
122
+ return attachUsage({ ...last, reason: "timeout" });
123
+ }
124
+ }
125
+ const statusSignature = summaryStatusSignature(last);
126
+ if (!opts.quietStatus || hasFix || statusSignature !== lastStatusSignature) {
127
+ process.stderr.write(`[aggregate poll tick ${tick} / +${Math.round(elapsedMs / 1000)}s] ${last.prs
128
+ .map((item) => `#${item.pr} ${item.action.toUpperCase()}`)
129
+ .join(", ")}\n`);
130
+ }
131
+ lastStatusSignature = statusSignature;
132
+ await sleep(sleepMs);
133
+ }
134
+ }
135
+ async function aggregateQuotaWarning(result, bands, intervalSeconds) {
136
+ const usage = summarizeApiTelemetry()?.graphql;
137
+ const [owner, repo] = result.repo.split("/");
138
+ if (!usage || !owner || !repo)
139
+ return undefined;
140
+ return evaluateWorktreeGraphqlQuotaWarning({ owner, repo }, bands.map((band) => ({
141
+ ...band,
142
+ pollIntervalMinutes: Math.max(band.pollIntervalMinutes, intervalSeconds / 60),
143
+ })), usage, true);
144
+ }
145
+ function summaryStatusSignature(result) {
146
+ return JSON.stringify(result.prs.map((item) => ({
147
+ pr: item.pr,
148
+ action: item.action,
149
+ state: item.state,
150
+ mergeable: item.mergeable,
151
+ mergeStateStatus: item.mergeStateStatus,
152
+ reviewDecision: item.reviewDecision,
153
+ checks: item.checks,
154
+ review: item.review,
155
+ })));
156
+ }
157
+ function isMissingStack(error) {
158
+ return (error instanceof ShepherdError && error.message.includes("not part of a native GitHub stack"));
159
+ }
160
+ function attachUsage(result) {
161
+ const apiUsage = summarizeApiTelemetry();
162
+ return apiUsage ? { ...result, apiUsage } : result;
163
+ }
@@ -25,5 +25,7 @@ interface ReadyDelayState {
25
25
  * When `shouldCancel == true`, the formatter tells loop-capable agents to cancel
26
26
  * the loop and tells one-shot agents to stop.
27
27
  */
28
- export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string): Promise<ReadyDelayState>;
28
+ export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string, options?: {
29
+ retainElapsed?: boolean;
30
+ }): Promise<ReadyDelayState>;
29
31
  export {};
@@ -18,7 +18,7 @@ import { resolvePrStatePath } from "../state/base.mjs";
18
18
  * When `shouldCancel == true`, the formatter tells loop-capable agents to cancel
19
19
  * the loop and tells one-shot agents to stop.
20
20
  */
21
- export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, owner, repo) {
21
+ export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, owner, repo, options = {}) {
22
22
  const markerPath = readySincePath(prNumber, owner, repo);
23
23
  if (!isReady) {
24
24
  // Reset the timer.
@@ -47,7 +47,8 @@ export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, own
47
47
  const elapsed = now - readySince;
48
48
  const remaining = readyDelaySeconds - elapsed;
49
49
  if (remaining <= 0) {
50
- await safeUnlink(markerPath);
50
+ if (!options.retainElapsed)
51
+ await safeUnlink(markerPath);
51
52
  return { isReady: true, shouldCancel: true, remainingSeconds: 0 };
52
53
  }
53
54
  return { isReady: true, shouldCancel: false, remainingSeconds: remaining };
@@ -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>;