Files
shockbot/entry.ts
T
Adam Wolf e6374a952c idk
2026-06-01 20:07:02 -05:00

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();