"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; 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.LOGO_URL = exports.setCommenterContext = exports.repo = exports.context = void 0; // Removed @actions imports for Probot migration const octokit_1 = require("./octokit"); const context_1 = require("./context"); const pino_1 = __importDefault(require("pino")); const p_retry_1 = __importDefault(require("p-retry")); const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' }); const info = (msg) => logger.info(msg); const warning = (msg) => logger.warn(msg); const getInput = (name) => process.env[name.toUpperCase()] || ''; // Retry configuration for GitHub API calls const GITHUB_RETRY_OPTIONS = { retries: 3, minTimeout: 1000, maxTimeout: 10000, factor: 2, onFailedAttempt: (error) => { logger.warn({ attemptNumber: error.attemptNumber, retriesLeft: error.retriesLeft, error: error.message }, 'GitHub API call failed, retrying...'); } }; // Wrapper for retryable GitHub API calls async function withRetry(fn) { return (0, p_retry_1.default)(fn, GITHUB_RETRY_OPTIONS); } 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) => { // context and repo are now Proxies }; exports.setCommenterContext = setCommenterContext; exports.LOGO_URL = 'https://www.prixai.xyz/logo.png'; exports.COMMENT_GREETING = `PRIX Logo AI PR Reviewer`; exports.COMMENT_TAG = ''; exports.COMMENT_REPLY_TAG = ''; exports.SUMMARIZE_TAG = ''; exports.IN_PROGRESS_START_TAG = ''; exports.IN_PROGRESS_END_TAG = ''; exports.DESCRIPTION_START_TAG = ''; exports.DESCRIPTION_END_TAG = ''; exports.RAW_SUMMARY_START_TAG = ` `; exports.SHORT_SUMMARY_START_TAG = ` `; exports.COMMIT_ID_START_TAG = ''; exports.COMMIT_ID_END_TAG = ''; exports.VERIFIED_TAG = ''; exports.VERIFICATION_FAILED_TAG = ''; exports.ISSUE_REPLY_TAG = ''; exports.PRIX_BRANDING = `--- šŸš€ **[Get your own AI PR reviewer at PRIX](https://www.prixai.xyz/)** — Automated code reviews for your team`; exports.PLAN_BRANDING = `--- šŸš€ **[Get your own AI PR reviewer at PRIX](https://www.prixai.xyz/)** — Automated code reviews for your team`; class Commenter { /** * @param mode Can be "create", "replace". Default is "replace". */ 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) { // add this response to the description field of the PR as release notes by looking // for the tag (marker) try { // get latest description from PR const pr = await octokit_1.octokit.rest.pulls.get({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, request: { timeout: 10000 } }); 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.rest.pulls.update({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, body: newDescription, request: { timeout: 10000 } }); } catch (e) { warning(`Failed to get PR: ${e}, skipping adding release notes to description.`); } } reviewCommentsBuffer = []; async bufferReviewComment(path, startLine, endLine, message, verified, verificationFeedback) { // Premium UI Formatting: Convert "[LEVEL] | [TYPE]" to emoji badges 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}${exports.PRIX_BRANDING}`; this.reviewCommentsBuffer.push({ path, startLine, endLine, message: formattedMessage, verified, verificationFeedback }); } async deletePendingReview(pullNumber) { try { const reviews = await octokit_1.octokit.rest.pulls.listReviews({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, request: { timeout: 10000 } }); 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.rest.pulls.deletePendingReview({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, // eslint-disable-next-line camelcase review_id: pendingReview.id, request: { timeout: 10000 } }); } 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} ${exports.PRIX_BRANDING}`; if (this.reviewCommentsBuffer.length === 0) { // Submit empty review with statusMsg info(`Submitting empty review for PR #${pullNumber}`); try { await octokit_1.octokit.rest.pulls.createReview({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, // eslint-disable-next-line camelcase commit_id: commitId, event: 'COMMENT', body: bodyHeader, request: { timeout: 10000 } }); } catch (e) { warning(`Failed to submit empty review: ${e}`); } return; } // De-duplicate and clear existing bot comments in the same range 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.rest.pulls.deleteReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase comment_id: c.id, request: { timeout: 10000 } }); } catch (e) { warning(`Failed to delete review comment: ${e}`); } } } } await this.deletePendingReview(pullNumber); // Unified Review: All comments are standalone threads to support suggestions const pushableComments = this.reviewCommentsBuffer.map(comment => this.generateCommentData(comment)); try { const review = await octokit_1.octokit.rest.pulls.createReview({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, // eslint-disable-next-line camelcase commit_id: commitId, comments: pushableComments.map(comment => comment), request: { timeout: 10000 } }); info(`Submitting review for PR #${pullNumber}, total comments: ${pushableComments.length}, review id: ${review.data.id}`); await octokit_1.octokit.rest.pulls.submitReview({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, // eslint-disable-next-line camelcase review_id: review.data.id, event: 'COMMENT', body: bodyHeader, request: { timeout: 10000 } }); } 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.start_line || comment.line}-${comment.line}: ${comment.body}`); const commentData = { owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, // eslint-disable-next-line camelcase commit_id: commitId, ...comment }; try { await octokit_1.octokit.rest.pulls.createReviewComment({ ...commentData, request: { timeout: 10000 } }); } 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) { // eslint-disable-next-line camelcase commentData.start_line = comment.startLine; // eslint-disable-next-line camelcase commentData.start_side = 'RIGHT'; } return commentData; } async reviewCommentReply(pullNumber, topLevelComment, message) { const reply = `${exports.COMMENT_GREETING} ${message} ${exports.COMMENT_REPLY_TAG}${exports.PRIX_BRANDING} `; try { // Post the reply to the user comment await octokit_1.octokit.rest.pulls.createReplyForReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, body: reply, // eslint-disable-next-line camelcase comment_id: topLevelComment.id, request: { timeout: 10000 } }); } catch (error) { warning(`Failed to reply to the top-level comment ${error}`); try { await octokit_1.octokit.rest.pulls.createReplyForReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: pullNumber, body: `Could not post the reply to the top-level comment due to the following error: ${error}`, // eslint-disable-next-line camelcase comment_id: topLevelComment.id, request: { timeout: 10000 } }); } catch (e) { warning(`Failed to reply to the top-level comment ${e}`); } } try { if (topLevelComment.body.includes(exports.COMMENT_TAG)) { // replace COMMENT_TAG with COMMENT_REPLY_TAG in topLevelComment const newBody = topLevelComment.body.replace(exports.COMMENT_TAG, exports.COMMENT_REPLY_TAG); await octokit_1.octokit.rest.pulls.updateReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase comment_id: topLevelComment.id, body: newBody, request: { timeout: 10000 } }); } } 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); // find all top most comments 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) { // get conversation chain 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.rest.pulls.listReviewComments({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: target, page, // eslint-disable-next-line camelcase per_page: 100, request: { timeout: 10000 } }); 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 { // get comment ID from the response const response = await octokit_1.octokit.rest.issues.createComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase issue_number: target, body, request: { timeout: 10000 } }); // add comment to issueCommentsCache 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.rest.issues.updateComment({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase comment_id: cmt.id, body, request: { timeout: 10000 } }); } 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.rest.issues.listComments({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase issue_number: target, page, // eslint-disable-next-line camelcase per_page: 100, request: { timeout: 10000 } }); 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; } } // function that takes a comment body and returns the list of commit ids that have been reviewed // commit ids are comments between the commit_ids_reviewed_start and commit_ids_reviewed_end markers // 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); // remove the markers from each id and extract the id and remove empty strings return ids .split('', '').trim()) .filter(id => id !== ''); } // get review commit ids comment block from the body as a string // including markers 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); } // add a commit id to the list of reviewed commit ids // if the marker doesn't exist, add it 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\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}\n${commentBody.substring(end)}`; } // given a list of commit ids provide the highest commit id that has been reviewed 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.rest.pulls.listCommits({ owner: exports.repo.owner, repo: exports.repo.repo, // eslint-disable-next-line camelcase pull_number: exports.context.payload.pull_request.number, // eslint-disable-next-line camelcase per_page: 100, page, request: { timeout: 10000 } }); allCommits.push(...commits.data.map(commit => commit.sha)); page++; } while (commits.data.length > 0); } return allCommits; } // add in-progress status to the comment body addInProgressStatus(commentBody, statusMsg) { const start = commentBody.indexOf(exports.IN_PROGRESS_START_TAG); const end = commentBody.indexOf(exports.IN_PROGRESS_END_TAG); // add to the beginning of the comment body if the marker doesn't exist // otherwise do nothing 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; } // remove in-progress status from the comment body removeInProgressStatus(commentBody) { const start = commentBody.indexOf(exports.IN_PROGRESS_START_TAG); const end = commentBody.indexOf(exports.IN_PROGRESS_END_TAG); // remove the in-progress status if the marker exists // otherwise do nothing if (start !== -1 && end !== -1) { return (commentBody.substring(0, start) + commentBody.substring(end + exports.IN_PROGRESS_END_TAG.length)); } return commentBody; } } exports.Commenter = Commenter; //# sourceMappingURL=commenter.js.map