123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import type { ActionConfig, PRContext, ChunkReview } from "./types.ts";
|
|
import { getPR, getPRDiff, addReaction, removeReaction, postReviewComment } from "./gitea.ts";
|
|
import { parseDiff, chunkFile } from "./diff.ts";
|
|
import { createMcpServer } from "./mcp.ts";
|
|
import { reviewChunk, aggregateFindings } from "./review.ts";
|
|
import { formatReview } from "./comment.ts";
|
|
|
|
function readConfig(): ActionConfig {
|
|
return {
|
|
prompt: process.env.INPUT_PROMPT ?? "Review this pull request",
|
|
model: process.env.INPUT_MODEL ?? "qwen3.6:35b",
|
|
contextWindow: parseInt(process.env.INPUT_CONTEXT_WINDOW ?? "4096", 10),
|
|
maxToolCalls: parseInt(process.env.INPUT_MAX_TOOL_CALLS ?? "10", 10),
|
|
};
|
|
}
|
|
|
|
function readEvent(): Record<string, unknown> {
|
|
const path = process.env.GITHUB_EVENT_PATH;
|
|
if (!path) return {};
|
|
try {
|
|
return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const config = readConfig();
|
|
const eventName = process.env.GITHUB_EVENT_NAME ?? "";
|
|
const event = readEvent();
|
|
|
|
const repository = process.env.GITHUB_REPOSITORY ?? "";
|
|
const slashIdx = repository.indexOf("/");
|
|
if (slashIdx === -1) throw new Error(`Invalid GITHUB_REPOSITORY: ${repository}`);
|
|
const owner = repository.slice(0, slashIdx);
|
|
const repo = repository.slice(slashIdx + 1);
|
|
|
|
let prNumber: number;
|
|
let prompt: string;
|
|
let triggerCommentId: number | null = null;
|
|
|
|
if (eventName === "pull_request") {
|
|
prNumber = event.number as number;
|
|
prompt = config.prompt;
|
|
} else if (eventName === "issue_comment") {
|
|
const issue = event.issue as Record<string, unknown>;
|
|
if (!issue?.pull_request) {
|
|
console.log("issue_comment on non-PR issue, skipping");
|
|
return;
|
|
}
|
|
const comment = event.comment as Record<string, unknown>;
|
|
const commentBody = String(comment?.body ?? "");
|
|
if (!commentBody.toLowerCase().includes("@shockbot")) {
|
|
console.log("comment does not mention @shockbot, skipping");
|
|
return;
|
|
}
|
|
prNumber = issue.number as number;
|
|
triggerCommentId = comment.id as number;
|
|
prompt = commentBody.replace(/@shockbot\s*/gi, "").trim() || config.prompt;
|
|
} else {
|
|
console.log(`Unsupported event: ${eventName}, skipping`);
|
|
return;
|
|
}
|
|
|
|
const prData = await getPR(owner, repo, prNumber);
|
|
const headSha = prData.head?.sha;
|
|
if (!headSha) throw new Error(`Could not get head SHA for PR #${prNumber}`);
|
|
|
|
const pr: PRContext = { owner, repo, prNumber, headSha };
|
|
|
|
if (triggerCommentId !== null) {
|
|
await addReaction(pr, triggerCommentId, "eyes").catch(() => {});
|
|
}
|
|
|
|
try {
|
|
const rawDiff = await getPRDiff(pr);
|
|
if (!rawDiff.trim()) {
|
|
await postReviewComment(pr, "shockbot: no diff found for this PR.");
|
|
return;
|
|
}
|
|
|
|
const files = parseDiff(rawDiff);
|
|
if (files.length === 0) {
|
|
await postReviewComment(pr, "shockbot: no reviewable files in this PR (all files skipped).");
|
|
return;
|
|
}
|
|
|
|
const chunks = files.flatMap((f) => chunkFile(f, config.contextWindow));
|
|
const mcpServer = createMcpServer(pr, config.maxToolCalls);
|
|
|
|
const results: ChunkReview[] = [];
|
|
for (const chunk of chunks) {
|
|
const result = await reviewChunk(
|
|
chunk,
|
|
prompt,
|
|
config.model,
|
|
config.contextWindow,
|
|
mcpServer,
|
|
);
|
|
results.push(result);
|
|
}
|
|
|
|
const aggregated = aggregateFindings(results);
|
|
const body = formatReview(aggregated, files.map((f) => f.filename), config.model);
|
|
await postReviewComment(pr, body);
|
|
|
|
if (triggerCommentId !== null) {
|
|
await removeReaction(pr, triggerCommentId, "eyes").catch(() => {});
|
|
await addReaction(pr, triggerCommentId, "+1").catch(() => {});
|
|
}
|
|
} catch (err) {
|
|
console.error("Review failed:", err);
|
|
if (triggerCommentId !== null) {
|
|
await removeReaction(pr, triggerCommentId, "eyes").catch(() => {});
|
|
await addReaction(pr, triggerCommentId, "-1").catch(() => {});
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|