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:
+1
-1
@@ -7,6 +7,6 @@ PORT=8787
|
|||||||
|
|
||||||
DEFAULT_BASE_BRANCH=main
|
DEFAULT_BASE_BRANCH=main
|
||||||
MAX_INLINE_COMMENTS=5
|
MAX_INLINE_COMMENTS=5
|
||||||
REVIEW_TIMEOUT_MS=120000
|
REVIEW_TIMEOUT_MS=600000
|
||||||
DEDUPE_TTL_SECONDS=1800
|
DEDUPE_TTL_SECONDS=1800
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ Optional:
|
|||||||
|
|
||||||
- `DEFAULT_BASE_BRANCH` (default: `main`)
|
- `DEFAULT_BASE_BRANCH` (default: `main`)
|
||||||
- `MAX_INLINE_COMMENTS` (default: `5`)
|
- `MAX_INLINE_COMMENTS` (default: `5`)
|
||||||
- `REVIEW_TIMEOUT_MS` (default: `120000`)
|
- `REVIEW_TIMEOUT_MS` (default: `600000`, 10 minutes)
|
||||||
- `DEDUPE_TTL_SECONDS` (default: `1800`)
|
- `DEDUPE_TTL_SECONDS` (default: `1800`)
|
||||||
- `LOG_LEVEL` (default: `info`; options: `debug`, `info`, `warn`, `error`)
|
- `LOG_LEVEL` (default: `info`; options: `debug`, `info`, `warn`, `error`)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ const envSchema = z.object({
|
|||||||
PORT: z.coerce.number().int().positive().default(8787),
|
PORT: z.coerce.number().int().positive().default(8787),
|
||||||
DEFAULT_BASE_BRANCH: z.string().default("main"),
|
DEFAULT_BASE_BRANCH: z.string().default("main"),
|
||||||
MAX_INLINE_COMMENTS: z.coerce.number().int().positive().default(5),
|
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)
|
DEDUPE_TTL_SECONDS: z.coerce.number().int().positive().default(1800)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
async getFileIfExists(owner: string, repo: string, path: string, ref: string): Promise<string | null> {
|
||||||
const encodedPath = path
|
const encodedPath = path
|
||||||
.split("/")
|
.split("/")
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+126
-84
@@ -4,6 +4,7 @@ import { runCursorReview } from "../cursor/review-agent.js";
|
|||||||
import { DedupeStore } from "../domain/dedupe-store.js";
|
import { DedupeStore } from "../domain/dedupe-store.js";
|
||||||
import { shouldProcessEvent } from "../domain/should-process-event.js";
|
import { shouldProcessEvent } from "../domain/should-process-event.js";
|
||||||
import { GiteaClient } from "../gitea/client.js";
|
import { GiteaClient } from "../gitea/client.js";
|
||||||
|
import { deleteProgressComment, postProgressComment } from "../gitea/comments-api.js";
|
||||||
import { deletePriorBotReviews, postReview } from "../gitea/review-api.js";
|
import { deletePriorBotReviews, postReview } from "../gitea/review-api.js";
|
||||||
import { removeBotFromReviewers } from "../gitea/reviewer-api.js";
|
import { removeBotFromReviewers } from "../gitea/reviewer-api.js";
|
||||||
import { buildReviewPrompt } from "../prompt/build-review-prompt.js";
|
import { buildReviewPrompt } from "../prompt/build-review-prompt.js";
|
||||||
@@ -113,93 +114,134 @@ async function executeReview(params: {
|
|||||||
return "skipped";
|
return "skipped";
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await retry({
|
let progressCommentId: number | undefined;
|
||||||
fn: () => gitea.getPullFiles(owner, repo, prNumber),
|
try {
|
||||||
retries: 2,
|
try {
|
||||||
initialDelayMs: 300,
|
progressCommentId = await postProgressComment({ gitea, owner, repo, prNumber });
|
||||||
operationName: "getPullFiles",
|
log("info", "Posted progress comment on PR", {
|
||||||
correlationId: input.correlationId
|
correlation_id: input.correlationId,
|
||||||
});
|
pr_number: prNumber,
|
||||||
const maxInlineComments = repoConfig.max_inline_comments ?? input.env.MAX_INLINE_COMMENTS;
|
comment_id: progressCommentId
|
||||||
const prompt = buildReviewPrompt({
|
});
|
||||||
owner,
|
} catch (error) {
|
||||||
repo,
|
log("warn", "Failed to post progress comment on PR", {
|
||||||
pull,
|
correlation_id: input.correlationId,
|
||||||
files,
|
pr_number: prNumber,
|
||||||
maxInlineComments,
|
error: error instanceof Error ? error.message : String(error)
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
await retry({
|
const files = await retry({
|
||||||
fn: () =>
|
fn: () => gitea.getPullFiles(owner, repo, prNumber),
|
||||||
deletePriorBotReviews({
|
retries: 2,
|
||||||
gitea,
|
initialDelayMs: 300,
|
||||||
owner,
|
operationName: "getPullFiles",
|
||||||
repo,
|
correlationId: input.correlationId
|
||||||
prNumber,
|
});
|
||||||
botLogin: input.env.GITEA_BOT_LOGIN
|
const maxInlineComments = repoConfig.max_inline_comments ?? input.env.MAX_INLINE_COMMENTS;
|
||||||
}),
|
const prompt = buildReviewPrompt({
|
||||||
retries: 2,
|
owner,
|
||||||
initialDelayMs: 300,
|
repo,
|
||||||
operationName: "deletePriorBotReviews",
|
pull,
|
||||||
correlationId: input.correlationId
|
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({
|
await retry({
|
||||||
fn: () =>
|
fn: () =>
|
||||||
postReview({
|
deletePriorBotReviews({
|
||||||
gitea,
|
gitea,
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
prNumber,
|
prNumber,
|
||||||
files,
|
botLogin: input.env.GITEA_BOT_LOGIN
|
||||||
review,
|
}),
|
||||||
maxInlineComments
|
retries: 2,
|
||||||
}),
|
initialDelayMs: 300,
|
||||||
retries: 2,
|
operationName: "deletePriorBotReviews",
|
||||||
initialDelayMs: 300,
|
correlationId: input.correlationId
|
||||||
operationName: "postReview",
|
});
|
||||||
correlationId: input.correlationId
|
|
||||||
});
|
|
||||||
|
|
||||||
await retry({
|
await retry({
|
||||||
fn: () =>
|
fn: () =>
|
||||||
removeBotFromReviewers({
|
postReview({
|
||||||
gitea,
|
gitea,
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
prNumber,
|
prNumber,
|
||||||
botLogin: input.env.GITEA_BOT_LOGIN
|
files,
|
||||||
}),
|
review,
|
||||||
retries: 2,
|
maxInlineComments
|
||||||
initialDelayMs: 300,
|
}),
|
||||||
operationName: "removeBotFromReviewers",
|
retries: 2,
|
||||||
correlationId: input.correlationId
|
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", {
|
input.dedupe.complete(dedupeKey);
|
||||||
correlation_id: input.correlationId,
|
|
||||||
owner,
|
log("info", "Review completed", {
|
||||||
repo,
|
correlation_id: input.correlationId,
|
||||||
pr_number: prNumber,
|
owner,
|
||||||
head_sha: headSha,
|
repo,
|
||||||
outcome: "success"
|
pr_number: prNumber,
|
||||||
});
|
head_sha: headSha,
|
||||||
return "success";
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user