From 9dab0f935602a5b2337136b6d139b81c09206b3d Mon Sep 17 00:00:00 2001 From: Harjot Gill Date: Sun, 12 Mar 2023 12:41:20 -0700 Subject: [PATCH] Rename repo - ChatGPT -> OpenAI (#20) ### Summary by OpenAI **Release Notes:** - Refactor: Updated import statements, added semicolons and removed unnecessary whitespace in `src/options.ts`. Renamed repository from ChatGPT to OpenAI and updated references to ChatGPT to OpenAI in `README.md` and `src/bot.ts`. Added polyfill for `fetch` and related classes in `src/fetch-polyfill.js` and removed previous implementation of polyfill in `src/fetch-polyfill.ts`. Replaced references to `chatgpt` with `openai` in `src/main.ts` and `src/bot.ts`. --- README.md | 36 ++- action.yml | 53 ++-- dist/index.js | 143 +++++----- ...-notes.png => openai-pr-release-notes.png} | Bin ...gpt-pr-review.png => openai-pr-review.png} | Bin ...t-pr-summary.png => openai-pr-summary.png} | Bin package-lock.json | 4 +- package.json | 6 +- src/bot.ts | 22 +- src/commenter.ts | 200 ++++++------- src/fetch-polyfill.js | 20 ++ src/fetch-polyfill.ts | 8 - src/main.ts | 79 +++--- src/options.ts | 213 +++++++------- src/review.ts | 264 +++++++++--------- 15 files changed, 521 insertions(+), 527 deletions(-) rename docs/images/{chatgpt-pr-release-notes.png => openai-pr-release-notes.png} (100%) rename docs/images/{chatgpt-pr-review.png => openai-pr-review.png} (100%) rename docs/images/{chatgpt-pr-summary.png => openai-pr-summary.png} (100%) create mode 100644 src/fetch-polyfill.js delete mode 100644 src/fetch-polyfill.ts diff --git a/README.md b/README.md index f3123492..c962655f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# ChatGPT-based PR reviewer and summarizer +# OpenAI GPT based PR reviewer and summarizer ![AI](./docs/images/ai.png) ## Overview -This [ChatGPT](https://platform.openai.com/docs/guides/chat) based GitHub Action -provides a summary, release notes and review of pull requests. The prompts have -been tuned for a concise response. To prevent excessive notifications, this -action can be configured to skip adding review comments when the changes look -good for the most part. +This [OpenAI Chat](https://platform.openai.com/docs/guides/chat) based GitHub +Action provides a summary, release notes and review of pull requests. The +prompts have been tuned for a concise response. To prevent excessive +notifications, this action can be configured to skip adding review comments when +the changes look good for the most part. NOTES: @@ -18,14 +18,14 @@ NOTES: - OpenAI's API is used instead of ChatGPT session on their portal. OpenAI API has a [more conservative data usage policy](https://openai.com/policies/api-data-usage-policies) - compared to ChatGPT. + compared to their ChatGPT offering. ### Features - Code review your pull requests ```yaml - - uses: fluxninja/chatgpt-pr-reviewer@main + - uses: fluxninja/openai-pr-reviewer@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -55,7 +55,7 @@ jobs: repository: ${{github.event.pull_request.head.repo.full_name}} ref: ${{github.event.pull_request.head.ref}} submodules: false - - uses: fluxninja/chatgpt-pr-reviewer@main + - uses: fluxninja/openai-pr-reviewer@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -66,11 +66,11 @@ jobs: ### Screenshots -![PR Summary](./docs/images/chatgpt-pr-summary.png) +![PR Summary](./docs/images/openai-pr-summary.png) -![PR Release Notes](./docs/images/chatgpt-pr-release-notes.png) +![PR Release Notes](./docs/images/openai-pr-release-notes.png) -![PR Review](./docs/images/chatgpt-pr-review.png) +![PR Review](./docs/images/openai-pr-review.png) ### Configuration @@ -86,12 +86,12 @@ See also: [./action.yml](./action.yml) #### Inputs -- `debug`: Enable debug mode, will show messages and responses between ChatGPT +- `debug`: Enable debug mode, will show messages and responses between OpenAI server in CI logs. - `review_comment_lgtm`: Leave comments even the patch is LGTM - `path_filters`: Rules to filter files to be reviewed. - `temperature`: Temperature of the GPT-3 model. -- `system_message`: The message to be sent to ChatGPT to start a conversation. +- `system_message`: The message to be sent to OpenAI to start a conversation. ### Prompt templates: @@ -146,7 +146,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} submodules: false - - uses: fluxninja/chatgpt-pr-reviewer@main + - uses: fluxninja/openai-pr-reviewer@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -157,15 +157,11 @@ jobs: See also: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target -### Inspect the messages between ChatGPT server +### Inspect the messages between OpenAI server Set `debug: true` in the workflow file to enable debug mode, which will show the messages -[1]: - https://github.com/marketplace?type=&verification=&query=chatgpt-pr-reviewer+ -[2]: https://www.npmjs.com/package/chatgpt - ### Special Thanks This GitHub Action is based on diff --git a/action.yml b/action.yml index 1a638b95..bdc5c9e6 100644 --- a/action.yml +++ b/action.yml @@ -1,22 +1,22 @@ -name: 'ChatGPT PR Reviewer & Summarizer' -description: 'ChatGPT based PR reviewer and summarizer' +name: "OpenAI-based PR Reviewer & Summarizer" +description: "OpenAI-based PR Reviewer and Summarizer" branding: - icon: 'aperture' - color: 'orange' -author: 'FluxNinja, Inc.' + icon: "aperture" + color: "orange" +author: "FluxNinja, Inc." inputs: debug: required: false - description: 'Enable debug mode' - default: 'false' + description: "Enable debug mode" + default: "false" temperature: required: false - description: 'Temperature for ChatGPT model' - default: '0.0' + description: "Temperature for GPT model" + default: "0.0" review_comment_lgtm: required: false - description: 'Leave comments even if the patch is LGTM' - default: 'false' + description: "Leave comments even if the patch is LGTM" + default: "false" path_filters: required: false description: | @@ -44,21 +44,16 @@ inputs: !**/vendor/** system_message: required: false - description: 'System message to be sent to ChatGPT' + description: "System message to be sent to OpenAI" default: | You are a very experienced software engineer. You are able to thoroughly review code and uncover issues, such as bugs, potential data races, livelocks, starvation, suspension, order violation, atomicity violation, consistency issues, complexity issues, error handling and so on. We will be doing code reviews today. Please prefer markdown format in your responses. - chatgpt_reverse_proxy: - required: false - description: | - The URL of the chatgpt reverse proxy, see also https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy - default: https://chat.duti.tech/api/conversation summarize_beginning: required: false - description: 'The prompt for the whole pull request' + description: "The prompt for the whole pull request" default: | $system_message @@ -79,7 +74,7 @@ inputs: Reply "OK" to confirm that you are ready to receive the diffs for summarization. summarize_file_diff: required: false - description: 'The prompt for each file diff' + description: "The prompt for each file diff" default: | Providing diff for `$filename`. @@ -93,7 +88,7 @@ inputs: ``` summarize: required: false - description: 'The prompt for final summarization response' + description: "The prompt for final summarization response" default: | This is the end of summarization session. Please provide the final response as follows in the `markdown` format with the following content: @@ -106,7 +101,7 @@ inputs: request. summarize_release_notes: required: false - description: 'The prompt for generating release notes' + description: "The prompt for generating release notes" default: | Next, release notes in `markdown` format for this pull request that focuses on the purpose of this PR. If needed, you can classify the changes as "New Feature", "Bug fix", @@ -116,7 +111,7 @@ inputs: used as is in our release notes. review_beginning: required: false - description: 'The beginning prompt of a code review dialog' + description: "The beginning prompt of a code review dialog" default: | $system_message @@ -128,14 +123,14 @@ inputs: > $description. - ChatGPT generated summary is as follows, + OpenAI generated summary is as follows, > $summary Reply "OK" to confirm that you are ready for further instructions. review_file: required: false - description: 'The prompt for each file' + description: "The prompt for each file" default: | Providing `$filename` content as context. Please use this context when reviewing patches. @@ -144,7 +139,7 @@ inputs: ``` review_file_diff: required: false - description: 'The prompt for each file diff' + description: "The prompt for each file diff" default: | Providing entire diff for `$filename` as context. Please use this context when reviewing patches. @@ -153,7 +148,7 @@ inputs: ``` review_patch_begin: required: false - description: 'The prompt for each file diff' + description: "The prompt for each file diff" default: | Next, I will send you a series of patches, each of them consists of a diff snippet, and you need to do a brief code review for every patch, and tell me any bug risk or improvement @@ -164,7 +159,7 @@ inputs: preferred for your responses. Reply "OK" to confirm. review_patch: required: false - description: 'The prompt for each chunks/patches' + description: "The prompt for each chunks/patches" default: | $filename @@ -172,5 +167,5 @@ inputs: $patch ``` runs: - using: 'node16' - main: 'dist/index.js' + using: "node16" + main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js index 5bce6cf1..27af8e40 100644 --- a/dist/index.js +++ b/dist/index.js @@ -25760,6 +25760,7 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) { } ;// CONCATENATED MODULE: ./lib/fetch-polyfill.js +// fetch-polyfill.js if (!globalThis.fetch) { globalThis.fetch = fetch; @@ -26968,7 +26969,7 @@ class Bot { }); } else { - const err = "Unable to initialize the chatgpt API, both 'OPENAI_API_KEY' environment variable are not available"; + const err = "Unable to initialize the OpenAI API, both 'OPENAI_API_KEY' environment variable are not available"; throw new Error(err); } } @@ -26990,7 +26991,7 @@ class Bot { return ["", {}]; } if (this.options.debug) { - core.info(`sending to chatgpt: ${message}`); + core.info(`sending to openai: ${message}`); } let response = null; if (this.turbo) { @@ -27007,21 +27008,21 @@ class Bot { } } else { - core.setFailed("The chatgpt API is not initialized"); + core.setFailed("The OpenAI API is not initialized"); } let response_text = ""; if (response) { response_text = response.text; } else { - core.warning("chatgpt response is null"); + core.warning("openai response is null"); } // remove the prefix "with " in the response if (response_text.startsWith("with ")) { response_text = response_text.substring(5); } if (this.options.debug) { - core.info(`chatgpt responses: ${response_text}`); + core.info(`openai responses: ${response_text}`); } const new_ids = { parentMessageId: response?.id, @@ -27047,9 +27048,9 @@ __nccwpck_require__.a(__webpack_module__, async (__webpack_handle_async_dependen async function run() { - const options = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Options */ .Ei(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('debug'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('chatgpt_reverse_proxy'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('review_comment_lgtm'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getMultilineInput('path_filters'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('system_message'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('temperature')); - const prompts = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Prompts */ .jc(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch_begin'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_release_notes')); - // initialize chatgpt bot + const options = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Options */ .Ei(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput("debug"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput("review_comment_lgtm"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getMultilineInput("path_filters"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("system_message"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("temperature")); + const prompts = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Prompts */ .jc(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("review_beginning"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("review_file"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("review_file_diff"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("review_patch_begin"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("review_patch"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("summarize_beginning"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("summarize_file_diff"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("summarize"), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("summarize_release_notes")); + // initialize openai bot let bot = null; try { bot = new _bot_js__WEBPACK_IMPORTED_MODULE_1__/* .Bot */ .r(options); @@ -27063,20 +27064,20 @@ async function run() { } catch (e) { if (e instanceof Error) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(`Failed to run the chatgpt-actions: ${e.message}, backtrace: ${e.stack}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(`Failed to run: ${e.message}, backtrace: ${e.stack}`); } else { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(`Failed to run the chatgpt-actions: ${e}, backtrace: ${e.stack}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed(`Failed to run: ${e}, backtrace: ${e.stack}`); } } } process - .on('unhandledRejection', (reason, p) => { - console.error(reason, 'Unhandled Rejection at Promise', p); + .on("unhandledRejection", (reason, p) => { + console.error(reason, "Unhandled Rejection at Promise", p); _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Unhandled Rejection at Promise: ${reason}, promise is ${p}`); }) - .on('uncaughtException', (e) => { - console.error(e, 'Uncaught Exception thrown'); + .on("uncaughtException", (e) => { + console.error(e, "Uncaught Exception thrown"); _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Uncaught Exception thrown: ${e}, backtrace: ${e.stack}`); }); await run(); @@ -28595,7 +28596,7 @@ class Prompts { summarize_file_diff; summarize; summarize_release_notes; - constructor(review_beginning = '', review_file = '', review_file_diff = '', review_patch_begin = '', review_patch = '', summarize_beginning = '', summarize_file_diff = '', summarize = '', summarize_release_notes = '') { + constructor(review_beginning = "", review_file = "", review_file_diff = "", review_patch_begin = "", review_patch = "", summarize_beginning = "", summarize_file_diff = "", summarize = "", summarize_release_notes = "") { this.review_beginning = review_beginning; this.review_file = review_file; this.review_file_diff = review_file_diff; @@ -28644,7 +28645,7 @@ class Inputs { file_diff; patch; diff; - constructor(system_message = '', title = '', description = '', summary = '', filename = '', file_content = '', file_diff = '', patch = '', diff = '') { + constructor(system_message = "", title = "", description = "", summary = "", filename = "", file_content = "", file_diff = "", patch = "", diff = "") { this.system_message = system_message; this.title = title; this.description = description; @@ -28657,48 +28658,46 @@ class Inputs { } render(content) { if (!content) { - return ''; + return ""; } if (this.system_message) { - content = content.replace('$system_message', this.system_message); + content = content.replace("$system_message", this.system_message); } if (this.title) { - content = content.replace('$title', this.title); + content = content.replace("$title", this.title); } if (this.description) { - content = content.replace('$description', this.description); + content = content.replace("$description", this.description); } if (this.summary) { - content = content.replace('$summary', this.summary); + content = content.replace("$summary", this.summary); } if (this.filename) { - content = content.replace('$filename', this.filename); + content = content.replace("$filename", this.filename); } if (this.file_content) { - content = content.replace('$file_content', this.file_content); + content = content.replace("$file_content", this.file_content); } if (this.file_diff) { - content = content.replace('$file_diff', this.file_diff); + content = content.replace("$file_diff", this.file_diff); } if (this.patch) { - content = content.replace('$patch', this.patch); + content = content.replace("$patch", this.patch); } if (this.diff) { - content = content.replace('$diff', this.diff); + content = content.replace("$diff", this.diff); } return content; } } class Options { debug; - chatgpt_reverse_proxy; review_comment_lgtm; path_filters; system_message; temperature; - constructor(debug, chatgpt_reverse_proxy, review_comment_lgtm = false, path_filters = null, system_message = '', temperature = '0.0') { + constructor(debug, review_comment_lgtm = false, path_filters = null, system_message = "", temperature = "0.0") { this.debug = debug; - this.chatgpt_reverse_proxy = chatgpt_reverse_proxy; this.review_comment_lgtm = review_comment_lgtm; this.path_filters = new PathFilter(path_filters); this.system_message = system_message; @@ -28719,7 +28718,7 @@ class PathFilter { for (const rule of rules) { const trimmed = rule?.trim(); if (trimmed) { - if (trimmed.startsWith('!')) { + if (trimmed.startsWith("!")) { this.rules.push([trimmed.substring(1).trim(), true]); } else { @@ -28775,16 +28774,16 @@ var dist_node = __nccwpck_require__(1231); -const token = core.getInput('token') - ? core.getInput('token') +const token = core.getInput("token") + ? core.getInput("token") : process.env.GITHUB_TOKEN; const octokit = new dist_node/* Octokit */.v({ auth: `token ${token}` }); const context = github.context; const repo = context.repo; -const COMMENT_GREETING = `:robot: ChatGPT`; -const DEFAULT_TAG = ''; -const description_tag = ''; -const description_tag_end = ''; +const COMMENT_GREETING = `:robot: OpenAI`; +const DEFAULT_TAG = ""; +const description_tag = ""; +const description_tag_end = ""; class Commenter { /** * @param mode Can be "create", "replace", "append" and "prepend". Default is "replace". @@ -28818,7 +28817,7 @@ class Commenter { owner: repo.owner, repo: repo.repo, pull_number, - body: new_description + body: new_description, }); } else { @@ -28829,7 +28828,7 @@ class Commenter { owner: repo.owner, repo: repo.repo, pull_number, - body: new_description + body: new_description, }); } } @@ -28856,7 +28855,7 @@ ${tag}`; owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: message + body: message, }); return; } @@ -28869,7 +28868,7 @@ ${tag}`; body: message, commit_id, path, - line + line, }); } } @@ -28880,7 +28879,7 @@ const list_review_comments = async (target, page = 1) => { repo: repo.repo, pull_number: target, page: page, - per_page: 100 + per_page: 100, }); if (!comments) { return []; @@ -28913,16 +28912,16 @@ const comment = async (message, tag, mode) => { ${message} ${tag}`; - if (mode == 'create') { + if (mode == "create") { await create(body, tag, target); } - else if (mode == 'replace') { + else if (mode == "replace") { await replace(body, tag, target); } - else if (mode == 'append') { + else if (mode == "append") { await append(body, tag, target); } - else if (mode == 'prepend') { + else if (mode == "prepend") { await prepend(body, tag, target); } else { @@ -28935,7 +28934,7 @@ const create = async (body, tag, target) => { owner: repo.owner, repo: repo.repo, issue_number: target, - body: body + body: body, }); }; const replace = async (body, tag, target) => { @@ -28945,7 +28944,7 @@ const replace = async (body, tag, target) => { owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: body + body: body, }); } else { @@ -28959,7 +28958,7 @@ const append = async (body, tag, target) => { owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: `${comment.body} ${body}` + body: `${comment.body} ${body}`, }); } else { @@ -28973,7 +28972,7 @@ const prepend = async (body, tag, target) => { owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: `${body} ${comment.body}` + body: `${body} ${comment.body}`, }); } else { @@ -28995,7 +28994,7 @@ const list_comments = async (target, page = 1) => { repo: repo.repo, issue_number: target, page: page, - per_page: 100 + per_page: 100, }); if (!comments) { return []; @@ -29031,16 +29030,16 @@ function get_token_count(input) { -const review_token = core.getInput('token') - ? core.getInput('token') +const review_token = core.getInput("token") + ? core.getInput("token") : process.env.GITHUB_TOKEN; const review_octokit = new dist_node/* Octokit */.v({ auth: `token ${review_token}` }); const review_context = github.context; const review_repo = review_context.repo; const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500; const codeReview = async (bot, options, prompts) => { - if (review_context.eventName !== 'pull_request' && - review_context.eventName !== 'pull_request_target') { + if (review_context.eventName !== "pull_request" && + review_context.eventName !== "pull_request_target") { core.warning(`Skipped: current event is ${review_context.eventName}, only support pull_request event`); return; } @@ -29061,7 +29060,7 @@ const codeReview = async (bot, options, prompts) => { owner: review_repo.owner, repo: review_repo.repo, base: review_context.payload.pull_request.base.sha, - head: review_context.payload.pull_request.head.sha + head: review_context.payload.pull_request.head.sha, }); const { files, commits } = diff.data; if (!files) { @@ -29076,18 +29075,18 @@ const codeReview = async (bot, options, prompts) => { continue; } // retrieve file contents - let file_content = ''; + let file_content = ""; try { const contents = await review_octokit.repos.getContent({ owner: review_repo.owner, repo: review_repo.repo, path: file.filename, - ref: review_context.payload.pull_request.base.sha + ref: review_context.payload.pull_request.base.sha, }); if (contents.data) { if (!Array.isArray(contents.data)) { - if (contents.data.type === 'file' && contents.data.content) { - file_content = Buffer.from(contents.data.content, 'base64').toString(); + if (contents.data.type === "file" && contents.data.content) { + file_content = Buffer.from(contents.data.content, "base64").toString(); } } } @@ -29095,7 +29094,7 @@ const codeReview = async (bot, options, prompts) => { catch (error) { core.warning(`Failed to get file contents: ${error}, skipping.`); } - let file_diff = ''; + let file_diff = ""; if (file.patch) { core.info(`diff for ${file.filename}: ${file.patch}`); file_diff = file.patch; @@ -29123,7 +29122,7 @@ const codeReview = async (bot, options, prompts) => { // summarize diff const [summarize_resp, summarize_diff_ids] = await bot.chat(prompts.render_summarize_file_diff(inputs), next_summarize_ids); if (!summarize_resp) { - core.info('summarize: nothing obtained from chatgpt'); + core.info("summarize: nothing obtained from openai"); } else { next_summarize_ids = summarize_diff_ids; @@ -29134,30 +29133,30 @@ const codeReview = async (bot, options, prompts) => { // final summary const [summarize_final_response, summarize_final_response_ids] = await bot.chat(prompts.render_summarize(inputs), next_summarize_ids); if (!summarize_final_response) { - core.info('summarize: nothing obtained from chatgpt'); + core.info("summarize: nothing obtained from openai"); } else { inputs.summary = summarize_final_response; next_summarize_ids = summarize_final_response_ids; - const tag = ''; - await commenter.comment(`${summarize_final_response}`, tag, 'replace'); + const tag = ""; + await commenter.comment(`${summarize_final_response}`, tag, "replace"); } // final release notes const [release_notes_response, release_notes_ids] = await bot.chat(prompts.render_summarize_release_notes(inputs), next_summarize_ids); if (!release_notes_response) { - core.info('release notes: nothing obtained from chatgpt'); + core.info("release notes: nothing obtained from openai"); } else { next_summarize_ids = release_notes_ids; const description = inputs.description; - let message = '### Summary by ChatGPT\n\n'; + let message = "### Summary by OpenAI\n\n"; message += release_notes_response; commenter.update_description(review_context.payload.pull_request.number, description, message); } // Review Stage const [, review_begin_ids] = await bot.chat(prompts.render_review_beginning(inputs), {}); let next_review_ids = review_begin_ids; - for (const [filename, file_content, file_diff, patches] of files_to_review) { + for (const [filename, file_content, file_diff, patches,] of files_to_review) { inputs.filename = filename; inputs.file_content = file_content; inputs.file_diff = file_diff; @@ -29169,7 +29168,7 @@ const codeReview = async (bot, options, prompts) => { // review file const [resp, review_file_ids] = await bot.chat(prompts.render_review_file(inputs), next_review_ids); if (!resp) { - core.info('review: nothing obtained from chatgpt'); + core.info("review: nothing obtained from openai"); } else { next_review_ids = review_file_ids; @@ -29185,7 +29184,7 @@ const codeReview = async (bot, options, prompts) => { // review diff const [resp, review_diff_ids] = await bot.chat(prompts.render_review_file_diff(inputs), next_review_ids); if (!resp) { - core.info('review: nothing obtained from chatgpt'); + core.info("review: nothing obtained from openai"); } else { next_review_ids = review_diff_ids; @@ -29199,15 +29198,15 @@ const codeReview = async (bot, options, prompts) => { const [, patch_begin_ids] = await bot.chat(prompts.render_review_patch_begin(inputs), next_review_ids); next_review_ids = patch_begin_ids; for (const [line, patch] of patches) { - core.info(`Reviewing ${filename}:${line} with chatgpt ...`); + core.info(`Reviewing ${filename}:${line} with openai ...`); inputs.patch = patch; const [response, patch_ids] = await bot.chat(prompts.render_review_patch(inputs), next_review_ids); if (!response) { - core.info('review: nothing obtained from chatgpt'); + core.info("review: nothing obtained from openai"); continue; } next_review_ids = patch_ids; - if (!options.review_comment_lgtm && response.includes('LGTM')) { + if (!options.review_comment_lgtm && response.includes("LGTM")) { continue; } try { diff --git a/docs/images/chatgpt-pr-release-notes.png b/docs/images/openai-pr-release-notes.png similarity index 100% rename from docs/images/chatgpt-pr-release-notes.png rename to docs/images/openai-pr-release-notes.png diff --git a/docs/images/chatgpt-pr-review.png b/docs/images/openai-pr-review.png similarity index 100% rename from docs/images/chatgpt-pr-review.png rename to docs/images/openai-pr-review.png diff --git a/docs/images/chatgpt-pr-summary.png b/docs/images/openai-pr-summary.png similarity index 100% rename from docs/images/chatgpt-pr-summary.png rename to docs/images/openai-pr-summary.png diff --git a/package-lock.json b/package-lock.json index 78226a7c..1b268672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "chatgpt-pr-reviewer", + "name": "openai-pr-reviewer", "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "chatgpt-pr-reviewer", + "name": "openai-pr-reviewer", "version": "0.0.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0ddfbc7b..ceaad110 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "chatgpt-pr-reviewer", + "name": "openai-pr-reviewer", "version": "0.0.0", "private": true, "type": "module", - "description": "A collection of ChatGPT assistants, e.g., code viewer, labeler, assigner, etc.", + "description": "OpenAI-based PR Reviewer and Summarizer.", "main": "lib/main.js", "scripts": { "build": "tsc", @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/fluxninja/chatgpt-pr-reviewer.git" + "url": "git+https://github.com/fluxninja/openai-pr-reviewer.git" }, "keywords": [ "actions", diff --git a/src/bot.ts b/src/bot.ts index 3b16b9a4..4bb11eff 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,7 +1,7 @@ -import * as core from "@actions/core"; -import * as chatgpt from "chatgpt"; import "./fetch-polyfill.js"; import * as optionsJs from "./options.js"; +import * as core from "@actions/core"; +import * as openai from "chatgpt"; // define type to save parentMessageId and conversationId export type Ids = { @@ -10,14 +10,14 @@ export type Ids = { }; export class Bot { - private turbo: chatgpt.ChatGPTAPI | null = null; // not free + private turbo: openai.ChatGPTAPI | null = null; // not free private options: optionsJs.Options; constructor(options: optionsJs.Options) { this.options = options; if (process.env.OPENAI_API_KEY) { - this.turbo = new chatgpt.ChatGPTAPI({ + this.turbo = new openai.ChatGPTAPI({ systemMessage: options.system_message, apiKey: process.env.OPENAI_API_KEY, debug: options.debug, @@ -29,7 +29,7 @@ export class Bot { }); } else { const err = - "Unable to initialize the chatgpt API, both 'OPENAI_API_KEY' environment variable are not available"; + "Unable to initialize the OpenAI API, both 'OPENAI_API_KEY' environment variable are not available"; throw new Error(err); } } @@ -51,12 +51,12 @@ export class Bot { return ["", {}]; } if (this.options.debug) { - core.info(`sending to chatgpt: ${message}`); + core.info(`sending to openai: ${message}`); } - let response: chatgpt.ChatMessage | null = null; + let response: openai.ChatMessage | null = null; if (this.turbo) { - let opts: chatgpt.SendMessageOptions = {}; + let opts: openai.SendMessageOptions = {}; if (ids.parentMessageId) { opts.parentMessageId = ids.parentMessageId; } @@ -69,20 +69,20 @@ export class Bot { ); } } else { - core.setFailed("The chatgpt API is not initialized"); + core.setFailed("The OpenAI API is not initialized"); } let response_text = ""; if (response) { response_text = response.text; } else { - core.warning("chatgpt response is null"); + core.warning("openai response is null"); } // remove the prefix "with " in the response if (response_text.startsWith("with ")) { response_text = response_text.substring(5); } if (this.options.debug) { - core.info(`chatgpt responses: ${response_text}`); + core.info(`openai responses: ${response_text}`); } const new_ids: Ids = { parentMessageId: response?.id, diff --git a/src/commenter.ts b/src/commenter.ts index da2755e9..a2bbd8fb 100644 --- a/src/commenter.ts +++ b/src/commenter.ts @@ -1,83 +1,83 @@ -import * as core from '@actions/core' -import * as github from '@actions/github' -import {Octokit} from '@octokit/action' +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import { Octokit } from "@octokit/action"; -const token = core.getInput('token') - ? core.getInput('token') - : process.env.GITHUB_TOKEN -const octokit = new Octokit({auth: `token ${token}`}) -const context = github.context -const repo = context.repo +const token = core.getInput("token") + ? core.getInput("token") + : process.env.GITHUB_TOKEN; +const octokit = new Octokit({ auth: `token ${token}` }); +const context = github.context; +const repo = context.repo; -const COMMENT_GREETING = `:robot: ChatGPT` +const COMMENT_GREETING = `:robot: OpenAI`; -const DEFAULT_TAG = '' +const DEFAULT_TAG = ""; const description_tag = - '' + ""; const description_tag_end = - '' + ""; export class Commenter { /** * @param mode Can be "create", "replace", "append" and "prepend". Default is "replace". */ async comment(message: string, tag: string, mode: string) { - await comment(message, tag, mode) + await comment(message, tag, mode); } get_description(description: string) { // remove our summary from description by looking for description_tag and description_tag_end - const start = description.indexOf(description_tag) - const end = description.indexOf(description_tag_end) + const start = description.indexOf(description_tag); + const end = description.indexOf(description_tag_end); if (start >= 0 && end >= 0) { return ( description.slice(0, start) + description.slice(end + description_tag_end.length) - ) + ); } - return description + return description; } async update_description( pull_number: number, description: string, - message: string + message: string, ) { // add this response to the description field of the PR as release notes by looking // for the tag (marker) try { // find the tag in the description and replace the content between the tag and the tag_end // if not found, add the tag and the content to the end of the description - const tag_index = description.indexOf(description_tag) - const tag_end_index = description.indexOf(description_tag_end) - const comment = `\n\n${description_tag}\n${message}\n${description_tag_end}` + const tag_index = description.indexOf(description_tag); + const tag_end_index = description.indexOf(description_tag_end); + const comment = `\n\n${description_tag}\n${message}\n${description_tag_end}`; if (tag_index === -1 || tag_end_index === -1) { - let new_description = description - new_description += comment + let new_description = description; + new_description += comment; await octokit.pulls.update({ owner: repo.owner, repo: repo.repo, pull_number, - body: new_description - }) + body: new_description, + }); } else { - let new_description = description.substring(0, tag_index) - new_description += comment + let new_description = description.substring(0, tag_index); + new_description += comment; new_description += description.substring( - tag_end_index + description_tag_end.length - ) + tag_end_index + description_tag_end.length, + ); await octokit.pulls.update({ owner: repo.owner, repo: repo.repo, pull_number, - body: new_description - }) + body: new_description, + }); } } catch (e: any) { core.warning( - `Failed to get PR: ${e}, skipping adding release notes to description.` - ) + `Failed to get PR: ${e}, skipping adding release notes to description.`, + ); } } @@ -86,16 +86,16 @@ export class Commenter { commit_id: string, path: string, line: number, - message: string + message: string, ) { - const tag = DEFAULT_TAG + const tag = DEFAULT_TAG; message = `${COMMENT_GREETING} ${message} -${tag}` +${tag}`; // replace comment made by this action - const comments = await list_review_comments(pull_number) + const comments = await list_review_comments(pull_number); for (const comment of comments) { if (comment.path === path && comment.position === line) { // look for tag @@ -108,9 +108,9 @@ ${tag}` owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: message - }) - return + body: message, + }); + return; } } } @@ -122,144 +122,144 @@ ${tag}` body: message, commit_id, path, - line - }) + line, + }); } } // recursively list review comments const list_review_comments = async (target: number, page: number = 1) => { - let {data: comments} = await octokit.pulls.listReviewComments({ + let { data: comments } = await octokit.pulls.listReviewComments({ owner: repo.owner, repo: repo.repo, pull_number: target, page: page, - per_page: 100 - }) + per_page: 100, + }); if (!comments) { - return [] + return []; } if (comments.length >= 100) { - comments = comments.concat(await list_review_comments(target, page + 1)) - return comments + comments = comments.concat(await list_review_comments(target, page + 1)); + return comments; } else { - return comments + return comments; } -} +}; const comment = async (message: string, tag: string, mode: string) => { - let target: number + let target: number; if (context.payload.pull_request) { - target = context.payload.pull_request.number + target = context.payload.pull_request.number; } else if (context.payload.issue) { - target = context.payload.issue.number + target = context.payload.issue.number; } else { core.warning( - `Skipped: context.payload.pull_request and context.payload.issue are both null` - ) - return + `Skipped: context.payload.pull_request and context.payload.issue are both null`, + ); + return; } if (!tag) { - tag = DEFAULT_TAG + tag = DEFAULT_TAG; } const body = `${COMMENT_GREETING} ${message} -${tag}` +${tag}`; - if (mode == 'create') { - await create(body, tag, target) - } else if (mode == 'replace') { - await replace(body, tag, target) - } else if (mode == 'append') { - await append(body, tag, target) - } else if (mode == 'prepend') { - await prepend(body, tag, target) + if (mode == "create") { + await create(body, tag, target); + } else if (mode == "replace") { + await replace(body, tag, target); + } else if (mode == "append") { + await append(body, tag, target); + } else if (mode == "prepend") { + await prepend(body, tag, target); } else { - core.warning(`Unknown mode: ${mode}, use "replace" instead`) - await replace(body, tag, target) + core.warning(`Unknown mode: ${mode}, use "replace" instead`); + await replace(body, tag, target); } -} +}; const create = async (body: string, tag: string, target: number) => { await octokit.issues.createComment({ owner: repo.owner, repo: repo.repo, issue_number: target, - body: body - }) -} + body: body, + }); +}; const replace = async (body: string, tag: string, target: number) => { - const comment = await find_comment_with_tag(tag, target) + const comment = await find_comment_with_tag(tag, target); if (comment) { await octokit.issues.updateComment({ owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: body - }) + body: body, + }); } else { - await create(body, tag, target) + await create(body, tag, target); } -} +}; const append = async (body: string, tag: string, target: number) => { - const comment = await find_comment_with_tag(tag, target) + const comment = await find_comment_with_tag(tag, target); if (comment) { await octokit.issues.updateComment({ owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: `${comment.body} ${body}` - }) + body: `${comment.body} ${body}`, + }); } else { - await create(body, tag, target) + await create(body, tag, target); } -} +}; const prepend = async (body: string, tag: string, target: number) => { - const comment = await find_comment_with_tag(tag, target) + const comment = await find_comment_with_tag(tag, target); if (comment) { await octokit.issues.updateComment({ owner: repo.owner, repo: repo.repo, comment_id: comment.id, - body: `${body} ${comment.body}` - }) + body: `${body} ${comment.body}`, + }); } else { - await create(body, tag, target) + await create(body, tag, target); } -} +}; const find_comment_with_tag = async (tag: string, target: number) => { - const comments = await list_comments(target) + const comments = await list_comments(target); for (let comment of comments) { if (comment.body && comment.body.includes(tag)) { - return comment + return comment; } } - return null -} + return null; +}; const list_comments = async (target: number, page: number = 1) => { - let {data: comments} = await octokit.issues.listComments({ + let { data: comments } = await octokit.issues.listComments({ owner: repo.owner, repo: repo.repo, issue_number: target, page: page, - per_page: 100 - }) + per_page: 100, + }); if (!comments) { - return [] + return []; } if (comments.length >= 100) { - comments = comments.concat(await list_comments(target, page + 1)) - return comments + comments = comments.concat(await list_comments(target, page + 1)); + return comments; } else { - return comments + return comments; } -} +}; diff --git a/src/fetch-polyfill.js b/src/fetch-polyfill.js new file mode 100644 index 00000000..0b76df36 --- /dev/null +++ b/src/fetch-polyfill.js @@ -0,0 +1,20 @@ +// fetch-polyfill.js +import fetch, { + Blob, + blobFrom, + blobFromSync, + File, + fileFrom, + fileFromSync, + FormData, + Headers, + Request, + Response, +} from 'node-fetch' + +if (!globalThis.fetch) { + globalThis.fetch = fetch + globalThis.Headers = Headers + globalThis.Request = Request + globalThis.Response = Response +} diff --git a/src/fetch-polyfill.ts b/src/fetch-polyfill.ts deleted file mode 100644 index 07c3f732..00000000 --- a/src/fetch-polyfill.ts +++ /dev/null @@ -1,8 +0,0 @@ -import fetch, * as nodeFetch from "node-fetch"; - -if (!globalThis.fetch) { - globalThis.fetch = fetch; - globalThis.Headers = nodeFetch.Headers; - globalThis.Request = nodeFetch.Request; - globalThis.Response = nodeFetch.Response; -} diff --git a/src/main.ts b/src/main.ts index d094cfa9..e370ad62 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,63 +1,58 @@ -import * as core from '@actions/core' -import {Bot} from './bot.js' -import {Options, Prompts} from './options.js' -import {codeReview} from './review.js' +import * as core from "@actions/core"; +import { Bot } from "./bot.js"; +import { Options, Prompts } from "./options.js"; +import { codeReview } from "./review.js"; async function run(): Promise { const options: Options = new Options( - core.getBooleanInput('debug'), - core.getInput('chatgpt_reverse_proxy'), - core.getBooleanInput('review_comment_lgtm'), - core.getMultilineInput('path_filters'), - core.getInput('system_message'), - core.getInput('temperature') - ) + core.getBooleanInput("debug"), + core.getBooleanInput("review_comment_lgtm"), + core.getMultilineInput("path_filters"), + core.getInput("system_message"), + core.getInput("temperature"), + ); const prompts: Prompts = new Prompts( - core.getInput('review_beginning'), - core.getInput('review_file'), - core.getInput('review_file_diff'), - core.getInput('review_patch_begin'), - core.getInput('review_patch'), - core.getInput('summarize_beginning'), - core.getInput('summarize_file_diff'), - core.getInput('summarize'), - core.getInput('summarize_release_notes') - ) + core.getInput("review_beginning"), + core.getInput("review_file"), + core.getInput("review_file_diff"), + core.getInput("review_patch_begin"), + core.getInput("review_patch"), + core.getInput("summarize_beginning"), + core.getInput("summarize_file_diff"), + core.getInput("summarize"), + core.getInput("summarize_release_notes"), + ); - // initialize chatgpt bot - let bot: Bot | null = null + // initialize openai bot + let bot: Bot | null = null; try { - bot = new Bot(options) + bot = new Bot(options); } catch (e: any) { core.warning( - `Skipped: failed to create bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}` - ) - return + `Skipped: failed to create bot, please check your openai_api_key: ${e}, backtrace: ${e.stack}`, + ); + return; } try { - await codeReview(bot, options, prompts) + await codeReview(bot, options, prompts); } catch (e: any) { if (e instanceof Error) { - core.setFailed( - `Failed to run the chatgpt-actions: ${e.message}, backtrace: ${e.stack}` - ) + core.setFailed(`Failed to run: ${e.message}, backtrace: ${e.stack}`); } else { - core.setFailed( - `Failed to run the chatgpt-actions: ${e}, backtrace: ${e.stack}` - ) + core.setFailed(`Failed to run: ${e}, backtrace: ${e.stack}`); } } } process - .on('unhandledRejection', (reason, p) => { - console.error(reason, 'Unhandled Rejection at Promise', p) - core.warning(`Unhandled Rejection at Promise: ${reason}, promise is ${p}`) - }) - .on('uncaughtException', (e: any) => { - console.error(e, 'Uncaught Exception thrown') - core.warning(`Uncaught Exception thrown: ${e}, backtrace: ${e.stack}`) + .on("unhandledRejection", (reason, p) => { + console.error(reason, "Unhandled Rejection at Promise", p); + core.warning(`Unhandled Rejection at Promise: ${reason}, promise is ${p}`); }) + .on("uncaughtException", (e: any) => { + console.error(e, "Uncaught Exception thrown"); + core.warning(`Uncaught Exception thrown: ${e}, backtrace: ${e.stack}`); + }); -await run() +await run(); diff --git a/src/options.ts b/src/options.ts index 669cde9c..9a37f5b8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,189 +1,186 @@ -import * as core from '@actions/core' -import {minimatch} from 'minimatch' +import * as core from "@actions/core"; +import { minimatch } from "minimatch"; export class Prompts { - review_beginning: string - review_file: string - review_file_diff: string - review_patch_begin: string - review_patch: string - summarize_beginning: string - summarize_file_diff: string - summarize: string - summarize_release_notes: string + review_beginning: string; + review_file: string; + review_file_diff: string; + review_patch_begin: string; + review_patch: string; + summarize_beginning: string; + summarize_file_diff: string; + summarize: string; + summarize_release_notes: string; constructor( - review_beginning = '', - review_file = '', - review_file_diff = '', - review_patch_begin = '', - review_patch = '', - summarize_beginning = '', - summarize_file_diff = '', - summarize = '', - summarize_release_notes = '' + review_beginning = "", + review_file = "", + review_file_diff = "", + review_patch_begin = "", + review_patch = "", + summarize_beginning = "", + summarize_file_diff = "", + summarize = "", + summarize_release_notes = "", ) { - this.review_beginning = review_beginning - this.review_file = review_file - this.review_file_diff = review_file_diff - this.review_patch_begin = review_patch_begin - this.review_patch = review_patch - this.summarize_beginning = summarize_beginning - this.summarize_file_diff = summarize_file_diff - this.summarize = summarize - this.summarize_release_notes = summarize_release_notes + this.review_beginning = review_beginning; + this.review_file = review_file; + this.review_file_diff = review_file_diff; + this.review_patch_begin = review_patch_begin; + this.review_patch = review_patch; + this.summarize_beginning = summarize_beginning; + this.summarize_file_diff = summarize_file_diff; + this.summarize = summarize; + this.summarize_release_notes = summarize_release_notes; } render_review_beginning(inputs: Inputs): string { - return inputs.render(this.review_beginning) + return inputs.render(this.review_beginning); } render_review_file(inputs: Inputs): string { - return inputs.render(this.review_file) + return inputs.render(this.review_file); } render_review_file_diff(inputs: Inputs): string { - return inputs.render(this.review_file_diff) + return inputs.render(this.review_file_diff); } render_review_patch_begin(inputs: Inputs): string { - return inputs.render(this.review_patch_begin) + return inputs.render(this.review_patch_begin); } render_review_patch(inputs: Inputs): string { - return inputs.render(this.review_patch) + return inputs.render(this.review_patch); } render_summarize_beginning(inputs: Inputs): string { - return inputs.render(this.summarize_beginning) + return inputs.render(this.summarize_beginning); } render_summarize_file_diff(inputs: Inputs): string { - return inputs.render(this.summarize_file_diff) + return inputs.render(this.summarize_file_diff); } render_summarize(inputs: Inputs): string { - return inputs.render(this.summarize) + return inputs.render(this.summarize); } render_summarize_release_notes(inputs: Inputs): string { - return inputs.render(this.summarize_release_notes) + return inputs.render(this.summarize_release_notes); } } export class Inputs { - system_message: string - title: string - description: string - summary: string - filename: string - file_content: string - file_diff: string - patch: string - diff: string + system_message: string; + title: string; + description: string; + summary: string; + filename: string; + file_content: string; + file_diff: string; + patch: string; + diff: string; constructor( - system_message = '', - title = '', - description = '', - summary = '', - filename = '', - file_content = '', - file_diff = '', - patch = '', - diff = '' + system_message = "", + title = "", + description = "", + summary = "", + filename = "", + file_content = "", + file_diff = "", + patch = "", + diff = "", ) { - this.system_message = system_message - this.title = title - this.description = description - this.summary = summary - this.filename = filename - this.file_content = file_content - this.file_diff = file_diff - this.patch = patch - this.diff = diff + this.system_message = system_message; + this.title = title; + this.description = description; + this.summary = summary; + this.filename = filename; + this.file_content = file_content; + this.file_diff = file_diff; + this.patch = patch; + this.diff = diff; } render(content: string): string { if (!content) { - return '' + return ""; } if (this.system_message) { - content = content.replace('$system_message', this.system_message) + content = content.replace("$system_message", this.system_message); } if (this.title) { - content = content.replace('$title', this.title) + content = content.replace("$title", this.title); } if (this.description) { - content = content.replace('$description', this.description) + content = content.replace("$description", this.description); } if (this.summary) { - content = content.replace('$summary', this.summary) + content = content.replace("$summary", this.summary); } if (this.filename) { - content = content.replace('$filename', this.filename) + content = content.replace("$filename", this.filename); } if (this.file_content) { - content = content.replace('$file_content', this.file_content) + content = content.replace("$file_content", this.file_content); } if (this.file_diff) { - content = content.replace('$file_diff', this.file_diff) + content = content.replace("$file_diff", this.file_diff); } if (this.patch) { - content = content.replace('$patch', this.patch) + content = content.replace("$patch", this.patch); } if (this.diff) { - content = content.replace('$diff', this.diff) + content = content.replace("$diff", this.diff); } - return content + return content; } } export class Options { - debug: boolean - chatgpt_reverse_proxy: string - review_comment_lgtm: boolean - path_filters: PathFilter - system_message: string - temperature: number + debug: boolean; + review_comment_lgtm: boolean; + path_filters: PathFilter; + system_message: string; + temperature: number; constructor( debug: boolean, - chatgpt_reverse_proxy: string, review_comment_lgtm = false, path_filters: string[] | null = null, - system_message = '', - temperature = '0.0' + system_message = "", + temperature = "0.0", ) { - this.debug = debug - this.chatgpt_reverse_proxy = chatgpt_reverse_proxy - this.review_comment_lgtm = review_comment_lgtm - this.path_filters = new PathFilter(path_filters) - this.system_message = system_message + this.debug = debug; + this.review_comment_lgtm = review_comment_lgtm; + this.path_filters = new PathFilter(path_filters); + this.system_message = system_message; // convert temperature to number - this.temperature = parseFloat(temperature) + this.temperature = parseFloat(temperature); } check_path(path: string): boolean { - const ok = this.path_filters.check(path) - core.info(`checking path: ${path} => ${ok}`) - return ok + const ok = this.path_filters.check(path); + core.info(`checking path: ${path} => ${ok}`); + return ok; } } export class PathFilter { - private rules: [string /* rule */, boolean /* exclude */][] + private rules: [string /* rule */, boolean /* exclude */][]; constructor(rules: string[] | null = null) { - this.rules = [] + this.rules = []; if (rules) { for (const rule of rules) { - const trimmed = rule?.trim() + const trimmed = rule?.trim(); if (trimmed) { - if (trimmed.startsWith('!')) { - this.rules.push([trimmed.substring(1).trim(), true]) + if (trimmed.startsWith("!")) { + this.rules.push([trimmed.substring(1).trim(), true]); } else { - this.rules.push([trimmed, false]) + this.rules.push([trimmed, false]); } } } @@ -191,23 +188,23 @@ export class PathFilter { } check(path: string): boolean { - let include_all = this.rules.length == 0 - let matched = false + let include_all = this.rules.length == 0; + let matched = false; for (const [rule, exclude] of this.rules) { if (exclude) { if (minimatch(path, rule)) { - return false + return false; } - include_all = true + include_all = true; } else { if (minimatch(path, rule)) { - matched = true - include_all = false + matched = true; + include_all = false; } else { - return false + return false; } } } - return include_all || matched + return include_all || matched; } } diff --git a/src/review.ts b/src/review.ts index b3fe642a..2bfdddf3 100644 --- a/src/review.ts +++ b/src/review.ts @@ -1,108 +1,108 @@ -import * as core from '@actions/core' -import * as github from '@actions/github' -import {Octokit} from '@octokit/action' -import {Bot} from './bot.js' -import {Commenter} from './commenter.js' -import {Inputs, Options, Prompts} from './options.js' -import * as tokenizer from './tokenizer.js' +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import { Octokit } from "@octokit/action"; +import { Bot } from "./bot.js"; +import { Commenter } from "./commenter.js"; +import { Inputs, Options, Prompts } from "./options.js"; +import * as tokenizer from "./tokenizer.js"; -const token = core.getInput('token') - ? core.getInput('token') - : process.env.GITHUB_TOKEN -const octokit = new Octokit({auth: `token ${token}`}) -const context = github.context -const repo = context.repo +const token = core.getInput("token") + ? core.getInput("token") + : process.env.GITHUB_TOKEN; +const octokit = new Octokit({ auth: `token ${token}` }); +const context = github.context; +const repo = context.repo; -const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500 +const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500; export const codeReview = async ( bot: Bot, options: Options, - prompts: Prompts + prompts: Prompts, ) => { if ( - context.eventName !== 'pull_request' && - context.eventName !== 'pull_request_target' + context.eventName !== "pull_request" && + context.eventName !== "pull_request_target" ) { core.warning( - `Skipped: current event is ${context.eventName}, only support pull_request event` - ) - return + `Skipped: current event is ${context.eventName}, only support pull_request event`, + ); + return; } if (!context.payload.pull_request) { - core.warning(`Skipped: context.payload.pull_request is null`) - return + core.warning(`Skipped: context.payload.pull_request is null`); + return; } - const commenter: Commenter = new Commenter() + const commenter: Commenter = new Commenter(); - const inputs: Inputs = new Inputs() - inputs.title = context.payload.pull_request.title + const inputs: Inputs = new Inputs(); + inputs.title = context.payload.pull_request.title; if (context.payload.pull_request.body) { inputs.description = commenter.get_description( - context.payload.pull_request.body - ) + context.payload.pull_request.body, + ); } // as gpt-3.5-turbo isn't paying attention to system message, add to inputs for now - inputs.system_message = options.system_message + inputs.system_message = options.system_message; // collect diff chunks const diff = await octokit.repos.compareCommits({ owner: repo.owner, repo: repo.repo, base: context.payload.pull_request.base.sha, - head: context.payload.pull_request.head.sha - }) - const {files, commits} = diff.data + head: context.payload.pull_request.head.sha, + }); + const { files, commits } = diff.data; if (!files) { - core.warning(`Skipped: diff.data.files is null`) - return + core.warning(`Skipped: diff.data.files is null`); + return; } // find patches to review - const files_to_review: [string, string, string, [number, string][]][] = [] + const files_to_review: [string, string, string, [number, string][]][] = []; for (const file of files) { if (!options.check_path(file.filename)) { - core.info(`skip for excluded path: ${file.filename}`) - continue + core.info(`skip for excluded path: ${file.filename}`); + continue; } // retrieve file contents - let file_content = '' + let file_content = ""; try { const contents = await octokit.repos.getContent({ owner: repo.owner, repo: repo.repo, path: file.filename, - ref: context.payload.pull_request.base.sha - }) + ref: context.payload.pull_request.base.sha, + }); if (contents.data) { if (!Array.isArray(contents.data)) { - if (contents.data.type === 'file' && contents.data.content) { + if (contents.data.type === "file" && contents.data.content) { file_content = Buffer.from( contents.data.content, - 'base64' - ).toString() + "base64", + ).toString(); } } } } catch (error) { - core.warning(`Failed to get file contents: ${error}, skipping.`) + core.warning(`Failed to get file contents: ${error}, skipping.`); } - let file_diff = '' + let file_diff = ""; if (file.patch) { - core.info(`diff for ${file.filename}: ${file.patch}`) - file_diff = file.patch + core.info(`diff for ${file.filename}: ${file.patch}`); + file_diff = file.patch; } - const patches: [number, string][] = [] + const patches: [number, string][] = []; for (const patch of split_patch(file.patch)) { - const line = patch_comment_line(patch) - patches.push([line, patch]) + const line = patch_comment_line(patch); + patches.push([line, patch]); } if (patches.length > 0) { - files_to_review.push([file.filename, file_content, file_diff, patches]) + files_to_review.push([file.filename, file_content, file_diff, patches]); } } @@ -110,143 +110,143 @@ export const codeReview = async ( // Summary Stage const [, summarize_begin_ids] = await bot.chat( prompts.render_summarize_beginning(inputs), - {} - ) - let next_summarize_ids = summarize_begin_ids + {}, + ); + let next_summarize_ids = summarize_begin_ids; for (const [filename, file_content, file_diff] of files_to_review) { - inputs.filename = filename - inputs.file_content = file_content - inputs.file_diff = file_diff + inputs.filename = filename; + inputs.file_content = file_content; + inputs.file_diff = file_diff; if (file_diff.length > 0) { - const file_diff_tokens = tokenizer.get_token_count(file_diff) + const file_diff_tokens = tokenizer.get_token_count(file_diff); if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // summarize diff const [summarize_resp, summarize_diff_ids] = await bot.chat( prompts.render_summarize_file_diff(inputs), - next_summarize_ids - ) + next_summarize_ids, + ); if (!summarize_resp) { - core.info('summarize: nothing obtained from chatgpt') + core.info("summarize: nothing obtained from openai"); } else { - next_summarize_ids = summarize_diff_ids + next_summarize_ids = summarize_diff_ids; } } } } // final summary const [summarize_final_response, summarize_final_response_ids] = - await bot.chat(prompts.render_summarize(inputs), next_summarize_ids) + await bot.chat(prompts.render_summarize(inputs), next_summarize_ids); if (!summarize_final_response) { - core.info('summarize: nothing obtained from chatgpt') + core.info("summarize: nothing obtained from openai"); } else { - inputs.summary = summarize_final_response + inputs.summary = summarize_final_response; - next_summarize_ids = summarize_final_response_ids + next_summarize_ids = summarize_final_response_ids; const tag = - '' - await commenter.comment(`${summarize_final_response}`, tag, 'replace') + ""; + await commenter.comment(`${summarize_final_response}`, tag, "replace"); } // final release notes const [release_notes_response, release_notes_ids] = await bot.chat( prompts.render_summarize_release_notes(inputs), - next_summarize_ids - ) + next_summarize_ids, + ); if (!release_notes_response) { - core.info('release notes: nothing obtained from chatgpt') + core.info("release notes: nothing obtained from openai"); } else { - next_summarize_ids = release_notes_ids - const description = inputs.description - let message = '### Summary by ChatGPT\n\n' - message += release_notes_response + next_summarize_ids = release_notes_ids; + const description = inputs.description; + let message = "### Summary by OpenAI\n\n"; + message += release_notes_response; commenter.update_description( context.payload.pull_request.number, description, - message - ) + message, + ); } // Review Stage const [, review_begin_ids] = await bot.chat( prompts.render_review_beginning(inputs), - {} - ) - let next_review_ids = review_begin_ids + {}, + ); + let next_review_ids = review_begin_ids; for (const [ filename, file_content, file_diff, - patches + patches, ] of files_to_review) { - inputs.filename = filename - inputs.file_content = file_content - inputs.file_diff = file_diff + inputs.filename = filename; + inputs.file_content = file_content; + inputs.file_diff = file_diff; // reset chat session for each file while reviewing - next_review_ids = review_begin_ids + next_review_ids = review_begin_ids; if (file_content.length > 0) { - const file_content_tokens = tokenizer.get_token_count(file_content) + const file_content_tokens = tokenizer.get_token_count(file_content); if (file_content_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // review file const [resp, review_file_ids] = await bot.chat( prompts.render_review_file(inputs), - next_review_ids - ) + next_review_ids, + ); if (!resp) { - core.info('review: nothing obtained from chatgpt') + core.info("review: nothing obtained from openai"); } else { - next_review_ids = review_file_ids + next_review_ids = review_file_ids; } } else { core.info( - `skip sending content of file: ${inputs.filename} due to token count: ${file_content_tokens}` - ) + `skip sending content of file: ${inputs.filename} due to token count: ${file_content_tokens}`, + ); } } if (file_diff.length > 0) { - const file_diff_tokens = tokenizer.get_token_count(file_diff) + const file_diff_tokens = tokenizer.get_token_count(file_diff); if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // review diff const [resp, review_diff_ids] = await bot.chat( prompts.render_review_file_diff(inputs), - next_review_ids - ) + next_review_ids, + ); if (!resp) { - core.info('review: nothing obtained from chatgpt') + core.info("review: nothing obtained from openai"); } else { - next_review_ids = review_diff_ids + next_review_ids = review_diff_ids; } } else { core.info( - `skip sending diff of file: ${inputs.filename} due to token count: ${file_diff_tokens}` - ) + `skip sending diff of file: ${inputs.filename} due to token count: ${file_diff_tokens}`, + ); } } // review_patch_begin const [, patch_begin_ids] = await bot.chat( prompts.render_review_patch_begin(inputs), - next_review_ids - ) - next_review_ids = patch_begin_ids + next_review_ids, + ); + next_review_ids = patch_begin_ids; for (const [line, patch] of patches) { - core.info(`Reviewing ${filename}:${line} with chatgpt ...`) - inputs.patch = patch + core.info(`Reviewing ${filename}:${line} with openai ...`); + inputs.patch = patch; const [response, patch_ids] = await bot.chat( prompts.render_review_patch(inputs), - next_review_ids - ) + next_review_ids, + ); if (!response) { - core.info('review: nothing obtained from chatgpt') - continue + core.info("review: nothing obtained from openai"); + continue; } - next_review_ids = patch_ids - if (!options.review_comment_lgtm && response.includes('LGTM')) { - continue + next_review_ids = patch_ids; + if (!options.review_comment_lgtm && response.includes("LGTM")) { + continue; } try { await commenter.review_comment( @@ -254,55 +254,55 @@ export const codeReview = async ( commits[commits.length - 1].sha, filename, line, - `${response}` - ) + `${response}`, + ); } catch (e: any) { core.warning(`Failed to comment: ${e}, skipping. backtrace: ${e.stack} filename: ${filename} line: ${line} - patch: ${patch}`) + patch: ${patch}`); } } } } -} +}; // Write a function that takes diff for a single file as a string // and splits the diff into separate patches const split_patch = (patch: string | null | undefined): string[] => { if (!patch) { - return [] + return []; } - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm; - const result: string[] = [] - let last = -1 - let match: RegExpExecArray | null + const result: string[] = []; + let last = -1; + let match: RegExpExecArray | null; while ((match = pattern.exec(patch)) !== null) { if (last === -1) { - last = match.index + last = match.index; } else { - result.push(patch.substring(last, match.index)) - last = match.index + result.push(patch.substring(last, match.index)); + last = match.index; } } if (last !== -1) { - result.push(patch.substring(last)) + result.push(patch.substring(last)); } - return result -} + return result; +}; const patch_comment_line = (patch: string): number => { - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm - const match = pattern.exec(patch) + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm; + const match = pattern.exec(patch); if (match) { - const begin = parseInt(match[4]) - const diff = parseInt(match[5]) - return begin + diff - 1 + const begin = parseInt(match[4]); + const diff = parseInt(match[5]); + return begin + diff - 1; } else { - return -1 + return -1; } -} +};