diff --git a/action.yml b/action.yml index 6e9bca52..4feab8e3 100644 --- a/action.yml +++ b/action.yml @@ -62,6 +62,11 @@ inputs: required: false description: 'Disable release notes' default: 'false' + openai_base_url: + required: false + description: + 'The url of the openai api interface.' + default: 'https://api.openai.com/v1' openai_light_model: required: false description: diff --git a/dist/index.js b/dist/index.js index cba169a9..edb5b83e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2161,14 +2161,14 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) { } ;// CONCATENATED MODULE: ./lib/fetch-polyfill.js -// fetch-polyfill.js - -if (!globalThis.fetch) { - globalThis.fetch = fetch; - globalThis.Headers = Headers; - globalThis.Request = Request; - globalThis.Response = Response; -} +// fetch-polyfill.js + +if (!globalThis.fetch) { + globalThis.fetch = fetch; + globalThis.Headers = Headers; + globalThis.Request = Request; + globalThis.Response = Response; +} // EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js var core = __nccwpck_require__(2186); @@ -3389,114 +3389,115 @@ var ChatGPTUnofficialProxyAPI = class { //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./lib/utils.js - -const retry = async (fn, args, times) => { - for (let i = 0; i < times; i++) { - try { - return await fn(...args); - } - catch (error) { - if (i === times - 1) { - throw error; - } - (0,core.warning)(`Function failed on try ${i + 1}, retrying...`); - continue; - } - } -}; + +const retry = async (fn, args, times) => { + for (let i = 0; i < times; i++) { + try { + return await fn(...args); + } + catch (error) { + if (i === times - 1) { + throw error; + } + (0,core.warning)(`Function failed on try ${i + 1}, retrying...`); + continue; + } + } +}; ;// CONCATENATED MODULE: ./lib/bot.js - - - - -class Bot { - api = null; // not free - options; - constructor(options, openaiOptions) { - this.options = options; - if (process.env.OPENAI_API_KEY) { - this.api = new ChatGPTAPI({ - systemMessage: options.systemMessage, - apiKey: process.env.OPENAI_API_KEY, - apiOrg: process.env.OPENAI_API_ORG ?? undefined, - debug: options.debug, - maxModelTokens: openaiOptions.tokenLimits.maxTokens, - maxResponseTokens: openaiOptions.tokenLimits.responseTokens, - completionParams: { - temperature: options.openaiModelTemperature, - model: openaiOptions.model - } - }); - } - else { - const err = "Unable to initialize the OpenAI API, both 'OPENAI_API_KEY' environment variable are not available"; - throw new Error(err); - } - } - chat = async (message, ids) => { - let res = ['', {}]; - try { - res = await this.chat_(message, ids); - return res; - } - catch (e) { - if (e instanceof ChatGPTError) { - (0,core.warning)(`Failed to chat: ${e}, backtrace: ${e.stack}`); - } - return res; - } - }; - chat_ = async (message, ids) => { - // record timing - const start = Date.now(); - if (!message) { - return ['', {}]; - } - let response; - if (this.api != null) { - const opts = { - timeoutMs: this.options.openaiTimeoutMS - }; - if (ids.parentMessageId) { - opts.parentMessageId = ids.parentMessageId; - } - try { - response = await retry(this.api.sendMessage.bind(this.api), [message, opts], this.options.openaiRetries); - } - catch (e) { - if (e instanceof ChatGPTError) { - (0,core.info)(`response: ${response}, failed to send message to openai: ${e}, backtrace: ${e.stack}`); - } - } - const end = Date.now(); - (0,core.info)(`response: ${JSON.stringify(response)}`); - (0,core.info)(`openai sendMessage (including retries) response time: ${end - start} ms`); - } - else { - (0,core.setFailed)('The OpenAI API is not initialized'); - } - let responseText = ''; - if (response != null) { - responseText = response.text; - } - else { - (0,core.warning)('openai response is null'); - } - // remove the prefix "with " in the response - if (responseText.startsWith('with ')) { - responseText = responseText.substring(5); - } - if (this.options.debug) { - (0,core.info)(`openai responses: ${responseText}`); - } - const newIds = { - parentMessageId: response?.id, - conversationId: response?.conversationId - }; - return [responseText, newIds]; - }; -} + + + + +class Bot { + api = null; // not free + options; + constructor(options, openaiOptions) { + this.options = options; + if (process.env.OPENAI_API_KEY) { + this.api = new ChatGPTAPI({ + apiBaseUrl: options.apiBaseUrl, + systemMessage: options.systemMessage, + apiKey: process.env.OPENAI_API_KEY, + apiOrg: process.env.OPENAI_API_ORG ?? undefined, + debug: options.debug, + maxModelTokens: openaiOptions.tokenLimits.maxTokens, + maxResponseTokens: openaiOptions.tokenLimits.responseTokens, + completionParams: { + temperature: options.openaiModelTemperature, + model: openaiOptions.model + } + }); + } + else { + const err = "Unable to initialize the OpenAI API, both 'OPENAI_API_KEY' environment variable are not available"; + throw new Error(err); + } + } + chat = async (message, ids) => { + let res = ['', {}]; + try { + res = await this.chat_(message, ids); + return res; + } + catch (e) { + if (e instanceof ChatGPTError) { + (0,core.warning)(`Failed to chat: ${e}, backtrace: ${e.stack}`); + } + return res; + } + }; + chat_ = async (message, ids) => { + // record timing + const start = Date.now(); + if (!message) { + return ['', {}]; + } + let response; + if (this.api != null) { + const opts = { + timeoutMs: this.options.openaiTimeoutMS + }; + if (ids.parentMessageId) { + opts.parentMessageId = ids.parentMessageId; + } + try { + response = await retry(this.api.sendMessage.bind(this.api), [message, opts], this.options.openaiRetries); + } + catch (e) { + if (e instanceof ChatGPTError) { + (0,core.info)(`response: ${response}, failed to send message to openai: ${e}, backtrace: ${e.stack}`); + } + } + const end = Date.now(); + (0,core.info)(`response: ${JSON.stringify(response)}`); + (0,core.info)(`openai sendMessage (including retries) response time: ${end - start} ms`); + } + else { + (0,core.setFailed)('The OpenAI API is not initialized'); + } + let responseText = ''; + if (response != null) { + responseText = response.text; + } + else { + (0,core.warning)('openai response is null'); + } + // remove the prefix "with " in the response + if (responseText.startsWith('with ')) { + responseText = responseText.substring(5); + } + if (this.options.debug) { + (0,core.info)(`openai responses: ${responseText}`); + } + const newIds = { + parentMessageId: response?.id, + conversationId: response?.conversationId + }; + return [responseText, newIds]; + }; +} /***/ }), @@ -3519,582 +3520,582 @@ class Bot { /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(5438); /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__nccwpck_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(3258); + +// eslint-disable-next-line camelcase + + +// eslint-disable-next-line camelcase +const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; +const repo = context.repo; +const COMMENT_GREETING = ':robot: OpenAI'; +const COMMENT_TAG = ''; +const COMMENT_REPLY_TAG = ''; +const SUMMARIZE_TAG = ''; +const DESCRIPTION_START_TAG = ''; +const DESCRIPTION_END_TAG = ''; +const RAW_SUMMARY_START_TAG = ` + +`; +const COMMIT_ID_START_TAG = ''; +const COMMIT_ID_END_TAG = ''; +class Commenter { + /** + * @param mode Can be "create", "replace". Default is "replace". + */ + async comment(message, tag, mode) { + let target; + if (context.payload.pull_request != null) { + target = context.payload.pull_request.number; + } + else if (context.payload.issue != null) { + target = context.payload.issue.number; + } + else { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Skipped: context.payload.pull_request and context.payload.issue are both null'); + return; + } + if (!tag) { + tag = COMMENT_TAG; + } + const body = `${COMMENT_GREETING} + +${message} + +${tag}`; + if (mode === 'create') { + await this.create(body, target); + } + else if (mode === 'replace') { + await this.replace(body, tag, target); + } + else { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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.indexOf(endTag); + if (start >= 0 && end >= 0) { + return content.slice(0, start) + content.slice(end + endTag.length); + } + return content; + } + getRawSummary(summary) { + return this.getContentWithinTags(summary, RAW_SUMMARY_START_TAG, RAW_SUMMARY_END_TAG); + } + getDescription(description) { + return this.removeContentWithinTags(description, DESCRIPTION_START_TAG, DESCRIPTION_END_TAG); + } + getReleaseNotes(description) { + const releaseNotes = this.getContentWithinTags(description, DESCRIPTION_START_TAG, 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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.get */ .K.pulls.get({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: pullNumber + }); + let body = ''; + if (pr.data.body) { + body = pr.data.body; + } + const description = this.getDescription(body); + const messageClean = this.removeContentWithinTags(message, DESCRIPTION_START_TAG, DESCRIPTION_END_TAG); + const newDescription = `${description}\n${DESCRIPTION_START_TAG}\n${messageClean}\n${DESCRIPTION_END_TAG}`; + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.update */ .K.pulls.update({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: pullNumber, + body: newDescription + }); + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get PR: ${e}, skipping adding release notes to description.`); + } + } + reviewCommentsBuffer = []; + async bufferReviewComment(path, startLine, endLine, message) { + message = `${COMMENT_GREETING} + +${message} + +${COMMENT_TAG}`; + this.reviewCommentsBuffer.push({ + path, + startLine, + endLine, + message + }); + } + async submitReview(pullNumber, commitId) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Submitting review for PR #${pullNumber}, total comments: ${this.reviewCommentsBuffer.length}`); + try { + let commentCounter = 0; + for (const comment of this.reviewCommentsBuffer) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Posting comment: ${comment.message}`); + let found = false; + const comments = await this.getCommentsAtRange(pullNumber, comment.path, comment.startLine, comment.endLine); + for (const c of comments) { + if (c.body.includes(COMMENT_TAG)) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Updating review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`); + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.updateReviewComment */ .K.pulls.updateReviewComment({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + comment_id: c.id, + body: comment.message + }); + found = true; + break; + } + } + if (!found) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Creating new review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`); + const commentData = { + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: pullNumber, + // eslint-disable-next-line camelcase + commit_id: commitId, + body: comment.message, + path: comment.path, + line: comment.endLine + }; + if (comment.startLine !== comment.endLine) { + // eslint-disable-next-line camelcase + commentData.start_side = 'RIGHT'; + // eslint-disable-next-line camelcase + commentData.start_line = comment.startLine; + } + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReviewComment */ .K.pulls.createReviewComment(commentData); + } + commentCounter++; + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Comment ${commentCounter}/${this.reviewCommentsBuffer.length} posted`); + } + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to submit review: ${e}`); + throw e; + } + } + async reviewCommentReply(pullNumber, topLevelComment, message) { + const reply = `${COMMENT_GREETING} + +${message} + +${COMMENT_REPLY_TAG} +`; + try { + // Post the reply to the user comment + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReplyForReviewComment */ .K.pulls.createReplyForReviewComment({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: pullNumber, + body: reply, + // eslint-disable-next-line camelcase + comment_id: topLevelComment.id + }); + } + catch (error) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to reply to the top-level comment ${error}`); + try { + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReplyForReviewComment */ .K.pulls.createReplyForReviewComment({ + owner: repo.owner, + repo: 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 + }); + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to reply to the top-level comment ${e}`); + } + } + try { + if (topLevelComment.body.includes(COMMENT_TAG)) { + // replace COMMENT_TAG with COMMENT_REPLY_TAG in topLevelComment + const newBody = topLevelComment.body.replace(COMMENT_TAG, COMMENT_REPLY_TAG); + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.updateReviewComment */ .K.pulls.updateReviewComment({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + comment_id: topLevelComment.id, + body: newBody + }); + } + } + catch (error) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.listReviewComments */ .K.pulls.listReviewComments({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: target, + page, + // eslint-disable-next-line camelcase + per_page: 100 + }); + allComments.push(...comments); + page++; + if (!comments || comments.length < 100) { + break; + } + } + this.reviewCommentsCache[target] = allComments; + return allComments; + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to list review comments: ${e}`); + return allComments; + } + } + async create(body, target) { + try { + // get commend ID from the response + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.createComment */ .K.issues.createComment({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + issue_number: target, + body + }); + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to create comment: ${e}`); + } + } + async replace(body, tag, target) { + try { + const cmt = await this.findCommentWithTag(tag, target); + if (cmt) { + await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.updateComment */ .K.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + comment_id: cmt.id, + body + }); + } + else { + await this.create(body, target); + } + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.listComments */ .K.issues.listComments({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + issue_number: target, + page, + // eslint-disable-next-line camelcase + per_page: 100 + }); + allComments.push(...comments); + page++; + if (!comments || comments.length < 100) { + break; + } + } + this.issueCommentsCache[target] = allComments; + return allComments; + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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(COMMIT_ID_START_TAG); + const end = commentBody.indexOf(COMMIT_ID_END_TAG); + if (start === -1 || end === -1) { + return []; + } + const ids = commentBody.substring(start + 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(COMMIT_ID_START_TAG); + const end = commentBody.indexOf(COMMIT_ID_END_TAG); + if (start === -1 || end === -1) { + return ''; + } + return commentBody.substring(start, end + 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(COMMIT_ID_START_TAG); + const end = commentBody.indexOf(COMMIT_ID_END_TAG); + if (start === -1 || end === -1) { + return `${commentBody}\n${COMMIT_ID_START_TAG}\n\n${COMMIT_ID_END_TAG}`; + } + const ids = commentBody.substring(start + COMMIT_ID_START_TAG.length, end); + return `${commentBody.substring(0, start + 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 (context && context.payload && context.payload.pull_request != null) { + do { + commits = await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.listCommits */ .K.pulls.listCommits({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: context.payload.pull_request.number, + // eslint-disable-next-line camelcase + per_page: 100, + page + }); + allCommits.push(...commits.data.map(commit => commit.sha)); + page++; + } while (commits.data.length > 0); + } + return allCommits; + } +} -// eslint-disable-next-line camelcase - - -// eslint-disable-next-line camelcase -const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; -const repo = context.repo; -const COMMENT_GREETING = ':robot: OpenAI'; -const COMMENT_TAG = ''; -const COMMENT_REPLY_TAG = ''; -const SUMMARIZE_TAG = ''; -const DESCRIPTION_START_TAG = ''; -const DESCRIPTION_END_TAG = ''; -const RAW_SUMMARY_START_TAG = ` - -`; -const COMMIT_ID_START_TAG = ''; -const COMMIT_ID_END_TAG = ''; -class Commenter { - /** - * @param mode Can be "create", "replace". Default is "replace". - */ - async comment(message, tag, mode) { - let target; - if (context.payload.pull_request != null) { - target = context.payload.pull_request.number; - } - else if (context.payload.issue != null) { - target = context.payload.issue.number; - } - else { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Skipped: context.payload.pull_request and context.payload.issue are both null'); - return; - } - if (!tag) { - tag = COMMENT_TAG; - } - const body = `${COMMENT_GREETING} -${message} +/***/ }), -${tag}`; - if (mode === 'create') { - await this.create(body, target); - } - else if (mode === 'replace') { - await this.replace(body, tag, target); - } - else { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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.indexOf(endTag); - if (start >= 0 && end >= 0) { - return content.slice(0, start) + content.slice(end + endTag.length); - } - return content; - } - getRawSummary(summary) { - return this.getContentWithinTags(summary, RAW_SUMMARY_START_TAG, RAW_SUMMARY_END_TAG); - } - getDescription(description) { - return this.removeContentWithinTags(description, DESCRIPTION_START_TAG, DESCRIPTION_END_TAG); - } - getReleaseNotes(description) { - const releaseNotes = this.getContentWithinTags(description, DESCRIPTION_START_TAG, 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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.get */ .K.pulls.get({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: pullNumber - }); - let body = ''; - if (pr.data.body) { - body = pr.data.body; - } - const description = this.getDescription(body); - const messageClean = this.removeContentWithinTags(message, DESCRIPTION_START_TAG, DESCRIPTION_END_TAG); - const newDescription = `${description}\n${DESCRIPTION_START_TAG}\n${messageClean}\n${DESCRIPTION_END_TAG}`; - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.update */ .K.pulls.update({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: pullNumber, - body: newDescription - }); - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get PR: ${e}, skipping adding release notes to description.`); - } - } - reviewCommentsBuffer = []; - async bufferReviewComment(path, startLine, endLine, message) { - message = `${COMMENT_GREETING} +/***/ 6180: +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { -${message} +"use strict"; +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "k": () => (/* binding */ Inputs) +/* harmony export */ }); +class Inputs { + systemMessage; + title; + description; + rawSummary; + releaseNotes; + filename; + fileContent; + fileDiff; + patches; + diff; + commentChain; + comment; + constructor(systemMessage = '', title = 'no title provided', description = 'no description provided', rawSummary = '', releaseNotes = '', filename = '', fileContent = 'file contents cannot be provided', fileDiff = 'file diff cannot be provided', patches = '', diff = 'no diff', commentChain = 'no other comments on this patch', comment = 'no comment provided') { + this.systemMessage = systemMessage; + this.title = title; + this.description = description; + this.rawSummary = rawSummary; + this.releaseNotes = releaseNotes; + this.filename = filename; + this.fileContent = fileContent; + this.fileDiff = fileDiff; + this.patches = patches; + this.diff = diff; + this.commentChain = commentChain; + this.comment = comment; + } + clone() { + return new Inputs(this.systemMessage, this.title, this.description, this.rawSummary, this.releaseNotes, this.filename, this.fileContent, this.fileDiff, this.patches, this.diff, this.commentChain, this.comment); + } + render(content) { + if (!content) { + return ''; + } + if (this.systemMessage) { + content = content.replace('$system_message', this.systemMessage); + } + if (this.title) { + content = content.replace('$title', this.title); + } + if (this.description) { + content = content.replace('$description', this.description); + } + if (this.rawSummary) { + content = content.replace('$raw_summary', this.rawSummary); + } + if (this.releaseNotes) { + content = content.replace('$release_notes', this.releaseNotes); + } + if (this.filename) { + content = content.replace('$filename', this.filename); + } + if (this.fileContent) { + content = content.replace('$file_content', this.fileContent); + } + if (this.fileDiff) { + content = content.replace('$file_diff', this.fileDiff); + } + if (this.patches) { + content = content.replace('$patches', this.patches); + } + if (this.diff) { + content = content.replace('$diff', this.diff); + } + if (this.commentChain) { + content = content.replace('$comment_chain', this.commentChain); + } + if (this.comment) { + content = content.replace('$comment', this.comment); + } + return content; + } +} -${COMMENT_TAG}`; - this.reviewCommentsBuffer.push({ - path, - startLine, - endLine, - message - }); - } - async submitReview(pullNumber, commitId) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Submitting review for PR #${pullNumber}, total comments: ${this.reviewCommentsBuffer.length}`); - try { - let commentCounter = 0; - for (const comment of this.reviewCommentsBuffer) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Posting comment: ${comment.message}`); - let found = false; - const comments = await this.getCommentsAtRange(pullNumber, comment.path, comment.startLine, comment.endLine); - for (const c of comments) { - if (c.body.includes(COMMENT_TAG)) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Updating review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`); - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.updateReviewComment */ .K.pulls.updateReviewComment({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - comment_id: c.id, - body: comment.message - }); - found = true; - break; - } - } - if (!found) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Creating new review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`); - const commentData = { - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: pullNumber, - // eslint-disable-next-line camelcase - commit_id: commitId, - body: comment.message, - path: comment.path, - line: comment.endLine - }; - if (comment.startLine !== comment.endLine) { - // eslint-disable-next-line camelcase - commentData.start_side = 'RIGHT'; - // eslint-disable-next-line camelcase - commentData.start_line = comment.startLine; - } - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReviewComment */ .K.pulls.createReviewComment(commentData); - } - commentCounter++; - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Comment ${commentCounter}/${this.reviewCommentsBuffer.length} posted`); - } - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to submit review: ${e}`); - throw e; - } - } - async reviewCommentReply(pullNumber, topLevelComment, message) { - const reply = `${COMMENT_GREETING} -${message} - -${COMMENT_REPLY_TAG} -`; - try { - // Post the reply to the user comment - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReplyForReviewComment */ .K.pulls.createReplyForReviewComment({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: pullNumber, - body: reply, - // eslint-disable-next-line camelcase - comment_id: topLevelComment.id - }); - } - catch (error) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to reply to the top-level comment ${error}`); - try { - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.createReplyForReviewComment */ .K.pulls.createReplyForReviewComment({ - owner: repo.owner, - repo: 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 - }); - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to reply to the top-level comment ${e}`); - } - } - try { - if (topLevelComment.body.includes(COMMENT_TAG)) { - // replace COMMENT_TAG with COMMENT_REPLY_TAG in topLevelComment - const newBody = topLevelComment.body.replace(COMMENT_TAG, COMMENT_REPLY_TAG); - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.updateReviewComment */ .K.pulls.updateReviewComment({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - comment_id: topLevelComment.id, - body: newBody - }); - } - } - catch (error) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.listReviewComments */ .K.pulls.listReviewComments({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: target, - page, - // eslint-disable-next-line camelcase - per_page: 100 - }); - allComments.push(...comments); - page++; - if (!comments || comments.length < 100) { - break; - } - } - this.reviewCommentsCache[target] = allComments; - return allComments; - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to list review comments: ${e}`); - return allComments; - } - } - async create(body, target) { - try { - // get commend ID from the response - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.createComment */ .K.issues.createComment({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - issue_number: target, - body - }); - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to create comment: ${e}`); - } - } - async replace(body, tag, target) { - try { - const cmt = await this.findCommentWithTag(tag, target); - if (cmt) { - await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.updateComment */ .K.issues.updateComment({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - comment_id: cmt.id, - body - }); - } - else { - await this.create(body, target); - } - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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__WEBPACK_IMPORTED_MODULE_2__/* .octokit.issues.listComments */ .K.issues.listComments({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - issue_number: target, - page, - // eslint-disable-next-line camelcase - per_page: 100 - }); - allComments.push(...comments); - page++; - if (!comments || comments.length < 100) { - break; - } - } - this.issueCommentsCache[target] = allComments; - return allComments; - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.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(COMMIT_ID_START_TAG); - const end = commentBody.indexOf(COMMIT_ID_END_TAG); - if (start === -1 || end === -1) { - return []; - } - const ids = commentBody.substring(start + 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(COMMIT_ID_START_TAG); - const end = commentBody.indexOf(COMMIT_ID_END_TAG); - if (start === -1 || end === -1) { - return ''; - } - return commentBody.substring(start, end + 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(COMMIT_ID_START_TAG); - const end = commentBody.indexOf(COMMIT_ID_END_TAG); - if (start === -1 || end === -1) { - return `${commentBody}\n${COMMIT_ID_START_TAG}\n\n${COMMIT_ID_END_TAG}`; - } - const ids = commentBody.substring(start + COMMIT_ID_START_TAG.length, end); - return `${commentBody.substring(0, start + 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 (context && context.payload && context.payload.pull_request != null) { - do { - commits = await _octokit__WEBPACK_IMPORTED_MODULE_2__/* .octokit.pulls.listCommits */ .K.pulls.listCommits({ - owner: repo.owner, - repo: repo.repo, - // eslint-disable-next-line camelcase - pull_number: context.payload.pull_request.number, - // eslint-disable-next-line camelcase - per_page: 100, - page - }); - allCommits.push(...commits.data.map(commit => commit.sha)); - page++; - } while (commits.data.length > 0); - } - return allCommits; - } -} - - -/***/ }), - -/***/ 6180: -/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { - -"use strict"; -/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { -/* harmony export */ "k": () => (/* binding */ Inputs) -/* harmony export */ }); -class Inputs { - systemMessage; - title; - description; - rawSummary; - releaseNotes; - filename; - fileContent; - fileDiff; - patches; - diff; - commentChain; - comment; - constructor(systemMessage = '', title = 'no title provided', description = 'no description provided', rawSummary = '', releaseNotes = '', filename = '', fileContent = 'file contents cannot be provided', fileDiff = 'file diff cannot be provided', patches = '', diff = 'no diff', commentChain = 'no other comments on this patch', comment = 'no comment provided') { - this.systemMessage = systemMessage; - this.title = title; - this.description = description; - this.rawSummary = rawSummary; - this.releaseNotes = releaseNotes; - this.filename = filename; - this.fileContent = fileContent; - this.fileDiff = fileDiff; - this.patches = patches; - this.diff = diff; - this.commentChain = commentChain; - this.comment = comment; - } - clone() { - return new Inputs(this.systemMessage, this.title, this.description, this.rawSummary, this.releaseNotes, this.filename, this.fileContent, this.fileDiff, this.patches, this.diff, this.commentChain, this.comment); - } - render(content) { - if (!content) { - return ''; - } - if (this.systemMessage) { - content = content.replace('$system_message', this.systemMessage); - } - if (this.title) { - content = content.replace('$title', this.title); - } - if (this.description) { - content = content.replace('$description', this.description); - } - if (this.rawSummary) { - content = content.replace('$raw_summary', this.rawSummary); - } - if (this.releaseNotes) { - content = content.replace('$release_notes', this.releaseNotes); - } - if (this.filename) { - content = content.replace('$filename', this.filename); - } - if (this.fileContent) { - content = content.replace('$file_content', this.fileContent); - } - if (this.fileDiff) { - content = content.replace('$file_diff', this.fileDiff); - } - if (this.patches) { - content = content.replace('$patches', this.patches); - } - if (this.diff) { - content = content.replace('$diff', this.diff); - } - if (this.commentChain) { - content = content.replace('$comment_chain', this.commentChain); - } - if (this.comment) { - content = content.replace('$comment', this.comment); - } - return content; - } -} - - -/***/ }), +/***/ }), /***/ 3109: /***/ ((module, __webpack_exports__, __nccwpck_require__) => { @@ -4109,64 +4110,64 @@ __nccwpck_require__.r(__webpack_exports__); /* harmony import */ var _prompts__WEBPACK_IMPORTED_MODULE_5__ = __nccwpck_require__(4272); /* harmony import */ var _review__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(2612); /* harmony import */ var _review_comment__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(5947); - - - - - - -async function run() { - const options = new _options__WEBPACK_IMPORTED_MODULE_2__/* .Options */ .Ei((0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('debug'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('disable_review'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('disable_release_notes'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('max_files'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('review_simple_changes'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('review_comment_lgtm'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getMultilineInput)('path_filters'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('system_message'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_light_model'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_heavy_model'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_model_temperature'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_retries'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_timeout_ms'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_concurrency_limit')); - // print options - options.print(); - const prompts = new _prompts__WEBPACK_IMPORTED_MODULE_5__/* .Prompts */ .j((0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('summarize'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('summarize_release_notes')); - // Create two bots, one for summary and one for review - let lightBot = null; - try { - lightBot = new _bot__WEBPACK_IMPORTED_MODULE_1__/* .Bot */ .r(options, new _options__WEBPACK_IMPORTED_MODULE_2__/* .OpenAIOptions */ .i0(options.openaiLightModel, options.lightTokenLimits)); - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: failed to create summary bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}`); - return; - } - let heavyBot = null; - try { - heavyBot = new _bot__WEBPACK_IMPORTED_MODULE_1__/* .Bot */ .r(options, new _options__WEBPACK_IMPORTED_MODULE_2__/* .OpenAIOptions */ .i0(options.openaiHeavyModel, options.heavyTokenLimits)); - } - catch (e) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: failed to create review bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}`); - return; - } - try { - // check if the event is pull_request - if (process.env.GITHUB_EVENT_NAME === 'pull_request' || - process.env.GITHUB_EVENT_NAME === 'pull_request_target') { - await (0,_review__WEBPACK_IMPORTED_MODULE_3__/* .codeReview */ .z)(lightBot, heavyBot, options, prompts); - } - else if (process.env.GITHUB_EVENT_NAME === 'pull_request_review_comment') { - await (0,_review_comment__WEBPACK_IMPORTED_MODULE_4__/* .handleReviewComment */ .V)(heavyBot, options, prompts); - } - else { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Skipped: this action only works on push events or pull_request'); - } - } - catch (e) { - if (e instanceof Error) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed)(`Failed to run: ${e.message}, backtrace: ${e.stack}`); - } - else { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed)(`Failed to run: ${e}, backtrace: ${e.stack}`); - } - } -} -process - .on('unhandledRejection', (reason, p) => { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Unhandled Rejection at Promise: ${reason}, promise is ${p}`); -}) - .on('uncaughtException', (e) => { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Uncaught Exception thrown: ${e}, backtrace: ${e.stack}`); -}); -await run(); + + + + + + +async function run() { + const options = new _options__WEBPACK_IMPORTED_MODULE_2__/* .Options */ .Ei((0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('debug'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('disable_review'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('disable_release_notes'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('max_files'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('review_simple_changes'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput)('review_comment_lgtm'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getMultilineInput)('path_filters'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('system_message'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_light_model'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_heavy_model'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_model_temperature'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_retries'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_timeout_ms'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_concurrency_limit'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('openai_base_url')); + // print options + options.print(); + const prompts = new _prompts__WEBPACK_IMPORTED_MODULE_5__/* .Prompts */ .j((0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('summarize'), (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('summarize_release_notes')); + // Create two bots, one for summary and one for review + let lightBot = null; + try { + lightBot = new _bot__WEBPACK_IMPORTED_MODULE_1__/* .Bot */ .r(options, new _options__WEBPACK_IMPORTED_MODULE_2__/* .OpenAIOptions */ .i0(options.openaiLightModel, options.lightTokenLimits)); + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: failed to create summary bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}`); + return; + } + let heavyBot = null; + try { + heavyBot = new _bot__WEBPACK_IMPORTED_MODULE_1__/* .Bot */ .r(options, new _options__WEBPACK_IMPORTED_MODULE_2__/* .OpenAIOptions */ .i0(options.openaiHeavyModel, options.heavyTokenLimits)); + } + catch (e) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: failed to create review bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}`); + return; + } + try { + // check if the event is pull_request + if (process.env.GITHUB_EVENT_NAME === 'pull_request' || + process.env.GITHUB_EVENT_NAME === 'pull_request_target') { + await (0,_review__WEBPACK_IMPORTED_MODULE_3__/* .codeReview */ .z)(lightBot, heavyBot, options, prompts); + } + else if (process.env.GITHUB_EVENT_NAME === 'pull_request_review_comment') { + await (0,_review_comment__WEBPACK_IMPORTED_MODULE_4__/* .handleReviewComment */ .V)(heavyBot, options, prompts); + } + else { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Skipped: this action only works on push events or pull_request'); + } + } + catch (e) { + if (e instanceof Error) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed)(`Failed to run: ${e.message}, backtrace: ${e.stack}`); + } + else { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed)(`Failed to run: ${e}, backtrace: ${e.stack}`); + } + } +} +process + .on('unhandledRejection', (reason, p) => { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Unhandled Rejection at Promise: ${reason}, promise is ${p}`); +}) + .on('uncaughtException', (e) => { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Uncaught Exception thrown: ${e}, backtrace: ${e.stack}`); +}); +await run(); __webpack_async_result__(); } catch(e) { __webpack_async_result__(e); } }, 1); @@ -4185,32 +4186,32 @@ __webpack_async_result__(); /* harmony import */ var _octokit_action__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(1231); /* harmony import */ var _octokit_plugin_retry__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(6298); /* harmony import */ var _octokit_plugin_throttling__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(9968); - - - - -const token = (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('token') || process.env.GITHUB_TOKEN; -const RetryAndThrottlingOctokit = _octokit_action__WEBPACK_IMPORTED_MODULE_1__/* .Octokit.plugin */ .v.plugin(_octokit_plugin_throttling__WEBPACK_IMPORTED_MODULE_2__/* .throttling */ .O, _octokit_plugin_retry__WEBPACK_IMPORTED_MODULE_3__/* .retry */ .XD); -const octokit = new RetryAndThrottlingOctokit({ - auth: `token ${token}`, - throttle: { - onRateLimit: (retryAfter, options, _o, retryCount) => { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Request quota exhausted for request ${options.method} ${options.url} -Retry after: ${retryAfter} seconds -Retry count: ${retryCount} -`); - return true; - }, - onSecondaryRateLimit: (_retryAfter, options) => { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`SecondaryRateLimit detected for request ${options.method} ${options.url}`); - return true; - } - }, - retry: { - doNotRetry: ['429'], - maxRetries: 10 - } -}); + + + + +const token = (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput)('token') || process.env.GITHUB_TOKEN; +const RetryAndThrottlingOctokit = _octokit_action__WEBPACK_IMPORTED_MODULE_1__/* .Octokit.plugin */ .v.plugin(_octokit_plugin_throttling__WEBPACK_IMPORTED_MODULE_2__/* .throttling */ .O, _octokit_plugin_retry__WEBPACK_IMPORTED_MODULE_3__/* .retry */ .XD); +const octokit = new RetryAndThrottlingOctokit({ + auth: `token ${token}`, + throttle: { + onRateLimit: (retryAfter, options, _o, retryCount) => { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Request quota exhausted for request ${options.method} ${options.url} +Retry after: ${retryAfter} seconds +Retry count: ${retryCount} +`); + return true; + }, + onSecondaryRateLimit: (_retryAfter, options) => { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`SecondaryRateLimit detected for request ${options.method} ${options.url}`); + return true; + } + }, + retry: { + doNotRetry: ['429'], + maxRetries: 10 + } +}); /***/ }), @@ -5996,148 +5997,151 @@ minimatch.escape = escape_escape; minimatch.unescape = unescape_unescape; //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./lib/limits.js -class TokenLimits { - maxTokens; - requestTokens; - responseTokens; - constructor(model = 'gpt-3.5-turbo') { - if (model === 'gpt-4-32k') { - this.maxTokens = 32600; - this.responseTokens = 4000; - } - else if (model === 'gpt-4') { - this.maxTokens = 8000; - this.responseTokens = 2000; - } - else { - this.maxTokens = 3900; - this.responseTokens = 1000; - } - this.requestTokens = this.maxTokens - this.responseTokens; - } - string() { - return `max_tokens=${this.maxTokens}, request_tokens=${this.requestTokens}, response_tokens=${this.responseTokens}`; - } -} +class TokenLimits { + maxTokens; + requestTokens; + responseTokens; + constructor(model = 'gpt-3.5-turbo') { + if (model === 'gpt-4-32k') { + this.maxTokens = 32600; + this.responseTokens = 4000; + } + else if (model === 'gpt-4') { + this.maxTokens = 8000; + this.responseTokens = 2000; + } + else { + this.maxTokens = 3900; + this.responseTokens = 1000; + } + this.requestTokens = this.maxTokens - this.responseTokens; + } + string() { + return `max_tokens=${this.maxTokens}, request_tokens=${this.requestTokens}, response_tokens=${this.responseTokens}`; + } +} ;// CONCATENATED MODULE: ./lib/options.js - - - -class Options { - debug; - disableReview; - disableReleaseNotes; - maxFiles; - reviewSimpleChanges; - reviewCommentLGTM; - pathFilters; - systemMessage; - openaiLightModel; - openaiHeavyModel; - openaiModelTemperature; - openaiRetries; - openaiTimeoutMS; - openaiConcurrencyLimit; - lightTokenLimits; - heavyTokenLimits; - constructor(debug, disableReview, disableReleaseNotes, maxFiles = '0', reviewSimpleChanges = false, reviewCommentLGTM = false, pathFilters = null, systemMessage = '', openaiLightModel = 'gpt-3.5-turbo', openaiHeavyModel = 'gpt-3.5-turbo', openaiModelTemperature = '0.0', openaiRetries = '3', openaiTimeoutMS = '120000', openaiConcurrencyLimit = '4') { - this.debug = debug; - this.disableReview = disableReview; - this.disableReleaseNotes = disableReleaseNotes; - this.maxFiles = parseInt(maxFiles); - this.reviewSimpleChanges = reviewSimpleChanges; - this.reviewCommentLGTM = reviewCommentLGTM; - this.pathFilters = new PathFilter(pathFilters); - this.systemMessage = systemMessage; - this.openaiLightModel = openaiLightModel; - this.openaiHeavyModel = openaiHeavyModel; - this.openaiModelTemperature = parseFloat(openaiModelTemperature); - this.openaiRetries = parseInt(openaiRetries); - this.openaiTimeoutMS = parseInt(openaiTimeoutMS); - this.openaiConcurrencyLimit = parseInt(openaiConcurrencyLimit); - this.lightTokenLimits = new TokenLimits(openaiLightModel); - this.heavyTokenLimits = new TokenLimits(openaiHeavyModel); - } - // print all options using core.info - print() { - (0,core.info)(`debug: ${this.debug}`); - (0,core.info)(`disable_review: ${this.disableReview}`); - (0,core.info)(`disable_release_notes: ${this.disableReleaseNotes}`); - (0,core.info)(`max_files: ${this.maxFiles}`); - (0,core.info)(`review_simple_changes: ${this.reviewSimpleChanges}`); - (0,core.info)(`review_comment_lgtm: ${this.reviewCommentLGTM}`); - (0,core.info)(`path_filters: ${this.pathFilters}`); - (0,core.info)(`system_message: ${this.systemMessage}`); - (0,core.info)(`openai_light_model: ${this.openaiLightModel}`); - (0,core.info)(`openai_heavy_model: ${this.openaiHeavyModel}`); - (0,core.info)(`openai_model_temperature: ${this.openaiModelTemperature}`); - (0,core.info)(`openai_retries: ${this.openaiRetries}`); - (0,core.info)(`openai_timeout_ms: ${this.openaiTimeoutMS}`); - (0,core.info)(`openai_concurrency_limit: ${this.openaiConcurrencyLimit}`); - (0,core.info)(`summary_token_limits: ${this.lightTokenLimits.string()}`); - (0,core.info)(`review_token_limits: ${this.heavyTokenLimits.string()}`); - } - checkPath(path) { - const ok = this.pathFilters.check(path); - (0,core.info)(`checking path: ${path} => ${ok}`); - return ok; - } -} -class PathFilter { - rules; - constructor(rules = null) { - this.rules = []; - if (rules != null) { - for (const rule of rules) { - const trimmed = rule?.trim(); - if (trimmed) { - if (trimmed.startsWith('!')) { - this.rules.push([trimmed.substring(1).trim(), true]); - } - else { - this.rules.push([trimmed, false]); - } - } - } - } - } - check(path) { - if (this.rules.length === 0) { - return true; - } - let included = false; - let excluded = false; - let inclusionRuleExists = false; - for (const [rule, exclude] of this.rules) { - if (minimatch(path, rule)) { - if (exclude) { - excluded = true; - } - else { - included = true; - } - } - if (!exclude) { - inclusionRuleExists = true; - } - } - return (!inclusionRuleExists || included) && !excluded; - } -} -class OpenAIOptions { - model; - tokenLimits; - constructor(model = 'gpt-3.5-turbo', tokenLimits = null) { - this.model = model; - if (tokenLimits != null) { - this.tokenLimits = tokenLimits; - } - else { - this.tokenLimits = new TokenLimits(model); - } - } -} + + + +class Options { + debug; + disableReview; + disableReleaseNotes; + maxFiles; + reviewSimpleChanges; + reviewCommentLGTM; + pathFilters; + systemMessage; + openaiLightModel; + openaiHeavyModel; + openaiModelTemperature; + openaiRetries; + openaiTimeoutMS; + openaiConcurrencyLimit; + lightTokenLimits; + heavyTokenLimits; + apiBaseUrl; + constructor(debug, disableReview, disableReleaseNotes, maxFiles = '0', reviewSimpleChanges = false, reviewCommentLGTM = false, pathFilters = null, systemMessage = '', openaiLightModel = 'gpt-3.5-turbo', openaiHeavyModel = 'gpt-3.5-turbo', openaiModelTemperature = '0.0', openaiRetries = '3', openaiTimeoutMS = '120000', openaiConcurrencyLimit = '4', apiBaseUrl = "https://api.openai.com/v1") { + this.debug = debug; + this.disableReview = disableReview; + this.disableReleaseNotes = disableReleaseNotes; + this.maxFiles = parseInt(maxFiles); + this.reviewSimpleChanges = reviewSimpleChanges; + this.reviewCommentLGTM = reviewCommentLGTM; + this.pathFilters = new PathFilter(pathFilters); + this.systemMessage = systemMessage; + this.openaiLightModel = openaiLightModel; + this.openaiHeavyModel = openaiHeavyModel; + this.openaiModelTemperature = parseFloat(openaiModelTemperature); + this.openaiRetries = parseInt(openaiRetries); + this.openaiTimeoutMS = parseInt(openaiTimeoutMS); + this.openaiConcurrencyLimit = parseInt(openaiConcurrencyLimit); + this.lightTokenLimits = new TokenLimits(openaiLightModel); + this.heavyTokenLimits = new TokenLimits(openaiHeavyModel); + this.apiBaseUrl = apiBaseUrl; + } + // print all options using core.info + print() { + (0,core.info)(`debug: ${this.debug}`); + (0,core.info)(`disable_review: ${this.disableReview}`); + (0,core.info)(`disable_release_notes: ${this.disableReleaseNotes}`); + (0,core.info)(`max_files: ${this.maxFiles}`); + (0,core.info)(`review_simple_changes: ${this.reviewSimpleChanges}`); + (0,core.info)(`review_comment_lgtm: ${this.reviewCommentLGTM}`); + (0,core.info)(`path_filters: ${this.pathFilters}`); + (0,core.info)(`system_message: ${this.systemMessage}`); + (0,core.info)(`openai_light_model: ${this.openaiLightModel}`); + (0,core.info)(`openai_heavy_model: ${this.openaiHeavyModel}`); + (0,core.info)(`openai_model_temperature: ${this.openaiModelTemperature}`); + (0,core.info)(`openai_retries: ${this.openaiRetries}`); + (0,core.info)(`openai_timeout_ms: ${this.openaiTimeoutMS}`); + (0,core.info)(`openai_concurrency_limit: ${this.openaiConcurrencyLimit}`); + (0,core.info)(`summary_token_limits: ${this.lightTokenLimits.string()}`); + (0,core.info)(`review_token_limits: ${this.heavyTokenLimits.string()}`); + (0,core.info)(`api_base_url: ${this.apiBaseUrl}`); + } + checkPath(path) { + const ok = this.pathFilters.check(path); + (0,core.info)(`checking path: ${path} => ${ok}`); + return ok; + } +} +class PathFilter { + rules; + constructor(rules = null) { + this.rules = []; + if (rules != null) { + for (const rule of rules) { + const trimmed = rule?.trim(); + if (trimmed) { + if (trimmed.startsWith('!')) { + this.rules.push([trimmed.substring(1).trim(), true]); + } + else { + this.rules.push([trimmed, false]); + } + } + } + } + } + check(path) { + if (this.rules.length === 0) { + return true; + } + let included = false; + let excluded = false; + let inclusionRuleExists = false; + for (const [rule, exclude] of this.rules) { + if (minimatch(path, rule)) { + if (exclude) { + excluded = true; + } + else { + included = true; + } + } + if (!exclude) { + inclusionRuleExists = true; + } + } + return (!inclusionRuleExists || included) && !excluded; + } +} +class OpenAIOptions { + model; + tokenLimits; + constructor(model = 'gpt-3.5-turbo', tokenLimits = null) { + this.model = model; + if (tokenLimits != null) { + this.tokenLimits = tokenLimits; + } + else { + this.tokenLimits = new TokenLimits(model); + } + } +} /***/ }), @@ -6149,263 +6153,263 @@ class OpenAIOptions { /* harmony export */ __nccwpck_require__.d(__webpack_exports__, { /* harmony export */ "j": () => (/* binding */ Prompts) /* harmony export */ }); -class Prompts { - summarize; - summarizeReleaseNotes; - summarizeFileDiff = `GitHub pull request title: -\`$title\` - -Description: -\`\`\` -$description -\`\`\` - -Diff: -\`\`\`diff -$file_diff -\`\`\` - -I would like you to summarize the diff within 50 words. -`; - triageFileDiff = `Below the summary, I would also like you to triage the diff as \`NEEDS_REVIEW\` or -\`APPROVED\` based on the following criteria: - -- If the diff involves any modifications to the logic or functionality, even if they - seem minor, triage it as \`NEEDS_REVIEW\`. This includes changes to control structures, - function calls, or variable assignments that might impact the behavior of the code. -- If the diff only contains very minor changes that don't affect the code logic, such as - fixing typos, formatting, or renaming variables for clarity, triage it as \`APPROVED\`. - -Please evaluate the diff thoroughly and take into account factors such as the number of -lines changed, the potential impact on the overall system, and the likelihood of -introducing new bugs or security vulnerabilities. -When in doubt, always err on the side of caution and triage the diff as \`NEEDS_REVIEW\`. - -You must follow the format below strictly for triaging the diff and -do not add any additional text in your response: -[TRIAGE]: -`; - summarizeChangesets = `Provided below are changesets in this pull request. Changesets -are in chronlogical order and new changesets are appended to the -end of the list. The format consists of filename(s) and the summary -of changes for those files. There is a separator between each changeset. -Your task is to de-deduplicate and group together files with -related/similar changes into a single changeset. Respond with the updated -changesets using the same format as the input. - -$raw_summary -`; - summarizePrefix = `Here is the summary of changes you have generated for files: - \`\`\` - $raw_summary - \`\`\` - -`; - reviewFileDiff = `GitHub pull request title: -\`$title\` - -Description: -\`\`\` -$description -\`\`\` - -Content of \`$filename\` prior to changes: -\`\`\` -$file_content -\`\`\` - -Format for changes: - ---new_hunk--- - \`\`\` - - \`\`\` - - ---old_hunk--- - \`\`\` - - \`\`\` - - ---comment_chains--- - \`\`\` - - \`\`\` - - ---end_change_section--- - ... - -Instructions: - -- The format for changes provided above consists of multiple change - sections, each containing a new hunk (annotated with line numbers), - an old hunk, and optionally, existing comment chains. Note that the - old hunk code has been replaced by the new hunk. The line number - annotation on each line in the new hunk is of the format - \`\`. -- Your task is to review ONLY the new hunks line by line, ONLY pointing out - substantive issues within line number ranges. Provide the exact line - number range (inclusive) for each issue. Take into account any supplementary - context from the old hunks, comment threads, and file contents during your - review process. Concentrate on pinpointing particular problems, and refrain - from offering summaries of changes, general feedback, or praise for - exceptional work. Additionally, avoid reiterating the provided code in your - review comments. -- IMPORTANT: Respond only in the response format (consisting of review - sections). Each review section must have a line number range and a review - comment for that range. Do not include general feedback or summaries. You - may optionally include a single replacement suggestion snippet and/or - multiple new code snippets in the review comment. Separate review sections - using separators. -- IMPORTANT: Line number ranges for each review section must be within the - range of a specific new hunk. must belong to the same - hunk as the . The line number range is sufficient to map - your comment to the code changes in the GitHub pull request. -- Use Markdown format for review comment text and fenced code blocks for - code snippets. Do not annotate code snippets with line numbers. -- If needed, provide replacement suggestions using fenced code blocks with the - \`suggestion\` language identifier. The line number range must map exactly - to the range (inclusive) that needs to be replaced within a new hunk. For - instance, if 2 lines of code in a hunk need to be replaced with 15 lines - of code, the line number range must be those exact 2 lines. If an entire - hunk need to be replaced with new code, then the line number range must - be the entire hunk. -- Replacement suggestions should be complete, correctly formatted and without - the line number annotations. Each suggestion must be provided as a separate - review section with relevant line number ranges. -- If needed, suggest new code using the correct language identifier in the - fenced code blocks. These snippets may be added to a different file, - such as test cases. Multiple new code snippets are allowed within a single - review section. -- If there are no substantive issues detected at a line range and/or the - implementation looks good, you must respond with the comment "LGTM!" and - nothing else for the respective line range in a review section. -- Reflect on your comments and line number ranges before sending the final - response to ensure accuracy of line number ranges and replacement snippets. - -Response format expected: - -: - - --- - -: - - \`\`\`suggestion - - \`\`\` - --- - -: - - \`\`\` - - \`\`\` - --- - ... - -Example changes: - ---new_hunk--- - 1: def add(x, y): - 2: z = x+y - 3: retrn z - 4: - 5: def multiply(x, y): - 6: return x * y - - ---old_hunk--- - def add(x, y): - return x + y - -Example response: - 1-3: - There's a typo in the return statement. - \`\`\`suggestion - def add(x, y): - z = x + y - return z - \`\`\` - --- - 5-6: - LGTM! - --- - -Changes for review are below: -$patches -`; - comment = `A comment was made on a GitHub pull request review for a -diff hunk on file \`$filename\`. I would like you to follow -the instructions in that comment. - -Pull request title: -\`$title\` - -Description: -\`\`\` -$description -\`\`\` - -Content of file prior to changes: -\`\`\` -$file_content -\`\`\` - -Entire diff: -\`\`\`diff -$file_diff -\`\`\` - -Diff being commented on: -\`\`\`diff -$diff -\`\`\` - -The format of a comment in the chain is: -\`user: comment\` - -Comment chain (including the new comment): -\`\`\` -$comment_chain -\`\`\` - -Please reply directly to the new comment (instead of suggesting -a reply) and your reply will be posted as-is. - -If the comment contains instructions/requests for you, please comply. -For example, if the comment is asking you to generate documentation -comments on the code, in your reply please generate the required code. - -In your reply, please make sure to begin the reply by tagging the user -with "@user". - -The comment/request that you need to directly reply to: -\`\`\` -$comment -\`\`\` -`; - constructor(summarize = '', summarizeReleaseNotes = '') { - this.summarize = summarize; - this.summarizeReleaseNotes = summarizeReleaseNotes; - } - renderSummarizeFileDiff(inputs, reviewSimpleChanges) { - let prompt = this.summarizeFileDiff; - if (reviewSimpleChanges === false) { - prompt += this.triageFileDiff; - } - return inputs.render(prompt); - } - renderSummarizeChangesets(inputs) { - return inputs.render(this.summarizeChangesets); - } - renderSummarize(inputs) { - const prompt = this.summarizePrefix + this.summarize; - return inputs.render(prompt); - } - renderSummarizeReleaseNotes(inputs) { - return inputs.render(this.summarizeReleaseNotes); - } - renderComment(inputs) { - return inputs.render(this.comment); - } - renderReviewFileDiff(inputs) { - return inputs.render(this.reviewFileDiff); - } -} +class Prompts { + summarize; + summarizeReleaseNotes; + summarizeFileDiff = `GitHub pull request title: +\`$title\` + +Description: +\`\`\` +$description +\`\`\` + +Diff: +\`\`\`diff +$file_diff +\`\`\` + +I would like you to summarize the diff within 50 words. +`; + triageFileDiff = `Below the summary, I would also like you to triage the diff as \`NEEDS_REVIEW\` or +\`APPROVED\` based on the following criteria: + +- If the diff involves any modifications to the logic or functionality, even if they + seem minor, triage it as \`NEEDS_REVIEW\`. This includes changes to control structures, + function calls, or variable assignments that might impact the behavior of the code. +- If the diff only contains very minor changes that don't affect the code logic, such as + fixing typos, formatting, or renaming variables for clarity, triage it as \`APPROVED\`. + +Please evaluate the diff thoroughly and take into account factors such as the number of +lines changed, the potential impact on the overall system, and the likelihood of +introducing new bugs or security vulnerabilities. +When in doubt, always err on the side of caution and triage the diff as \`NEEDS_REVIEW\`. + +You must follow the format below strictly for triaging the diff and +do not add any additional text in your response: +[TRIAGE]: +`; + summarizeChangesets = `Provided below are changesets in this pull request. Changesets +are in chronlogical order and new changesets are appended to the +end of the list. The format consists of filename(s) and the summary +of changes for those files. There is a separator between each changeset. +Your task is to de-deduplicate and group together files with +related/similar changes into a single changeset. Respond with the updated +changesets using the same format as the input. + +$raw_summary +`; + summarizePrefix = `Here is the summary of changes you have generated for files: + \`\`\` + $raw_summary + \`\`\` + +`; + reviewFileDiff = `GitHub pull request title: +\`$title\` + +Description: +\`\`\` +$description +\`\`\` + +Content of \`$filename\` prior to changes: +\`\`\` +$file_content +\`\`\` + +Format for changes: + ---new_hunk--- + \`\`\` + + \`\`\` + + ---old_hunk--- + \`\`\` + + \`\`\` + + ---comment_chains--- + \`\`\` + + \`\`\` + + ---end_change_section--- + ... + +Instructions: + +- The format for changes provided above consists of multiple change + sections, each containing a new hunk (annotated with line numbers), + an old hunk, and optionally, existing comment chains. Note that the + old hunk code has been replaced by the new hunk. The line number + annotation on each line in the new hunk is of the format + \`\`. +- Your task is to review ONLY the new hunks line by line, ONLY pointing out + substantive issues within line number ranges. Provide the exact line + number range (inclusive) for each issue. Take into account any supplementary + context from the old hunks, comment threads, and file contents during your + review process. Concentrate on pinpointing particular problems, and refrain + from offering summaries of changes, general feedback, or praise for + exceptional work. Additionally, avoid reiterating the provided code in your + review comments. +- IMPORTANT: Respond only in the response format (consisting of review + sections). Each review section must have a line number range and a review + comment for that range. Do not include general feedback or summaries. You + may optionally include a single replacement suggestion snippet and/or + multiple new code snippets in the review comment. Separate review sections + using separators. +- IMPORTANT: Line number ranges for each review section must be within the + range of a specific new hunk. must belong to the same + hunk as the . The line number range is sufficient to map + your comment to the code changes in the GitHub pull request. +- Use Markdown format for review comment text and fenced code blocks for + code snippets. Do not annotate code snippets with line numbers. +- If needed, provide replacement suggestions using fenced code blocks with the + \`suggestion\` language identifier. The line number range must map exactly + to the range (inclusive) that needs to be replaced within a new hunk. For + instance, if 2 lines of code in a hunk need to be replaced with 15 lines + of code, the line number range must be those exact 2 lines. If an entire + hunk need to be replaced with new code, then the line number range must + be the entire hunk. +- Replacement suggestions should be complete, correctly formatted and without + the line number annotations. Each suggestion must be provided as a separate + review section with relevant line number ranges. +- If needed, suggest new code using the correct language identifier in the + fenced code blocks. These snippets may be added to a different file, + such as test cases. Multiple new code snippets are allowed within a single + review section. +- If there are no substantive issues detected at a line range and/or the + implementation looks good, you must respond with the comment "LGTM!" and + nothing else for the respective line range in a review section. +- Reflect on your comments and line number ranges before sending the final + response to ensure accuracy of line number ranges and replacement snippets. + +Response format expected: + -: + + --- + -: + + \`\`\`suggestion + + \`\`\` + --- + -: + + \`\`\` + + \`\`\` + --- + ... + +Example changes: + ---new_hunk--- + 1: def add(x, y): + 2: z = x+y + 3: retrn z + 4: + 5: def multiply(x, y): + 6: return x * y + + ---old_hunk--- + def add(x, y): + return x + y + +Example response: + 1-3: + There's a typo in the return statement. + \`\`\`suggestion + def add(x, y): + z = x + y + return z + \`\`\` + --- + 5-6: + LGTM! + --- + +Changes for review are below: +$patches +`; + comment = `A comment was made on a GitHub pull request review for a +diff hunk on file \`$filename\`. I would like you to follow +the instructions in that comment. + +Pull request title: +\`$title\` + +Description: +\`\`\` +$description +\`\`\` + +Content of file prior to changes: +\`\`\` +$file_content +\`\`\` + +Entire diff: +\`\`\`diff +$file_diff +\`\`\` + +Diff being commented on: +\`\`\`diff +$diff +\`\`\` + +The format of a comment in the chain is: +\`user: comment\` + +Comment chain (including the new comment): +\`\`\` +$comment_chain +\`\`\` + +Please reply directly to the new comment (instead of suggesting +a reply) and your reply will be posted as-is. + +If the comment contains instructions/requests for you, please comply. +For example, if the comment is asking you to generate documentation +comments on the code, in your reply please generate the required code. + +In your reply, please make sure to begin the reply by tagging the user +with "@user". + +The comment/request that you need to directly reply to: +\`\`\` +$comment +\`\`\` +`; + constructor(summarize = '', summarizeReleaseNotes = '') { + this.summarize = summarize; + this.summarizeReleaseNotes = summarizeReleaseNotes; + } + renderSummarizeFileDiff(inputs, reviewSimpleChanges) { + let prompt = this.summarizeFileDiff; + if (reviewSimpleChanges === false) { + prompt += this.triageFileDiff; + } + return inputs.render(prompt); + } + renderSummarizeChangesets(inputs) { + return inputs.render(this.summarizeChangesets); + } + renderSummarize(inputs) { + const prompt = this.summarizePrefix + this.summarize; + return inputs.render(prompt); + } + renderSummarizeReleaseNotes(inputs) { + return inputs.render(this.summarizeReleaseNotes); + } + renderComment(inputs) { + return inputs.render(this.comment); + } + renderReviewFileDiff(inputs) { + return inputs.render(this.reviewFileDiff); + } +} /***/ }), @@ -6425,178 +6429,178 @@ $comment /* harmony import */ var _inputs__WEBPACK_IMPORTED_MODULE_5__ = __nccwpck_require__(6180); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(3258); /* harmony import */ var _tokenizer__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(652); + +// eslint-disable-next-line camelcase + + + + + +// eslint-disable-next-line camelcase +const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; +const repo = context.repo; +const ASK_BOT = '@openai'; +const handleReviewComment = async (heavyBot, options, prompts) => { + const commenter = new _commenter__WEBPACK_IMPORTED_MODULE_2__/* .Commenter */ .Es(); + const inputs = new _inputs__WEBPACK_IMPORTED_MODULE_5__/* .Inputs */ .k(); + if (context.eventName !== 'pull_request_review_comment') { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} is not a pull_request_review_comment event`); + return; + } + if (!context.payload) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing payload`); + return; + } + const comment = context.payload.comment; + if (comment == null) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing comment`); + return; + } + if (context.payload.pull_request == null || + context.payload.repository == null) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing pull_request`); + return; + } + inputs.title = context.payload.pull_request.title; + if (context.payload.pull_request.body) { + inputs.description = commenter.getDescription(context.payload.pull_request.body); + inputs.releaseNotes = commenter.getReleaseNotes(context.payload.pull_request.body); + } + // check if the comment was created and not edited or deleted + if (context.payload.action !== 'created') { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is not created`); + return; + } + // Check if the comment is not from the bot itself + if (!comment.body.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) && + !comment.body.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD)) { + const pullNumber = context.payload.pull_request.number; + inputs.comment = `${comment.user.login}: ${comment.body}`; + inputs.diff = comment.diff_hunk; + inputs.filename = comment.path; + const { chain: commentChain, topLevelComment } = await commenter.getCommentChain(pullNumber, comment); + if (!topLevelComment) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Failed to find the top-level comment to reply to'); + return; + } + inputs.commentChain = commentChain; + // check whether this chain contains replies from the bot + if (commentChain.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) || + commentChain.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD) || + comment.body.includes(ASK_BOT)) { + let fileContent = ''; + try { + const contents = await _octokit__WEBPACK_IMPORTED_MODULE_3__/* .octokit.repos.getContent */ .K.repos.getContent({ + owner: repo.owner, + repo: repo.repo, + path: comment.path, + ref: context.payload.pull_request.base.sha + }); + if (contents.data) { + if (!Array.isArray(contents.data)) { + if (contents.data.type === 'file' && contents.data.content) { + fileContent = Buffer.from(contents.data.content, 'base64').toString(); + } + } + } + } + catch (error) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get file contents: ${error}, skipping.`); + } + let fileDiff = ''; + try { + // get diff for this file by comparing the base and head commits + const diffAll = await _octokit__WEBPACK_IMPORTED_MODULE_3__/* .octokit.repos.compareCommits */ .K.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha + }); + if (diffAll.data) { + const files = diffAll.data.files; + if (files != null) { + const file = files.find(f => f.filename === comment.path); + if (file != null && file.patch) { + fileDiff = file.patch; + } + } + } + } + catch (error) { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get file diff: ${error}, skipping.`); + } + // use file diff if no diff was found in the comment + if (inputs.diff.length === 0) { + if (fileDiff.length > 0) { + inputs.diff = fileDiff; + fileDiff = ''; + } + else { + await commenter.reviewCommentReply(pullNumber, topLevelComment, 'Cannot reply to this comment as diff could not be found.'); + return; + } + } + // get tokens so far + let tokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(prompts.renderComment(inputs)); + if (tokens > options.heavyTokenLimits.requestTokens) { + await commenter.reviewCommentReply(pullNumber, topLevelComment, 'Cannot reply to this comment as diff being commented is too large and exceeds the token limit.'); + return; + } + // pack file content and diff into the inputs if they are not too long + if (fileContent.length > 0) { + // count occurrences of $file_content in prompt + const fileContentCount = prompts.comment.split('$file_content').length - 1; + const fileContentTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(fileContent); + if (fileContentCount > 0 && + tokens + fileContentTokens * fileContentCount <= + options.heavyTokenLimits.requestTokens) { + tokens += fileContentTokens * fileContentCount; + inputs.fileContent = fileContent; + } + } + if (fileDiff.length > 0) { + // count occurrences of $file_diff in prompt + const fileDiffCount = prompts.comment.split('$file_diff').length - 1; + const fileDiffTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(fileDiff); + if (fileDiffCount > 0 && + tokens + fileDiffTokens * fileDiffCount <= + options.heavyTokenLimits.requestTokens) { + tokens += fileDiffTokens * fileDiffCount; + inputs.fileDiff = fileDiff; + } + } + // get summary of the PR + const summary = await commenter.findCommentWithTag(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, pullNumber); + if (summary) { + // pack summary into the inputs if it is not too long + const rawSummary = commenter.getRawSummary(summary.body); + const summaryTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(rawSummary); + if (tokens + summaryTokens <= options.heavyTokenLimits.requestTokens) { + tokens += summaryTokens; + inputs.rawSummary = rawSummary; + } + } + const [reply] = await heavyBot.chat(prompts.renderComment(inputs), {}); + await commenter.reviewCommentReply(pullNumber, topLevelComment, reply); + } + } + else { + (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Skipped: ${context.eventName} event is from the bot itself`); + } +}; -// eslint-disable-next-line camelcase +/***/ }), +/***/ 2612: +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { +"use strict"; - -// eslint-disable-next-line camelcase -const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; -const repo = context.repo; -const ASK_BOT = '@openai'; -const handleReviewComment = async (heavyBot, options, prompts) => { - const commenter = new _commenter__WEBPACK_IMPORTED_MODULE_2__/* .Commenter */ .Es(); - const inputs = new _inputs__WEBPACK_IMPORTED_MODULE_5__/* .Inputs */ .k(); - if (context.eventName !== 'pull_request_review_comment') { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} is not a pull_request_review_comment event`); - return; - } - if (!context.payload) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing payload`); - return; - } - const comment = context.payload.comment; - if (comment == null) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing comment`); - return; - } - if (context.payload.pull_request == null || - context.payload.repository == null) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is missing pull_request`); - return; - } - inputs.title = context.payload.pull_request.title; - if (context.payload.pull_request.body) { - inputs.description = commenter.getDescription(context.payload.pull_request.body); - inputs.releaseNotes = commenter.getReleaseNotes(context.payload.pull_request.body); - } - // check if the comment was created and not edited or deleted - if (context.payload.action !== 'created') { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Skipped: ${context.eventName} event is not created`); - return; - } - // Check if the comment is not from the bot itself - if (!comment.body.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) && - !comment.body.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD)) { - const pullNumber = context.payload.pull_request.number; - inputs.comment = `${comment.user.login}: ${comment.body}`; - inputs.diff = comment.diff_hunk; - inputs.filename = comment.path; - const { chain: commentChain, topLevelComment } = await commenter.getCommentChain(pullNumber, comment); - if (!topLevelComment) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)('Failed to find the top-level comment to reply to'); - return; - } - inputs.commentChain = commentChain; - // check whether this chain contains replies from the bot - if (commentChain.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) || - commentChain.includes(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD) || - comment.body.includes(ASK_BOT)) { - let fileContent = ''; - try { - const contents = await _octokit__WEBPACK_IMPORTED_MODULE_3__/* .octokit.repos.getContent */ .K.repos.getContent({ - owner: repo.owner, - repo: repo.repo, - path: comment.path, - ref: context.payload.pull_request.base.sha - }); - if (contents.data) { - if (!Array.isArray(contents.data)) { - if (contents.data.type === 'file' && contents.data.content) { - fileContent = Buffer.from(contents.data.content, 'base64').toString(); - } - } - } - } - catch (error) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get file contents: ${error}, skipping.`); - } - let fileDiff = ''; - try { - // get diff for this file by comparing the base and head commits - const diffAll = await _octokit__WEBPACK_IMPORTED_MODULE_3__/* .octokit.repos.compareCommits */ .K.repos.compareCommits({ - owner: repo.owner, - repo: repo.repo, - base: context.payload.pull_request.base.sha, - head: context.payload.pull_request.head.sha - }); - if (diffAll.data) { - const files = diffAll.data.files; - if (files != null) { - const file = files.find(f => f.filename === comment.path); - if (file != null && file.patch) { - fileDiff = file.patch; - } - } - } - } - catch (error) { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.warning)(`Failed to get file diff: ${error}, skipping.`); - } - // use file diff if no diff was found in the comment - if (inputs.diff.length === 0) { - if (fileDiff.length > 0) { - inputs.diff = fileDiff; - fileDiff = ''; - } - else { - await commenter.reviewCommentReply(pullNumber, topLevelComment, 'Cannot reply to this comment as diff could not be found.'); - return; - } - } - // get tokens so far - let tokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(prompts.renderComment(inputs)); - if (tokens > options.heavyTokenLimits.requestTokens) { - await commenter.reviewCommentReply(pullNumber, topLevelComment, 'Cannot reply to this comment as diff being commented is too large and exceeds the token limit.'); - return; - } - // pack file content and diff into the inputs if they are not too long - if (fileContent.length > 0) { - // count occurrences of $file_content in prompt - const fileContentCount = prompts.comment.split('$file_content').length - 1; - const fileContentTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(fileContent); - if (fileContentCount > 0 && - tokens + fileContentTokens * fileContentCount <= - options.heavyTokenLimits.requestTokens) { - tokens += fileContentTokens * fileContentCount; - inputs.fileContent = fileContent; - } - } - if (fileDiff.length > 0) { - // count occurrences of $file_diff in prompt - const fileDiffCount = prompts.comment.split('$file_diff').length - 1; - const fileDiffTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(fileDiff); - if (fileDiffCount > 0 && - tokens + fileDiffTokens * fileDiffCount <= - options.heavyTokenLimits.requestTokens) { - tokens += fileDiffTokens * fileDiffCount; - inputs.fileDiff = fileDiff; - } - } - // get summary of the PR - const summary = await commenter.findCommentWithTag(_commenter__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, pullNumber); - if (summary) { - // pack summary into the inputs if it is not too long - const rawSummary = commenter.getRawSummary(summary.body); - const summaryTokens = (0,_tokenizer__WEBPACK_IMPORTED_MODULE_4__/* .getTokenCount */ .V)(rawSummary); - if (tokens + summaryTokens <= options.heavyTokenLimits.requestTokens) { - tokens += summaryTokens; - inputs.rawSummary = rawSummary; - } - } - const [reply] = await heavyBot.chat(prompts.renderComment(inputs), {}); - await commenter.reviewCommentReply(pullNumber, topLevelComment, reply); - } - } - else { - (0,_actions_core__WEBPACK_IMPORTED_MODULE_0__.info)(`Skipped: ${context.eventName} event is from the bot itself`); - } -}; - - -/***/ }), - -/***/ 2612: -/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { - -"use strict"; - -// EXPORTS -__nccwpck_require__.d(__webpack_exports__, { - "z": () => (/* binding */ codeReview) -}); +// EXPORTS +__nccwpck_require__.d(__webpack_exports__, { + "z": () => (/* binding */ codeReview) +}); // EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js var core = __nccwpck_require__(2186); @@ -6750,700 +6754,700 @@ var octokit = __nccwpck_require__(3258); // EXTERNAL MODULE: ./lib/tokenizer.js var tokenizer = __nccwpck_require__(652); ;// CONCATENATED MODULE: ./lib/review.js - -// eslint-disable-next-line camelcase - - - - - - -// eslint-disable-next-line camelcase -const context = github.context; -const repo = context.repo; -const ignoreKeyword = '@openai: ignore'; -const codeReview = async (lightBot, heavyBot, options, prompts) => { - const commenter = new lib_commenter/* Commenter */.Es(); - const openaiConcurrencyLimit = pLimit(options.openaiConcurrencyLimit); - if (context.eventName !== 'pull_request' && - context.eventName !== 'pull_request_target') { - (0,core.warning)(`Skipped: current event is ${context.eventName}, only support pull_request event`); - return; - } - if (context.payload.pull_request == null) { - (0,core.warning)('Skipped: context.payload.pull_request is null'); - return; - } - const inputs = new lib_inputs/* Inputs */.k(); - inputs.title = context.payload.pull_request.title; - if (context.payload.pull_request.body != null) { - inputs.description = commenter.getDescription(context.payload.pull_request.body); - inputs.releaseNotes = commenter.getReleaseNotes(context.payload.pull_request.body); - } - // if the description contains ignore_keyword, skip - if (inputs.description.includes(ignoreKeyword)) { - (0,core.info)('Skipped: description contains ignore_keyword'); - return; - } - // as gpt-3.5-turbo isn't paying attention to system message, add to inputs for now - inputs.systemMessage = options.systemMessage; - // get SUMMARIZE_TAG message - const existingSummarizeCmt = await commenter.findCommentWithTag(lib_commenter/* SUMMARIZE_TAG */.Rp, context.payload.pull_request.number); - let existingCommitIdsBlock = ''; - if (existingSummarizeCmt != null) { - inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmt.body); - existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmt.body); - } - const allCommitIds = await commenter.getAllCommitIds(); - // find highest reviewed commit id - let highestReviewedCommitId = ''; - if (existingCommitIdsBlock !== '') { - highestReviewedCommitId = commenter.getHighestReviewedCommitId(allCommitIds, commenter.getReviewedCommitIds(existingCommitIdsBlock)); - } - if (highestReviewedCommitId === '' || - highestReviewedCommitId === context.payload.pull_request.head.sha) { - (0,core.info)(`Will review from the base commit: ${context.payload.pull_request.base.sha}`); - highestReviewedCommitId = context.payload.pull_request.base.sha; - } - else { - (0,core.info)(`Will review from commit: ${highestReviewedCommitId}`); - } - // Fetch the diff between the highest reviewed commit and the latest commit of the PR branch - const incrementalDiff = await octokit/* octokit.repos.compareCommits */.K.repos.compareCommits({ - owner: repo.owner, - repo: repo.repo, - base: highestReviewedCommitId, - head: context.payload.pull_request.head.sha - }); - // Fetch the diff between the target branch's base commit and the latest commit of the PR branch - const targetBranchDiff = await octokit/* octokit.repos.compareCommits */.K.repos.compareCommits({ - owner: repo.owner, - repo: repo.repo, - base: context.payload.pull_request.base.sha, - head: context.payload.pull_request.head.sha - }); - const incrementalFiles = incrementalDiff.data.files; - const targetBranchFiles = targetBranchDiff.data.files; - if (incrementalFiles == null || targetBranchFiles == null) { - (0,core.warning)('Skipped: files data is missing'); - return; - } - // Filter out any file that is changed compared to the incremental changes - const files = targetBranchFiles.filter(targetBranchFile => incrementalFiles.some(incrementalFile => incrementalFile.filename === targetBranchFile.filename)); - if (files.length === 0) { - (0,core.warning)('Skipped: files is null'); - return; - } - const commits = incrementalDiff.data.commits; - if (commits.length === 0) { - (0,core.warning)('Skipped: ommits is null'); - return; - } - // skip files if they are filtered out - const filterSelectedFiles = []; - const filterIgnoredFiles = []; - for (const file of files) { - if (!options.checkPath(file.filename)) { - (0,core.info)(`skip for excluded path: ${file.filename}`); - filterIgnoredFiles.push(file); - } - else { - filterSelectedFiles.push(file); - } - } - // find hunks to review - const filteredFiles = await Promise.all(filterSelectedFiles.map(async (file) => { - // retrieve file contents - let fileContent = ''; - if (context.payload.pull_request == null) { - (0,core.warning)('Skipped: context.payload.pull_request is null'); - return null; - } - try { - const contents = await octokit/* octokit.repos.getContent */.K.repos.getContent({ - owner: repo.owner, - repo: repo.repo, - path: file.filename, - ref: context.payload.pull_request.base.sha - }); - if (contents.data != null) { - if (!Array.isArray(contents.data)) { - if (contents.data.type === 'file' && - contents.data.content != null) { - fileContent = Buffer.from(contents.data.content, 'base64').toString(); - } - } - } - } - catch (e) { - (0,core.warning)(`Failed to get file contents: ${e}`); - } - let fileDiff = ''; - if (file.patch != null) { - fileDiff = file.patch; - } - const patches = []; - for (const patch of splitPatch(file.patch)) { - const patchLines = patchStartEndLine(patch); - if (patchLines == null) { - continue; - } - const hunks = parsePatch(patch); - if (hunks == null) { - continue; - } - const hunksStr = ` ----new_hunk--- -\`\`\` -${hunks.newHunk} -\`\`\` - ----old_hunk--- -\`\`\` -${hunks.oldHunk} -\`\`\` -`; - patches.push([ - patchLines.newHunk.startLine, - patchLines.newHunk.endLine, - hunksStr - ]); - } - if (patches.length > 0) { - return [file.filename, fileContent, fileDiff, patches]; - } - else { - return null; - } - })); - // Filter out any null results - const filesAndChanges = filteredFiles.filter(file => file !== null); - if (filesAndChanges.length === 0) { - (0,core.error)('Skipped: no files to review'); - return; - } - const summariesFailed = []; - const doSummary = async (filename, fileContent, fileDiff) => { - const ins = inputs.clone(); - if (fileDiff.length === 0) { - (0,core.warning)(`summarize: file_diff is empty, skip ${filename}`); - summariesFailed.push(`${filename} (empty diff)`); - return null; - } - ins.filename = filename; - // render prompt based on inputs so far - let tokens = (0,tokenizer/* getTokenCount */.V)(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges)); - const diffTokens = (0,tokenizer/* getTokenCount */.V)(fileDiff); - if (tokens + diffTokens > options.lightTokenLimits.requestTokens) { - (0,core.info)(`summarize: diff tokens exceeds limit, skip ${filename}`); - summariesFailed.push(`${filename} (diff tokens exceeds limit)`); - return null; - } - ins.fileDiff = fileDiff; - tokens += fileDiff.length; - // optionally pack file_content - if (fileContent.length > 0) { - // count occurrences of $file_content in prompt - const fileContentCount = prompts.summarizeFileDiff.split('$file_content').length - 1; - const fileContentTokens = (0,tokenizer/* getTokenCount */.V)(fileContent); - if (fileContentCount > 0 && - tokens + fileContentTokens * fileContentCount <= - options.lightTokenLimits.requestTokens) { - tokens += fileContentTokens * fileContentCount; - ins.fileContent = fileContent; - } - } - // summarize content - try { - const [summarizeResp] = await lightBot.chat(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges), {}); - if (summarizeResp === '') { - (0,core.info)('summarize: nothing obtained from openai'); - summariesFailed.push(`${filename} (nothing obtained from openai)`); - return null; - } - else { - if (options.reviewSimpleChanges === false) { - // parse the comment to look for triage classification - // Format is : [TRIAGE]: - // if the change needs review return true, else false - const triageRegex = /\[TRIAGE\]:\s*(NEEDS_REVIEW|APPROVED)/; - const triageMatch = summarizeResp.match(triageRegex); - if (triageMatch != null) { - const triage = triageMatch[1]; - const needsReview = triage === 'NEEDS_REVIEW'; - // remove this line from the comment - const summary = summarizeResp.replace(triageRegex, '').trim(); - (0,core.info)(`filename: ${filename}, triage: ${triage}`); - return [filename, summary, needsReview]; - } - } - return [filename, summarizeResp, true]; - } - } - catch (e) { - (0,core.warning)(`summarize: error from openai: ${e}`); - summariesFailed.push(`${filename} (error from openai: ${e})})`); - return null; - } - }; - const summaryPromises = []; - const skippedFiles = []; - for (const [filename, fileContent, fileDiff] of filesAndChanges) { - if (options.maxFiles <= 0 || summaryPromises.length < options.maxFiles) { - summaryPromises.push(openaiConcurrencyLimit(async () => await doSummary(filename, fileContent, fileDiff))); - } - else { - skippedFiles.push(filename); - } - } - const summaries = (await Promise.all(summaryPromises)).filter(summary => summary !== null); - if (summaries.length > 0) { - const batchSize = 10; - // join summaries into one in the batches of batchSize - // and ask the bot to summarize the summaries - for (let i = 0; i < summaries.length; i += batchSize) { - const summariesBatch = summaries.slice(i, i + batchSize); - for (const [filename, summary] of summariesBatch) { - inputs.rawSummary += `--- -${filename}: ${summary} -`; - } - // ask chatgpt to summarize the summaries - const [summarizeResp] = await heavyBot.chat(prompts.renderSummarizeChangesets(inputs), {}); - if (summarizeResp === '') { - (0,core.warning)('summarize: nothing obtained from openai'); - } - else { - inputs.rawSummary = summarizeResp; - } - } - } - let nextSummarizeIds = {}; - // final summary - const [summarizeFinalResponse, summarizeFinalResponseIds] = await heavyBot.chat(prompts.renderSummarize(inputs), nextSummarizeIds); - if (summarizeFinalResponse === '') { - (0,core.info)('summarize: nothing obtained from openai'); - } - else { - nextSummarizeIds = summarizeFinalResponseIds; - } - if (options.disableReleaseNotes === false) { - // final release notes - const [releaseNotesResponse, releaseNotesIds] = await heavyBot.chat(prompts.renderSummarizeReleaseNotes(inputs), nextSummarizeIds); - if (releaseNotesResponse === '') { - (0,core.info)('release notes: nothing obtained from openai'); - } - else { - nextSummarizeIds = releaseNotesIds; - inputs.releaseNotes = releaseNotesResponse.replace(/(^|\n)> .*/g, ''); - let message = '### Summary by OpenAI\n\n'; - message += releaseNotesResponse; - try { - await commenter.updateDescription(context.payload.pull_request.number, message); - } - catch (e) { - (0,core.warning)(`release notes: error from github: ${e.message}`); - } - } - } - let summarizeComment = `${summarizeFinalResponse} -${lib_commenter/* RAW_SUMMARY_START_TAG */.oi} -${inputs.rawSummary} -${lib_commenter/* RAW_SUMMARY_END_TAG */.rV} ---- - -### Chat with 🤖 OpenAI Bot (\`@openai\`) -- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file. -- Invite the bot into a review comment chain by tagging \`@openai\` in a reply. - -### Code suggestions -- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. -- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off. - -### Ignoring further reviews -- Type \`@openai: ignore\` anywhere in the PR description to ignore further reviews from the bot. - ---- - -${filterIgnoredFiles.length > 0 - ? ` -
-Files ignored due to filter (${filterIgnoredFiles.length}) - -### Ignored files - -* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')} - -
-` - : ''} - -${skippedFiles.length > 0 - ? ` -
-Files not processed due to max files limit (${skippedFiles.length}) - -### Not processed - -* ${skippedFiles.join('\n* ')} - -
-` - : ''} - -${summariesFailed.length > 0 - ? ` -
-Files not summarized due to errors (${summariesFailed.length}) - -### Failed to summarize - -* ${summariesFailed.join('\n* ')} - -
-` - : ''} -`; - if (!options.disableReview) { - const filesAndChangesReview = filesAndChanges.filter(([filename]) => { - const needsReview = summaries.find(([summaryFilename]) => summaryFilename === filename)?.[2] ?? true; - return needsReview; - }); - const reviewsSkipped = filesAndChanges - .filter(([filename]) => !filesAndChangesReview.some(([reviewFilename]) => reviewFilename === filename)) - .map(([filename]) => filename); - // failed reviews array - const reviewsFailed = []; - const doReview = async (filename, fileContent, patches) => { - // make a copy of inputs - const ins = inputs.clone(); - ins.filename = filename; - // calculate tokens based on inputs so far - let tokens = (0,tokenizer/* getTokenCount */.V)(prompts.renderReviewFileDiff(ins)); - // loop to calculate total patch tokens - let patchesToPack = 0; - for (const [, , patch] of patches) { - const patchTokens = (0,tokenizer/* getTokenCount */.V)(patch); - if (tokens + patchTokens > options.heavyTokenLimits.requestTokens) { - break; - } - tokens += patchTokens; - patchesToPack += 1; - } - // try packing file_content into this request - const fileContentCount = prompts.reviewFileDiff.split('$file_content').length - 1; - const fileContentTokens = (0,tokenizer/* getTokenCount */.V)(fileContent); - if (fileContentCount > 0 && - tokens + fileContentTokens * fileContentCount <= - options.heavyTokenLimits.requestTokens) { - ins.fileContent = fileContent; - tokens += fileContentTokens * fileContentCount; - } - let patchesPacked = 0; - for (const [startLine, endLine, patch] of patches) { - if (context.payload.pull_request == null) { - (0,core.warning)('No pull request found, skipping.'); - continue; - } - // see if we can pack more patches into this request - if (patchesPacked >= patchesToPack) { - (0,core.info)(`unable to pack more patches into this request, packed: ${patchesPacked}, to pack: ${patchesToPack}`); - break; - } - patchesPacked += 1; - let commentChain = ''; - try { - const allChains = await commenter.getCommentChainsWithinRange(context.payload.pull_request.number, filename, startLine, endLine, lib_commenter/* COMMENT_REPLY_TAG */.aD); - if (allChains.length > 0) { - (0,core.info)(`Found comment chains: ${allChains} for ${filename}`); - commentChain = allChains; - } - } - catch (e) { - (0,core.warning)(`Failed to get comments: ${e}, skipping. backtrace: ${e.stack}`); - } - // try packing comment_chain into this request - const commentChainTokens = (0,tokenizer/* getTokenCount */.V)(commentChain); - if (tokens + commentChainTokens > - options.heavyTokenLimits.requestTokens) { - commentChain = ''; - } - else { - tokens += commentChainTokens; - } - ins.patches += ` -${patch} -`; - if (commentChain !== '') { - ins.patches += ` ----comment_chains--- -\`\`\` -${commentChain} -\`\`\` -`; - } - ins.patches += ` ----end_change_section--- -`; - } - // perform review - try { - const [response] = await heavyBot.chat(prompts.renderReviewFileDiff(ins), {}); - if (response === '') { - (0,core.info)('review: nothing obtained from openai'); - reviewsFailed.push(`${filename} (no response)`); - return; - } - // parse review - const reviews = parseReview(response, patches, options.debug); - for (const review of reviews) { - // check for LGTM - if (!options.reviewCommentLGTM && - (review.comment.includes('LGTM') || - review.comment.includes('looks good to me'))) { - continue; - } - if (context.payload.pull_request == null) { - (0,core.warning)('No pull request found, skipping.'); - continue; - } - try { - await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`); - } - catch (e) { - reviewsFailed.push(`${filename} comment failed (${e})`); - } - } - } - catch (e) { - (0,core.warning)(`Failed to review: ${e}, skipping. backtrace: ${e.stack}`); - reviewsFailed.push(`${filename} (${e})`); - } - }; - const reviewPromises = []; - for (const [filename, fileContent, , patches] of filesAndChangesReview) { - if (options.maxFiles <= 0 || reviewPromises.length < options.maxFiles) { - reviewPromises.push(openaiConcurrencyLimit(async () => { - await doReview(filename, fileContent, patches); - })); - } - else { - skippedFiles.push(filename); - } - } - await Promise.all(reviewPromises); - summarizeComment += ` ---- -In the recent run, only the files that changed from the \`base\` of the PR and between \`${highestReviewedCommitId}\` and \`${context.payload.pull_request.head.sha}\` commits were reviewed. - -${reviewsFailed.length > 0 - ? `
-Files not reviewed due to errors in the recent run (${reviewsFailed.length}) - -### Failed to review in the last run - -* ${reviewsFailed.join('\n* ')} - -
-` - : ''} - -${reviewsSkipped.length > 0 - ? `
-Files not reviewed due to simple changes (${reviewsSkipped.length}) - -### Skipped review in the recent run - -* ${reviewsSkipped.join('\n* ')} - -
-` - : ''} -`; - // add existing_comment_ids_block with latest head sha - summarizeComment += `\n${commenter.addReviewedCommitId(existingCommitIdsBlock, context.payload.pull_request.head.sha)}`; - } - // post the final summary comment - await commenter.comment(`${summarizeComment}`, lib_commenter/* SUMMARIZE_TAG */.Rp, 'replace'); - // post the review - await commenter.submitReview(context.payload.pull_request.number, commits[commits.length - 1].sha); -}; -const splitPatch = (patch) => { - if (patch == null) { - return []; - } - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm; - const result = []; - let last = -1; - let match; - while ((match = pattern.exec(patch)) !== null) { - if (last === -1) { - last = match.index; - } - else { - result.push(patch.substring(last, match.index)); - last = match.index; - } - } - if (last !== -1) { - result.push(patch.substring(last)); - } - return result; -}; -const patchStartEndLine = (patch) => { - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm; - const match = pattern.exec(patch); - if (match != null) { - const oldBegin = parseInt(match[2]); - const oldDiff = parseInt(match[3]); - const newBegin = parseInt(match[4]); - const newDiff = parseInt(match[5]); - return { - oldHunk: { - startLine: oldBegin, - endLine: oldBegin + oldDiff - 1 - }, - newHunk: { - startLine: newBegin, - endLine: newBegin + newDiff - 1 - } - }; - } - else { - return null; - } -}; -const parsePatch = (patch) => { - const hunkInfo = patchStartEndLine(patch); - if (hunkInfo == null) { - return null; - } - const oldHunkLines = []; - const newHunkLines = []; - // let old_line = hunkInfo.old_hunk.start_line - let newLine = hunkInfo.newHunk.startLine; - const lines = patch.split('\n').slice(1); // Skip the @@ line - // Remove the last line if it's empty - if (lines[lines.length - 1] === '') { - lines.pop(); - } - for (const line of lines) { - if (line.startsWith('-')) { - oldHunkLines.push(`${line.substring(1)}`); - // old_line++ - } - else if (line.startsWith('+')) { - newHunkLines.push(`${newLine}: ${line.substring(1)}`); - newLine++; - } - else { - oldHunkLines.push(`${line}`); - newHunkLines.push(`${newLine}: ${line}`); - // old_line++ - newLine++; - } - } - return { - oldHunk: oldHunkLines.join('\n'), - newHunk: newHunkLines.join('\n') - }; -}; -function parseReview(response, patches, debug = false) { - const reviews = []; - const lines = response.split('\n'); - const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/; - const commentSeparator = '---'; - let currentStartLine = null; - let currentEndLine = null; - let currentComment = ''; - function storeReview() { - if (currentStartLine !== null && currentEndLine !== null) { - const sanitizedComment = sanitizeComment(currentComment.trim()); - const review = { - startLine: currentStartLine, - endLine: currentEndLine, - comment: sanitizedComment.trim() - }; - let withinPatch = false; - let bestPatchStartLine = patches[0][0]; - let bestPatchEndLine = patches[0][1]; - let maxIntersection = 0; - for (const [startLine, endLine] of patches) { - const intersectionStart = Math.max(review.startLine, startLine); - const intersectionEnd = Math.min(review.endLine, endLine); - const intersectionLength = Math.max(0, intersectionEnd - intersectionStart + 1); - if (intersectionLength > maxIntersection) { - maxIntersection = intersectionLength; - bestPatchStartLine = startLine; - bestPatchEndLine = endLine; - withinPatch = - intersectionLength === review.endLine - review.startLine + 1; - } - if (withinPatch) - break; - } - if (!withinPatch) { - review.comment = `> Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [${review.startLine}-${review.endLine}] - -${review.comment}`; - review.startLine = bestPatchStartLine; - review.endLine = bestPatchEndLine; - } - reviews.push(review); - (0,core.info)(`Stored comment for line range ${currentStartLine}-${currentEndLine}: ${currentComment.trim()}`); - } - } - function sanitizeComment(comment) { - const suggestionStart = '```suggestion'; - const suggestionEnd = '```'; - const lineNumberRegex = /^ *(\d+): /gm; - let suggestionStartIndex = comment.indexOf(suggestionStart); - while (suggestionStartIndex !== -1) { - const suggestionEndIndex = comment.indexOf(suggestionEnd, suggestionStartIndex + suggestionStart.length); - if (suggestionEndIndex === -1) - break; - const suggestionBlock = comment.substring(suggestionStartIndex + suggestionStart.length, suggestionEndIndex); - const sanitizedBlock = suggestionBlock.replace(lineNumberRegex, ''); - comment = - comment.slice(0, suggestionStartIndex + suggestionStart.length) + - sanitizedBlock + - comment.slice(suggestionEndIndex); - suggestionStartIndex = comment.indexOf(suggestionStart, suggestionStartIndex + - suggestionStart.length + - sanitizedBlock.length + - suggestionEnd.length); - } - return comment; - } - for (const line of lines) { - const lineNumberRangeMatch = line.match(lineNumberRangeRegex); - if (lineNumberRangeMatch != null) { - storeReview(); - currentStartLine = parseInt(lineNumberRangeMatch[1], 10); - currentEndLine = parseInt(lineNumberRangeMatch[2], 10); - currentComment = ''; - if (debug) { - (0,core.info)(`Found line number range: ${currentStartLine}-${currentEndLine}`); - } - continue; - } - if (line.trim() === commentSeparator) { - storeReview(); - currentStartLine = null; - currentEndLine = null; - currentComment = ''; - if (debug) { - (0,core.info)('Found comment separator'); - } - continue; - } - if (currentStartLine !== null && currentEndLine !== null) { - currentComment += `${line}\n`; - } - } - storeReview(); - return reviews; -} + +// eslint-disable-next-line camelcase + + + + + + +// eslint-disable-next-line camelcase +const context = github.context; +const repo = context.repo; +const ignoreKeyword = '@openai: ignore'; +const codeReview = async (lightBot, heavyBot, options, prompts) => { + const commenter = new lib_commenter/* Commenter */.Es(); + const openaiConcurrencyLimit = pLimit(options.openaiConcurrencyLimit); + if (context.eventName !== 'pull_request' && + context.eventName !== 'pull_request_target') { + (0,core.warning)(`Skipped: current event is ${context.eventName}, only support pull_request event`); + return; + } + if (context.payload.pull_request == null) { + (0,core.warning)('Skipped: context.payload.pull_request is null'); + return; + } + const inputs = new lib_inputs/* Inputs */.k(); + inputs.title = context.payload.pull_request.title; + if (context.payload.pull_request.body != null) { + inputs.description = commenter.getDescription(context.payload.pull_request.body); + inputs.releaseNotes = commenter.getReleaseNotes(context.payload.pull_request.body); + } + // if the description contains ignore_keyword, skip + if (inputs.description.includes(ignoreKeyword)) { + (0,core.info)('Skipped: description contains ignore_keyword'); + return; + } + // as gpt-3.5-turbo isn't paying attention to system message, add to inputs for now + inputs.systemMessage = options.systemMessage; + // get SUMMARIZE_TAG message + const existingSummarizeCmt = await commenter.findCommentWithTag(lib_commenter/* SUMMARIZE_TAG */.Rp, context.payload.pull_request.number); + let existingCommitIdsBlock = ''; + if (existingSummarizeCmt != null) { + inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmt.body); + existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmt.body); + } + const allCommitIds = await commenter.getAllCommitIds(); + // find highest reviewed commit id + let highestReviewedCommitId = ''; + if (existingCommitIdsBlock !== '') { + highestReviewedCommitId = commenter.getHighestReviewedCommitId(allCommitIds, commenter.getReviewedCommitIds(existingCommitIdsBlock)); + } + if (highestReviewedCommitId === '' || + highestReviewedCommitId === context.payload.pull_request.head.sha) { + (0,core.info)(`Will review from the base commit: ${context.payload.pull_request.base.sha}`); + highestReviewedCommitId = context.payload.pull_request.base.sha; + } + else { + (0,core.info)(`Will review from commit: ${highestReviewedCommitId}`); + } + // Fetch the diff between the highest reviewed commit and the latest commit of the PR branch + const incrementalDiff = await octokit/* octokit.repos.compareCommits */.K.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: highestReviewedCommitId, + head: context.payload.pull_request.head.sha + }); + // Fetch the diff between the target branch's base commit and the latest commit of the PR branch + const targetBranchDiff = await octokit/* octokit.repos.compareCommits */.K.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha + }); + const incrementalFiles = incrementalDiff.data.files; + const targetBranchFiles = targetBranchDiff.data.files; + if (incrementalFiles == null || targetBranchFiles == null) { + (0,core.warning)('Skipped: files data is missing'); + return; + } + // Filter out any file that is changed compared to the incremental changes + const files = targetBranchFiles.filter(targetBranchFile => incrementalFiles.some(incrementalFile => incrementalFile.filename === targetBranchFile.filename)); + if (files.length === 0) { + (0,core.warning)('Skipped: files is null'); + return; + } + const commits = incrementalDiff.data.commits; + if (commits.length === 0) { + (0,core.warning)('Skipped: ommits is null'); + return; + } + // skip files if they are filtered out + const filterSelectedFiles = []; + const filterIgnoredFiles = []; + for (const file of files) { + if (!options.checkPath(file.filename)) { + (0,core.info)(`skip for excluded path: ${file.filename}`); + filterIgnoredFiles.push(file); + } + else { + filterSelectedFiles.push(file); + } + } + // find hunks to review + const filteredFiles = await Promise.all(filterSelectedFiles.map(async (file) => { + // retrieve file contents + let fileContent = ''; + if (context.payload.pull_request == null) { + (0,core.warning)('Skipped: context.payload.pull_request is null'); + return null; + } + try { + const contents = await octokit/* octokit.repos.getContent */.K.repos.getContent({ + owner: repo.owner, + repo: repo.repo, + path: file.filename, + ref: context.payload.pull_request.base.sha + }); + if (contents.data != null) { + if (!Array.isArray(contents.data)) { + if (contents.data.type === 'file' && + contents.data.content != null) { + fileContent = Buffer.from(contents.data.content, 'base64').toString(); + } + } + } + } + catch (e) { + (0,core.warning)(`Failed to get file contents: ${e}`); + } + let fileDiff = ''; + if (file.patch != null) { + fileDiff = file.patch; + } + const patches = []; + for (const patch of splitPatch(file.patch)) { + const patchLines = patchStartEndLine(patch); + if (patchLines == null) { + continue; + } + const hunks = parsePatch(patch); + if (hunks == null) { + continue; + } + const hunksStr = ` +---new_hunk--- +\`\`\` +${hunks.newHunk} +\`\`\` + +---old_hunk--- +\`\`\` +${hunks.oldHunk} +\`\`\` +`; + patches.push([ + patchLines.newHunk.startLine, + patchLines.newHunk.endLine, + hunksStr + ]); + } + if (patches.length > 0) { + return [file.filename, fileContent, fileDiff, patches]; + } + else { + return null; + } + })); + // Filter out any null results + const filesAndChanges = filteredFiles.filter(file => file !== null); + if (filesAndChanges.length === 0) { + (0,core.error)('Skipped: no files to review'); + return; + } + const summariesFailed = []; + const doSummary = async (filename, fileContent, fileDiff) => { + const ins = inputs.clone(); + if (fileDiff.length === 0) { + (0,core.warning)(`summarize: file_diff is empty, skip ${filename}`); + summariesFailed.push(`${filename} (empty diff)`); + return null; + } + ins.filename = filename; + // render prompt based on inputs so far + let tokens = (0,tokenizer/* getTokenCount */.V)(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges)); + const diffTokens = (0,tokenizer/* getTokenCount */.V)(fileDiff); + if (tokens + diffTokens > options.lightTokenLimits.requestTokens) { + (0,core.info)(`summarize: diff tokens exceeds limit, skip ${filename}`); + summariesFailed.push(`${filename} (diff tokens exceeds limit)`); + return null; + } + ins.fileDiff = fileDiff; + tokens += fileDiff.length; + // optionally pack file_content + if (fileContent.length > 0) { + // count occurrences of $file_content in prompt + const fileContentCount = prompts.summarizeFileDiff.split('$file_content').length - 1; + const fileContentTokens = (0,tokenizer/* getTokenCount */.V)(fileContent); + if (fileContentCount > 0 && + tokens + fileContentTokens * fileContentCount <= + options.lightTokenLimits.requestTokens) { + tokens += fileContentTokens * fileContentCount; + ins.fileContent = fileContent; + } + } + // summarize content + try { + const [summarizeResp] = await lightBot.chat(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges), {}); + if (summarizeResp === '') { + (0,core.info)('summarize: nothing obtained from openai'); + summariesFailed.push(`${filename} (nothing obtained from openai)`); + return null; + } + else { + if (options.reviewSimpleChanges === false) { + // parse the comment to look for triage classification + // Format is : [TRIAGE]: + // if the change needs review return true, else false + const triageRegex = /\[TRIAGE\]:\s*(NEEDS_REVIEW|APPROVED)/; + const triageMatch = summarizeResp.match(triageRegex); + if (triageMatch != null) { + const triage = triageMatch[1]; + const needsReview = triage === 'NEEDS_REVIEW'; + // remove this line from the comment + const summary = summarizeResp.replace(triageRegex, '').trim(); + (0,core.info)(`filename: ${filename}, triage: ${triage}`); + return [filename, summary, needsReview]; + } + } + return [filename, summarizeResp, true]; + } + } + catch (e) { + (0,core.warning)(`summarize: error from openai: ${e}`); + summariesFailed.push(`${filename} (error from openai: ${e})})`); + return null; + } + }; + const summaryPromises = []; + const skippedFiles = []; + for (const [filename, fileContent, fileDiff] of filesAndChanges) { + if (options.maxFiles <= 0 || summaryPromises.length < options.maxFiles) { + summaryPromises.push(openaiConcurrencyLimit(async () => await doSummary(filename, fileContent, fileDiff))); + } + else { + skippedFiles.push(filename); + } + } + const summaries = (await Promise.all(summaryPromises)).filter(summary => summary !== null); + if (summaries.length > 0) { + const batchSize = 10; + // join summaries into one in the batches of batchSize + // and ask the bot to summarize the summaries + for (let i = 0; i < summaries.length; i += batchSize) { + const summariesBatch = summaries.slice(i, i + batchSize); + for (const [filename, summary] of summariesBatch) { + inputs.rawSummary += `--- +${filename}: ${summary} +`; + } + // ask chatgpt to summarize the summaries + const [summarizeResp] = await heavyBot.chat(prompts.renderSummarizeChangesets(inputs), {}); + if (summarizeResp === '') { + (0,core.warning)('summarize: nothing obtained from openai'); + } + else { + inputs.rawSummary = summarizeResp; + } + } + } + let nextSummarizeIds = {}; + // final summary + const [summarizeFinalResponse, summarizeFinalResponseIds] = await heavyBot.chat(prompts.renderSummarize(inputs), nextSummarizeIds); + if (summarizeFinalResponse === '') { + (0,core.info)('summarize: nothing obtained from openai'); + } + else { + nextSummarizeIds = summarizeFinalResponseIds; + } + if (options.disableReleaseNotes === false) { + // final release notes + const [releaseNotesResponse, releaseNotesIds] = await heavyBot.chat(prompts.renderSummarizeReleaseNotes(inputs), nextSummarizeIds); + if (releaseNotesResponse === '') { + (0,core.info)('release notes: nothing obtained from openai'); + } + else { + nextSummarizeIds = releaseNotesIds; + inputs.releaseNotes = releaseNotesResponse.replace(/(^|\n)> .*/g, ''); + let message = '### Summary by OpenAI\n\n'; + message += releaseNotesResponse; + try { + await commenter.updateDescription(context.payload.pull_request.number, message); + } + catch (e) { + (0,core.warning)(`release notes: error from github: ${e.message}`); + } + } + } + let summarizeComment = `${summarizeFinalResponse} +${lib_commenter/* RAW_SUMMARY_START_TAG */.oi} +${inputs.rawSummary} +${lib_commenter/* RAW_SUMMARY_END_TAG */.rV} +--- + +### Chat with 🤖 OpenAI Bot (\`@openai\`) +- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file. +- Invite the bot into a review comment chain by tagging \`@openai\` in a reply. + +### Code suggestions +- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. +- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off. + +### Ignoring further reviews +- Type \`@openai: ignore\` anywhere in the PR description to ignore further reviews from the bot. + +--- + +${filterIgnoredFiles.length > 0 + ? ` +
+Files ignored due to filter (${filterIgnoredFiles.length}) + +### Ignored files + +* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')} + +
+` + : ''} + +${skippedFiles.length > 0 + ? ` +
+Files not processed due to max files limit (${skippedFiles.length}) + +### Not processed + +* ${skippedFiles.join('\n* ')} + +
+` + : ''} + +${summariesFailed.length > 0 + ? ` +
+Files not summarized due to errors (${summariesFailed.length}) + +### Failed to summarize + +* ${summariesFailed.join('\n* ')} + +
+` + : ''} +`; + if (!options.disableReview) { + const filesAndChangesReview = filesAndChanges.filter(([filename]) => { + const needsReview = summaries.find(([summaryFilename]) => summaryFilename === filename)?.[2] ?? true; + return needsReview; + }); + const reviewsSkipped = filesAndChanges + .filter(([filename]) => !filesAndChangesReview.some(([reviewFilename]) => reviewFilename === filename)) + .map(([filename]) => filename); + // failed reviews array + const reviewsFailed = []; + const doReview = async (filename, fileContent, patches) => { + // make a copy of inputs + const ins = inputs.clone(); + ins.filename = filename; + // calculate tokens based on inputs so far + let tokens = (0,tokenizer/* getTokenCount */.V)(prompts.renderReviewFileDiff(ins)); + // loop to calculate total patch tokens + let patchesToPack = 0; + for (const [, , patch] of patches) { + const patchTokens = (0,tokenizer/* getTokenCount */.V)(patch); + if (tokens + patchTokens > options.heavyTokenLimits.requestTokens) { + break; + } + tokens += patchTokens; + patchesToPack += 1; + } + // try packing file_content into this request + const fileContentCount = prompts.reviewFileDiff.split('$file_content').length - 1; + const fileContentTokens = (0,tokenizer/* getTokenCount */.V)(fileContent); + if (fileContentCount > 0 && + tokens + fileContentTokens * fileContentCount <= + options.heavyTokenLimits.requestTokens) { + ins.fileContent = fileContent; + tokens += fileContentTokens * fileContentCount; + } + let patchesPacked = 0; + for (const [startLine, endLine, patch] of patches) { + if (context.payload.pull_request == null) { + (0,core.warning)('No pull request found, skipping.'); + continue; + } + // see if we can pack more patches into this request + if (patchesPacked >= patchesToPack) { + (0,core.info)(`unable to pack more patches into this request, packed: ${patchesPacked}, to pack: ${patchesToPack}`); + break; + } + patchesPacked += 1; + let commentChain = ''; + try { + const allChains = await commenter.getCommentChainsWithinRange(context.payload.pull_request.number, filename, startLine, endLine, lib_commenter/* COMMENT_REPLY_TAG */.aD); + if (allChains.length > 0) { + (0,core.info)(`Found comment chains: ${allChains} for ${filename}`); + commentChain = allChains; + } + } + catch (e) { + (0,core.warning)(`Failed to get comments: ${e}, skipping. backtrace: ${e.stack}`); + } + // try packing comment_chain into this request + const commentChainTokens = (0,tokenizer/* getTokenCount */.V)(commentChain); + if (tokens + commentChainTokens > + options.heavyTokenLimits.requestTokens) { + commentChain = ''; + } + else { + tokens += commentChainTokens; + } + ins.patches += ` +${patch} +`; + if (commentChain !== '') { + ins.patches += ` +---comment_chains--- +\`\`\` +${commentChain} +\`\`\` +`; + } + ins.patches += ` +---end_change_section--- +`; + } + // perform review + try { + const [response] = await heavyBot.chat(prompts.renderReviewFileDiff(ins), {}); + if (response === '') { + (0,core.info)('review: nothing obtained from openai'); + reviewsFailed.push(`${filename} (no response)`); + return; + } + // parse review + const reviews = parseReview(response, patches, options.debug); + for (const review of reviews) { + // check for LGTM + if (!options.reviewCommentLGTM && + (review.comment.includes('LGTM') || + review.comment.includes('looks good to me'))) { + continue; + } + if (context.payload.pull_request == null) { + (0,core.warning)('No pull request found, skipping.'); + continue; + } + try { + await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`); + } + catch (e) { + reviewsFailed.push(`${filename} comment failed (${e})`); + } + } + } + catch (e) { + (0,core.warning)(`Failed to review: ${e}, skipping. backtrace: ${e.stack}`); + reviewsFailed.push(`${filename} (${e})`); + } + }; + const reviewPromises = []; + for (const [filename, fileContent, , patches] of filesAndChangesReview) { + if (options.maxFiles <= 0 || reviewPromises.length < options.maxFiles) { + reviewPromises.push(openaiConcurrencyLimit(async () => { + await doReview(filename, fileContent, patches); + })); + } + else { + skippedFiles.push(filename); + } + } + await Promise.all(reviewPromises); + summarizeComment += ` +--- +In the recent run, only the files that changed from the \`base\` of the PR and between \`${highestReviewedCommitId}\` and \`${context.payload.pull_request.head.sha}\` commits were reviewed. + +${reviewsFailed.length > 0 + ? `
+Files not reviewed due to errors in the recent run (${reviewsFailed.length}) + +### Failed to review in the last run + +* ${reviewsFailed.join('\n* ')} + +
+` + : ''} + +${reviewsSkipped.length > 0 + ? `
+Files not reviewed due to simple changes (${reviewsSkipped.length}) + +### Skipped review in the recent run + +* ${reviewsSkipped.join('\n* ')} + +
+` + : ''} +`; + // add existing_comment_ids_block with latest head sha + summarizeComment += `\n${commenter.addReviewedCommitId(existingCommitIdsBlock, context.payload.pull_request.head.sha)}`; + } + // post the final summary comment + await commenter.comment(`${summarizeComment}`, lib_commenter/* SUMMARIZE_TAG */.Rp, 'replace'); + // post the review + await commenter.submitReview(context.payload.pull_request.number, commits[commits.length - 1].sha); +}; +const splitPatch = (patch) => { + if (patch == null) { + return []; + } + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm; + const result = []; + let last = -1; + let match; + while ((match = pattern.exec(patch)) !== null) { + if (last === -1) { + last = match.index; + } + else { + result.push(patch.substring(last, match.index)); + last = match.index; + } + } + if (last !== -1) { + result.push(patch.substring(last)); + } + return result; +}; +const patchStartEndLine = (patch) => { + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm; + const match = pattern.exec(patch); + if (match != null) { + const oldBegin = parseInt(match[2]); + const oldDiff = parseInt(match[3]); + const newBegin = parseInt(match[4]); + const newDiff = parseInt(match[5]); + return { + oldHunk: { + startLine: oldBegin, + endLine: oldBegin + oldDiff - 1 + }, + newHunk: { + startLine: newBegin, + endLine: newBegin + newDiff - 1 + } + }; + } + else { + return null; + } +}; +const parsePatch = (patch) => { + const hunkInfo = patchStartEndLine(patch); + if (hunkInfo == null) { + return null; + } + const oldHunkLines = []; + const newHunkLines = []; + // let old_line = hunkInfo.old_hunk.start_line + let newLine = hunkInfo.newHunk.startLine; + const lines = patch.split('\n').slice(1); // Skip the @@ line + // Remove the last line if it's empty + if (lines[lines.length - 1] === '') { + lines.pop(); + } + for (const line of lines) { + if (line.startsWith('-')) { + oldHunkLines.push(`${line.substring(1)}`); + // old_line++ + } + else if (line.startsWith('+')) { + newHunkLines.push(`${newLine}: ${line.substring(1)}`); + newLine++; + } + else { + oldHunkLines.push(`${line}`); + newHunkLines.push(`${newLine}: ${line}`); + // old_line++ + newLine++; + } + } + return { + oldHunk: oldHunkLines.join('\n'), + newHunk: newHunkLines.join('\n') + }; +}; +function parseReview(response, patches, debug = false) { + const reviews = []; + const lines = response.split('\n'); + const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/; + const commentSeparator = '---'; + let currentStartLine = null; + let currentEndLine = null; + let currentComment = ''; + function storeReview() { + if (currentStartLine !== null && currentEndLine !== null) { + const sanitizedComment = sanitizeComment(currentComment.trim()); + const review = { + startLine: currentStartLine, + endLine: currentEndLine, + comment: sanitizedComment.trim() + }; + let withinPatch = false; + let bestPatchStartLine = patches[0][0]; + let bestPatchEndLine = patches[0][1]; + let maxIntersection = 0; + for (const [startLine, endLine] of patches) { + const intersectionStart = Math.max(review.startLine, startLine); + const intersectionEnd = Math.min(review.endLine, endLine); + const intersectionLength = Math.max(0, intersectionEnd - intersectionStart + 1); + if (intersectionLength > maxIntersection) { + maxIntersection = intersectionLength; + bestPatchStartLine = startLine; + bestPatchEndLine = endLine; + withinPatch = + intersectionLength === review.endLine - review.startLine + 1; + } + if (withinPatch) + break; + } + if (!withinPatch) { + review.comment = `> Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [${review.startLine}-${review.endLine}] + +${review.comment}`; + review.startLine = bestPatchStartLine; + review.endLine = bestPatchEndLine; + } + reviews.push(review); + (0,core.info)(`Stored comment for line range ${currentStartLine}-${currentEndLine}: ${currentComment.trim()}`); + } + } + function sanitizeComment(comment) { + const suggestionStart = '```suggestion'; + const suggestionEnd = '```'; + const lineNumberRegex = /^ *(\d+): /gm; + let suggestionStartIndex = comment.indexOf(suggestionStart); + while (suggestionStartIndex !== -1) { + const suggestionEndIndex = comment.indexOf(suggestionEnd, suggestionStartIndex + suggestionStart.length); + if (suggestionEndIndex === -1) + break; + const suggestionBlock = comment.substring(suggestionStartIndex + suggestionStart.length, suggestionEndIndex); + const sanitizedBlock = suggestionBlock.replace(lineNumberRegex, ''); + comment = + comment.slice(0, suggestionStartIndex + suggestionStart.length) + + sanitizedBlock + + comment.slice(suggestionEndIndex); + suggestionStartIndex = comment.indexOf(suggestionStart, suggestionStartIndex + + suggestionStart.length + + sanitizedBlock.length + + suggestionEnd.length); + } + return comment; + } + for (const line of lines) { + const lineNumberRangeMatch = line.match(lineNumberRangeRegex); + if (lineNumberRangeMatch != null) { + storeReview(); + currentStartLine = parseInt(lineNumberRangeMatch[1], 10); + currentEndLine = parseInt(lineNumberRangeMatch[2], 10); + currentComment = ''; + if (debug) { + (0,core.info)(`Found line number range: ${currentStartLine}-${currentEndLine}`); + } + continue; + } + if (line.trim() === commentSeparator) { + storeReview(); + currentStartLine = null; + currentEndLine = null; + currentComment = ''; + if (debug) { + (0,core.info)('Found comment separator'); + } + continue; + } + if (currentStartLine !== null && currentEndLine !== null) { + currentComment += `${line}\n`; + } + } + storeReview(); + return reviews; +} /***/ }), @@ -7457,16 +7461,16 @@ ${review.comment}`; /* harmony export */ }); /* unused harmony export encode */ /* harmony import */ var _dqbd_tiktoken__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(3171); -// eslint-disable-next-line camelcase - -const tokenizer = (0,_dqbd_tiktoken__WEBPACK_IMPORTED_MODULE_0__/* .get_encoding */ .iw)('cl100k_base'); -function encode(input) { - return tokenizer.encode(input); -} -function getTokenCount(input) { - input = input.replace(/<\|endoftext\|>/g, ''); - return encode(input).length; -} +// eslint-disable-next-line camelcase + +const tokenizer = (0,_dqbd_tiktoken__WEBPACK_IMPORTED_MODULE_0__/* .get_encoding */ .iw)('cl100k_base'); +function encode(input) { + return tokenizer.encode(input); +} +function getTokenCount(input) { + input = input.replace(/<\|endoftext\|>/g, ''); + return encode(input).length; +} /***/ }), diff --git a/src/bot.ts b/src/bot.ts index 532ba101..0a392b94 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -26,6 +26,7 @@ export class Bot { this.options = options if (process.env.OPENAI_API_KEY) { this.api = new ChatGPTAPI({ + apiBaseUrl: options.apiBaseUrl, systemMessage: options.systemMessage, apiKey: process.env.OPENAI_API_KEY, apiOrg: process.env.OPENAI_API_ORG ?? undefined, diff --git a/src/main.ts b/src/main.ts index 4bc4f546..e0b69db0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,7 +26,8 @@ async function run(): Promise { getInput('openai_model_temperature'), getInput('openai_retries'), getInput('openai_timeout_ms'), - getInput('openai_concurrency_limit') + getInput('openai_concurrency_limit'), + getInput('openai_base_url') ) // print options diff --git a/src/options.ts b/src/options.ts index 37558b2c..f56a4348 100644 --- a/src/options.ts +++ b/src/options.ts @@ -19,6 +19,7 @@ export class Options { openaiConcurrencyLimit: number lightTokenLimits: TokenLimits heavyTokenLimits: TokenLimits + apiBaseUrl: string constructor( debug: boolean, @@ -34,7 +35,8 @@ export class Options { openaiModelTemperature = '0.0', openaiRetries = '3', openaiTimeoutMS = '120000', - openaiConcurrencyLimit = '4' + openaiConcurrencyLimit = '4', + apiBaseUrl = "https://api.openai.com/v1" ) { this.debug = debug this.disableReview = disableReview @@ -52,6 +54,7 @@ export class Options { this.openaiConcurrencyLimit = parseInt(openaiConcurrencyLimit) this.lightTokenLimits = new TokenLimits(openaiLightModel) this.heavyTokenLimits = new TokenLimits(openaiHeavyModel) + this.apiBaseUrl = apiBaseUrl } // print all options using core.info @@ -72,6 +75,7 @@ export class Options { info(`openai_concurrency_limit: ${this.openaiConcurrencyLimit}`) info(`summary_token_limits: ${this.lightTokenLimits.string()}`) info(`review_token_limits: ${this.heavyTokenLimits.string()}`) + info(`api_base_url: ${this.apiBaseUrl}`) } checkPath(path: string): boolean {