#17 fix: handle pull_request_review_comment payload differences in Gitea (#19)

This commit is contained in:
leoarry
2026-06-08 09:13:54 +01:00
committed by GitHub
parent dfdfdd91fe
commit b744372179
6 changed files with 83 additions and 54 deletions

View File

@@ -120,7 +120,9 @@ export function buildDisallowedToolsString(
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list // If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
const allowedList = normalizeToolList(allowedTools); const allowedList = normalizeToolList(allowedTools);
if (allowedList.length > 0) { if (allowedList.length > 0) {
disallowedTools = disallowedTools.filter((tool) => !allowedList.includes(tool)); disallowedTools = disallowedTools.filter(
(tool) => !allowedList.includes(tool),
);
} }
let allDisallowedTools = disallowedTools.join(","); let allDisallowedTools = disallowedTools.join(",");
@@ -164,16 +166,20 @@ export function prepareContext(
let commentBody: string | undefined; let commentBody: string | undefined;
if (isIssueCommentEvent(context)) { if (isIssueCommentEvent(context)) {
commentId = context.payload.comment.id.toString(); commentId = context.payload.comment?.id?.toString();
commentBody = context.payload.comment.body; commentBody = context.payload.comment?.body;
triggerUsername = context.payload.comment.user.login; triggerUsername = context.payload.comment?.user?.login;
} else if (isPullRequestReviewEvent(context)) { } else if (isPullRequestReviewEvent(context)) {
commentBody = context.payload.review.body ?? ""; commentBody =
triggerUsername = context.payload.review.user.login; context.payload.review?.body ?? context.payload.review?.content ?? "";
triggerUsername =
context.payload.review?.user?.login ?? context.payload.sender?.login;
} else if (isPullRequestReviewCommentEvent(context)) { } else if (isPullRequestReviewCommentEvent(context)) {
commentId = context.payload.comment.id.toString(); commentId = context.payload.comment?.id?.toString();
commentBody = context.payload.comment.body; commentBody =
triggerUsername = context.payload.comment.user.login; context.payload.comment?.body ?? context.payload.review?.content;
triggerUsername =
context.payload.comment?.user?.login ?? context.payload.sender?.login;
} else if (isIssuesEvent(context)) { } else if (isIssuesEvent(context)) {
triggerUsername = context.payload.issue.user.login; triggerUsername = context.payload.issue.user.login;
} }
@@ -595,18 +601,7 @@ ${sanitizeContent(context.directPrompt)}
</direct_prompt>` </direct_prompt>`
: "" : ""
} }
${ ${`<comment_tool_info>
eventData.eventName === "pull_request_review_comment"
? `<comment_tool_info>
IMPORTANT: For this inline PR review comment, you have been provided with ONLY the mcp__gitea__update_pull_request_comment tool to update this specific review comment.
Tool usage example for mcp__gitea__update_pull_request_comment:
{
"body": "Your comment text here"
}
All four parameters (owner, repo, commentId, body) are required.
</comment_tool_info>`
: `<comment_tool_info>
IMPORTANT: For this event type, you have been provided with ONLY the mcp__gitea__update_issue_comment tool to update comments. IMPORTANT: For this event type, you have been provided with ONLY the mcp__gitea__update_issue_comment tool to update comments.
Tool usage example for mcp__gitea__update_issue_comment: Tool usage example for mcp__gitea__update_issue_comment:
@@ -617,8 +612,7 @@ Tool usage example for mcp__gitea__update_issue_comment:
"body": "Your comment text here" "body": "Your comment text here"
} }
All four parameters (owner, repo, commentId, body) are required. All four parameters (owner, repo, commentId, body) are required.
</comment_tool_info>` </comment_tool_info>`}
}
Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed. Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed.
@@ -632,7 +626,7 @@ Follow these steps:
1. Create a Todo List: 1. Create a Todo List:
- Use your Gitea comment to maintain a detailed task list based on the request. - Use your Gitea comment to maintain a detailed task list based on the request.
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete). - Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
- Update the comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__gitea__update_pull_request_comment" : "mcp__gitea__update_issue_comment"} with each task completion. - Update the comment using mcp__gitea__update_issue_comment with each task completion.
2. Gather Context: 2. Gather Context:
- Analyze the pre-fetched data provided above. - Analyze the pre-fetched data provided above.
@@ -738,8 +732,8 @@ ${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. Final Up
Important Notes: Important Notes:
- All communication must happen through Gitea PR comments. - All communication must happen through Gitea PR comments.
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__gitea__update_pull_request_comment" : "mcp__gitea__update_issue_comment"} with comment_id: ${context.claudeCommentId}. - Never create new comments. Only update the existing comment using mcp__gitea__update_issue_comment with comment_id: ${context.claudeCommentId}.
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__gitea__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""} - This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? `\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__gitea__update_issue_comment. Do NOT just respond with a normal response, the user will not see it.` : ""}
- You communicate exclusively by editing your single comment - not through any other means. - You communicate exclusively by editing your single comment - not through any other means.
- Use this spinner HTML when work is in progress: <img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" /> - Use this spinner HTML when work is in progress: <img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : eventData.claudeBranch ? `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch}). Do not create additional branches.` : `- IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). First check for existing claude branches for this ${eventData.isPR ? "PR" : "issue"} and use them if found, otherwise create a new branch using mcp__local_git_ops__create_branch.`} ${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : eventData.claudeBranch ? `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch}). Do not create additional branches.` : `- IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). First check for existing claude branches for this ${eventData.isPR ? "PR" : "issue"} and use them if found, otherwise create a new branch using mcp__local_git_ops__create_branch.`}
@@ -750,8 +744,8 @@ ${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing bra
- mcp__local_git_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"} - mcp__local_git_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"}
- Display the todo list as a checklist in the Gitea comment and mark things off as you go. - Display the todo list as a checklist in the Gitea comment and mark things off as you go.
- All communication must happen through Gitea PR comments. - All communication must happen through Gitea PR comments.
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__gitea__update_pull_request_comment" : "mcp__gitea__update_issue_comment"}. - Never create new comments. Only update the existing comment using mcp__gitea__update_issue_comment.
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__gitea__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""} - This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? `\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__gitea__update_issue_comment. Do NOT just respond with a normal response, the user will not see it.` : ""}
- You communicate exclusively by editing your single comment - not through any other means. - You communicate exclusively by editing your single comment - not through any other means.
- Use this spinner HTML when work is in progress: <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" /> - Use this spinner HTML when work is in progress: <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`} ${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`}

