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

301
dist/index.js vendored
View file

@ -157,19 +157,51 @@ 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.
*
* Example usage:
* const prDetails = await getPrDetails("opened");
* console.log(prDetails);
*/
function getPrDetails(event) {
return __awaiter(this, void 0, void 0, function* () {
const eventPath = process.env.GITHUB_EVENT_PATH || "";
const eventData = JSON.parse(
(0, fs_1.readFileSync)(eventPath, "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.
*/
function getPrFromEvent(eventData) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
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(
(0, fs_1.readFileSync)(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
console.log("Github event data:", eventData);
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"
@ -191,6 +223,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({
@ -203,6 +282,23 @@ 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.
*
* Example usage:
* const parsedDiff = parseDiff(diff);
* const prDetails = await getPRDetails();
* const comments = await analyzeCode(parsedDiff, prDetails);
*/
function analyzeCode(parsedDiff, prDetails) {
return __awaiter(this, void 0, void 0, function* () {
const comments = [];
@ -255,6 +351,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>"}]}
@ -286,6 +390,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* () {
@ -331,6 +445,14 @@ ${chunk.changes
}
});
}
/**
* 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) {
@ -343,6 +465,18 @@ ${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* () {
yield octokit.pulls.createReview({
@ -354,11 +488,59 @@ ${chunk.changes
});
});
}
/**
* 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) {
const excludePatterns = core
.getInput("exclude")
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0); // Filter out empty strings;
const includePatterns = core
.getInput("include")
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0); // Filter out empty strings;
const filteredDiff = parsedDiff.filter((file) => {
const excluded =
excludePatterns.length > 0 &&
excludePatterns.some((pattern) => {
var _a;
return (0, minimatch_1.default)(
(_a = file.to) !== null && _a !== void 0 ? _a : "",
pattern
);
});
const included =
includePatterns.length === 0 ||
includePatterns.some((pattern) => {
var _a;
return (0, minimatch_1.default)(
(_a = file.to) !== null && _a !== void 0 ? _a : "",
pattern
);
});
// 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 prDetails = yield getPRDetails();
let diff;
const eventData = JSON.parse(
(0, fs_1.readFileSync)(
(_a = process.env.GITHUB_EVENT_PATH) !== null && _a !== void 0
@ -367,75 +549,48 @@ ${chunk.changes
"utf8"
)
);
console.log("Github triggered event data:", eventData);
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 if (eventData.action === "placeholder") {
diff = yield getDiff(
prDetails.owner,
prDetails.repo,
prDetails.pull_number
);
} else {
console.log("GitHub triggered event data:", eventData);
const prDetails = yield getPrDetails(eventData);
if (!prDetails) {
console.log(
`Unsupported event: action=${eventData.action}, process.env.GITHUB_EVENT_NAME=${process.env.GITHUB_EVENT_NAME}`
"No associated pull request found for this push event."
);
return;
}
let diff;
switch (eventData.action) {
case "opened":
case "push":
diff = yield getDiff(
prDetails.owner,
prDetails.repo,
prDetails.pull_number
);
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);
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 = (0, parse_diff_1.default)(diff);
const excludePatterns = core
.getInput("exclude")
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0); // Filter out empty strings;
const includePatterns = core
.getInput("include")
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0); // Filter out empty strings;
const filteredDiff = parsedDiff.filter((file) => {
const excluded =
excludePatterns.length > 0 &&
excludePatterns.some((pattern) => {
var _a;
return (0, minimatch_1.default)(
(_a = file.to) !== null && _a !== void 0 ? _a : "",
pattern
);
});
const included =
includePatterns.length === 0 ||
includePatterns.some((pattern) => {
var _a;
return (0, minimatch_1.default)(
(_a = file.to) !== null && _a !== void 0 ? _a : "",
pattern
);
});
// Excluded patterns take precedence over included patterns.
return !excluded && included;
});
const filteredDiff = filterDiffs(parsedDiff);
const comments = yield analyzeCode(filteredDiff, prDetails);
if (comments.length > 0) {
yield createReviewComment(

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

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(