Add PR progress comment and increase Cursor review timeout.

Post a temporary in-progress comment while reviewing, remove it when done, default timeout to 10 minutes, and skip retries on timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daan Schouteden
2026-06-03 11:28:24 +02:00
parent 28488d0be9
commit 881719f743
6 changed files with 178 additions and 87 deletions
+1 -1
View File
@@ -7,6 +7,6 @@ PORT=8787
DEFAULT_BASE_BRANCH=main
MAX_INLINE_COMMENTS=5
REVIEW_TIMEOUT_MS=120000
REVIEW_TIMEOUT_MS=600000
DEDUPE_TTL_SECONDS=1800
LOG_LEVEL=info
+1 -1
View File
@@ -154,7 +154,7 @@ Optional:
- `DEFAULT_BASE_BRANCH` (default: `main`)
- `MAX_INLINE_COMMENTS` (default: `5`)
- `REVIEW_TIMEOUT_MS` (default: `120000`)
- `REVIEW_TIMEOUT_MS` (default: `600000`, 10 minutes)
- `DEDUPE_TTL_SECONDS` (default: `1800`)
- `LOG_LEVEL` (default: `info`; options: `debug`, `info`, `warn`, `error`)
+1 -1
View File
@@ -9,7 +9,7 @@ const envSchema = z.object({
PORT: z.coerce.number().int().positive().default(8787),
DEFAULT_BASE_BRANCH: z.string().default("main"),
MAX_INLINE_COMMENTS: z.coerce.number().int().positive().default(5),
REVIEW_TIMEOUT_MS: z.coerce.number().int().positive().default(120000),
REVIEW_TIMEOUT_MS: z.coerce.number().int().positive().default(600000),
DEDUPE_TTL_SECONDS: z.coerce.number().int().positive().default(1800)
});
+18
View File
@@ -59,6 +59,24 @@ export class GiteaClient {
});
}
async createIssueComment(
owner: string,
repo: string,
issueIndex: number,
body: string
): Promise<{ id: number }> {
return this.requestJson(`/repos/${owner}/${repo}/issues/${issueIndex}/comments`, {
method: "POST",
body: { body }
});
}
async deleteIssueComment(owner: string, repo: string, commentId: number): Promise<void> {
await this.requestJson(`/repos/${owner}/${repo}/issues/comments/${commentId}`, {
method: "DELETE"
});
}
async getFileIfExists(owner: string, repo: string, path: string, ref: string): Promise<string | null> {
const encodedPath = path
.split("/")
+31
View File
@@ -0,0 +1,31 @@
import { GiteaClient } from "./client.js";
export const PROGRESS_COMMENT_MARKER = "<!-- gitea-pr-review-bot-progress -->";
export const PROGRESS_COMMENT_BODY =
`${PROGRESS_COMMENT_MARKER}\n\n` +
"🤖 **PR review in progress…** This comment will be removed when the review is ready.";
export async function postProgressComment(input: {
gitea: GiteaClient;
owner: string;
repo: string;
prNumber: number;
}): Promise<number> {
const comment = await input.gitea.createIssueComment(
input.owner,
input.repo,
input.prNumber,
PROGRESS_COMMENT_BODY
);
return comment.id;
}
export async function deleteProgressComment(input: {
gitea: GiteaClient;
owner: string;
repo: string;
commentId: number;
}): Promise<void> {
await input.gitea.deleteIssueComment(input.owner, input.repo, input.commentId);
}
+44 -2
View File
@@ -4,6 +4,7 @@ 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 { deletePriorBotReviews, postReview } from "../gitea/review-api.js";
import { removeBotFromReviewers } from "../gitea/reviewer-api.js";
import { buildReviewPrompt } from "../prompt/build-review-prompt.js";
@@ -113,6 +114,23 @@ async function executeReview(params: {
return "skipped";
}
let progressCommentId: number | undefined;
try {
try {
progressCommentId = await postProgressComment({ gitea, owner, repo, prNumber });
log("info", "Posted progress comment on PR", {
correlation_id: input.correlationId,
pr_number: prNumber,
comment_id: progressCommentId
});
} catch (error) {
log("warn", "Failed to post progress comment on PR", {
correlation_id: input.correlationId,
pr_number: prNumber,
error: error instanceof Error ? error.message : String(error)
});
}
const files = await retry({
fn: () => gitea.getPullFiles(owner, repo, prNumber),
retries: 2,
@@ -138,10 +156,11 @@ async function executeReview(params: {
model: repoConfig.model,
correlationId: input.correlationId
}),
retries: 2,
retries: 1,
initialDelayMs: 500,
operationName: "runCursorReview",
correlationId: input.correlationId
correlationId: input.correlationId,
shouldRetry: (error) => !isTimeoutError(error)
});
await retry({
@@ -202,4 +221,27 @@ async function executeReview(params: {
outcome: "success"
});
return "success";
} finally {
if (progressCommentId !== undefined) {
try {
await deleteProgressComment({ gitea, owner, repo, commentId: progressCommentId });
log("info", "Removed progress comment from PR", {
correlation_id: input.correlationId,
pr_number: prNumber,
comment_id: progressCommentId
});
} catch (error) {
log("warn", "Failed to remove progress comment from PR", {
correlation_id: input.correlationId,
pr_number: prNumber,
comment_id: progressCommentId,
error: error instanceof Error ? error.message : String(error)
});
}
}
}
}
function isTimeoutError(error: unknown): boolean {
return error instanceof Error && error.message.toLowerCase().includes("timed out");
}