Skip to content
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 #18

Merged
merged 6 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/code-review.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Code Reviewer

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'name' field should be a descriptive name for the workflow, not 'Code Reviewer'.

run-name: Action started by ${{ github.actor }}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'run-name' field is not a valid field in a workflow. Remove it.


on:
pull_request:
types:
- opened
- synchronize

permissions: write-all

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'permissions' field is not a valid field in a workflow. Remove it.


jobs:
code-review:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4

- name: Code Review the pull request
uses: ./code-review
with:
token: ${{ secrets.GITHUB_TOKEN }}
openai_key: ${{ secrets.OPENAI_KEY }}
max_tokens: 900
exclude: "**/*.json, **/*.md, **/*.g.dart"
append_prompt: |
- Give a maximum of 4 suggestions

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line number in the diff is incorrect. Please update it to line 28.

- Do not suggest code formatting issues.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line number in the diff is incorrect. Please update it to line 29.

- Do not suggest imports issues.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a newline at the end of the file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line number in the diff is incorrect. Please update it to line 30.

11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## <small>0.0.2 (2023-09-10)</small>
* chore: add example and fix typo ([a0cff7b](https://github.com/emanuel-braz/action-release/commit/a0cff7b))
## 0.1.0 (2023-11-09)
* feat: add ai code review

## <small>0.0.1 (2023-09-10)</small>
* initial commit, migrate repo ([769459d](https://github.com/emanuel-braz/action-release/commit/769459d))
## 0.0.2 (2023-09-10)
* chore: add example and fix typo

## 0.0.1 (2023-09-10)
* initial commit
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#### [Generate chat GPT message](simple-chat-gpt/README.md)

#### [Code review Pull Requests](code-review/README.md)

#### Soon more actions will be added to have a complete Gitflow utilities.:
- [ ] Manage PR
- [ ] Manage Issue
Expand Down
70 changes: 70 additions & 0 deletions code-review/README.md
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.
248 changes: 248 additions & 0 deletions code-review/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
require('child_process')
.execSync(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using npm ci instead of npm install to install dependencies.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more specific import statement for the SimpleChatGptService class.

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 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>"}]`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a template literal instead of concatenating strings.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable instructionJsonFormat is defined but not used. Consider removing it.


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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable contentSystemMessage is defined but not used. Consider removing it.

- You will provide suggestions only if there are issues or bugs in the code, otherwise return an empty array.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incorrect. It should be 'You will provide suggestions only if there are issues or bugs in the code, otherwise return an empty array.'

- Do not give positive comments or compliments.
- Don't suggest removing empty line
- Never suggest adding newline at end of file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incorrect. It should be 'Never suggest adding newline at end of file.'

- Never suggest to remove trailing or leading whitespace.
- Never suggest to remove the spaces.
- Don't suggest adding comment to code.
- If no issues are found, return an empty array.
- 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 }>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of createComment function should be an array, not an object.

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);
});
Loading