Skip to content

Commit b753365

Browse files
committed
support pullRequestReviewCommentCreated
1 parent 2022d05 commit b753365

2 files changed

Lines changed: 152 additions & 22 deletions

File tree

src/action.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import {
55
createPullRequest,
66
commitAndPush,
77
postComment,
8-
GitHubEventIssuesOpened,
9-
GitHubEventIssueCommentCreated,
10-
GitHubEventPullRequestCommentCreated,
118
generatePrompt,
129
} from './github.js';
1310
import { generateCommitMessage } from './claude.js';
@@ -43,7 +40,11 @@ async function handleResult(
4340
userPrompt,
4441
{
4542
issueNumber: (agentEvent.type === 'issuesOpened' || agentEvent.type === 'issueCommentCreated') ? agentEvent.github.issue.number : undefined,
46-
prNumber: agentEvent.type === 'pullRequestCommentCreated' ? agentEvent.github.issue.number : undefined,
43+
prNumber: agentEvent.type === 'pullRequestCommentCreated'
44+
? agentEvent.github.issue.number
45+
: agentEvent.type === 'pullRequestReviewCommentCreated'
46+
? agentEvent.github.pull_request.number
47+
: undefined,
4748
}
4849
);
4950

@@ -53,16 +54,16 @@ async function handleResult(
5354
workspace,
5455
octokit,
5556
repo,
56-
agentEvent.github as GitHubEventIssuesOpened | GitHubEventIssueCommentCreated,
57+
agentEvent.github,
5758
commitMessage,
5859
output
5960
);
60-
} else if (agentEvent.type === 'pullRequestCommentCreated') {
61+
} else if (agentEvent.type === 'pullRequestCommentCreated' || agentEvent.type === 'pullRequestReviewCommentCreated') {
6162
await commitAndPush(
6263
workspace,
6364
octokit,
6465
repo,
65-
agentEvent.github as GitHubEventPullRequestCommentCreated,
66+
agentEvent.github,
6667
commitMessage,
6768
output
6869
);

src/github.ts

Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ import * as github from '@actions/github';
33
import { execaSync } from 'execa';
44
import * as fs from 'fs';
55
import { genContentsString } from './contents.js';
6-
import { eventNames } from 'process';
76

87
// --- Type Definitions ---
98

109
export type AgentEvent =
1110
| { type: 'issuesOpened', github: GitHubEventIssuesOpened }
1211
| { type: 'issueCommentCreated', github: GitHubEventIssueCommentCreated }
1312
| { type: 'pullRequestCommentCreated', github: GitHubEventPullRequestCommentCreated }
13+
| { type: 'pullRequestReviewCommentCreated', github: GitHubEventPullRequestReviewCommentCreated }
1414
;
1515

1616
export type GitHubEvent =
1717
| GitHubEventIssuesOpened
1818
| GitHubEventIssueCommentCreated
19-
| GitHubEventPullRequestCommentCreated;
19+
| GitHubEventPullRequestCommentCreated
20+
| GitHubEventPullRequestReviewCommentCreated;
2021

2122
export type GitHubEventIssuesOpened = {
2223
action: 'opened';
@@ -35,6 +36,23 @@ export type GitHubEventPullRequestCommentCreated = {
3536
comment: GithubComment;
3637
}
3738

39+
export type GitHubEventPullRequestReviewCommentCreated = {
40+
action: 'created';
41+
pull_request: {
42+
number: number;
43+
title?: string;
44+
body?: string;
45+
};
46+
comment: {
47+
id: number;
48+
body: string;
49+
path: string;
50+
in_reply_to_id?: number;
51+
position?: number;
52+
line?: number;
53+
};
54+
}
55+
3856
export type GithubComment = {
3957
id: number;
4058
body: string;
@@ -84,9 +102,9 @@ export async function cloneRepository(
84102

85103
// Determine branch to clone
86104
let branchToClone: string;
87-
if (event.type === 'pullRequestCommentCreated') {
105+
if (event.type === 'pullRequestCommentCreated' || event.type === 'pullRequestReviewCommentCreated') {
88106
// For PR comments, clone the PR's head branch
89-
const prNumber = event.github.issue.number;
107+
const prNumber = event.type === 'pullRequestCommentCreated' ? event.github.issue.number : event.github.pull_request.number;
90108
try {
91109
const prData = await octokit.rest.pulls.get({ ...repo, pull_number: prNumber });
92110
branchToClone = prData.data.head.ref;
@@ -136,6 +154,10 @@ export function getEventType(payload: any): AgentEvent | null {
136154
if (payload.action === 'created' && payload.issue && payload.issue.pull_request && payload.comment) {
137155
return { type: 'pullRequestCommentCreated', github: payload };
138156
}
157+
// Check for Pull Request Review Comment (comment on a specific line of code)
158+
if (payload.action === 'created' && payload.pull_request && payload.comment && payload.comment.path) {
159+
return { type: 'pullRequestReviewCommentCreated', github: payload };
160+
}
139161
return null;
140162
}
141163

@@ -157,14 +179,22 @@ export async function addEyeReaction(
157179
content: 'eyes'
158180
});
159181
core.info(`Added eye reaction to issue #${event.issue.number}`);
160-
} else if (event.action === 'created' && 'comment' in event) {
161-
// Add eye reaction to comment
182+
} else if (event.action === 'created' && 'comment' in event && 'issue' in event) {
183+
// Add eye reaction to comment on issue or PR conversation
162184
await octokit.rest.reactions.createForIssueComment({
163185
...repo,
164186
comment_id: event.comment.id,
165187
content: 'eyes'
166188
});
167189
core.info(`Added eye reaction to comment on issue/PR #${event.issue.number}`);
190+
} else if (event.action === 'created' && 'comment' in event && 'pull_request' in event) {
191+
// Add eye reaction to PR review comment
192+
await octokit.rest.reactions.createForPullRequestReviewComment({
193+
...repo,
194+
comment_id: event.comment.id,
195+
content: 'eyes'
196+
});
197+
core.info(`Added eye reaction to review comment on PR #${event.pull_request.number}`);
168198
}
169199
} catch (error) {
170200
core.warning(`Failed to add reaction: ${error instanceof Error ? error.message : error}`);
@@ -178,7 +208,7 @@ export function extractText(event: GitHubEvent): string | null {
178208
if (event.action === 'opened' && 'issue' in event) {
179209
return event.issue.body;
180210
}
181-
// Ensure 'comment' exists before accessing 'body'
211+
// Ensure 'comment' exists before accessing 'body' for issue/PR comments
182212
if (event.action === 'created' && 'comment' in event && event.comment) {
183213
return event.comment.body;
184214
}
@@ -258,11 +288,12 @@ export async function commitAndPush(
258288
workspace: string,
259289
octokit: Octokit,
260290
repo: RepoContext,
261-
event: GitHubEventPullRequestCommentCreated,
291+
event: GitHubEventPullRequestCommentCreated | GitHubEventPullRequestReviewCommentCreated,
262292
commitMessage: string,
263293
output: string
264294
): Promise<void> {
265-
const prNumber = event.issue.number; // In PR comments, issue.number is the PR number
295+
// Get PR number from the event - different location based on event type
296+
const prNumber = 'issue' in event ? event.issue.number : event.pull_request.number;
266297

267298
try {
268299
// Get current branch name from the PR context
@@ -333,18 +364,46 @@ export async function postComment(
333364
event: GitHubEvent,
334365
body: string
335366
): Promise<void> {
336-
const issueNumber = event.issue.number;
337-
338367
try {
368+
if ('issue' in event) {
369+
// For regular issues and PR conversation comments
370+
const issueNumber = event.issue.number;
339371
await octokit.rest.issues.createComment({
340372
...repo,
341373
issue_number: issueNumber,
342374
body: body,
343375
});
344376
core.info(`Comment posted to Issue/PR #${issueNumber}`);
377+
} else if ('pull_request' in event) {
378+
// For PR review comments
379+
const prNumber = event.pull_request.number;
380+
const commentId = event.comment.id;
381+
const inReplyTo = event.comment.in_reply_to_id;
382+
383+
try {
384+
await octokit.rest.pulls.createReplyForReviewComment({
385+
...repo,
386+
pull_number: prNumber,
387+
comment_id: inReplyTo ?? commentId, // Use the original comment ID if no reply
388+
body: body,
389+
});
390+
core.info(`Comment posted to PR #${prNumber} Reply to comment #${commentId}`);
391+
392+
} catch (commentError) {
393+
// If we can't determine if it's a top-level comment, fall back to creating a regular PR comment
394+
core.warning(`Failed to check if comment is top-level: ${commentError instanceof Error ? commentError.message : commentError}`);
395+
core.info(`Falling back to creating a regular PR comment instead of a reply`);
396+
await octokit.rest.issues.createComment({
397+
...repo,
398+
issue_number: prNumber,
399+
body: body,
400+
});
401+
core.info(`Regular comment posted to PR #${prNumber}`);
402+
}
403+
}
345404
} catch (error) {
346-
core.error(`Failed to post comment to Issue/PR #${issueNumber}: ${error}`);
347-
// Don't re-throw here, as posting a comment failure might not be critical
405+
core.error(`Failed to post comment: ${error instanceof Error ? error.message : error}`);
406+
// Don't re-throw here, as posting a comment failure might not be critical
348407
}
349408
}
350409

@@ -361,12 +420,22 @@ export async function generatePrompt(
361420
const contents = await getContentsData(octokit, repo, event);
362421

363422
let prFiles: string[] = [];
423+
let contextInfo: string = '';
364424

365-
if (event.type === 'pullRequestCommentCreated') {
425+
if (event.type === 'pullRequestCommentCreated' || event.type === 'pullRequestReviewCommentCreated') {
366426
// Get the changed files in the PR
367427
prFiles = await getChangedFiles(octokit, repo, event);
368428
}
369429

430+
// For PR review comments, add information about the file path and line
431+
if (event.type === 'pullRequestReviewCommentCreated') {
432+
const comment = event.github.comment;
433+
contextInfo = `Comment on file: ${comment.path}`;
434+
if (comment.line) {
435+
contextInfo += `, line: ${comment.line}`;
436+
}
437+
}
438+
370439
let historyPropmt = genContentsString(contents.content, userPrompt);
371440
for (const comment of contents.comments) {
372441
historyPropmt += genContentsString(comment, userPrompt);
@@ -376,6 +445,9 @@ export async function generatePrompt(
376445
if (historyPropmt) {
377446
prompt += `[History]\n${historyPropmt}\n\n`;
378447
}
448+
if (contextInfo) {
449+
prompt += `[Context]\n${contextInfo}\n\n`;
450+
}
379451
if (prFiles.length > 0) {
380452
prompt += `[Changed Files]\n${prFiles.join('\n')}\n\n`;
381453
}
@@ -394,9 +466,19 @@ export async function getChangedFiles(
394466
repo: RepoContext,
395467
event: AgentEvent
396468
): Promise<string[]> {
469+
let prNumber: number;
470+
471+
if (event.type === 'pullRequestCommentCreated') {
472+
prNumber = event.github.issue.number;
473+
} else if (event.type === 'pullRequestReviewCommentCreated') {
474+
prNumber = event.github.pull_request.number;
475+
} else {
476+
throw new Error(`Cannot get changed files for event type: ${event.type}`);
477+
}
478+
397479
const prFilesResponse = await octokit.rest.pulls.listFiles({
398480
...repo,
399-
pull_number: event.github.issue.number,
481+
pull_number: prNumber,
400482
});
401483
return prFilesResponse.data.map(file => file.filename);
402484
}
@@ -411,6 +493,8 @@ export async function getContentsData(
411493
return await getIssueData(octokit, repo, event.github.issue.number);
412494
} else if (event.type === 'pullRequestCommentCreated') {
413495
return await getPullRequestData(octokit, repo, event.github.issue.number);
496+
} else if (event.type === 'pullRequestReviewCommentCreated') {
497+
return await getPullRequestReviewCommentsData(octokit, repo, event.github.pull_request.number, event.github.comment.in_reply_to_id ?? event.github.comment.id);
414498
}
415499
throw new Error('Invalid event type for data retrieval');
416500
}
@@ -457,6 +541,51 @@ async function getIssueData(
457541
}
458542
}
459543

544+
/**
545+
* Retrieves the body and all review comment bodies for a specific pull request.
546+
* Note: PR review comments are fetched via the pulls API endpoint.
547+
*/
548+
async function getPullRequestReviewCommentsData(
549+
octokit: Octokit,
550+
repo: RepoContext,
551+
pullNumber: number,
552+
targetCommentId: number
553+
): Promise<GithubContentsData> {
554+
core.info(`Fetching data for pull request review comments #${pullNumber}...`);
555+
try {
556+
// Get PR body
557+
const prResponse = await octokit.rest.pulls.get({
558+
...repo,
559+
pull_number: pullNumber,
560+
});
561+
const content = {
562+
number: prResponse.data.number,
563+
title: prResponse.data.title,
564+
body: prResponse.data.body ?? '',
565+
login: prResponse.data.user?.login ?? 'anonymous'
566+
};
567+
568+
// Get PR review comments
569+
const commentsData = await octokit.paginate(octokit.rest.pulls.listReviewComments, {
570+
...repo,
571+
pull_number: pullNumber,
572+
per_page: 100, // Fetch 100 per page for efficiency
573+
});
574+
575+
// Filter comments to include only those related to the target comment ID
576+
const comments = commentsData.filter(comment => comment.id === targetCommentId || comment.in_reply_to_id === targetCommentId).map(comment => ({
577+
body: comment.body ?? '',
578+
login: comment.user?.login ?? 'anonymous'
579+
}));
580+
core.info(`Fetched ${commentsData.length} review comments for PR #${pullNumber}.`);
581+
582+
return { content, comments };
583+
} catch (error) {
584+
core.error(`Failed to get data for pull request review comments #${pullNumber}: ${error}`);
585+
throw new Error(`Could not retrieve data for pull request review comments #${pullNumber}: ${error instanceof Error ? error.message : error}`);
586+
}
587+
}
588+
460589
/**
461590
* Retrieves the body and all comment bodies for a specific pull request.
462591
* Note: PR comments are fetched via the issues API endpoint.

0 commit comments

Comments
 (0)