diff --git a/.github/workflows/code-review.yaml b/.github/workflows/code-review.yaml
new file mode 100644
index 0000000..72a2bf3
--- /dev/null
+++ b/.github/workflows/code-review.yaml
@@ -0,0 +1,29 @@
+name: Code Reviewer
+run-name: Action started by ${{ github.actor }}
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+
+permissions: write-all
+
+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
+ - Do not suggest code formatting issues.
+ - Do not suggest imports issues.
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d708b0..ca78738 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
-## 0.0.2 (2023-09-10)
-* 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
-## 0.0.1 (2023-09-10)
-* initial commit, migrate repo ([769459d](https://github.com/emanuel-braz/action-release/commit/769459d))
\ No newline at end of file
+## 0.0.2 (2023-09-10)
+* chore: add example and fix typo
+
+## 0.0.1 (2023-09-10)
+* initial commit
\ No newline at end of file
diff --git a/README.md b/README.md
index b5884e6..c299c40 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/code-review/README.md b/code-review/README.md
new file mode 100644
index 0000000..0855313
--- /dev/null
+++ b/code-review/README.md
@@ -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/code-review@0.1.0
+ 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.
\ No newline at end of file
diff --git a/code-review/action.js b/code-review/action.js
new file mode 100644
index 0000000..f7bb873
--- /dev/null
+++ b/code-review/action.js
@@ -0,0 +1,247 @@
+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 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": , "reviewComment": ""}]`;
+
+ 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 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 or adding line to end of file.
+- Ignore "\\ No newline at end of file" information.
+- Don't suggest to remove trailing or leading whitespace.
+- Don't suggest to remove the spaces.
+- Don't suggest adding comment to code.
+- 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);
+});
diff --git a/code-review/action.yml b/code-review/action.yml
new file mode 100644
index 0000000..771dafa
--- /dev/null
+++ b/code-review/action.yml
@@ -0,0 +1,35 @@
+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"
+ max_tokens:
+ description: "OpenAI API max tokens."
+ default: "900"
+ required: false
+ 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
diff --git a/package.json b/package.json
index e726d7d..d9fb33c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "braz-actions",
- "version": "0.0.7",
+ "version": "0.1.0",
"private": true,
"description": "GitHub Actions",
"main": "create-update-release/action.js",
diff --git a/services/simple_chat_gpt_service.js b/services/simple_chat_gpt_service.js
index 0ceb1ed..b84e466 100644
--- a/services/simple_chat_gpt_service.js
+++ b/services/simple_chat_gpt_service.js
@@ -29,6 +29,18 @@ class SimpleChatGptService {
throw error;
}
}
+
+ // chatCompletionParams: ChatCompletionParams
+ async fromParams({ chatCompletionParams }) {
+ try {
+ const response = await this.service.chatCompletions(chatCompletionParams);
+ const message = response.choices[0].message.content;
+ return message;
+ } catch (error) {
+ console.error('[SimpleChatGptService]', error);
+ throw error;
+ }
+ }
}
module.exports = SimpleChatGptService;
\ No newline at end of file