-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathprogressComment.ts
More file actions
261 lines (250 loc) · 8.03 KB
/
Copy pathprogressComment.ts
File metadata and controls
261 lines (250 loc) · 8.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
* Single source of truth for reading, updating, deleting, and creating "progress comments" —
* the GitHub comments Pullfrog uses to surface a run's status.
*
* A progress comment can be one of two distinct GitHub entities with non-overlapping IDs and
* distinct REST endpoints:
* - "issue": a top-level issue/PR timeline comment (octokit.rest.issues.*Comment)
* - "review": an inline PR review-thread comment (octokit.rest.pulls.*ReviewComment)
*
* Callers carry a `ProgressComment` (id + type) value end-to-end so the right endpoint is always
* picked. Adding a third comment type later means one new branch in this file, not six.
*/
export type ProgressCommentType = "issue" | "review";
export type ProgressComment = {
id: number;
type: ProgressCommentType;
};
/**
* Parse the on-the-wire `{ id: string; type }` shape (the form carried in `JsonPayload`)
* into the in-memory `ProgressComment` shape. Returns undefined when the id isn't a
* positive integer so callers can short-circuit cleanly. Callers handle logging.
*/
export function parseProgressComment(
raw: { id: string; type: ProgressCommentType } | null | undefined
): ProgressComment | undefined {
if (!raw?.id) return undefined;
const id = parseInt(raw.id, 10);
if (Number.isNaN(id) || id <= 0) return undefined;
return { id, type: raw.type };
}
// minimal Octokit shape needed by the progress-comment helpers. structural so the helper
// can be called from both the action package (@octokit/rest v22) and the root project
// (@octokit/rest v21) without a nominal type clash. only the methods used here are listed.
interface CommentResponse {
data: { id: number; body?: string | null | undefined; html_url: string; node_id?: string };
}
export interface ProgressCommentOctokit {
rest: {
issues: {
createComment: (params: {
owner: string;
repo: string;
issue_number: number;
body: string;
}) => Promise<CommentResponse>;
getComment: (params: {
owner: string;
repo: string;
comment_id: number;
}) => Promise<CommentResponse>;
updateComment: (params: {
owner: string;
repo: string;
comment_id: number;
body: string;
}) => Promise<CommentResponse>;
deleteComment: (params: {
owner: string;
repo: string;
comment_id: number;
}) => Promise<unknown>;
};
pulls: {
createReplyForReviewComment: (params: {
owner: string;
repo: string;
pull_number: number;
comment_id: number;
body: string;
}) => Promise<CommentResponse>;
getReviewComment: (params: {
owner: string;
repo: string;
comment_id: number;
}) => Promise<CommentResponse>;
updateReviewComment: (params: {
owner: string;
repo: string;
comment_id: number;
body: string;
}) => Promise<CommentResponse>;
deleteReviewComment: (params: {
owner: string;
repo: string;
comment_id: number;
}) => Promise<unknown>;
};
};
}
interface ApiCtx {
octokit: ProgressCommentOctokit;
owner: string;
repo: string;
}
/**
* Fetch a progress comment via the appropriate REST endpoint for its type.
* Returns the common subset of fields callers actually use.
*/
export async function getProgressComment(
ctx: ApiCtx,
comment: ProgressComment
): Promise<{ id: number; body: string | undefined; html_url: string }> {
const result = await (comment.type === "review"
? ctx.octokit.rest.pulls.getReviewComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
})
: ctx.octokit.rest.issues.getComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
}));
return {
id: result.data.id,
body: result.data.body ?? undefined,
html_url: result.data.html_url,
};
}
/**
* Update a progress comment in place via the appropriate REST endpoint.
* Returns the common subset of fields callers actually use.
*/
export async function updateProgressComment(
ctx: ApiCtx,
comment: ProgressComment,
body: string
): Promise<{
id: number;
body: string | undefined;
html_url: string;
node_id: string | undefined;
}> {
const result = await (comment.type === "review"
? ctx.octokit.rest.pulls.updateReviewComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
body,
})
: ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
body,
}));
return {
id: result.data.id,
body: result.data.body ?? undefined,
html_url: result.data.html_url,
node_id: result.data.node_id,
};
}
/**
* Delete a progress comment via the appropriate REST endpoint.
* Lower-level than `deleteProgressComment` in mcp/comment.ts — that one also clears
* tool state. Callers that don't have a ToolContext (post cleanup, error handlers)
* should use this directly; the higher-level wrapper delegates here.
*/
export async function deleteProgressCommentApi(
ctx: ApiCtx,
comment: ProgressComment
): Promise<void> {
if (comment.type === "review") {
await ctx.octokit.rest.pulls.deleteReviewComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
});
return;
}
await ctx.octokit.rest.issues.deleteComment({
owner: ctx.owner,
repo: ctx.repo,
comment_id: comment.id,
});
}
/**
* Discriminated target for `createLeapingProgressComment`. The two variants map to the two
* distinct GitHub create endpoints; review-reply additionally needs the parent comment ID.
*/
export type CreateProgressCommentTarget =
| { kind: "issue"; issueNumber: number }
| { kind: "reviewReply"; pullNumber: number; replyToCommentId: number };
export interface CreatedProgressComment {
comment: ProgressComment;
body: string | undefined;
html_url: string;
}
/**
* Create the initial "Leaping into action..." progress comment.
*
* Reliability: when `kind: "reviewReply"` fails (e.g. the parent comment was deleted or the
* thread is otherwise unreachable), falls back to a top-level issue comment on the same PR
* rather than leaving the run with no progress surface. The fallback is logged.
*
* (PR # === issue # in GitHub's number space, so `pullNumber` doubles as the fallback target.)
*/
export async function createLeapingProgressComment(
ctx: ApiCtx,
target: CreateProgressCommentTarget,
body: string
): Promise<CreatedProgressComment> {
if (target.kind === "reviewReply") {
try {
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
owner: ctx.owner,
repo: ctx.repo,
pull_number: target.pullNumber,
comment_id: target.replyToCommentId,
body,
});
return {
comment: { id: result.data.id, type: "review" },
body: result.data.body ?? undefined,
html_url: result.data.html_url,
};
} catch (error) {
// console.warn (not the action-flavored log.warning) because this helper runs in
// both the action runtime and the Next.js webhook context, and we don't want a
// ::warning:: GitHub Actions annotation leaking into Vercel logs.
console.warn(
`[progressComment] review reply failed (parent ${target.replyToCommentId} on PR #${target.pullNumber}), falling back to issue comment:`,
error
);
const fallback = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.repo,
issue_number: target.pullNumber,
body,
});
return {
comment: { id: fallback.data.id, type: "issue" },
body: fallback.data.body ?? undefined,
html_url: fallback.data.html_url,
};
}
}
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.repo,
issue_number: target.issueNumber,
body,
});
return {
comment: { id: result.data.id, type: "issue" },
body: result.data.body ?? undefined,
html_url: result.data.html_url,
};
}