mirror of
				https://github.com/freeedcom/ai-codereviewer.git
				synced 2025-10-31 06:00:53 +00:00 
			
		
		
		
	Support push trigger + plenty refactoring [review]
		
	This commit is contained in:
		
					parent
					
						
							
								65cf847966
							
						
					
				
			
			
				commit
				
					
						e8074efc8b
					
				
			
		
					 3 changed files with 442 additions and 134 deletions
				
			
		
							
								
								
									
										301
									
								
								dist/index.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										301
									
								
								dist/index.js
									
										
									
									
										vendored
									
									
								
							|  | @ -157,19 +157,51 @@ require("./sourcemap-register.js"); | ||||||
|         defaultQuery: { "api-version": OPENAI_API_VERSION }, |         defaultQuery: { "api-version": OPENAI_API_VERSION }, | ||||||
|         defaultHeaders: { "api-key": OPENAI_API_KEY }, |         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; |         var _a, _b; | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|           console.log("Fetching pull request details..."); |           const { repository, number } = eventData; | ||||||
|           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") |  | ||||||
|           ); |  | ||||||
|           if (!repository || !number) { |           if (!repository || !number) { | ||||||
|             throw new Error( |             throw new Error( | ||||||
|               "Invalid event payload: missing repository or number" |               "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) { |       function getDiff(owner, repo, pull_number) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|           const response = yield octokit.pulls.get({ |           const response = yield octokit.pulls.get({ | ||||||
|  | @ -203,6 +282,23 @@ require("./sourcemap-register.js"); | ||||||
|           return response.data; |           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) { |       function analyzeCode(parsedDiff, prDetails) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|           const comments = []; |           const comments = []; | ||||||
|  | @ -255,6 +351,14 @@ require("./sourcemap-register.js"); | ||||||
|           return comments; |           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) { |       function createPrompt(file, chunk, prDetails) { | ||||||
|         return `Your task is to review pull requests. Instructions:
 |         return `Your task is to review pull requests. Instructions:
 | ||||||
| - Provide the response in following JSON format:  {"reviews": [{"lineNumber":  <line_number>, "reviewComment": "<review comment>"}]} | - 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) { |       function getAIResponse(prompt) { | ||||||
|         var _a, _b; |         var _a, _b; | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         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) { |       function createComment(file, chunk, aiResponses) { | ||||||
|         return aiResponses.flatMap((aiResponse) => { |         return aiResponses.flatMap((aiResponse) => { | ||||||
|           if (!file.to) { |           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) { |       function createReviewComment(owner, repo, pull_number, comments) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|           yield octokit.pulls.createReview({ |           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() { |       function main() { | ||||||
|         var _a; |         var _a; | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|           const prDetails = yield getPRDetails(); |  | ||||||
|           let diff; |  | ||||||
|           const eventData = JSON.parse( |           const eventData = JSON.parse( | ||||||
|             (0, fs_1.readFileSync)( |             (0, fs_1.readFileSync)( | ||||||
|               (_a = process.env.GITHUB_EVENT_PATH) !== null && _a !== void 0 |               (_a = process.env.GITHUB_EVENT_PATH) !== null && _a !== void 0 | ||||||
|  | @ -367,75 +549,48 @@ ${chunk.changes | ||||||
|               "utf8" |               "utf8" | ||||||
|             ) |             ) | ||||||
|           ); |           ); | ||||||
|           console.log("Github triggered event data:", eventData); |           console.log("GitHub triggered event data:", eventData); | ||||||
|           if (eventData.action === "opened") { |           const prDetails = yield getPrDetails(eventData); | ||||||
|             diff = yield getDiff( |           if (!prDetails) { | ||||||
|               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( |             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; |             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) { |           if (!diff) { | ||||||
|             console.log("No diff found"); |             console.log("No diff found"); | ||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
|           const parsedDiff = (0, parse_diff_1.default)(diff); |           const parsedDiff = (0, parse_diff_1.default)(diff); | ||||||
|           const excludePatterns = core |           const filteredDiff = filterDiffs(parsedDiff); | ||||||
|             .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 comments = yield analyzeCode(filteredDiff, prDetails); |           const comments = yield analyzeCode(filteredDiff, prDetails); | ||||||
|           if (comments.length > 0) { |           if (comments.length > 0) { | ||||||
|             yield createReviewComment( |             yield createReviewComment( | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								dist/index.js.map
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/index.js.map
									
										
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										273
									
								
								src/main.ts
									
										
									
									
									
								
							
							
						
						
									
										273
									
								
								src/main.ts
									
										
									
									
									
								
							|  | @ -59,6 +59,10 @@ const openai = new OpenAI({ | ||||||
|   defaultHeaders: { "api-key": OPENAI_API_KEY }, |   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 { | interface PRDetails { | ||||||
|   owner: string; |   owner: string; | ||||||
|   repo: string; |   repo: string; | ||||||
|  | @ -67,18 +71,48 @@ interface PRDetails { | ||||||
|   description: string; |   description: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function getPRDetails(): Promise<PRDetails> { | /** | ||||||
|   console.log("Fetching pull request details..."); |  * Retrieves pull request details based on the GitHub event type. | ||||||
|   console.log("GITHUB_EVENT_PATH:", process.env.GITHUB_EVENT_PATH); |  * | ||||||
|   console.log("GITHUB_EVENT_NAME:", process.env.GITHUB_EVENT_NAME); |  * This function reads the GitHub event data from the environment, logs it for debugging purposes, | ||||||
|   const eventData = JSON.parse( |  * and then determines the appropriate method to fetch pull request details based on the event type. | ||||||
|     readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8") |  * It supports the "opened", "synchronize", and "push" events. | ||||||
|   ); |  * | ||||||
|   console.log("Github event data:", eventData); |  * @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( |   console.log("GitHub Event Data:", eventData); // Log the event data for debugging
 | ||||||
|     readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8") | 
 | ||||||
|   ); |   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) { |   if (!repository || !number) { | ||||||
|     throw new Error("Invalid event payload: missing repository or 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( | async function getDiff( | ||||||
|   owner: string, |   owner: string, | ||||||
|   repo: string, |   repo: string, | ||||||
|  | @ -112,6 +190,23 @@ async function getDiff( | ||||||
|   return response.data; |   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( | async function analyzeCode( | ||||||
|   parsedDiff: File[], |   parsedDiff: File[], | ||||||
|   prDetails: PRDetails |   prDetails: PRDetails | ||||||
|  | @ -158,6 +253,14 @@ async function analyzeCode( | ||||||
|   return comments; |   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 { | function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string { | ||||||
|   return `Your task is to review pull requests. Instructions:
 |   return `Your task is to review pull requests. Instructions:
 | ||||||
| - Provide the response in following JSON format:  {"reviews": [{"lineNumber":  <line_number>, "reviewComment": "<review comment>"}]} | - 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<{ | async function getAIResponse(prompt: string): Promise<Array<{ | ||||||
|   lineNumber: string; |   lineNumber: string; | ||||||
|   reviewComment: 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( | function createComment( | ||||||
|   file: File, |   file: File, | ||||||
|   chunk: Chunk, |   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( | async function createReviewComment( | ||||||
|   owner: string, |   owner: string, | ||||||
|   repo: string, |   repo: string, | ||||||
|  | @ -262,55 +395,22 @@ async function createReviewComment( | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function main() { | /** | ||||||
|   const prDetails = await getPRDetails(); |  * Filters the parsed diff files based on include and exclude patterns. | ||||||
|   let diff: string | null; |  * | ||||||
|   const eventData = JSON.parse( |  * This function reads the `exclude` and `include` patterns from the GitHub Action inputs, | ||||||
|     readFileSync(process.env.GITHUB_EVENT_PATH ?? "", "utf8") |  * 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. | ||||||
|   console.log("Github triggered event data:", eventData); |  * | ||||||
|   if (eventData.action === "opened") { |  * @param {File[]} parsedDiff - An array of parsed diff files to be filtered. | ||||||
|     diff = await getDiff( |  * @returns {File[]} - An array of filtered diff files. | ||||||
|       prDetails.owner, |  * | ||||||
|       prDetails.repo, |  * Example usage: | ||||||
|       prDetails.pull_number |  * const parsedDiff = parseDiff(diff); | ||||||
|     ); |  * const filteredDiff = filterDiffs(parsedDiff); | ||||||
|   } else if (eventData.action === "synchronize") { |  */ | ||||||
|     const newBaseSha = eventData.before; | function filterDiffs(parsedDiff: File[]): File[] { | ||||||
|     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); |  | ||||||
| 
 |  | ||||||
|   const excludePatterns = core |   const excludePatterns = core | ||||||
|     .getInput("exclude") |     .getInput("exclude") | ||||||
|     .split(",") |     .split(",") | ||||||
|  | @ -336,6 +436,59 @@ async function main() { | ||||||
|     return !excluded && included; |     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); |   const comments = await analyzeCode(filteredDiff, prDetails); | ||||||
|   if (comments.length > 0) { |   if (comments.length > 0) { | ||||||
|     await createReviewComment( |     await createReviewComment( | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue