pr-shepherd 0.48.0 → 0.50.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 -2
- 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 +9 -9
- package/bin/cli/help-command-pages.mjs +8 -8
- 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 +10 -10
- package/bin/cli/iterate-instructions.mjs +9 -2
- package/bin/cli/iterate-lean.mjs +11 -0
- 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/cli-parser.mjs +3 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -8
- package/bin/commands/iterate/check-instructions.mjs +2 -11
- package/bin/commands/iterate/escalate.mjs +30 -2
- package/bin/commands/iterate/fix-code.mjs +62 -20
- package/bin/commands/iterate/render.mjs +1 -1
- package/bin/commands/iterate/stall.mjs +30 -0
- package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
- 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/commands/resolve-mutate.mjs +22 -68
- package/bin/comments/resolve.d.mts +5 -0
- package/bin/comments/resolve.mjs +2 -11
- 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/state/fix-attempts.d.mts +3 -4
- package/bin/state/fix-attempts.mjs +2 -3
- package/bin/types/escalate.d.mts +10 -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 +5 -10
|
@@ -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 {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type SeenMarker } from "../state/seen-comments.mts";
|
|
2
|
+
import type { PollSummaryReview } from "../types.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
+
export declare function summarizePollSummaryReview(raw: RawSummaryPr, seen: Map<string, SeenMarker>, viewerCanAdminister: boolean): Promise<PollSummaryReview>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
2
|
+
import { applyRules } from "../classify/apply.mjs";
|
|
3
|
+
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
4
|
+
import { loadConfig } from "../config/load.mjs";
|
|
5
|
+
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
6
|
+
import { classifyItem } from "../state/seen-comments.mjs";
|
|
7
|
+
const THREAD_COMMENT_SEPARATOR = "\n\n--- thread comment ---\n\n";
|
|
8
|
+
export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister) {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const bots = normalizeBotUsernames(config.botUsernames);
|
|
11
|
+
const rules = await loadRules(discoverRuleFiles(getEffectiveCwd()));
|
|
12
|
+
let actionable = 0;
|
|
13
|
+
let incomplete = raw.comments.pageInfo.hasPreviousPage ||
|
|
14
|
+
raw.reviews.pageInfo.hasPreviousPage ||
|
|
15
|
+
raw.reviewThreads.pageInfo.hasPreviousPage;
|
|
16
|
+
for (const comment of raw.comments.nodes) {
|
|
17
|
+
if (!comment.isMinimized &&
|
|
18
|
+
!isSuppressed(rules, "pr-comment", comment) &&
|
|
19
|
+
classifyItem(comment.id, comment.body, seen) !== "unchanged")
|
|
20
|
+
actionable += 1;
|
|
21
|
+
}
|
|
22
|
+
const latestStateByAuthor = new Map((raw.latestReviews?.nodes ?? []).flatMap((review) => review.author ? [[review.author.login.toLowerCase(), review.state]] : []));
|
|
23
|
+
for (const review of raw.reviews.nodes) {
|
|
24
|
+
if (review.state === "COMMENTED") {
|
|
25
|
+
if (review.body.trim() !== "" &&
|
|
26
|
+
!review.isMinimized &&
|
|
27
|
+
!isSuppressed(rules, "review-summary", review) &&
|
|
28
|
+
classifyItem(review.id, review.body, seen) !== "unchanged")
|
|
29
|
+
actionable += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (review.state !== "CHANGES_REQUESTED")
|
|
33
|
+
continue;
|
|
34
|
+
const login = review.author?.login.toLowerCase();
|
|
35
|
+
if (login && latestStateByAuthor.get(login) === "APPROVED")
|
|
36
|
+
continue;
|
|
37
|
+
if (isSuppressed(rules, "changes-requested", review))
|
|
38
|
+
continue;
|
|
39
|
+
const isBot = review.author?.__typename === "Bot" || bots.has(review.author?.login.toLowerCase() ?? "");
|
|
40
|
+
if ((isBot && viewerCanAdminister) ||
|
|
41
|
+
classifyItem(review.id, review.body, seen) !== "unchanged")
|
|
42
|
+
actionable += 1;
|
|
43
|
+
}
|
|
44
|
+
for (const thread of raw.reviewThreads.nodes) {
|
|
45
|
+
if (thread.comments.pageInfo.hasPreviousPage)
|
|
46
|
+
incomplete = true;
|
|
47
|
+
const root = thread.rootComments?.nodes[0] ?? thread.comments.nodes[0];
|
|
48
|
+
const transcript = thread.comments.nodes
|
|
49
|
+
.map((comment) => comment.body)
|
|
50
|
+
.join(THREAD_COMMENT_SEPARATOR);
|
|
51
|
+
if (isSuppressed(rules, "review-thread", root, thread.id, thread.path, transcript))
|
|
52
|
+
continue;
|
|
53
|
+
const repeatable = root?.viewerDidAuthor === true ||
|
|
54
|
+
root?.author?.__typename === "Bot" ||
|
|
55
|
+
bots.has(root?.author?.login.toLowerCase() ?? "");
|
|
56
|
+
const unseen = classifyItem(thread.id, transcript, seen) !== "unchanged";
|
|
57
|
+
if ((!thread.isResolved && (repeatable || unseen)) ||
|
|
58
|
+
((thread.isResolved || thread.isOutdated) && unseen))
|
|
59
|
+
actionable += 1;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...(raw.comments.totalCount > 0 && { comments: raw.comments.totalCount }),
|
|
63
|
+
...(raw.reviews.totalCount > 0 && { reviews: raw.reviews.totalCount }),
|
|
64
|
+
...(raw.reviewThreads.totalCount > 0 && { threads: raw.reviewThreads.totalCount }),
|
|
65
|
+
...(actionable > 0 && { actionable }),
|
|
66
|
+
...(incomplete && { incomplete: true }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function isSuppressed(rules, kind, raw, id = raw?.id ?? "unknown", path, body = raw?.body ?? "") {
|
|
70
|
+
const item = {
|
|
71
|
+
kind,
|
|
72
|
+
id,
|
|
73
|
+
author: raw?.author?.login ?? "unknown",
|
|
74
|
+
authorType: raw?.author?.__typename === "Bot"
|
|
75
|
+
? "Bot"
|
|
76
|
+
: raw?.author?.__typename === "User"
|
|
77
|
+
? "User"
|
|
78
|
+
: "Unknown",
|
|
79
|
+
...(raw?.authorAssociation && { authorAssociation: raw.authorAssociation }),
|
|
80
|
+
body,
|
|
81
|
+
...(raw?.url && { url: raw.url }),
|
|
82
|
+
...(kind === "review-thread" && { path }),
|
|
83
|
+
};
|
|
84
|
+
return applyRules(rules, item).suppress === true;
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PollSummaryChecks, PollSummaryCommandOptions, PollSummaryItem, PollSummaryReview } from "../types.mts";
|
|
2
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
3
|
+
export declare function routePollSummary(raw: RawSummaryPr, checks: PollSummaryChecks, review: PollSummaryReview, opts: PollSummaryCommandOptions): Pick<PollSummaryItem, "action" | "reasons">;
|
|
4
|
+
export declare function normalizePollSummaryState(state: string): PollSummaryItem["state"];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
export function routePollSummary(raw, checks, review, opts) {
|
|
3
|
+
const actions = loadConfig().actions;
|
|
4
|
+
const state = normalizePollSummaryState(raw.state);
|
|
5
|
+
if (state === "MERGED" || state === "CLOSED") {
|
|
6
|
+
return { action: "cancel", reasons: [state.toLowerCase()] };
|
|
7
|
+
}
|
|
8
|
+
if (raw.mergeable === "CONFLICTING" || raw.mergeStateStatus === "DIRTY") {
|
|
9
|
+
return { action: "fix_code", reasons: ["merge-conflicts"] };
|
|
10
|
+
}
|
|
11
|
+
if ((checks.failing ?? 0) > 0)
|
|
12
|
+
return { action: "fix_code", reasons: ["failing-checks"] };
|
|
13
|
+
if ((review.actionable ?? 0) > 0) {
|
|
14
|
+
if (opts.merge && raw.isInMergeQueue && actions.workWhileQueued !== true) {
|
|
15
|
+
return { action: "wait", reasons: ["review-work-deferred-while-queued"] };
|
|
16
|
+
}
|
|
17
|
+
return { action: "fix_code", reasons: ["review-work"] };
|
|
18
|
+
}
|
|
19
|
+
if ((checks.inProgress ?? 0) > 0 ||
|
|
20
|
+
raw.mergeable === "UNKNOWN" ||
|
|
21
|
+
raw.mergeStateStatus === "UNKNOWN" ||
|
|
22
|
+
raw.mergeStateStatus === "BEHIND" ||
|
|
23
|
+
raw.mergeStateStatus === "BLOCKED" ||
|
|
24
|
+
raw.mergeStateStatus === "HAS_HOOKS") {
|
|
25
|
+
return { action: "wait", reasons: ["pending-or-unknown"] };
|
|
26
|
+
}
|
|
27
|
+
if (raw.isDraft) {
|
|
28
|
+
if (opts.noAutoMarkReady || actions.autoMarkReady === false) {
|
|
29
|
+
return { action: "wait", reasons: ["draft-auto-mark-ready-disabled"] };
|
|
30
|
+
}
|
|
31
|
+
return raw.viewerCanUpdate
|
|
32
|
+
? { action: "mark_ready", reasons: ["draft-appears-ready"] }
|
|
33
|
+
: { action: "escalate", reasons: ["mark-ready-authorization-required"] };
|
|
34
|
+
}
|
|
35
|
+
if (opts.merge && raw.isInMergeQueue) {
|
|
36
|
+
return { action: "wait", reasons: ["already-in-merge-queue"] };
|
|
37
|
+
}
|
|
38
|
+
if (opts.merge && raw.stack) {
|
|
39
|
+
return { action: "fix_code", reasons: ["authoritative-poll-required"] };
|
|
40
|
+
}
|
|
41
|
+
if (opts.merge && !raw.stack)
|
|
42
|
+
return { action: "merge", reasons: ["appears-ready"] };
|
|
43
|
+
return { action: "cancel", reasons: ["appears-ready"] };
|
|
44
|
+
}
|
|
45
|
+
export function normalizePollSummaryState(state) {
|
|
46
|
+
return state === "OPEN" || state === "CLOSED" || state === "MERGED" ? state : "UNKNOWN";
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryItem, PollSummarySelection } from "../types.mts";
|
|
2
|
+
import { type RepoInfo } from "./client.mts";
|
|
3
|
+
export interface FetchedPollSummary {
|
|
4
|
+
selection: PollSummarySelection;
|
|
5
|
+
prs: PollSummaryItem[];
|
|
6
|
+
}
|
|
7
|
+
export declare function fetchPollSummary(opts: PollSummaryCommandOptions, repo: RepoInfo): Promise<FetchedPollSummary>;
|