From b744372179e23cb860af91c26f43726ebc755487 Mon Sep 17 00:00:00 2001 From: leoarry Date: Mon, 8 Jun 2026 09:13:54 +0100 Subject: [PATCH] #17 fix: handle pull_request_review_comment payload differences in Gitea (#19) --- src/create-prompt/index.ts | 50 ++++++++----------- src/entrypoints/update-comment-link.ts | 25 ++++++---- src/github/context.ts | 24 +++++++-- src/github/operations/comment-logic.ts | 8 +-- .../operations/comments/create-initial.ts | 5 +- src/github/validation/trigger.ts | 25 ++++++---- 6 files changed, 83 insertions(+), 54 deletions(-) diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index cd55de2..94490fd 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -120,7 +120,9 @@ export function buildDisallowedToolsString( // If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list const allowedList = normalizeToolList(allowedTools); if (allowedList.length > 0) { - disallowedTools = disallowedTools.filter((tool) => !allowedList.includes(tool)); + disallowedTools = disallowedTools.filter( + (tool) => !allowedList.includes(tool), + ); } let allDisallowedTools = disallowedTools.join(","); @@ -164,16 +166,20 @@ export function prepareContext( let commentBody: string | undefined; if (isIssueCommentEvent(context)) { - commentId = context.payload.comment.id.toString(); - commentBody = context.payload.comment.body; - triggerUsername = context.payload.comment.user.login; + commentId = context.payload.comment?.id?.toString(); + commentBody = context.payload.comment?.body; + triggerUsername = context.payload.comment?.user?.login; } else if (isPullRequestReviewEvent(context)) { - commentBody = context.payload.review.body ?? ""; - triggerUsername = context.payload.review.user.login; + commentBody = + context.payload.review?.body ?? context.payload.review?.content ?? ""; + triggerUsername = + context.payload.review?.user?.login ?? context.payload.sender?.login; } else if (isPullRequestReviewCommentEvent(context)) { - commentId = context.payload.comment.id.toString(); - commentBody = context.payload.comment.body; - triggerUsername = context.payload.comment.user.login; + commentId = context.payload.comment?.id?.toString(); + commentBody = + context.payload.comment?.body ?? context.payload.review?.content; + triggerUsername = + context.payload.comment?.user?.login ?? context.payload.sender?.login; } else if (isIssuesEvent(context)) { triggerUsername = context.payload.issue.user.login; } @@ -595,18 +601,7 @@ ${sanitizeContent(context.directPrompt)} ` : "" } -${ - eventData.eventName === "pull_request_review_comment" - ? ` -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. -` - : ` +${` 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: @@ -617,8 +612,7 @@ Tool usage example for mcp__gitea__update_issue_comment: "body": "Your comment text here" } All four parameters (owner, repo, commentId, body) are required. -` -} +`} 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: - Use your Gitea comment to maintain a detailed task list based on the request. - 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: - Analyze the pre-fetched data provided above. @@ -738,8 +732,8 @@ ${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. Final Up Important Notes: - 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}. -- 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." : ""} +- 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.` : ""} - You communicate exclusively by editing your single comment - not through any other means. - Use this spinner HTML when work is in progress: ${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"} - 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. -- 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"}. -- 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." : ""} +- 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.` : ""} - You communicate exclusively by editing your single comment - not through any other means. - Use this spinner HTML when work is in progress: ${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.`} diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index d8cebf5..9e01f2b 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -41,15 +41,22 @@ async function run() { // GitHub has separate ID namespaces for review comments and issue comments // We need to use the correct API based on the event type if (isPullRequestReviewCommentEvent(context)) { - // For PR review comments, use the pulls API - console.log(`Fetching PR review comment ${commentId}`); - const response = await client.api.customRequest( - "GET", - `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, - ); - comment = response.data; - isPRReviewComment = true; - console.log("Successfully fetched as PR review comment"); + // Try the PR review comment endpoint first; Gitea may have created an + // issue comment instead (no comment.id in payload), so fall through on 404. + try { + console.log(`Fetching PR review comment ${commentId}`); + const response = await client.api.customRequest( + "GET", + `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, + ); + 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 diff --git a/src/github/context.ts b/src/github/context.ts index 4e0d866..8364a44 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -10,6 +10,20 @@ import type { import type { ModeName } from "../modes/types"; 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 = { runId: string; eventName: string; @@ -24,8 +38,8 @@ export type ParsedGitHubContext = { | IssuesEvent | IssueCommentEvent | PullRequestEvent - | PullRequestReviewEvent - | PullRequestReviewCommentEvent; + | GiteaPullRequestReviewEvent + | GiteaPullRequestReviewCommentEvent; entityNumber: number; isPR: boolean; inputs: { @@ -181,13 +195,15 @@ export function isPullRequestEvent( export function isPullRequestReviewEvent( context: ParsedGitHubContext, -): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } { +): context is ParsedGitHubContext & { payload: GiteaPullRequestReviewEvent } { return context.eventName === "pull_request_review"; } export function isPullRequestReviewCommentEvent( context: ParsedGitHubContext, -): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } { +): context is ParsedGitHubContext & { + payload: GiteaPullRequestReviewCommentEvent; +} { return context.eventName === "pull_request_review_comment"; } diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 7a927fb..28b100e 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -1,5 +1,3 @@ -import { GITEA_SERVER_URL } from "../api/config"; - export type ExecutionDetails = { cost_usd?: number; duration_ms?: number; @@ -166,7 +164,11 @@ export function updateCommentBody(input: CommentUpdateInput): string { .filter((segment) => segment); const [owner, repo] = segments; 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) { console.warn(`Failed to derive branch URL from job URL: ${error}`); diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index 9802213..962a271 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -31,7 +31,10 @@ export async function createInitialComment( console.log(`Repository: ${owner}/${repo}`); // 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`); response = await api.customRequest( "POST", diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 152095d..4fd6dd7 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -45,8 +45,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { // Check for issue label trigger if (isIssuesEvent(context) && context.eventAction === "labeled") { const triggerLabel = context.inputs.labelTrigger?.trim(); - const appliedLabel = (context.payload as IssuesLabeledEvent).label?.name - ?.trim(); + const appliedLabel = ( + context.payload as IssuesLabeledEvent + ).label?.name?.trim(); console.log( `Checking label trigger: expected='${triggerLabel}', applied='${appliedLabel}'`, @@ -55,7 +56,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { if ( triggerLabel && appliedLabel && - triggerLabel.localeCompare(appliedLabel, undefined, { sensitivity: "accent" }) === 0 + triggerLabel.localeCompare(appliedLabel, undefined, { + sensitivity: "accent", + }) === 0 ) { console.log(`Issue labeled with trigger label '${triggerLabel}'`); 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) const triggerUser = triggerPhrase.replace(/^@/, ""); - const requestedReviewers = context.payload.pull_request.requested_reviewers || []; - const isReviewerRequested = requestedReviewers.some(reviewer => - 'login' in reviewer && reviewer.login === triggerUser + const requestedReviewers = + context.payload.pull_request.requested_reviewers || []; + const isReviewerRequested = requestedReviewers.some( + (reviewer) => "login" in reviewer && reviewer.login === triggerUser, ); if (isReviewerRequested) { @@ -131,9 +135,12 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { // Check for pull request review body trigger if ( 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 const regex = new RegExp( `(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`, @@ -153,7 +160,7 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { ) { const commentBody = isIssueCommentEvent(context) ? 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 const regex = new RegExp( `(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,