ai-codereviewer/src/main.ts

213 lines
5.4 KiB
TypeScript
Raw Normal View History

2023-03-22 23:16:13 +02:00
import { readFileSync } from "fs";
import * as core from "@actions/core";
import { Configuration, OpenAIApi } from "openai";
import { Octokit } from "@octokit/rest";
import parseDiff, { Chunk, File } from "parse-diff";
import minimatch from "minimatch";
const GITHUB_TOKEN: string = core.getInput("GITHUB_TOKEN");
const OPENAI_API_KEY: string = core.getInput("OPENAI_API_KEY");
const octokit = new Octokit({ auth: GITHUB_TOKEN });
const configuration = new Configuration({
apiKey: OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
interface PRDetails {
owner: string;
repo: string;
pull_number: number;
title: string;
2023-03-23 00:58:11 +02:00
description: string;
2023-03-22 23:16:13 +02:00
}
async function getPRDetails(): Promise<PRDetails> {
const { repository, number } = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
2023-03-23 00:58:11 +02:00
const prResponse = await octokit.pulls.get({
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
});
2023-03-22 23:16:13 +02:00
return {
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
title: prResponse.data.title ?? "",
2023-03-23 00:58:11 +02:00
description: prResponse.data.body ?? "",
2023-03-22 23:16:13 +02:00
};
}
async function getDiff(
owner: string,
repo: string,
pull_number: number
): Promise<string | null> {
const response = await octokit.pulls.get({
owner,
repo,
pull_number,
mediaType: { format: "diff" },
});
// @ts-expect-error - response.data is a string
return response.data;
}
async function analyzeCode(
2023-03-23 00:58:11 +02:00
parsedDiff: File[],
prDetails: PRDetails
2023-03-22 23:16:13 +02:00
): Promise<Array<{ body: string; path: string; line: number }>> {
const comments: Array<{ body: string; path: string; line: number }> = [];
for (const file of parsedDiff) {
for (const chunk of file.chunks) {
const prompt = createPrompt(file, chunk, prDetails);
2023-03-22 23:16:13 +02:00
const aiResponse = await getAIResponse(prompt);
if (aiResponse) {
const newComments = createComment(file, chunk, aiResponse);
if (newComments) {
2023-03-28 00:45:51 +03:00
comments.push(...newComments);
2023-03-22 23:16:13 +02:00
}
}
}
}
return comments;
}
function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string {
return `- Provide the response in following JSON format: [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}]
- Provide comments and suggestions ONLY if there is something to improve, otherwise return an empty array.
- Write the comment in GitHub markdown.
- Don't give positive comments.
- Use the given description only for the overall context and only comment the code.
- Calculate the line number from \`@@ -WW,XX +YY,ZZ @@\` using following formula: \`YY + L = line_number\`, where \`YY\` is the starting line number from the diff hunk, and \`L\` is the number of lines (including unchanged lines) from the starting line until the line you want to comment on. Pay special attention to this instruction and ensure that you count lines accurately.
Review the following code diff in the file "${
2023-03-22 23:16:13 +02:00
file.to
}" and take the pull request title and description into account when writing the response.
2023-03-23 00:58:11 +02:00
Pull request title: ${prDetails.title}
Pull request description:
2023-03-23 00:58:11 +02:00
---
${prDetails.description}
2023-03-23 00:58:11 +02:00
---
Git diff to review:
\`\`\`diff
2023-03-22 23:16:13 +02:00
${chunk.content}
${chunk.changes.map((c) => c.content).join("\n")}
\`\`\`
2023-03-22 23:16:13 +02:00
`;
}
async function getAIResponse(prompt: string): Promise<Array<{
lineNumber: string;
reviewComment: string;
}> | null> {
2023-03-22 23:16:13 +02:00
const queryConfig = {
model: "gpt-4",
temperature: 0.2,
max_tokens: 700,
2023-03-22 23:16:13 +02:00
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
};
try {
const response = await openai.createChatCompletion({
...queryConfig,
messages: [
{
role: "system",
content: prompt,
},
],
});
2023-03-28 00:36:30 +03:00
const res = response.data.choices[0].message?.content?.trim() || "[]";
return JSON.parse(res);
2023-03-22 23:16:13 +02:00
} catch (error) {
console.error("Error:", error);
return null;
}
}
function createComment(
file: File,
chunk: Chunk,
aiResponses: Array<{
lineNumber: string;
reviewComment: string;
}>
): Array<{ body: string; path: string; line: number }> {
return aiResponses.flatMap((aiResponse) => {
if (!file.to) {
return [];
}
2023-03-22 23:16:13 +02:00
return {
body: aiResponse.reviewComment,
2023-03-22 23:16:13 +02:00
path: file.to,
line: Number(aiResponse.lineNumber),
2023-03-22 23:16:13 +02:00
};
});
2023-03-22 23:16:13 +02:00
}
async function createReviewComment(
owner: string,
repo: string,
pull_number: number,
comments: Array<{ body: string; path: string; line: number }>
): Promise<void> {
await octokit.pulls.createReview({
owner,
repo,
pull_number,
comments,
event: "COMMENT",
});
}
(async function main() {
const prDetails = await getPRDetails();
const diff = await getDiff(
prDetails.owner,
prDetails.repo,
prDetails.pull_number
);
if (!diff) {
console.log("No diff found");
return;
}
const parsedDiff = parseDiff(diff);
const excludePatterns = core
.getInput("exclude")
.split(",")
.map((s) => s.trim());
const filteredDiff = parsedDiff.filter((file) => {
return !excludePatterns.some((pattern) =>
minimatch(file.to ?? "", pattern)
);
});
const comments = await analyzeCode(filteredDiff, prDetails);
2023-03-22 23:16:13 +02:00
if (comments.length > 0) {
await createReviewComment(
prDetails.owner,
prDetails.repo,
prDetails.pull_number,
comments
);
}
})().catch((error) => {
console.error("Error:", error);
process.exit(1);
});