Support push trigger + plenty refactoring [review]

This commit is contained in:
Jimmy Royer 2024-09-18 17:20:23 -04:00
parent 65cf847966
commit e8074efc8b
3 changed files with 442 additions and 134 deletions

View file

@ -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,18 +71,48 @@ interface PRDetails {
description: string;
}
async function getPRDetails(): Promise<PRDetails> {
console.log("Fetching pull request details...");
console.log("GITHUB_EVENT_PATH:", process.env.GITHUB_EVENT_PATH);
console.log("GITHUB_EVENT_NAME:", process.env.GITHUB_EVENT_NAME);
const eventData = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
console.log("Github event data:", eventData);
/**
* 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.
*
* Example usage:
* const prDetails = await getPrDetails("opened");
* console.log(prDetails);
*/
async function getPrDetails(event: GitHubEvent): Promise<PRDetails> {
const eventPath = process.env.GITHUB_EVENT_PATH || "";
const eventData = JSON.parse(readFileSync(eventPath, "utf8"));
const { repository, number } = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
console.log("GitHub Event Data:", eventData); // Log the event data for debugging
switch (event) {
case "opened":
case "synchronize":
return getPrFromEvent(eventData);
case "push":
return getPrFromApi(eventData);
default:
throw new Error(`Unsupported event: ${event}`);
}
}
/**
* 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");
}
@ -97,6 +131,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,
@ -112,6 +190,23 @@ 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.
*
* Example usage:
* const parsedDiff = parseDiff(diff);
* const prDetails = await getPRDetails();
* const comments = await analyzeCode(parsedDiff, prDetails);
*/
async function analyzeCode(
parsedDiff: File[],
prDetails: PRDetails
@ -158,6 +253,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>"}]}
@ -190,6 +293,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;
@ -227,6 +340,14 @@ async function getAIResponse(prompt: string): Promise<Array<{
}
}
/**
* 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,
@ -247,6 +368,18 @@ 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,
@ -262,55 +395,22 @@ async function createReviewComment(
});
}
async function main() {
const prDetails = await getPRDetails();
let diff: string | null;
const eventData = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH ?? "", "utf8")
);
console.log("Github triggered event data:", eventData);
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 if (eventData.action === "placeholder") {
diff = await getDiff(
prDetails.owner,
prDetails.repo,
prDetails.pull_number
);
} 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.
*
* Example usage:
* const parsedDiff = parseDiff(diff);
* const filteredDiff = filterDiffs(parsedDiff);
*/
function filterDiffs(parsedDiff: File[]): File[] {
const excludePatterns = core
.getInput("exclude")
.split(",")
@ -336,6 +436,59 @@ async function main() {
return !excluded && included;
});
return filteredDiff;
}
async function main() {
const eventData = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH ?? "", "utf8")
);
console.log("GitHub triggered event data:", eventData);
const prDetails = await getPrDetails(eventData);
if (!prDetails) {
console.log("No associated pull request found for this push event.");
return;
}
let diff: string | null;
switch (eventData.action) {
case "opened":
case "push":
diff = await getDiff(
prDetails.owner,
prDetails.repo,
prDetails.pull_number
);
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);
default:
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);
const filteredDiff = filterDiffs(parsedDiff);
const comments = await analyzeCode(filteredDiff, prDetails);
if (comments.length > 0) {
await createReviewComment(