-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b43dd24
commit fbe8236
Showing
5 changed files
with
364 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
name: Code Reviewer | ||
run-name: Action started by ${{ github.actor }} | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- opened | ||
- synchronize | ||
|
||
permissions: write-all | ||
|
||
jobs: | ||
review: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout Repo | ||
uses: actions/checkout@v3 | ||
|
||
- name: AI Code Reviewer | ||
uses: ./code-review | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
openai_key: ${{ secrets.OPENAI_KEY }} | ||
exclude: "**/*.json, **/*.md, **/*.g.dart" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Code Reviewer | ||
|
||
Code Reviewer is a GitHub Action that leverages OpenAI's GPT API to provide intelligent feedback and suggestions on | ||
your pull requests. This powerful tool helps improve code quality and saves developers time by automating the code | ||
review process. | ||
|
||
## Features | ||
|
||
- Reviews pull requests using OpenAI's chat GPT API. | ||
- Provides intelligent comments and suggestions for improving your code. | ||
- Filters out files that match specified exclude patterns. | ||
- Easy to set up and integrate into your GitHub workflow. | ||
|
||
## Setup | ||
|
||
1. To use this GitHub Action, you need an OpenAI API key. If you don't have one, sign up for an API key | ||
at [OpenAI](https://beta.openai.com/signup). | ||
|
||
2. Add the OpenAI API key as a GitHub Secret in your repository with the name `openai_key`. You can find more | ||
information about GitHub Secrets [here](https://docs.github.com/en/actions/reference/encrypted-secrets). | ||
|
||
3. Create a `.github/workflows/main.yml` file in your repository and add the following content: | ||
|
||
```yaml | ||
name: Code Reviewer | ||
run-name: Action started by ${{ github.actor }} | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- opened | ||
- synchronize | ||
|
||
permissions: write-all | ||
|
||
jobs: | ||
review: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout Repo | ||
uses: actions/checkout@v3 | ||
|
||
- name: AI Code Reviewer | ||
uses: emanuel-braz/github-actions/[email protected] | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
openai_key: ${{ secrets.OPENAI_KEY }} | ||
exclude: "**/*.json, **/*.md, **/*.g.dart" # Optional: exclude patterns separated by commas | ||
``` | ||
- Customize the `exclude` input if you want to ignore certain file patterns from being reviewed. | ||
|
||
- Commit the changes to your repository, and AI Code Reviewer will start working on your future pull requests. | ||
|
||
## How It Works | ||
|
||
The AI Code Reviewer GitHub Action retrieves the pull request diff, filters out excluded files, and sends code chunks to | ||
the OpenAI API. It then generates review comments based on the AI's response and adds them to the pull request. | ||
|
||
## Contributing | ||
|
||
Contributions are welcome! Please feel free to submit issues or pull requests to improve the GitHub Actions. | ||
|
||
## License | ||
|
||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
require('child_process') | ||
.execSync( | ||
'npm install @actions/core @actions/github parse-diff minimatch fs', | ||
{ cwd: __dirname } | ||
); | ||
|
||
let parse = require('parse-diff'); | ||
const minimatch = require('minimatch'); | ||
const core = require('@actions/core'); | ||
const github = require('@actions/github'); | ||
const fs = require('fs'); | ||
const SimpleChatGptService = require('../services/simple_chat_gpt_service.js'); | ||
const ChatCompletionParams = require('../services/gpt/chat_completion_params.js'); | ||
const Logger = require('../utils/logger.js'); | ||
|
||
const logger = new Logger(true, core); | ||
const GITHUB_TOKEN = core.getInput("token"); | ||
const OPENAI_API_KEY = core.getInput("openai_key"); | ||
const OPENAI_API_MODEL = core.getInput("openai_key_model"); | ||
const overridePrompt = core.getInput("override_prompt"); | ||
const appendPrompt = core.getInput("append_prompt"); | ||
const excludePatterns = core | ||
.getInput("exclude") | ||
.split(",") | ||
.map((s) => s.trim()); | ||
|
||
const octokit = github.getOctokit(GITHUB_TOKEN); | ||
|
||
async function getPRDetails() { | ||
const { repository, number } = JSON.parse( | ||
fs.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, repo, pull_number) { | ||
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, prDetails) { | ||
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) { | ||
const messages = createMessages(file, chunk, prDetails); | ||
const aiResponse = await getAIResponse(messages); | ||
if (aiResponse) { | ||
const newComments = createComment(file, chunk, aiResponse); | ||
if (newComments) { | ||
comments.push(...newComments); | ||
} | ||
} | ||
} | ||
} | ||
return comments; | ||
} | ||
|
||
function createMessages(file, chunk, prDetails) { | ||
let systemPrompt = | ||
{ | ||
content: `You are a senior software engineer and your task is to review pull requests. Folow the instructions below. | ||
- Provide the response in following JSON format: [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}] | ||
- You NEVER give positive comments or compliments. | ||
- You NEVER consider removing empty line. | ||
- You NEVER consider to remove trailing whitespace | ||
- You NEVER consider adding comment to describe the purpose of methods | ||
- You will Provide comments and suggestions ONLY if there is something to improve, otherwise return an empty array. | ||
- You must write the comment in GitHub Markdown format. | ||
- Do use the given description only for the overall context and only comment the code.`, | ||
role: "system", | ||
}; | ||
|
||
let userPrompt = | ||
{ | ||
content: `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")} | ||
\`\`\` | ||
`, | ||
role: "user", | ||
}; | ||
|
||
return [systemPrompt, userPrompt]; | ||
} | ||
|
||
async function getAIResponse(messages) { | ||
|
||
try { | ||
const chatCompletionParams = new ChatCompletionParams({ | ||
messages: messages, | ||
model: OPENAI_API_MODEL, | ||
temperature: 0.2, | ||
max_tokens: 700, | ||
top_p: 1, | ||
frequency_penalty: 0, | ||
presence_penalty: 0, | ||
}); | ||
|
||
const simpleChatGptService = new SimpleChatGptService(OPENAI_API_KEY); | ||
const response = await simpleChatGptService.fromParams({ chatCompletionParams }); | ||
|
||
const result = response?.trim() || "[]"; | ||
logger.log(`AI response: ${result}`); | ||
return JSON.parse(result); | ||
} catch (error) { | ||
console.error("Error:", error); | ||
return null; | ||
} | ||
} | ||
|
||
// Array<{ body: string; path: string; line: number }> | ||
function createComment(file, chunk, aiResponses) { | ||
return aiResponses.flatMap((aiResponse) => { | ||
if (!file.to) { | ||
return []; | ||
} | ||
return { | ||
body: aiResponse.reviewComment, | ||
path: file.to, | ||
line: Number(aiResponse.lineNumber), | ||
}; | ||
}); | ||
} | ||
|
||
// comments: Array<{ body: string; path: string; line: number }> | ||
// return Promise<void> | ||
async function createReviewComment(owner, repo, pull_number, comments) { | ||
await octokit.pulls.createReview({ | ||
owner, | ||
repo, | ||
pull_number, | ||
comments, | ||
event: "COMMENT", | ||
}); | ||
} | ||
|
||
async function main() { | ||
|
||
const prDetails = await getPRDetails(); | ||
let diff; // string | null | ||
const eventData = JSON.parse( | ||
fs.readFileSync(process.env.GITHUB_EVENT_PATH ?? "", "utf8") | ||
); | ||
|
||
if (eventData.action === "opened") { | ||
diff = await getDiff( | ||
prDetails.owner, | ||
prDetails.repo, | ||
prDetails.pull_number | ||
); | ||
} else if (eventData.action === "synchronize") { | ||
const newBaseSha = eventData.before; | ||
const newHeadSha = eventData.after; | ||
|
||
const response = await octokit.repos.compareCommits({ | ||
headers: { | ||
accept: "application/vnd.github.v3.diff", | ||
}, | ||
owner: prDetails.owner, | ||
repo: prDetails.repo, | ||
base: newBaseSha, | ||
head: newHeadSha, | ||
}); | ||
|
||
diff = String(response.data); | ||
} else { | ||
console.log("Unsupported event:", process.env.GITHUB_EVENT_NAME); | ||
return; | ||
} | ||
|
||
if (!diff) { | ||
console.log("No diff found"); | ||
return; | ||
} | ||
|
||
const parsedDiff = parse(diff); | ||
|
||
const filteredDiff = parsedDiff.filter((file) => { | ||
return !excludePatterns.some((pattern) => | ||
minimatch.minimatch(file.to ?? "", pattern) | ||
); | ||
}); | ||
|
||
const comments = await analyzeCode(filteredDiff, prDetails); | ||
if (comments.length > 0) { | ||
await createReviewComment( | ||
prDetails.owner, | ||
prDetails.repo, | ||
prDetails.pull_number, | ||
comments | ||
); | ||
} | ||
} | ||
|
||
main().catch((error) => { | ||
console.error("Error:", error); | ||
process.exit(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
name: emanuel-braz/code-review | ||
description: "Perform code reviews and comment on diffs using OpenAI API." | ||
author: Emanuel Braz | ||
branding: | ||
icon: send | ||
color: gray-dark | ||
inputs: | ||
token: | ||
description: "GitHub token to interact with the repository." | ||
required: true | ||
openai_key: | ||
description: "OpenAI API key for GPT." | ||
required: true | ||
openai_key_model: | ||
description: "OpenAI API model." | ||
required: false | ||
default: "gpt-3.5-turbo" | ||
exclude: | ||
description: "Glob patterns to exclude files from the diff analysis" | ||
required: false | ||
default: "" | ||
override_prompt: | ||
description: "The text to be used to override the default prompt." | ||
required: false | ||
append_prompt: | ||
description: "The text to be used to append to the default prompt." | ||
required: false | ||
|
||
runs: | ||
using: "node16" | ||
main: ./action.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters