mirror of
https://github.com/freeedcom/ai-codereviewer.git
synced 2025-04-20 09:36:47 +00:00
Merge pull request #11 from cds-snc/feature/enable-action-on-commit-msg
Re-enable the ai review action with a few conditions
This commit is contained in:
commit
bb1ea9dcf3
6 changed files with 505 additions and 124 deletions
12
.github/workflows/code_review.yml
vendored
12
.github/workflows/code_review.yml
vendored
|
@ -1,12 +1,14 @@
|
|||
name: CDS Code Review with OpenAI
|
||||
# on:
|
||||
# pull_request:
|
||||
# types:
|
||||
# - opened
|
||||
# - synchronize
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
code_review:
|
||||
if: contains(github.event.head_commit.message, '[review]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
49
README.md
49
README.md
|
@ -14,12 +14,12 @@ review process.
|
|||
## 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).
|
||||
at [OpenAI](https://beta.openai.com/signup) or via the Azure OpenAI API.
|
||||
|
||||
1. Add the OpenAI API key as a GitHub Secret in your repository with the name `OPENAI_API_KEY`. You can find more
|
||||
2. Add the OpenAI API key as a GitHub Secret in your repository with the name `OPENAI_API_KEY`. You can find more
|
||||
information about GitHub Secrets [here](https://docs.github.com/en/actions/reference/encrypted-secrets).
|
||||
|
||||
1. Create a `.github/workflows/main.yml` file in your repository and add the following content:
|
||||
3. a. Create a `.github/workflows/main.yml` file in your repository and add the following content, if you want the AI review to trigger on every opened PR and corresponding updates:
|
||||
|
||||
```yaml
|
||||
name: CDS AI Code Reviewer
|
||||
|
@ -43,20 +43,53 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret)
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_MODEL: "gpt-4" # Optional: defaults to "gpt-4"
|
||||
OPENAI_API_VERSION: ${{ vars.OPENAI_API_VERSION }}
|
||||
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
||||
exclude: "**/*.json, **/*.md" # Optional: exclude patterns separated by commas
|
||||
include: "**/*.ts" # Optional: include patterns separated by commas
|
||||
```
|
||||
|
||||
1. Customize the `exclude` input if you want to ignore certain file patterns from being reviewed.
|
||||
3. b. For reduced cost with OpenAI, a different configuration is possible on the action. This will be a manual trigger on commits message that contains the `[review]` keyword. The action will process the entirety of the related pull request for which the commit was pushed for. Ideally, you want to use this trigger once when the pull request is ready. Copy the configuration below:
|
||||
|
||||
1. Customize the `include` input if you want to add only certain file patterns to be reviewed. Any file matching the include and which also matches the `exclude` will favor the latter: `exclude` > `include`.
|
||||
```yaml
|
||||
name: CDS Code Review with OpenAI
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
|
||||
1. Commit the changes to your repository, and CDS AI Code Reviewer will start working on your future pull requests.
|
||||
permissions: write-all
|
||||
jobs:
|
||||
code_review:
|
||||
if: contains(github.event.head_commit.message, '[review]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: CDS AI Code Reviewer
|
||||
uses: cds-snc/cds-ai-code-reviewer@main
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret)
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_MODEL: "gpt-4" # Optional: defaults to "gpt-4"
|
||||
OPENAI_API_VERSION: ${{ vars.OPENAI_API_VERSION }}
|
||||
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
||||
exclude: "**/*.json, **/*.md" # Optional: exclude patterns separated by commas
|
||||
include: "**/*.ts" # Optional: include patterns separated by commas
|
||||
```
|
||||
|
||||
4. Customize the `exclude` input if you want to ignore certain file patterns from being reviewed.
|
||||
|
||||
5. Customize the `include` input if you want to add only certain file patterns to be reviewed. Any file matching the include and which also matches the `exclude` will favor the latter: `exclude` > `include`.
|
||||
|
||||
6. Commit the changes to your repository, and CDS AI Code Reviewer will start working on your future pull requests.
|
||||
|
||||
## How It Works
|
||||
|
||||
The CDS 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.
|
||||
When used on `opened` and `synchronize` Github action triggers, the CDS 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.
|
||||
|
||||
When used on the `push` Github action trigger, the CDS AI Code Reviewer GitHub Action checks if a commit message contains the `[review]` keyword and when it does, it then retrieves the pull request related to the commit message, 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
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ./
|
||||
- name: Code Review
|
||||
|
|
268
dist/index.js
vendored
268
dist/index.js
vendored
|
@ -157,12 +157,48 @@ require("./sourcemap-register.js");
|
|||
defaultQuery: { "api-version": OPENAI_API_VERSION },
|
||||
defaultHeaders: { "api-key": OPENAI_API_KEY },
|
||||
});
|
||||
function getPRDetails() {
|
||||
/**
|
||||
* Retrieves pull request details based on the GitHub event type.
|
||||
*
|
||||
* This function reads the GitHub event data from the environment, logs it for debugging purposes,
|
||||
* and then determines the appropriate method to fetch pull request details based on the event type.
|
||||
* It supports the "opened", "synchronize", and "push" events.
|
||||
*
|
||||
* @param {GitHubEvent} event - The type of GitHub event ("opened", "synchronize", or "push").
|
||||
* @returns {Promise<PRDetails>} - A promise that resolves to the pull request details.
|
||||
*
|
||||
* @throws {Error} - Throws an error if the event type is unsupported.
|
||||
*/
|
||||
function getPrDetails(eventName, eventData) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const eventPath = process.env.GITHUB_EVENT_PATH || "";
|
||||
switch (eventName) {
|
||||
case "opened":
|
||||
case "synchronize":
|
||||
return getPrFromEvent(eventData);
|
||||
case "push":
|
||||
return getPrFromApi(eventData);
|
||||
default:
|
||||
throw new Error(`Unsupported event: action=${eventName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Retrieves pull request details from the given event data.
|
||||
*
|
||||
* @param eventData - The event data containing repository and pull request number.
|
||||
* @returns A promise that resolves to the pull request details.
|
||||
* @throws Will throw an error if the event payload is missing the repository or number.
|
||||
*/
|
||||
function getPrFromEvent(eventData) {
|
||||
var _a, _b;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const { repository, number } = JSON.parse(
|
||||
(0, fs_1.readFileSync)(process.env.GITHUB_EVENT_PATH || "", "utf8")
|
||||
const { repository, number } = eventData;
|
||||
if (!repository || !number) {
|
||||
throw new Error(
|
||||
"Invalid event payload: missing repository or number"
|
||||
);
|
||||
}
|
||||
const prResponse = yield octokit.pulls.get({
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
|
@ -179,6 +215,53 @@ require("./sourcemap-register.js");
|
|||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Retrieves the pull request details associated with a given push event from the GitHub API.
|
||||
*
|
||||
* @param eventData - The event data containing information about the push event.
|
||||
* @returns A promise that resolves to the details of the associated pull request.
|
||||
* @throws Will throw an error if no associated pull request is found for the given push event.
|
||||
*/
|
||||
function getPrFromApi(eventData) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const branchName = eventData.ref.replace("refs/heads/", "");
|
||||
const repoOwner = eventData.repository.owner.login;
|
||||
const repoName = eventData.repository.name;
|
||||
const { data: pullRequests } = yield octokit.pulls.list({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
state: "open",
|
||||
});
|
||||
const pullRequest = pullRequests.find(
|
||||
(pr) => pr.head.ref === branchName
|
||||
);
|
||||
if (!pullRequest) {
|
||||
throw new Error(
|
||||
"No associated pull request found for this push event."
|
||||
);
|
||||
}
|
||||
return {
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
pull_number: pullRequest.number,
|
||||
title: pullRequest.title,
|
||||
description:
|
||||
(_a = pullRequest.body) !== null && _a !== void 0 ? _a : "",
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Fetches the diff of a pull request from the GitHub repository.
|
||||
*
|
||||
* This function uses the GitHub API to retrieve the diff of a specified pull request.
|
||||
* The diff is returned as a string, or null if the request fails.
|
||||
*
|
||||
* @param {string} owner - The owner of the repository.
|
||||
* @param {string} repo - The name of the repository.
|
||||
* @param {number} pull_number - The number of the pull request.
|
||||
* @returns {Promise<string | null>} - A promise that resolves to the diff string or null if the request fails.
|
||||
*/
|
||||
function getDiff(owner, repo, pull_number) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const response = yield octokit.pulls.get({
|
||||
|
@ -191,6 +274,18 @@ require("./sourcemap-register.js");
|
|||
return response.data;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Analyzes the parsed diff files and generates review comments using AI.
|
||||
*
|
||||
* This function iterates over the parsed diff files and their respective chunks,
|
||||
* identifies valid line numbers for changes, and generates a prompt for the AI to review the code.
|
||||
* It then filters the AI responses to ensure they correspond to valid line numbers and creates
|
||||
* review comments based on the AI responses.
|
||||
*
|
||||
* @param {File[]} parsedDiff - An array of parsed diff files to be analyzed.
|
||||
* @param {PRDetails} prDetails - Details of the pull request, including owner, repo, pull number, title, and description.
|
||||
* @returns {Promise<Array<{ body: string; path: string; line: number }>>} - A promise that resolves to an array of review comments.
|
||||
*/
|
||||
function analyzeCode(parsedDiff, prDetails) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const comments = [];
|
||||
|
@ -243,6 +338,14 @@ require("./sourcemap-register.js");
|
|||
return comments;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Generates a prompt string for reviewing pull requests based on the provided file, chunk, and PR details.
|
||||
*
|
||||
* @param {File} file - The file object containing information about the file being reviewed.
|
||||
* @param {Chunk} chunk - The chunk object containing the diff content and changes.
|
||||
* @param {PRDetails} prDetails - The pull request details including title and description.
|
||||
* @returns {string} The generated prompt string for the review task.
|
||||
*/
|
||||
function createPrompt(file, chunk, prDetails) {
|
||||
return `Your task is to review pull requests. Instructions:
|
||||
- Provide the response in following JSON format: {"reviews": [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}]}
|
||||
|
@ -274,6 +377,16 @@ ${chunk.changes
|
|||
\`\`\`
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* Fetches AI-generated review comments for a given prompt.
|
||||
*
|
||||
* @param prompt - The input string to be sent to the AI model for generating responses.
|
||||
* @returns A promise that resolves to an array of review comments, each containing a line number and a review comment, or null if an error occurs.
|
||||
*
|
||||
* The function configures the query parameters for the AI model, sends the prompt to the model, and parses the response.
|
||||
* If the model supports JSON responses, it requests the response in JSON format.
|
||||
* In case of an error during the API call, it logs the error and returns null.
|
||||
*/
|
||||
function getAIResponse(prompt) {
|
||||
var _a, _b;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
|
@ -314,11 +427,19 @@ ${chunk.changes
|
|||
: _b.trim()) || "{}";
|
||||
return JSON.parse(res).reviews;
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
console.error("Could not fetch AI response:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Generates an array of review comments for a given file and chunk based on AI responses.
|
||||
*
|
||||
* @param file - The file object containing information about the file being reviewed.
|
||||
* @param chunk - The chunk object representing a portion of the file.
|
||||
* @param aiResponses - An array of AI response objects, each containing a line number and a review comment.
|
||||
* @returns An array of objects, each representing a review comment with a body, path, and line number.
|
||||
*/
|
||||
function createComment(file, chunk, aiResponses) {
|
||||
return aiResponses.flatMap((aiResponse) => {
|
||||
if (!file.to) {
|
||||
|
@ -331,60 +452,46 @@ ${chunk.changes
|
|||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates a review comment on a pull request.
|
||||
*
|
||||
* @param owner - The owner of the repository.
|
||||
* @param repo - The name of the repository.
|
||||
* @param pull_number - The number of the pull request.
|
||||
* @param comments - An array of comment objects, each containing:
|
||||
* - `body`: The text of the comment.
|
||||
* - `path`: The file path to which the comment applies.
|
||||
* - `line`: The line number in the file where the comment should be placed.
|
||||
* @returns A promise that resolves when the review comment has been created.
|
||||
*/
|
||||
function createReviewComment(owner, repo, pull_number, comments) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const validComments = comments.filter((comment) => comment.line > 0);
|
||||
if (validComments.length === 0) {
|
||||
console.log("No valid comments to post.");
|
||||
return;
|
||||
}
|
||||
yield octokit.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
comments,
|
||||
comments: validComments,
|
||||
event: "COMMENT",
|
||||
});
|
||||
});
|
||||
}
|
||||
function main() {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const prDetails = yield getPRDetails();
|
||||
let diff;
|
||||
const eventData = JSON.parse(
|
||||
(0, fs_1.readFileSync)(
|
||||
(_a = process.env.GITHUB_EVENT_PATH) !== null && _a !== void 0
|
||||
? _a
|
||||
: "",
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
if (eventData.action === "opened") {
|
||||
diff = yield getDiff(
|
||||
prDetails.owner,
|
||||
prDetails.repo,
|
||||
prDetails.pull_number
|
||||
);
|
||||
} else if (eventData.action === "synchronize") {
|
||||
const newBaseSha = eventData.before;
|
||||
const newHeadSha = eventData.after;
|
||||
const response = yield 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: action=${eventData.action}, process.env.GITHUB_EVENT_NAME=${process.env.GITHUB_EVENT_NAME}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!diff) {
|
||||
console.log("No diff found");
|
||||
return;
|
||||
}
|
||||
const parsedDiff = (0, parse_diff_1.default)(diff);
|
||||
/**
|
||||
* Filters the parsed diff files based on include and exclude patterns.
|
||||
*
|
||||
* This function reads the `exclude` and `include` patterns from the GitHub Action inputs,
|
||||
* trims and filters out any empty strings, and then applies these patterns to the parsed diff files.
|
||||
* Files that match the exclude patterns are excluded, and files that match the include patterns are included.
|
||||
* If both patterns are provided, the exclude patterns take precedence over the include patterns.
|
||||
*
|
||||
* @param {File[]} parsedDiff - An array of parsed diff files to be filtered.
|
||||
* @returns {File[]} - An array of filtered diff files.
|
||||
*/
|
||||
function filterDiffs(parsedDiff) {
|
||||
const excludePatterns = core
|
||||
.getInput("exclude")
|
||||
.split(",")
|
||||
|
@ -417,8 +524,73 @@ ${chunk.changes
|
|||
// Excluded patterns take precedence over included patterns.
|
||||
return !excluded && included;
|
||||
});
|
||||
return filteredDiff;
|
||||
}
|
||||
function main() {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const eventData = JSON.parse(
|
||||
(0, fs_1.readFileSync)(
|
||||
(_a = process.env.GITHUB_EVENT_PATH) !== null && _a !== void 0
|
||||
? _a
|
||||
: "",
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
const eventName = process.env.GITHUB_EVENT_NAME;
|
||||
const prDetails = yield getPrDetails(eventName, eventData);
|
||||
if (!prDetails) {
|
||||
console.log(
|
||||
"No associated pull request found for this push event."
|
||||
);
|
||||
return;
|
||||
}
|
||||
let diff;
|
||||
switch (eventName) {
|
||||
case "opened":
|
||||
case "push":
|
||||
diff = yield getDiff(
|
||||
prDetails.owner,
|
||||
prDetails.repo,
|
||||
prDetails.pull_number
|
||||
);
|
||||
break;
|
||||
case "synchronize":
|
||||
const newBaseSha = eventData.before;
|
||||
const newHeadSha = eventData.after;
|
||||
const response = yield octokit.repos.compareCommits({
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3.diff",
|
||||
},
|
||||
owner: prDetails.owner,
|
||||
repo: prDetails.repo,
|
||||
base: newBaseSha,
|
||||
head: newHeadSha,
|
||||
});
|
||||
diff = String(response.data);
|
||||
break;
|
||||
default:
|
||||
console.log(
|
||||
`Unsupported event: eventName=${eventName}, process.env.GITHUB_EVENT_NAME=${process.env.GITHUB_EVENT_NAME}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!diff) {
|
||||
console.log("No diff found");
|
||||
return;
|
||||
}
|
||||
const parsedDiff = (0, parse_diff_1.default)(diff);
|
||||
const filteredDiff = filterDiffs(parsedDiff);
|
||||
const comments = yield analyzeCode(filteredDiff, prDetails);
|
||||
if (comments.length > 0) {
|
||||
// We want to log the comments to be posted for debugging purposes, as
|
||||
// we see errors when used in the actual workflow but cannot figure out
|
||||
// why without seeing these logged comments.
|
||||
comments.forEach((comment) => {
|
||||
console.log(
|
||||
`Comment to be posted: ${comment.body} at ${comment.path}:${comment.line}`
|
||||
);
|
||||
});
|
||||
yield createReviewComment(
|
||||
prDetails.owner,
|
||||
prDetails.repo,
|
||||
|
|
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
270
src/main.ts
270
src/main.ts
|
@ -59,6 +59,10 @@ const openai = new OpenAI({
|
|||
defaultHeaders: { "api-key": OPENAI_API_KEY },
|
||||
});
|
||||
|
||||
// The supported Github events that this Github action can handle.
|
||||
type GitHubEvent = "opened" | "synchronize" | "push";
|
||||
|
||||
// Data structure to host the details of a pull request.
|
||||
interface PRDetails {
|
||||
owner: string;
|
||||
repo: string;
|
||||
|
@ -67,10 +71,47 @@ interface PRDetails {
|
|||
description: string;
|
||||
}
|
||||
|
||||
async function getPRDetails(): Promise<PRDetails> {
|
||||
const { repository, number } = JSON.parse(
|
||||
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
|
||||
);
|
||||
/**
|
||||
* Retrieves pull request details based on the GitHub event type.
|
||||
*
|
||||
* This function reads the GitHub event data from the environment, logs it for debugging purposes,
|
||||
* and then determines the appropriate method to fetch pull request details based on the event type.
|
||||
* It supports the "opened", "synchronize", and "push" events.
|
||||
*
|
||||
* @param {GitHubEvent} event - The type of GitHub event ("opened", "synchronize", or "push").
|
||||
* @returns {Promise<PRDetails>} - A promise that resolves to the pull request details.
|
||||
*
|
||||
* @throws {Error} - Throws an error if the event type is unsupported.
|
||||
*/
|
||||
async function getPrDetails(
|
||||
eventName: GitHubEvent,
|
||||
eventData: any
|
||||
): Promise<PRDetails> {
|
||||
const eventPath = process.env.GITHUB_EVENT_PATH || "";
|
||||
switch (eventName) {
|
||||
case "opened":
|
||||
case "synchronize":
|
||||
return getPrFromEvent(eventData);
|
||||
case "push":
|
||||
return getPrFromApi(eventData);
|
||||
default:
|
||||
throw new Error(`Unsupported event: action=${eventName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves pull request details from the given event data.
|
||||
*
|
||||
* @param eventData - The event data containing repository and pull request number.
|
||||
* @returns A promise that resolves to the pull request details.
|
||||
* @throws Will throw an error if the event payload is missing the repository or number.
|
||||
*/
|
||||
async function getPrFromEvent(eventData: any): Promise<PRDetails> {
|
||||
const { repository, number } = eventData;
|
||||
if (!repository || !number) {
|
||||
throw new Error("Invalid event payload: missing repository or number");
|
||||
}
|
||||
|
||||
const prResponse = await octokit.pulls.get({
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
|
@ -85,6 +126,50 @@ async function getPRDetails(): Promise<PRDetails> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the pull request details associated with a given push event from the GitHub API.
|
||||
*
|
||||
* @param eventData - The event data containing information about the push event.
|
||||
* @returns A promise that resolves to the details of the associated pull request.
|
||||
* @throws Will throw an error if no associated pull request is found for the given push event.
|
||||
*/
|
||||
async function getPrFromApi(eventData: any): Promise<PRDetails> {
|
||||
const branchName = eventData.ref.replace("refs/heads/", "");
|
||||
const repoOwner = eventData.repository.owner.login;
|
||||
const repoName = eventData.repository.name;
|
||||
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
state: "open",
|
||||
});
|
||||
|
||||
const pullRequest = pullRequests.find((pr) => pr.head.ref === branchName);
|
||||
|
||||
if (!pullRequest) {
|
||||
throw new Error("No associated pull request found for this push event.");
|
||||
}
|
||||
|
||||
return {
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
pull_number: pullRequest.number,
|
||||
title: pullRequest.title,
|
||||
description: pullRequest.body ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the diff of a pull request from the GitHub repository.
|
||||
*
|
||||
* This function uses the GitHub API to retrieve the diff of a specified pull request.
|
||||
* The diff is returned as a string, or null if the request fails.
|
||||
*
|
||||
* @param {string} owner - The owner of the repository.
|
||||
* @param {string} repo - The name of the repository.
|
||||
* @param {number} pull_number - The number of the pull request.
|
||||
* @returns {Promise<string | null>} - A promise that resolves to the diff string or null if the request fails.
|
||||
*/
|
||||
async function getDiff(
|
||||
owner: string,
|
||||
repo: string,
|
||||
|
@ -100,6 +185,18 @@ async function getDiff(
|
|||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the parsed diff files and generates review comments using AI.
|
||||
*
|
||||
* This function iterates over the parsed diff files and their respective chunks,
|
||||
* identifies valid line numbers for changes, and generates a prompt for the AI to review the code.
|
||||
* It then filters the AI responses to ensure they correspond to valid line numbers and creates
|
||||
* review comments based on the AI responses.
|
||||
*
|
||||
* @param {File[]} parsedDiff - An array of parsed diff files to be analyzed.
|
||||
* @param {PRDetails} prDetails - Details of the pull request, including owner, repo, pull number, title, and description.
|
||||
* @returns {Promise<Array<{ body: string; path: string; line: number }>>} - A promise that resolves to an array of review comments.
|
||||
*/
|
||||
async function analyzeCode(
|
||||
parsedDiff: File[],
|
||||
prDetails: PRDetails
|
||||
|
@ -146,6 +243,14 @@ async function analyzeCode(
|
|||
return comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a prompt string for reviewing pull requests based on the provided file, chunk, and PR details.
|
||||
*
|
||||
* @param {File} file - The file object containing information about the file being reviewed.
|
||||
* @param {Chunk} chunk - The chunk object containing the diff content and changes.
|
||||
* @param {PRDetails} prDetails - The pull request details including title and description.
|
||||
* @returns {string} The generated prompt string for the review task.
|
||||
*/
|
||||
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": <line_number>, "reviewComment": "<review comment>"}]}
|
||||
|
@ -178,6 +283,16 @@ ${chunk.changes
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches AI-generated review comments for a given prompt.
|
||||
*
|
||||
* @param prompt - The input string to be sent to the AI model for generating responses.
|
||||
* @returns A promise that resolves to an array of review comments, each containing a line number and a review comment, or null if an error occurs.
|
||||
*
|
||||
* The function configures the query parameters for the AI model, sends the prompt to the model, and parses the response.
|
||||
* If the model supports JSON responses, it requests the response in JSON format.
|
||||
* In case of an error during the API call, it logs the error and returns null.
|
||||
*/
|
||||
async function getAIResponse(prompt: string): Promise<Array<{
|
||||
lineNumber: string;
|
||||
reviewComment: string;
|
||||
|
@ -210,11 +325,19 @@ async function getAIResponse(prompt: string): Promise<Array<{
|
|||
const res = response.choices[0].message?.content?.trim() || "{}";
|
||||
return JSON.parse(res).reviews;
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
console.error("Could not fetch AI response:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of review comments for a given file and chunk based on AI responses.
|
||||
*
|
||||
* @param file - The file object containing information about the file being reviewed.
|
||||
* @param chunk - The chunk object representing a portion of the file.
|
||||
* @param aiResponses - An array of AI response objects, each containing a line number and a review comment.
|
||||
* @returns An array of objects, each representing a review comment with a body, path, and line number.
|
||||
*/
|
||||
function createComment(
|
||||
file: File,
|
||||
chunk: Chunk,
|
||||
|
@ -235,63 +358,51 @@ function createComment(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a review comment on a pull request.
|
||||
*
|
||||
* @param owner - The owner of the repository.
|
||||
* @param repo - The name of the repository.
|
||||
* @param pull_number - The number of the pull request.
|
||||
* @param comments - An array of comment objects, each containing:
|
||||
* - `body`: The text of the comment.
|
||||
* - `path`: The file path to which the comment applies.
|
||||
* - `line`: The line number in the file where the comment should be placed.
|
||||
* @returns A promise that resolves when the review comment has been created.
|
||||
*/
|
||||
async function createReviewComment(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pull_number: number,
|
||||
comments: Array<{ body: string; path: string; line: number }>
|
||||
): Promise<void> {
|
||||
const validComments = comments.filter((comment) => comment.line > 0);
|
||||
if (validComments.length === 0) {
|
||||
console.log("No valid comments to post.");
|
||||
return;
|
||||
}
|
||||
|
||||
await octokit.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
comments,
|
||||
comments: validComments,
|
||||
event: "COMMENT",
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const prDetails = await getPRDetails();
|
||||
let diff: string | null;
|
||||
const eventData = JSON.parse(
|
||||
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: action=${eventData.action}, process.env.GITHUB_EVENT_NAME=${process.env.GITHUB_EVENT_NAME}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!diff) {
|
||||
console.log("No diff found");
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedDiff = parseDiff(diff);
|
||||
|
||||
/**
|
||||
* Filters the parsed diff files based on include and exclude patterns.
|
||||
*
|
||||
* This function reads the `exclude` and `include` patterns from the GitHub Action inputs,
|
||||
* trims and filters out any empty strings, and then applies these patterns to the parsed diff files.
|
||||
* Files that match the exclude patterns are excluded, and files that match the include patterns are included.
|
||||
* If both patterns are provided, the exclude patterns take precedence over the include patterns.
|
||||
*
|
||||
* @param {File[]} parsedDiff - An array of parsed diff files to be filtered.
|
||||
* @returns {File[]} - An array of filtered diff files.
|
||||
*/
|
||||
function filterDiffs(parsedDiff: File[]): File[] {
|
||||
const excludePatterns = core
|
||||
.getInput("exclude")
|
||||
.split(",")
|
||||
|
@ -317,8 +428,71 @@ async function main() {
|
|||
return !excluded && included;
|
||||
});
|
||||
|
||||
return filteredDiff;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const eventData = JSON.parse(
|
||||
readFileSync(process.env.GITHUB_EVENT_PATH ?? "", "utf8")
|
||||
);
|
||||
const eventName = process.env.GITHUB_EVENT_NAME as GitHubEvent;
|
||||
|
||||
const prDetails = await getPrDetails(eventName, eventData);
|
||||
if (!prDetails) {
|
||||
console.log("No associated pull request found for this push event.");
|
||||
return;
|
||||
}
|
||||
|
||||
let diff: string | null;
|
||||
switch (eventName) {
|
||||
case "opened":
|
||||
case "push":
|
||||
diff = await getDiff(
|
||||
prDetails.owner,
|
||||
prDetails.repo,
|
||||
prDetails.pull_number
|
||||
);
|
||||
break;
|
||||
case "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);
|
||||
break;
|
||||
default:
|
||||
console.log(
|
||||
`Unsupported event: eventName=${eventName}, process.env.GITHUB_EVENT_NAME=${process.env.GITHUB_EVENT_NAME}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!diff) {
|
||||
console.log("No diff found");
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedDiff = parseDiff(diff);
|
||||
const filteredDiff = filterDiffs(parsedDiff);
|
||||
|
||||
const comments = await analyzeCode(filteredDiff, prDetails);
|
||||
if (comments.length > 0) {
|
||||
// We want to log the comments to be posted for debugging purposes, as
|
||||
// we see errors when used in the actual workflow but cannot figure out
|
||||
// why without seeing these logged comments.
|
||||
comments.forEach((comment) => {
|
||||
console.log(
|
||||
`Comment to be posted: ${comment.body} at ${comment.path}:${comment.line}`
|
||||
);
|
||||
});
|
||||
|
||||
await createReviewComment(
|
||||
prDetails.owner,
|
||||
prDetails.repo,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue