-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Code review #13
Code review #13
Changes from all commits
fbe8236
99824af
078c2a3
4ea08b5
f7e89f5
028ae69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: Code Reviewer | ||
run-name: Action started by ${{ github.actor }} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. O nome do trabalho em execução deve ser mais descritivo. |
||
|
||
on: | ||
pull_request: | ||
types: | ||
- opened | ||
- synchronize | ||
|
||
permissions: write-all | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As permissões 'write-all' são muito amplas. Considere restringir as permissões necessárias. |
||
|
||
jobs: | ||
code-review: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout Repo | ||
uses: actions/checkout@v4 | ||
|
||
- name: Code Review the pull request | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. O nome da etapa deve ser mais descritivo. |
||
uses: ./code-review | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
openai_key: ${{ secrets.OPENAI_KEY }} | ||
max_tokens: 900 | ||
exclude: "**/*.json, **/*.md, **/*.g.dart" | ||
append_prompt: | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A prompt de anexação deve ser mais clara e concisa. |
||
- Give a minimum of 0 suggestions and a maximum of 5 suggestions. | ||
- Do not suggest code formatting issues. | ||
- Translate the comment in all "reviewComment" properties to portuguese (pt-br). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# 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@v4 | ||
|
||
- name: AI Code Reviewer | ||
uses: emanuel-braz/github-actions/[email protected] | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
openai_key: ${{ secrets.OPENAI_KEY }} | ||
max_tokens: 900 | ||
exclude: "**/*.json, **/*.md, **/*.g.dart" # Optional: exclude patterns separated by commas | ||
append_prompt: | | ||
- Give a minimum of 0 suggestions and a maximum of 5 suggestions. | ||
- Translate the comment in all "reviewComment" properties to portuguese (pt-br). | ||
``` | ||
|
||
- Customize the `exclude` input if you want to ignore certain file patterns from being reviewed. | ||
|
||
- Commit the changes to your repository, and Code Reviewer Actions will start working on your future pull requests. | ||
|
||
## How It Works | ||
|
||
The 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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
require('child_process') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verifique se é necessário importar o módulo 'child_process'. |
||
.execSync( | ||
'npm install @actions/core @actions/github parse-diff minimatch fs', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verifique se é necessário instalar o pacote 'parse-diff'. |
||
{ 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verifique se é necessário importar o módulo '../services/simple_chat_gpt_service.js'. |
||
const ChatCompletionParams = require('../services/gpt/chat_completion_params.js'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verifique se é necessário importar o módulo '../services/gpt/chat_completion_params.js'. |
||
const Logger = require('../utils/logger.js'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verifique se é necessário importar o módulo '../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 maxTokens = core.getInput("max_tokens"); | ||
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) { | ||
const instructionJsonFormat = `- Always provide the response in following JSON format: [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}]`; | ||
|
||
var contentSystemMessage = `You are a senior software engineer and your task is to review pull requests for possible bugs or bad development practices. Follow the instructions below: | ||
- You will provide suggestions only if there is possible issues or bugs in the code, otherwise return an empty array. | ||
- Do not give positive comments or compliments. | ||
- Do not suggest removing empty line. | ||
- Do not suggest adding a new line at the end of the file. | ||
- Do not suggest to remove trailing or leading whitespace. | ||
- Do not suggest to remove the spaces. | ||
- Do not suggest adding comment to code. | ||
- Avoid giving an excessive amount of suggestions for a single file. Prioritize the most important suggestions. | ||
- Do use the given pull request title and description only for the overall context and only comment the code.`; | ||
|
||
if (overridePrompt) { | ||
contentSystemMessage = overridePrompt; | ||
} | ||
|
||
contentSystemMessage = `${contentSystemMessage}\n${instructionJsonFormat}`; | ||
|
||
if (appendPrompt) { | ||
contentSystemMessage = `${contentSystemMessage}\n\n${appendPrompt}`; | ||
} | ||
|
||
var systemPrompt = | ||
{ | ||
content: contentSystemMessage, | ||
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) { | ||
|
||
logger.log(`Max tokens: ${maxTokens}`); | ||
|
||
try { | ||
const chatCompletionParams = new ChatCompletionParams({ | ||
messages: messages, | ||
model: OPENAI_API_MODEL, | ||
temperature: 0, | ||
max_tokens: parseInt(maxTokens), | ||
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), | ||
}; | ||
}); | ||
} | ||
|
||
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); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
name: emanuel-braz/code-review | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. O nome do arquivo de ação deve ser 'action.yml', não 'code-review/action.yml'. |
||
description: "Perform code reviews and comment on diffs using OpenAI API." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A descrição da ação deve ser fornecida. |
||
author: Emanuel Braz | ||
branding: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A propriedade 'branding' está faltando. |
||
icon: send | ||
color: gray-dark | ||
inputs: | ||
token: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'token' deve ter uma descrição. |
||
description: "GitHub token to interact with the repository." | ||
required: true | ||
openai_key: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'openai_key' deve ter uma descrição. |
||
description: "OpenAI API key for GPT." | ||
required: true | ||
openai_key_model: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'openai_key_model' deve ter uma descrição. |
||
description: "OpenAI API model." | ||
required: false | ||
default: "gpt-3.5-turbo" | ||
max_tokens: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'max_tokens' deve ter uma descrição. |
||
description: "OpenAI API max tokens." | ||
default: "900" | ||
required: false | ||
exclude: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'exclude' deve ter uma descrição. |
||
description: "Glob patterns to exclude files from the diff analysis" | ||
required: false | ||
default: "" | ||
override_prompt: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'override_prompt' deve ter uma descrição. |
||
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." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A entrada 'append_prompt' deve ter uma descrição. |
||
required: false | ||
|
||
runs: | ||
using: "node16" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A propriedade 'runs' está faltando. |
||
main: ./action.js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
O nome do fluxo de trabalho deve ser mais descritivo.