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
+126 -84
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,93 +114,134 @@ async function executeReview(params: {
return "skipped";
}
const files = await retry({
fn: () => gitea.getPullFiles(owner, repo, prNumber),
retries: 2,
initialDelayMs: 300,
operationName: "getPullFiles",
correlationId: input.correlationId
});
const maxInlineComments = repoConfig.max_inline_comments ?? input.env.MAX_INLINE_COMMENTS;
const prompt = buildReviewPrompt({
owner,
repo,
pull,
files,
maxInlineComments,
repoRuleFiles: ruleFiles
});
const review = await retry({
fn: () =>
runCursorReview({
apiKey: input.env.CURSOR_API_KEY,
prompt,
timeoutMs: input.env.REVIEW_TIMEOUT_MS,
model: repoConfig.model,
correlationId: input.correlationId
}),
retries: 2,
initialDelayMs: 500,
operationName: "runCursorReview",
correlationId: input.correlationId
});
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)
});
}
await retry({
fn: () =>
deletePriorBotReviews({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300,
operationName: "deletePriorBotReviews",
correlationId: input.correlationId
});
const files = await retry({
fn: () => gitea.getPullFiles(owner, repo, prNumber),
retries: 2,
initialDelayMs: 300,
operationName: "getPullFiles",
correlationId: input.correlationId
});
const maxInlineComments = repoConfig.max_inline_comments ?? input.env.MAX_INLINE_COMMENTS;
const prompt = buildReviewPrompt({
owner,
repo,
pull,
files,
maxInlineComments,
repoRuleFiles: ruleFiles
});
const review = await retry({
fn: () =>
runCursorReview({
apiKey: input.env.CURSOR_API_KEY,
prompt,
timeoutMs: input.env.REVIEW_TIMEOUT_MS,
model: repoConfig.model,
correlationId: input.correlationId
}),
retries: 1,
initialDelayMs: 500,
operationName: "runCursorReview",
correlationId: input.correlationId,
shouldRetry: (error) => !isTimeoutError(error)
});
await retry({
fn: () =>
postReview({
gitea,
owner,
repo,
prNumber,
files,
review,
maxInlineComments
}),
retries: 2,
initialDelayMs: 300,
operationName: "postReview",
correlationId: input.correlationId
});
await retry({
fn: () =>
deletePriorBotReviews({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300,
operationName: "deletePriorBotReviews",
correlationId: input.correlationId
});
await retry({
fn: () =>
removeBotFromReviewers({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300,
operationName: "removeBotFromReviewers",
correlationId: input.correlationId
});
await retry({
fn: () =>
postReview({
gitea,
owner,
repo,
prNumber,
files,
review,
maxInlineComments
}),
retries: 2,
initialDelayMs: 300,
operationName: "postReview",
correlationId: input.correlationId
});
input.dedupe.complete(dedupeKey);
await retry({
fn: () =>
removeBotFromReviewers({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300,
operationName: "removeBotFromReviewers",
correlationId: input.correlationId
});
log("info", "Review completed", {
correlation_id: input.correlationId,
owner,
repo,
pr_number: prNumber,
head_sha: headSha,
outcome: "success"
});
return "success";
input.dedupe.complete(dedupeKey);
log("info", "Review completed", {
correlation_id: input.correlationId,
owner,
repo,
pr_number: prNumber,
head_sha: headSha,
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");
}