| "use strict"; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.Commenter = exports.PLAN_BRANDING = exports.PRIX_BRANDING = exports.ISSUE_REPLY_TAG = exports.VERIFICATION_FAILED_TAG = exports.VERIFIED_TAG = exports.COMMIT_ID_END_TAG = exports.COMMIT_ID_START_TAG = exports.SHORT_SUMMARY_END_TAG = exports.SHORT_SUMMARY_START_TAG = exports.RAW_SUMMARY_END_TAG = exports.RAW_SUMMARY_START_TAG = exports.DESCRIPTION_END_TAG = exports.DESCRIPTION_START_TAG = exports.IN_PROGRESS_END_TAG = exports.IN_PROGRESS_START_TAG = exports.SUMMARIZE_TAG = exports.COMMENT_REPLY_TAG = exports.COMMENT_TAG = exports.COMMENT_GREETING = exports.setCommenterContext = exports.repo = exports.context = void 0; |
| |
| const octokit_1 = require("./octokit"); |
| const context_1 = require("./context"); |
| const info = console.log; |
| const warning = console.warn; |
| const getInput = (name) => process.env[name.toUpperCase()] || ''; |
| exports.context = new Proxy({}, { |
| get(target, prop) { |
| return (context_1.als.getStore()?.probotContext)[prop]; |
| } |
| }); |
| exports.repo = new Proxy({}, { |
| get(target, prop) { |
| return (context_1.als.getStore()?.repo)[prop]; |
| } |
| }); |
| const setCommenterContext = (newContext) => { |
| |
| }; |
| exports.setCommenterContext = setCommenterContext; |
| exports.COMMENT_GREETING = `${getInput('bot_icon')} AI PR Reviewer`; |
| exports.COMMENT_TAG = '<!-- This is an auto-generated comment by AI PR Reviewer -->'; |
| exports.COMMENT_REPLY_TAG = '<!-- This is an auto-generated reply by AI PR Reviewer -->'; |
| exports.SUMMARIZE_TAG = '<!-- This is an auto-generated comment: summarize by AI PR Reviewer -->'; |
| exports.IN_PROGRESS_START_TAG = '<!-- This is an auto-generated comment: summarize review in progress by AI PR Reviewer -->'; |
| exports.IN_PROGRESS_END_TAG = '<!-- end of auto-generated comment: summarize review in progress by AI PR Reviewer -->'; |
| exports.DESCRIPTION_START_TAG = '<!-- This is an auto-generated comment: release notes by AI PR Reviewer -->'; |
| exports.DESCRIPTION_END_TAG = '<!-- end of auto-generated comment: release notes by AI PR Reviewer -->'; |
| exports.RAW_SUMMARY_START_TAG = `<!-- This is an auto-generated comment: raw summary by AI PR Reviewer --> |
| <!-- |
| `; |
| exports.RAW_SUMMARY_END_TAG = `--> |
| <!-- end of auto-generated comment: raw summary by AI PR Reviewer -->`; |
| exports.SHORT_SUMMARY_START_TAG = `<!-- This is an auto-generated comment: short summary by AI PR Reviewer --> |
| <!-- |
| `; |
| exports.SHORT_SUMMARY_END_TAG = `--> |
| <!-- end of auto-generated comment: short summary by OSS CodeRabbit -->`; |
| exports.COMMIT_ID_START_TAG = '<!-- commit_ids_reviewed_start -->'; |
| exports.COMMIT_ID_END_TAG = '<!-- commit_ids_reviewed_end -->'; |
| exports.VERIFIED_TAG = '<!-- verified_by_prix -->'; |
| exports.VERIFICATION_FAILED_TAG = '<!-- verification_failed -->'; |
| exports.ISSUE_REPLY_TAG = '<!-- This is an auto-generated issue analysis by PRIX -->'; |
| exports.PRIX_BRANDING = `--- |
| |
| *This PR was generated by [Prix](https://prix.dev). [View on Prix](https://app.prix.dev)*`; |
| exports.PLAN_BRANDING = `--- |
| |
| *This plan was generated by [Prix](https://prix.dev). [View on Prix](https://app.prix.dev)*`; |
| class Commenter { |
| |
| |
| |
| async comment(message, tag, mode) { |
| let target; |
| if (exports.context.payload.pull_request != null) { |
| target = exports.context.payload.pull_request.number; |
| } |
| else if (exports.context.payload.issue != null) { |
| target = exports.context.payload.issue.number; |
| } |
| else { |
| warning('Skipped: context.payload.pull_request and context.payload.issue are both null'); |
| return; |
| } |
| if (!tag) { |
| tag = exports.COMMENT_TAG; |
| } |
| const body = `${exports.COMMENT_GREETING} |
| |
| ${message} |
| |
| ${tag}${exports.PRIX_BRANDING}`; |
| if (mode === 'create') { |
| await this.create(body, target); |
| } |
| else if (mode === 'replace') { |
| await this.replace(body, tag, target); |
| } |
| else { |
| warning(`Unknown mode: ${mode}, use "replace" instead`); |
| await this.replace(body, tag, target); |
| } |
| } |
| getContentWithinTags(content, startTag, endTag) { |
| const start = content.indexOf(startTag); |
| const end = content.indexOf(endTag); |
| if (start >= 0 && end >= 0) { |
| return content.slice(start + startTag.length, end); |
| } |
| return ''; |
| } |
| removeContentWithinTags(content, startTag, endTag) { |
| const start = content.indexOf(startTag); |
| const end = content.lastIndexOf(endTag); |
| if (start >= 0 && end >= 0) { |
| return content.slice(0, start) + content.slice(end + endTag.length); |
| } |
| return content; |
| } |
| getRawSummary(summary) { |
| return this.getContentWithinTags(summary, exports.RAW_SUMMARY_START_TAG, exports.RAW_SUMMARY_END_TAG); |
| } |
| getShortSummary(summary) { |
| return this.getContentWithinTags(summary, exports.SHORT_SUMMARY_START_TAG, exports.SHORT_SUMMARY_END_TAG); |
| } |
| getDescription(description) { |
| return this.removeContentWithinTags(description, exports.DESCRIPTION_START_TAG, exports.DESCRIPTION_END_TAG); |
| } |
| getReleaseNotes(description) { |
| const releaseNotes = this.getContentWithinTags(description, exports.DESCRIPTION_START_TAG, exports.DESCRIPTION_END_TAG); |
| return releaseNotes.replace(/(^|\n)> .*/g, ''); |
| } |
| async updateDescription(pullNumber, message) { |
| |
| |
| try { |
| |
| const pr = await octokit_1.octokit.pulls.get({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber |
| }); |
| let body = ''; |
| if (pr.data.body) { |
| body = pr.data.body; |
| } |
| const description = this.getDescription(body); |
| const messageClean = this.removeContentWithinTags(message, exports.DESCRIPTION_START_TAG, exports.DESCRIPTION_END_TAG); |
| const newDescription = `${description}\n${exports.DESCRIPTION_START_TAG}\n${messageClean}\n${exports.DESCRIPTION_END_TAG}`; |
| await octokit_1.octokit.pulls.update({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| body: newDescription |
| }); |
| } |
| catch (e) { |
| warning(`Failed to get PR: ${e}, skipping adding release notes to description.`); |
| } |
| } |
| reviewCommentsBuffer = []; |
| async bufferReviewComment(path, startLine, endLine, message, verified, verificationFeedback) { |
| |
| const levelTypeRegex = /^(\w+)\s*\|\s*(\w+)/; |
| const match = message.match(levelTypeRegex); |
| let header = ''; |
| if (match != null) { |
| const level = match[1].toLowerCase(); |
| const type = match[2]; |
| let emoji = '⚠️'; |
| let color = 'Major'; |
| if (level === 'critical') { |
| emoji = '🚨'; |
| color = 'Critical'; |
| } |
| else if (level === 'major') { |
| emoji = '🔴'; |
| color = 'Major'; |
| } |
| else if (level === 'minor') { |
| emoji = '🟠'; |
| color = 'Minor'; |
| } |
| else if (level === 'info') { |
| emoji = 'ℹ️'; |
| color = 'Info'; |
| } |
| header = `**${emoji} Potential issue | ${color} (${type})**\n\n`; |
| message = message.replace(levelTypeRegex, '').trim(); |
| } |
| let verificationBadge = ''; |
| if (verified === true) { |
| verificationBadge = |
| '\n---\n✅ **Verified**: This suggestion passed syntax, lint, and AST validation.\n'; |
| } |
| else if (verified === false && verificationFeedback) { |
| verificationBadge = `\n---\n⚠️ **Verification Failed**: ${verificationFeedback}\n`; |
| } |
| const formattedMessage = `${exports.COMMENT_GREETING} |
| ${header}${message}${verificationBadge} |
| |
| ${exports.COMMENT_TAG}`; |
| this.reviewCommentsBuffer.push({ |
| path, |
| startLine, |
| endLine, |
| message: formattedMessage, |
| verified, |
| verificationFeedback |
| }); |
| } |
| async deletePendingReview(pullNumber) { |
| try { |
| const reviews = await octokit_1.octokit.pulls.listReviews({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber |
| }); |
| const pendingReview = reviews.data.find(review => review.state === 'PENDING'); |
| if (pendingReview) { |
| info(`Deleting pending review for PR #${pullNumber} id: ${pendingReview.id}`); |
| try { |
| await octokit_1.octokit.pulls.deletePendingReview({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| |
| review_id: pendingReview.id |
| }); |
| } |
| catch (e) { |
| warning(`Failed to delete pending review: ${e}`); |
| } |
| } |
| } |
| catch (e) { |
| warning(`Failed to list reviews: ${e}`); |
| } |
| } |
| async submitReview(pullNumber, commitId, statusMsg) { |
| const bodyHeader = `${exports.COMMENT_GREETING} |
| |
| ${statusMsg} |
| `; |
| if (this.reviewCommentsBuffer.length === 0) { |
| |
| info(`Submitting empty review for PR #${pullNumber}`); |
| try { |
| await octokit_1.octokit.pulls.createReview({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| |
| commit_id: commitId, |
| event: 'COMMENT', |
| body: bodyHeader |
| }); |
| } |
| catch (e) { |
| warning(`Failed to submit empty review: ${e}`); |
| } |
| return; |
| } |
| |
| for (const comment of this.reviewCommentsBuffer) { |
| const comments = await this.getCommentsAtRange(pullNumber, comment.path, comment.startLine, comment.endLine); |
| for (const c of comments) { |
| if (c.body.includes(exports.COMMENT_TAG)) { |
| try { |
| await octokit_1.octokit.pulls.deleteReviewComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| comment_id: c.id |
| }); |
| } |
| catch (e) { |
| warning(`Failed to delete review comment: ${e}`); |
| } |
| } |
| } |
| } |
| await this.deletePendingReview(pullNumber); |
| |
| const pushableComments = this.reviewCommentsBuffer.map(comment => this.generateCommentData(comment)); |
| try { |
| const review = await octokit_1.octokit.pulls.createReview({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| |
| commit_id: commitId, |
| comments: pushableComments.map(comment => comment) |
| }); |
| info(`Submitting review for PR #${pullNumber}, total comments: ${pushableComments.length}, review id: ${review.data.id}`); |
| await octokit_1.octokit.pulls.submitReview({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| |
| review_id: review.data.id, |
| event: 'COMMENT', |
| body: bodyHeader |
| }); |
| } |
| catch (e) { |
| warning(`Failed to create review: ${e}. Falling back to individual comments.`); |
| await this.deletePendingReview(pullNumber); |
| let commentCounter = 0; |
| for (const comment of pushableComments) { |
| info(`Creating new review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`); |
| const commentData = { |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| |
| commit_id: commitId, |
| ...comment |
| }; |
| try { |
| await octokit_1.octokit.pulls.createReviewComment(commentData); |
| } |
| catch (ee) { |
| warning(`Failed to create review comment: ${ee}`); |
| } |
| commentCounter++; |
| info(`Comment ${commentCounter}/${pushableComments.length} posted`); |
| } |
| } |
| } |
| generateCommentData(comment) { |
| const commentData = { |
| path: comment.path, |
| body: comment.message, |
| line: comment.endLine |
| }; |
| if (comment.startLine !== comment.endLine) { |
| |
| commentData.start_line = comment.startLine; |
| |
| commentData.start_side = 'RIGHT'; |
| } |
| return commentData; |
| } |
| async reviewCommentReply(pullNumber, topLevelComment, message) { |
| const reply = `${exports.COMMENT_GREETING} |
| |
| ${message} |
| |
| ${exports.COMMENT_REPLY_TAG} |
| `; |
| try { |
| |
| await octokit_1.octokit.pulls.createReplyForReviewComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| body: reply, |
| |
| comment_id: topLevelComment.id |
| }); |
| } |
| catch (error) { |
| warning(`Failed to reply to the top-level comment ${error}`); |
| try { |
| await octokit_1.octokit.pulls.createReplyForReviewComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: pullNumber, |
| body: `Could not post the reply to the top-level comment due to the following error: ${error}`, |
| |
| comment_id: topLevelComment.id |
| }); |
| } |
| catch (e) { |
| warning(`Failed to reply to the top-level comment ${e}`); |
| } |
| } |
| try { |
| if (topLevelComment.body.includes(exports.COMMENT_TAG)) { |
| |
| const newBody = topLevelComment.body.replace(exports.COMMENT_TAG, exports.COMMENT_REPLY_TAG); |
| await octokit_1.octokit.pulls.updateReviewComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| comment_id: topLevelComment.id, |
| body: newBody |
| }); |
| } |
| } |
| catch (error) { |
| warning(`Failed to update the top-level comment ${error}`); |
| } |
| } |
| async getCommentsWithinRange(pullNumber, path, startLine, endLine) { |
| const comments = await this.listReviewComments(pullNumber); |
| return comments.filter((comment) => comment.path === path && |
| comment.body !== '' && |
| ((comment.start_line !== undefined && |
| comment.start_line >= startLine && |
| comment.line <= endLine) || |
| (startLine === endLine && comment.line === endLine))); |
| } |
| async getCommentsAtRange(pullNumber, path, startLine, endLine) { |
| const comments = await this.listReviewComments(pullNumber); |
| return comments.filter((comment) => comment.path === path && |
| comment.body !== '' && |
| ((comment.start_line !== undefined && |
| comment.start_line === startLine && |
| comment.line === endLine) || |
| (startLine === endLine && comment.line === endLine))); |
| } |
| async getCommentChainsWithinRange(pullNumber, path, startLine, endLine, tag = '') { |
| const existingComments = await this.getCommentsWithinRange(pullNumber, path, startLine, endLine); |
| |
| const topLevelComments = []; |
| for (const comment of existingComments) { |
| if (!comment.in_reply_to_id) { |
| topLevelComments.push(comment); |
| } |
| } |
| let allChains = ''; |
| let chainNum = 0; |
| for (const topLevelComment of topLevelComments) { |
| |
| const chain = await this.composeCommentChain(existingComments, topLevelComment); |
| if (chain && chain.includes(tag)) { |
| chainNum += 1; |
| allChains += `Conversation Chain ${chainNum}: |
| ${chain} |
| --- |
| `; |
| } |
| } |
| return allChains; |
| } |
| async composeCommentChain(reviewComments, topLevelComment) { |
| const conversationChain = reviewComments |
| .filter((cmt) => cmt.in_reply_to_id === topLevelComment.id) |
| .map((cmt) => `${cmt.user.login}: ${cmt.body}`); |
| conversationChain.unshift(`${topLevelComment.user.login}: ${topLevelComment.body}`); |
| return conversationChain.join('\n---\n'); |
| } |
| async getCommentChain(pullNumber, comment) { |
| try { |
| const reviewComments = await this.listReviewComments(pullNumber); |
| const topLevelComment = await this.getTopLevelComment(reviewComments, comment); |
| const chain = await this.composeCommentChain(reviewComments, topLevelComment); |
| return { chain, topLevelComment }; |
| } |
| catch (e) { |
| warning(`Failed to get conversation chain: ${e}`); |
| return { |
| chain: '', |
| topLevelComment: null |
| }; |
| } |
| } |
| async getTopLevelComment(reviewComments, comment) { |
| let topLevelComment = comment; |
| while (topLevelComment.in_reply_to_id) { |
| const parentComment = reviewComments.find((cmt) => cmt.id === topLevelComment.in_reply_to_id); |
| if (parentComment) { |
| topLevelComment = parentComment; |
| } |
| else { |
| break; |
| } |
| } |
| return topLevelComment; |
| } |
| reviewCommentsCache = {}; |
| async listReviewComments(target) { |
| if (this.reviewCommentsCache[target]) { |
| return this.reviewCommentsCache[target]; |
| } |
| const allComments = []; |
| let page = 1; |
| try { |
| for (;;) { |
| const { data: comments } = await octokit_1.octokit.pulls.listReviewComments({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: target, |
| page, |
| |
| per_page: 100 |
| }); |
| allComments.push(...comments); |
| page++; |
| if (!comments || comments.length < 100) { |
| break; |
| } |
| } |
| this.reviewCommentsCache[target] = allComments; |
| return allComments; |
| } |
| catch (e) { |
| warning(`Failed to list review comments: ${e}`); |
| return allComments; |
| } |
| } |
| async create(body, target) { |
| try { |
| |
| const response = await octokit_1.octokit.issues.createComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| issue_number: target, |
| body |
| }); |
| |
| if (this.issueCommentsCache[target]) { |
| this.issueCommentsCache[target].push(response.data); |
| } |
| else { |
| this.issueCommentsCache[target] = [response.data]; |
| } |
| } |
| catch (e) { |
| warning(`Failed to create comment: ${e}`); |
| } |
| } |
| async replace(body, tag, target) { |
| try { |
| const cmt = await this.findCommentWithTag(tag, target); |
| if (cmt) { |
| await octokit_1.octokit.issues.updateComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| comment_id: cmt.id, |
| body |
| }); |
| } |
| else { |
| await this.create(body, target); |
| } |
| } |
| catch (e) { |
| warning(`Failed to replace comment: ${e}`); |
| } |
| } |
| async findCommentWithTag(tag, target) { |
| try { |
| const comments = await this.listComments(target); |
| for (const cmt of comments) { |
| if (cmt.body && cmt.body.includes(tag)) { |
| return cmt; |
| } |
| } |
| return null; |
| } |
| catch (e) { |
| warning(`Failed to find comment with tag: ${e}`); |
| return null; |
| } |
| } |
| issueCommentsCache = {}; |
| async listComments(target) { |
| if (this.issueCommentsCache[target]) { |
| return this.issueCommentsCache[target]; |
| } |
| const allComments = []; |
| let page = 1; |
| try { |
| for (;;) { |
| const { data: comments } = await octokit_1.octokit.issues.listComments({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| issue_number: target, |
| page, |
| |
| per_page: 100 |
| }); |
| allComments.push(...comments); |
| page++; |
| if (!comments || comments.length < 100) { |
| break; |
| } |
| } |
| this.issueCommentsCache[target] = allComments; |
| return allComments; |
| } |
| catch (e) { |
| warning(`Failed to list comments: ${e}`); |
| return allComments; |
| } |
| } |
| |
| |
| |
| getReviewedCommitIds(commentBody) { |
| const start = commentBody.indexOf(exports.COMMIT_ID_START_TAG); |
| const end = commentBody.indexOf(exports.COMMIT_ID_END_TAG); |
| if (start === -1 || end === -1) { |
| return []; |
| } |
| const ids = commentBody.substring(start + exports.COMMIT_ID_START_TAG.length, end); |
| |
| return ids |
| .split('<!--') |
| .map(id => id.replace('-->', '').trim()) |
| .filter(id => id !== ''); |
| } |
| |
| |
| getReviewedCommitIdsBlock(commentBody) { |
| const start = commentBody.indexOf(exports.COMMIT_ID_START_TAG); |
| const end = commentBody.indexOf(exports.COMMIT_ID_END_TAG); |
| if (start === -1 || end === -1) { |
| return ''; |
| } |
| return commentBody.substring(start, end + exports.COMMIT_ID_END_TAG.length); |
| } |
| |
| |
| addReviewedCommitId(commentBody, commitId) { |
| const start = commentBody.indexOf(exports.COMMIT_ID_START_TAG); |
| const end = commentBody.indexOf(exports.COMMIT_ID_END_TAG); |
| if (start === -1 || end === -1) { |
| return `${commentBody}\n${exports.COMMIT_ID_START_TAG}\n<!-- ${commitId} -->\n${exports.COMMIT_ID_END_TAG}`; |
| } |
| const ids = commentBody.substring(start + exports.COMMIT_ID_START_TAG.length, end); |
| return `${commentBody.substring(0, start + exports.COMMIT_ID_START_TAG.length)}${ids}<!-- ${commitId} -->\n${commentBody.substring(end)}`; |
| } |
| |
| getHighestReviewedCommitId(commitIds, reviewedCommitIds) { |
| for (let i = commitIds.length - 1; i >= 0; i--) { |
| if (reviewedCommitIds.includes(commitIds[i])) { |
| return commitIds[i]; |
| } |
| } |
| return ''; |
| } |
| async getAllCommitIds() { |
| const allCommits = []; |
| let page = 1; |
| let commits; |
| if (exports.context && exports.context.payload && exports.context.payload.pull_request != null) { |
| do { |
| commits = await octokit_1.octokit.pulls.listCommits({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| |
| pull_number: exports.context.payload.pull_request.number, |
| |
| per_page: 100, |
| page |
| }); |
| allCommits.push(...commits.data.map(commit => commit.sha)); |
| page++; |
| } while (commits.data.length > 0); |
| } |
| return allCommits; |
| } |
| |
| addInProgressStatus(commentBody, statusMsg) { |
| const start = commentBody.indexOf(exports.IN_PROGRESS_START_TAG); |
| const end = commentBody.indexOf(exports.IN_PROGRESS_END_TAG); |
| |
| |
| if (start === -1 || end === -1) { |
| return `${exports.IN_PROGRESS_START_TAG} |
| |
| Currently reviewing new changes in this PR... |
| |
| ${statusMsg} |
| |
| ${exports.IN_PROGRESS_END_TAG} |
| |
| --- |
| |
| ${commentBody}`; |
| } |
| return commentBody; |
| } |
| |
| removeInProgressStatus(commentBody) { |
| const start = commentBody.indexOf(exports.IN_PROGRESS_START_TAG); |
| const end = commentBody.indexOf(exports.IN_PROGRESS_END_TAG); |
| |
| |
| if (start !== -1 && end !== -1) { |
| return (commentBody.substring(0, start) + |
| commentBody.substring(end + exports.IN_PROGRESS_END_TAG.length)); |
| } |
| return commentBody; |
| } |
| } |
| exports.Commenter = Commenter; |
| |