diff --git a/src/analyzeCode.ts b/src/analyzeCode.ts new file mode 100644 index 0000000..dbd197d --- /dev/null +++ b/src/analyzeCode.ts @@ -0,0 +1,29 @@ +import {File} from "parse-diff"; +import {createPrompt, PRDetails} from "./createPrompt"; +import {getAIResponse} from "./getAIResponse"; +import {createComment} from "./createComment"; + +export async function analyzeCode( + parsedDiff: File[], + prDetails: PRDetails +): Promise> { + const comments: Array<{ body: string; path: string; line: number }> = []; + + for (const file of parsedDiff) { + if (file.to === "/dev/null") continue; // Ignore deleted files + for (const chunk of file.chunks) { + // Ignore diffs longer than 2000 characters + if (chunk.content.length > 4000) continue; + + const prompt = createPrompt(file, chunk, prDetails); + const aiResponse = await getAIResponse(prompt); + if (aiResponse) { + const newComments = createComment(file, chunk, aiResponse); + if (newComments) { + comments.push(...newComments); + } + } + } + } + return comments; +} diff --git a/src/createComment.ts b/src/createComment.ts new file mode 100644 index 0000000..44314cf --- /dev/null +++ b/src/createComment.ts @@ -0,0 +1,21 @@ +import {Chunk, File} from "parse-diff"; + +export 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 []; + } + return { + body: aiResponse.reviewComment, + path: file.to, + line: Number(aiResponse.lineNumber), + }; + }); +} diff --git a/src/createPrompt.ts b/src/createPrompt.ts new file mode 100644 index 0000000..d5fd302 --- /dev/null +++ b/src/createPrompt.ts @@ -0,0 +1,42 @@ +import {Chunk, File} from "parse-diff"; + +export interface PRDetails { + owner: string; + repo: string; + pull_number: number; + title: string; + description: string; +} + +export function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string { + return `Your task is to review pull requests. Instructions: +- Provide the response in following JSON format: {"reviews": [{"lineNumber": , "reviewComment": ""}]} +- Do not give positive comments or compliments. +- Provide review in Korean language. +- Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array. +- Write the comment in GitHub Markdown format. +- Use the given description only for the overall context and only comment the code. +- IMPORTANT: NEVER suggest adding comments to the code. + +Review the following code diff in the file "${ + file.to + }" and take the pull request title and description into account when writing the response. + +Pull request title: ${prDetails.title} +Pull request description: + +--- +${prDetails.description} +--- + +Git diff to review: + +\`\`\`diff +${chunk.content} +${chunk.changes + // @ts-expect-error - ln and ln2 exists where needed + .map((c) => `${c.ln ? c.ln : c.ln2} ${c.content}`) + .join("\n")} +\`\`\` +`; +} diff --git a/src/createReviewComment.ts b/src/createReviewComment.ts new file mode 100644 index 0000000..c779bdf --- /dev/null +++ b/src/createReviewComment.ts @@ -0,0 +1,20 @@ +import * as core from "@actions/core"; +import {Octokit} from "@octokit/rest"; + +const GITHUB_TOKEN: string = core.getInput("GITHUB_TOKEN"); +export const octokit = new Octokit({auth: GITHUB_TOKEN}); + +export async function createReviewComment( + owner: string, + repo: string, + pull_number: number, + comments: Array<{ body: string; path: string; line: number }> +): Promise { + await octokit.pulls.createReview({ + owner, + repo, + pull_number, + comments, + event: "COMMENT", + }); +} diff --git a/src/getAIResponse.ts b/src/getAIResponse.ts new file mode 100644 index 0000000..fcc19e2 --- /dev/null +++ b/src/getAIResponse.ts @@ -0,0 +1,48 @@ +import * as core from "@actions/core"; +import OpenAI from "openai"; + +const OPENAI_API_KEY: string = core.getInput("OPENAI_API_KEY"); +const OPENAI_API_MODEL: string = core.getInput("OPENAI_API_MODEL"); +const openai = new OpenAI({ + apiKey: OPENAI_API_KEY, + }); + +export async function getAIResponse(prompt: string): Promise | null> { + const queryConfig = { + model: OPENAI_API_MODEL, + temperature: 0.2, + max_tokens: 700, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + }; + + const response = + await openai.chat.completions.create( + { + ...queryConfig, + // return JSON if the model supports it: + ...(OPENAI_API_MODEL === "gpt-4-1106-preview" + ? {response_format: {type: "json_object"}} + : {}), + messages: [ + { + role: "system", + content: prompt, + }, + ], + }); + + const res = response.choices[0].message?.content?.trim() || "{}"; + console.log("GPT response:") + console.log(res); + + const cleanupResponse = res + .replace(/```json/g, "") + .replace(/```/g, ""); + + return JSON.parse(cleanupResponse).reviews; +} diff --git a/src/getDiff.ts b/src/getDiff.ts new file mode 100644 index 0000000..bf5ff55 --- /dev/null +++ b/src/getDiff.ts @@ -0,0 +1,16 @@ +import {octokit} from "./createReviewComment"; + +export async function getDiff( + owner: string, + repo: string, + pull_number: number +): Promise { + const response = await octokit.pulls.get({ + owner, + repo, + pull_number, + mediaType: {format: "diff"}, + }); + // @ts-expect-error - response.data is a string + return response.data; +} diff --git a/src/getPRDetails.ts b/src/getPRDetails.ts new file mode 100644 index 0000000..83a6d48 --- /dev/null +++ b/src/getPRDetails.ts @@ -0,0 +1,21 @@ +import {PRDetails} from "./createPrompt"; +import {readFileSync} from "fs"; +import {octokit} from "./createReviewComment"; + +export async function getPRDetails(): Promise { + const {repository, number} = JSON.parse( + readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8") + ); + const prResponse = await octokit.pulls.get({ + owner: repository.owner.login, + repo: repository.name, + pull_number: number, + }); + return { + owner: repository.owner.login, + repo: repository.name, + pull_number: number, + title: prResponse.data.title ?? "", + description: prResponse.data.body ?? "", + }; +} diff --git a/src/main.ts b/src/main.ts index 0702897..2912379 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,193 +1,11 @@ import {readFileSync} from "fs"; import * as core from "@actions/core"; -import OpenAI from "openai"; -import {Octokit} from "@octokit/rest"; -import parseDiff, {Chunk, File} from "parse-diff"; +import parseDiff 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 OPENAI_API_MODEL: string = core.getInput("OPENAI_API_MODEL"); - -const octokit = new Octokit({auth: GITHUB_TOKEN}); - -const openai = new OpenAI({ - apiKey: OPENAI_API_KEY, - }); - -interface PRDetails { - owner: string; - repo: string; - pull_number: number; - title: string; - description: string; -} - -async function getPRDetails(): Promise { - const {repository, number} = JSON.parse( - readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8") - ); - const prResponse = await octokit.pulls.get({ - owner: repository.owner.login, - repo: repository.name, - pull_number: number, - }); - return { - owner: repository.owner.login, - repo: repository.name, - pull_number: number, - title: prResponse.data.title ?? "", - description: prResponse.data.body ?? "", - }; -} - -async function getDiff( - owner: string, - repo: string, - pull_number: number -): Promise { - 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( - parsedDiff: File[], - prDetails: PRDetails -): Promise> { - const comments: Array<{ body: string; path: string; line: number }> = []; - - for (const file of parsedDiff) { - if (file.to === "/dev/null") continue; // Ignore deleted files - for (const chunk of file.chunks) { - // Ignore diffs longer than 2000 characters - if (chunk.content.length > 4000) continue; - - const prompt = createPrompt(file, chunk, prDetails); - const aiResponse = await getAIResponse(prompt); - if (aiResponse) { - const newComments = createComment(file, chunk, aiResponse); - if (newComments) { - comments.push(...newComments); - } - } - } - } - return comments; -} - -function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string { - return `Your task is to review pull requests. Instructions: -- Provide the response in following JSON format: {"reviews": [{"lineNumber": , "reviewComment": ""}]} -- Do not give positive comments or compliments. -- Provide review in Korean language. -- Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array. -- Write the comment in GitHub Markdown format. -- Use the given description only for the overall context and only comment the code. -- IMPORTANT: NEVER suggest adding comments to the code. - -Review the following code diff in the file "${ - file.to - }" and take the pull request title and description into account when writing the response. - -Pull request title: ${prDetails.title} -Pull request description: - ---- -${prDetails.description} ---- - -Git diff to review: - -\`\`\`diff -${chunk.content} -${chunk.changes - // @ts-expect-error - ln and ln2 exists where needed - .map((c) => `${c.ln ? c.ln : c.ln2} ${c.content}`) - .join("\n")} -\`\`\` -`; -} - -async function getAIResponse(prompt: string): Promise | null> { - const queryConfig = { - model: OPENAI_API_MODEL, - temperature: 0.2, - max_tokens: 700, - top_p: 1, - frequency_penalty: 0, - presence_penalty: 0, - }; - - const response = - await openai.chat.completions.create( - { - ...queryConfig, - // return JSON if the model supports it: - ...(OPENAI_API_MODEL === "gpt-4-1106-preview" - ? {response_format: {type: "json_object"}} - : {}), - messages: [ - { - role: "system", - content: prompt, - }, - ], - }); - - const res = response.choices[0].message?.content?.trim() || "{}"; - console.log("GPT response:") - console.log(res); - - const cleanupResponse = res - .replace(/```json/g, "") - .replace(/```/g, ""); - - return JSON.parse(cleanupResponse).reviews; -} - -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 []; - } - return { - body: aiResponse.reviewComment, - path: file.to, - line: Number(aiResponse.lineNumber), - }; - }); -} - -async function createReviewComment( - owner: string, - repo: string, - pull_number: number, - comments: Array<{ body: string; path: string; line: number }> -): Promise { - await octokit.pulls.createReview({ - owner, - repo, - pull_number, - comments, - event: "COMMENT", - }); -} +import {createReviewComment, octokit} from "./createReviewComment"; +import {analyzeCode} from "./analyzeCode"; +import {getPRDetails} from "./getPRDetails"; +import {getDiff} from "./getDiff"; async function main() { const prDetails = await getPRDetails();