This commit is contained in:
Daan Schouteden
2026-06-02 11:39:41 +02:00
commit 3d0e28f427
27 changed files with 3426 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
export async function retry<T>(input: {
fn: () => Promise<T>;
retries: number;
initialDelayMs: number;
shouldRetry?: (error: unknown) => boolean;
}): Promise<T> {
let attempt = 0;
let delay = input.initialDelayMs;
while (true) {
try {
return await input.fn();
} catch (error) {
attempt += 1;
const retryable = input.shouldRetry ? input.shouldRetry(error) : true;
if (!retryable || attempt > input.retries) {
throw error;
}
await sleep(delay);
delay *= 2;
}
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+141
View File
@@ -0,0 +1,141 @@
import { Env } from "../config/env.js";
import { loadRepoConfig } from "../config/load-repo-config.js";
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 { deletePriorBotReviews, postReview } from "../gitea/review-api.js";
import { removeBotFromReviewers } from "../gitea/reviewer-api.js";
import { buildReviewPrompt } from "../prompt/build-review-prompt.js";
import { RoutedEvent } from "../types/events.js";
import { retry } from "./retry.js";
export async function runReview(input: {
env: Env;
event: RoutedEvent;
dedupe: DedupeStore;
correlationId: string;
}): Promise<"skipped" | "success"> {
const owner = input.event.payload.repository.owner.login;
const repo = input.event.payload.repository.name;
const prNumber = input.event.payload.pull_request.number;
const headSha = input.event.payload.pull_request.head.sha;
const dedupeKey = input.dedupe.createKey({
owner,
repo,
prNumber,
headSha
});
if (input.dedupe.has(dedupeKey)) {
return "skipped";
}
input.dedupe.mark(dedupeKey);
const gitea = new GiteaClient(input.env.GITEA_BASE_URL, input.env.GITEA_TOKEN, 30000);
const pull = await retry({
fn: () => gitea.getPull(owner, repo, prNumber),
retries: 2,
initialDelayMs: 300
});
const { config: repoConfig, ruleFiles } = await retry({
fn: () =>
loadRepoConfig({
gitea,
owner,
repo,
ref: pull.head.ref
}),
retries: 2,
initialDelayMs: 300
});
const should = shouldProcessEvent({
event: input.event,
repoConfig,
defaultBaseBranch: input.env.DEFAULT_BASE_BRANCH,
botLogin: input.env.GITEA_BOT_LOGIN
});
if (!should.process) {
return "skipped";
}
const files = await retry({
fn: () => gitea.getPullFiles(owner, repo, prNumber),
retries: 2,
initialDelayMs: 300
});
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
}),
retries: 2,
initialDelayMs: 500
});
await retry({
fn: () =>
deletePriorBotReviews({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300
});
await retry({
fn: () =>
postReview({
gitea,
owner,
repo,
prNumber,
files,
review,
maxInlineComments
}),
retries: 2,
initialDelayMs: 300
});
await retry({
fn: () =>
removeBotFromReviewers({
gitea,
owner,
repo,
prNumber,
botLogin: input.env.GITEA_BOT_LOGIN
}),
retries: 2,
initialDelayMs: 300
});
console.log(
JSON.stringify({
correlation_id: input.correlationId,
owner,
repo,
pr_number: prNumber,
head_sha: headSha,
outcome: "success"
})
);
return "success";
}