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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -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/default-poll.mjs +1 -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 +10 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +5 -3
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/poll-handler.mjs +25 -4
- 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/poll-summary.d.mts +10 -0
- package/bin/commands/poll-summary.mjs +163 -0
- package/bin/commands/ready-delay.d.mts +3 -1
- package/bin/commands/ready-delay.mjs +3 -2
- 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/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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,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>;
|