diff --git a/src/gitea/comments-api.ts b/src/gitea/comments-api.ts index f471a7b..437ff6d 100644 --- a/src/gitea/comments-api.ts +++ b/src/gitea/comments-api.ts @@ -29,3 +29,29 @@ export async function deleteProgressComment(input: { }): Promise { await input.gitea.deleteIssueComment(input.owner, input.repo, input.commentId); } + +export const TIMEOUT_FAILURE_MARKER = ""; + +export function buildTimeoutFailureBody(timeoutMs: number): string { + const timeoutMinutes = Math.max(1, Math.round(timeoutMs / 60_000)); + return ( + `${TIMEOUT_FAILURE_MARKER}\n\n` + + `🤖 **PR review failed:** the automated review timed out after ${timeoutMinutes} minute(s). ` + + "You can request the bot again as reviewer or review the changes manually." + ); +} + +export async function postTimeoutFailureComment(input: { + gitea: GiteaClient; + owner: string; + repo: string; + prNumber: number; + timeoutMs: number; +}): Promise { + await input.gitea.createIssueComment( + input.owner, + input.repo, + input.prNumber, + buildTimeoutFailureBody(input.timeoutMs) + ); +} diff --git a/src/run/review-runner.ts b/src/run/review-runner.ts index d0176f5..b65880d 100644 --- a/src/run/review-runner.ts +++ b/src/run/review-runner.ts @@ -4,7 +4,11 @@ import { runCursorReview } from "../cursor/review-agent.js"; import { DedupeStore } from "../domain/dedupe-store.js"; import { shouldProcessEvent } from "../domain/should-process-event.js"; import { GiteaClient } from "../gitea/client.js"; -import { deleteProgressComment, postProgressComment } from "../gitea/comments-api.js"; +import { + deleteProgressComment, + postProgressComment, + postTimeoutFailureComment +} from "../gitea/comments-api.js"; import { deletePriorBotReviews, postReview } from "../gitea/review-api.js"; import { removeBotFromReviewers } from "../gitea/reviewer-api.js"; import { buildReviewPrompt } from "../prompt/build-review-prompt.js"; @@ -221,6 +225,19 @@ async function executeReview(params: { outcome: "success" }); return "success"; + } catch (error) { + if (isTimeoutError(error)) { + await handleTimeoutFailure({ + gitea, + owner, + repo, + prNumber, + botLogin: input.env.GITEA_BOT_LOGIN, + timeoutMs: input.env.REVIEW_TIMEOUT_MS, + correlationId: input.correlationId + }); + } + throw error; } finally { if (progressCommentId !== undefined) { try { @@ -245,3 +262,53 @@ async function executeReview(params: { function isTimeoutError(error: unknown): boolean { return error instanceof Error && error.message.toLowerCase().includes("timed out"); } + +async function handleTimeoutFailure(input: { + gitea: GiteaClient; + owner: string; + repo: string; + prNumber: number; + botLogin: string; + timeoutMs: number; + correlationId: string; +}): Promise { + try { + await postTimeoutFailureComment({ + gitea: input.gitea, + owner: input.owner, + repo: input.repo, + prNumber: input.prNumber, + timeoutMs: input.timeoutMs + }); + log("info", "Posted timeout failure comment on PR", { + correlation_id: input.correlationId, + pr_number: input.prNumber + }); + } catch (error) { + log("warn", "Failed to post timeout failure comment on PR", { + correlation_id: input.correlationId, + pr_number: input.prNumber, + error: error instanceof Error ? error.message : String(error) + }); + } + + try { + await removeBotFromReviewers({ + gitea: input.gitea, + owner: input.owner, + repo: input.repo, + prNumber: input.prNumber, + botLogin: input.botLogin + }); + log("info", "Removed bot from reviewers after timeout", { + correlation_id: input.correlationId, + pr_number: input.prNumber + }); + } catch (error) { + log("warn", "Failed to remove bot from reviewers after timeout", { + correlation_id: input.correlationId, + pr_number: input.prNumber, + error: error instanceof Error ? error.message : String(error) + }); + } +}