View File

@@ -41,15 +41,22 @@ async function run() {
// GitHub has separate ID namespaces for review comments and issue comments // GitHub has separate ID namespaces for review comments and issue comments
// We need to use the correct API based on the event type // We need to use the correct API based on the event type
if (isPullRequestReviewCommentEvent(context)) { if (isPullRequestReviewCommentEvent(context)) {
// For PR review comments, use the pulls API // Try the PR review comment endpoint first; Gitea may have created an
console.log(`Fetching PR review comment ${commentId}`); // issue comment instead (no comment.id in payload), so fall through on 404.
const response = await client.api.customRequest( try {
"GET", console.log(`Fetching PR review comment ${commentId}`);
`/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, const response = await client.api.customRequest(
); "GET",
comment = response.data; `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`,
isPRReviewComment = true; );
console.log("Successfully fetched as PR review comment"); comment = response.data;
isPRReviewComment = true;
console.log("Successfully fetched as PR review comment");
} catch {
console.log(
"PR review comment not found, falling back to issue comment",
);
}
} }
// For all other event types, use the issues API // For all other event types, use the issues API

View File

@@ -10,6 +10,20 @@ import type {
import type { ModeName } from "../modes/types"; import type { ModeName } from "../modes/types";
import { DEFAULT_MODE, isValidMode } from "../modes/registry"; import { DEFAULT_MODE, isValidMode } from "../modes/registry";
// Gitea review payloads use `review.content` instead of `review.body`, and
// `sender` instead of nested user objects. These types extend the GitHub base
// types to make both fields available without `as any` casts.
export type GiteaPullRequestReviewEvent = PullRequestReviewEvent & {
review?: { content?: string };
sender?: { login: string };
};
export type GiteaPullRequestReviewCommentEvent =
PullRequestReviewCommentEvent & {
review?: { type: string; content: string };
sender?: { login: string };
};
export type ParsedGitHubContext = { export type ParsedGitHubContext = {
runId: string; runId: string;
eventName: string; eventName: string;
@@ -24,8 +38,8 @@ export type ParsedGitHubContext = {
| IssuesEvent | IssuesEvent
| IssueCommentEvent | IssueCommentEvent
| PullRequestEvent | PullRequestEvent
| PullRequestReviewEvent | GiteaPullRequestReviewEvent
| PullRequestReviewCommentEvent; | GiteaPullRequestReviewCommentEvent;
entityNumber: number; entityNumber: number;
isPR: boolean; isPR: boolean;
inputs: { inputs: {
@@ -181,13 +195,15 @@ export function isPullRequestEvent(
export function isPullRequestReviewEvent( export function isPullRequestReviewEvent(
context: ParsedGitHubContext, context: ParsedGitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } { ): context is ParsedGitHubContext & { payload: GiteaPullRequestReviewEvent } {
return context.eventName === "pull_request_review"; return context.eventName === "pull_request_review";
} }
export function isPullRequestReviewCommentEvent( export function isPullRequestReviewCommentEvent(
context: ParsedGitHubContext, context: ParsedGitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } { ): context is ParsedGitHubContext & {
payload: GiteaPullRequestReviewCommentEvent;
} {
return context.eventName === "pull_request_review_comment"; return context.eventName === "pull_request_review_comment";
} }

View File

@@ -1,5 +1,3 @@
import { GITEA_SERVER_URL } from "../api/config";
export type ExecutionDetails = { export type ExecutionDetails = {
cost_usd?: number; cost_usd?: number;
duration_ms?: number; duration_ms?: number;
@@ -166,7 +164,11 @@ export function updateCommentBody(input: CommentUpdateInput): string {
.filter((segment) => segment); .filter((segment) => segment);
const [owner, repo] = segments; const [owner, repo] = segments;
if (owner && repo) { if (owner && repo) {
branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${finalBranchName}`; const serverUrl =
process.env.GITEA_SERVER_URL ||
process.env.GITHUB_SERVER_URL ||
"https://github.com";
branchUrl = `${serverUrl}/${owner}/${repo}/src/branch/${finalBranchName}`;
} }
} catch (error) { } catch (error) {
console.warn(`Failed to derive branch URL from job URL: ${error}`); console.warn(`Failed to derive branch URL from job URL: ${error}`);

View File

@@ -31,7 +31,10 @@ export async function createInitialComment(
console.log(`Repository: ${owner}/${repo}`); console.log(`Repository: ${owner}/${repo}`);
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id // Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
if (isPullRequestReviewCommentEvent(context)) { if (
isPullRequestReviewCommentEvent(context) &&
context.payload.comment?.id
) {
console.log(`Creating PR review comment reply`); console.log(`Creating PR review comment reply`);
response = await api.customRequest( response = await api.customRequest(
"POST", "POST",

View File

@@ -45,8 +45,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
// Check for issue label trigger // Check for issue label trigger
if (isIssuesEvent(context) && context.eventAction === "labeled") { if (isIssuesEvent(context) && context.eventAction === "labeled") {
const triggerLabel = context.inputs.labelTrigger?.trim(); const triggerLabel = context.inputs.labelTrigger?.trim();
const appliedLabel = (context.payload as IssuesLabeledEvent).label?.name const appliedLabel = (
?.trim(); context.payload as IssuesLabeledEvent
).label?.name?.trim();
console.log( console.log(
`Checking label trigger: expected='${triggerLabel}', applied='${appliedLabel}'`, `Checking label trigger: expected='${triggerLabel}', applied='${appliedLabel}'`,
@@ -55,7 +56,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
if ( if (
triggerLabel && triggerLabel &&
appliedLabel && appliedLabel &&
triggerLabel.localeCompare(appliedLabel, undefined, { sensitivity: "accent" }) === 0 triggerLabel.localeCompare(appliedLabel, undefined, {
sensitivity: "accent",
}) === 0
) { ) {
console.log(`Issue labeled with trigger label '${triggerLabel}'`); console.log(`Issue labeled with trigger label '${triggerLabel}'`);
return true; return true;
@@ -115,9 +118,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
// Check if trigger user is in requested reviewers (treat same as mention in text) // Check if trigger user is in requested reviewers (treat same as mention in text)
const triggerUser = triggerPhrase.replace(/^@/, ""); const triggerUser = triggerPhrase.replace(/^@/, "");
const requestedReviewers = context.payload.pull_request.requested_reviewers || []; const requestedReviewers =
const isReviewerRequested = requestedReviewers.some(reviewer => context.payload.pull_request.requested_reviewers || [];
'login' in reviewer && reviewer.login === triggerUser const isReviewerRequested = requestedReviewers.some(
(reviewer) => "login" in reviewer && reviewer.login === triggerUser,
); );
if (isReviewerRequested) { if (isReviewerRequested) {
@@ -131,9 +135,12 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
// Check for pull request review body trigger // Check for pull request review body trigger
if ( if (
isPullRequestReviewEvent(context) && isPullRequestReviewEvent(context) &&
(context.eventAction === "submitted" || context.eventAction === "edited") (context.eventAction === "submitted" ||
context.eventAction === "edited" ||
context.eventAction === "reviewed")
) { ) {
const reviewBody = context.payload.review.body || ""; const reviewBody =
context.payload.review?.body ?? context.payload.review?.content ?? "";
// Check for exact match with word boundaries or punctuation // Check for exact match with word boundaries or punctuation
const regex = new RegExp( const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`, `(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
@@ -153,7 +160,7 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
) { ) {
const commentBody = isIssueCommentEvent(context) const commentBody = isIssueCommentEvent(context)
? context.payload.comment.body ? context.payload.comment.body
: context.payload.comment.body; : (context.payload.comment?.body ?? context.payload.review?.content);
// Check for exact match with word boundaries or punctuation // Check for exact match with word boundaries or punctuation
const regex = new RegExp( const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`, `(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,