├── .prettierrc ├── action ├── src │ ├── index.d.ts │ ├── log.d.ts │ ├── bot.d.ts │ └── chat.d.ts ├── _tiktoken_bg.wasm ├── rollup.config.d.ts ├── middleware.d.ts ├── file.js ├── worker-pipeline.js ├── worker.js ├── worker1.js ├── 37.index.cjs.js └── multipart-parser-d1d13d05.js ├── api └── github │ ├── .DS_Store │ └── webhooks │ └── index.ts ├── src ├── log.ts ├── index.ts ├── github-action.cjs ├── aws-lambda.cjs ├── fetch-polyfill.cjs ├── chat.ts └── bot.ts ├── .gitignore ├── .dockerignore ├── action.yml ├── Dockerfile ├── pm2.config.cjs ├── jest.config.js ├── test ├── fixtures │ ├── issues.opened.json │ └── mock-cert.pem └── index.test.ts ├── .github └── workflows │ └── cr.yml ├── public └── index.html ├── serverless.yml ├── .env.example ├── middleware.ts ├── LICENSE ├── rollup.config.ts ├── package.json ├── CONTRIBUTING.md ├── README.zh-CN.md ├── README.ko.md ├── CODE_OF_CONDUCT.md ├── README.ja.md ├── README.zh-TW.md ├── README.md ├── app.yml └── tsconfig.json /.prettierrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /action/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /action/src/log.d.ts: -------------------------------------------------------------------------------- 1 | import log from "loglevel"; 2 | export default log; 3 | -------------------------------------------------------------------------------- /api/github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anc95/ChatGPT-CodeReview/HEAD/api/github/.DS_Store -------------------------------------------------------------------------------- /action/_tiktoken_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anc95/ChatGPT-CodeReview/HEAD/action/_tiktoken_bg.wasm -------------------------------------------------------------------------------- /action/src/bot.d.ts: -------------------------------------------------------------------------------- 1 | import { Probot } from 'probot'; 2 | export declare const robot: (app: Probot) => void; 3 | -------------------------------------------------------------------------------- /action/rollup.config.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: import("rollup").RollupOptions[]; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /action/middleware.d.ts: -------------------------------------------------------------------------------- 1 | export declare const config: { 2 | matcher: string; 3 | }; 4 | export default function middleware(request: any): Promise; 5 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import log, { LogLevelNames } from "loglevel"; 2 | 3 | log.setLevel((process.env.LOG_LEVEL as LogLevelNames) || "info"); 4 | 5 | export default log; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.pem 4 | !mock-cert.pem 5 | .env 6 | coverage 7 | lib 8 | *.cache/** 9 | dist/ 10 | lambda/ 11 | .serverless/ -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { run } from "probot"; 2 | import log from "./log.js"; 3 | import { robot } from "./bot.js"; 4 | 5 | log.info("Starting probot"); 6 | 7 | run(robot) -------------------------------------------------------------------------------- /src/github-action.cjs: -------------------------------------------------------------------------------- 1 | require('./fetch-polyfill.cjs'); 2 | const { run } = require('@probot/adapter-github-actions'); 3 | const { robot } = require('./bot'); 4 | require('./log'); 5 | 6 | run(robot); 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/.git 3 | **/README.md 4 | **/LICENSE 5 | **/.vscode 6 | **/npm-debug.log 7 | **/coverage 8 | **/.env 9 | **/.editorconfig 10 | **/dist 11 | **/*.pem 12 | Dockerfile 13 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: ChatGPT CodeReviewer 2 | description: 'A Code Review Action Powered By ChatGPT' 3 | branding: 4 | icon: 'gift' 5 | color: orange 6 | runs: 7 | using: 'node20' 8 | main: 'action/index.cjs' 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | WORKDIR /usr/src/app 3 | COPY package.json yarn.lock ./ 4 | RUN yarn install --production --frozen-lockfile && yarn cache clean 5 | ENV NODE_ENV="production" 6 | COPY . . 7 | CMD [ "yarn", "start" ] 8 | -------------------------------------------------------------------------------- /pm2.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | apps : [{ 3 | name : 'Bot', 4 | script : 'dist/index.js', 5 | interpreter_args : '-r dotenv/config', 6 | time: true 7 | }] 8 | } 9 | 10 | module.exports = config; -------------------------------------------------------------------------------- /api/github/webhooks/index.ts: -------------------------------------------------------------------------------- 1 | import { createNodeMiddleware, createProbot } from "probot"; 2 | 3 | import { robot as app } from "../../../src/bot.js" 4 | 5 | const probot = createProbot(); 6 | 7 | export default createNodeMiddleware(app, { probot, webhooksPath: '/api/github/webhooks' }); -------------------------------------------------------------------------------- /src/aws-lambda.cjs: -------------------------------------------------------------------------------- 1 | const { 2 | createLambdaFunction, 3 | createProbot, 4 | } = require("@probot/adapter-aws-lambda-serverless"); 5 | const appFn = require('./bot').robot; 6 | 7 | module.exports.webhooks = createLambdaFunction(appFn, { 8 | probot: createProbot(), 9 | }); 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src/", "/test/"], 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest", 5 | }, 6 | testRegex: "(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$", 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 8 | testEnvironment: "node", 9 | }; 10 | -------------------------------------------------------------------------------- /src/fetch-polyfill.cjs: -------------------------------------------------------------------------------- 1 | const { Headers, Request, Response } = require('node-fetch'); 2 | const fetch = require('node-fetch').default; 3 | 4 | if (!globalThis.fetch) { 5 | globalThis.fetch = fetch; 6 | globalThis.Headers = Headers; 7 | globalThis.Request = Request; 8 | globalThis.Response = Response; 9 | } 10 | -------------------------------------------------------------------------------- /action/src/chat.d.ts: -------------------------------------------------------------------------------- 1 | export declare class Chat { 2 | private openai; 3 | private isAzure; 4 | private isGithubModels; 5 | constructor(apikey: string); 6 | private generatePrompt; 7 | codeReview: (patch: string) => Promise<{ 8 | lgtm: boolean; 9 | review_comment: string; 10 | }>; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/issues.opened.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "issue": { 4 | "number": 1, 5 | "user": { 6 | "login": "hiimbex" 7 | } 8 | }, 9 | "repository": { 10 | "name": "testing-things", 11 | "owner": { 12 | "login": "hiimbex" 13 | } 14 | }, 15 | "installation": { 16 | "id": 2 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /action/file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pino = require('./pino') 4 | const { once } = require('events') 5 | 6 | module.exports = async function (opts = {}) { 7 | const destOpts = Object.assign({}, opts, { dest: opts.destination || 1, sync: false }) 8 | delete destOpts.destination 9 | const destination = pino.destination(destOpts) 10 | await once(destination, 'ready') 11 | return destination 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | models: read 7 | 8 | on: 9 | pull_request: 10 | types: [opened, reopened, synchronize] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: anc95/ChatGPT-CodeReview@main 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | LANGUAGE: English 20 | USE_GITHUB_MODELS: true 21 | LOG_LEVEL: debug 22 | INCLUDE_PATTERNS: src/*,.github/**/* 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello from Vercel 8 | 9 | 10 |

Hello from Vercel

11 | 12 |

Edit this file in public/index.html

13 | 14 |

15 | Your app receives webhook requests at 16 | POST /api/github/webhooks 17 |

18 | 19 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: cr-bot 2 | frameworkVersion: '3' 3 | useDotenv: true 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs18.x 8 | environment: 9 | APP_ID: ${env:APP_ID} 10 | WEBHOOK_SECRET: ${env:WEBHOOK_SECRET} 11 | PRIVATE_KEY_PATH: ${env:PRIVATE_KEY_PATH} 12 | 13 | package: 14 | patterns: 15 | - lambda/** 16 | - '!node_modules/**' 17 | - '!actions/**' 18 | - '!.git/**' 19 | 20 | functions: 21 | webhooks: 22 | handler: lambda.webhooks 23 | events: 24 | - httpApi: 25 | path: /api/github/webhooks 26 | method: post 27 | 28 | 29 | memorySize: 30 | - 256 31 | timeout: 30 32 | logRetentionInDays: 14 33 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # open ai apikey 2 | OPENAI_API_KEY= 3 | 4 | # The ID of your GitHub App 5 | APP_ID= 6 | WEBHOOK_SECRET= 7 | 8 | # Use `trace` to get verbose logging or `info` to show less 9 | LOG_LEVEL=debug 10 | 11 | # Go to https://smee.io/new set this to the URL that you are redirected to. 12 | WEBHOOK_PROXY_URL= 13 | PRIVATE_KEY= 14 | 15 | # BOT settings 16 | LANGUAGE= 17 | MODEL= 18 | temperature= 19 | top_p= 20 | max_tokens= 21 | TARGET_LABEL= 22 | MAX_PATCH_LENGTH= 23 | PROMPT=Below there is a code diff please help me do a code review 24 | IGNORE_PATTERNS=/node_modules,*.md # glob pattern or regex pattern to ignore files, separated by comma 25 | INCLUDE_PATTERNS=*.js,*.ts # glob pattern or regex pattern to include files, separated by comma 26 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { next, rewrite } from '@vercel/edge'; 2 | 3 | export const config = { 4 | matcher: '/api/github/webhooks', 5 | }; 6 | 7 | export default async function middleware(request: any) { 8 | let json; 9 | 10 | try { 11 | console.log('enter'); 12 | json = await request?.json?.(); 13 | } catch { 14 | return rewrite(new URL('https://github.com/apps/cr-gpt')); 15 | } 16 | 17 | if (!json) { 18 | console.log('received is not a json'); 19 | return rewrite(new URL('https://github.com/apps/cr-gpt')); 20 | } 21 | 22 | if (!json.before || !json.after || !json.commits) { 23 | console.log('invalid event'); 24 | return rewrite(new URL('https://github.com/apps/cr-gpt')); 25 | } 26 | 27 | console.log('GO next'); 28 | return next(); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2023, anc95 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /action/worker-pipeline.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EE = require('events') 4 | const loadTransportStreamBuilder = require('./transport-stream') 5 | const { pipeline, PassThrough } = require('stream') 6 | 7 | // This file is not checked by the code coverage tool, 8 | // as it is not reliable. 9 | 10 | /* istanbul ignore file */ 11 | 12 | module.exports = async function ({ targets }) { 13 | const streams = await Promise.all(targets.map(async (t) => { 14 | const fn = await loadTransportStreamBuilder(t.target) 15 | const stream = await fn(t.options) 16 | return stream 17 | })) 18 | const ee = new EE() 19 | 20 | const stream = new PassThrough({ 21 | autoDestroy: true, 22 | destroy (_, cb) { 23 | ee.on('error', cb) 24 | ee.on('closed', cb) 25 | } 26 | }) 27 | 28 | pipeline(stream, ...streams, function (err) { 29 | if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') { 30 | ee.emit('error', err) 31 | return 32 | } 33 | 34 | ee.emit('closed') 35 | }) 36 | 37 | return stream 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import esbuild from 'rollup-plugin-esbuild'; 2 | import { defineConfig } from 'rollup'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import nodeResolve from '@rollup/plugin-node-resolve'; 5 | 6 | export default defineConfig([ 7 | { 8 | input: 'middleware.ts', 9 | output: { 10 | file: 'dist/middleware.js', 11 | format: 'esm', 12 | inlineDynamicImports: true, 13 | }, 14 | plugins: [esbuild(), commonjs(), nodeResolve()], 15 | }, 16 | { 17 | input: 'src/bot.ts', 18 | output: { 19 | dir: 'dist/lib/', 20 | format: 'esm', 21 | inlineDynamicImports: false, 22 | }, 23 | plugins: [esbuild({ include: 'src/*.ts' })], 24 | }, 25 | { 26 | input: 'src/index.ts', 27 | output: { 28 | dir: 'dist/', 29 | format: 'esm', 30 | inlineDynamicImports: false, 31 | }, 32 | plugins: [esbuild()], 33 | }, 34 | { 35 | input: ['api/github/webhooks/index.ts'], 36 | output: { 37 | dir: 'dist/api/github/webhooks', 38 | format: 'esm', 39 | }, 40 | plugins: [esbuild()], 41 | }, 42 | ]); 43 | -------------------------------------------------------------------------------- /action/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pino = require('../pino.js') 4 | const build = require('pino-abstract-transport') 5 | const loadTransportStreamBuilder = require('./transport-stream') 6 | 7 | // This file is not checked by the code coverage tool, 8 | // as it is not reliable. 9 | 10 | /* istanbul ignore file */ 11 | 12 | module.exports = async function ({ targets, levels, dedupe }) { 13 | targets = await Promise.all(targets.map(async (t) => { 14 | const fn = await loadTransportStreamBuilder(t.target) 15 | const stream = await fn(t.options) 16 | return { 17 | level: t.level, 18 | stream 19 | } 20 | })) 21 | return build(process, { 22 | parse: 'lines', 23 | metadata: true, 24 | close (err, cb) { 25 | let expected = 0 26 | for (const transport of targets) { 27 | expected++ 28 | transport.stream.on('close', closeCb) 29 | transport.stream.end() 30 | } 31 | 32 | function closeCb () { 33 | if (--expected === 0) { 34 | cb(err) 35 | } 36 | } 37 | } 38 | }) 39 | 40 | function process (stream) { 41 | const multi = pino.multistream(targets, { levels, dedupe }) 42 | // TODO manage backpressure 43 | stream.on('data', function (chunk) { 44 | const { lastTime, lastMsg, lastObj, lastLevel } = this 45 | multi.lastLevel = lastLevel 46 | multi.lastTime = lastTime 47 | multi.lastMsg = lastMsg 48 | multi.lastObj = lastObj 49 | 50 | // TODO handle backpressure 51 | multi.write(chunk + '\n') 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/fixtures/mock-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAli7V49NdZe+XYC1pLaHM0te8kiDmZBJ1u2HJHN8GdbROB6NO 3 | VpC3xK7NxQn6xpvZ9ux20NvcDvGle+DOptZztBH+np6h2jZQ1/kD1yG1eQvVH4th 4 | /9oqHuIjmIfO8lIe4Hyd5Fw5xHkGqVETTGR+0c7kdZIlHmkOregUGtMYZRUi4YG+ 5 | q0w+uFemiHpGKXbeCIAvkq7aIkisEzvPWfSyYdA6WJHpxFk7tD7D8VkzABLVRHCq 6 | AuyqPG39BhGZcGLXx5rGK56kDBJkyTR1t3DkHpwX+JKNG5UYNwOG4LcQj1fteeta 7 | TdkYUMjIyWbanlMYyC+dq7B5fe7el99jXQ1gXwIDAQABAoIBADKfiPOpzKLOtzzx 8 | MbHzB0LO+75aHq7+1faayJrVxqyoYWELuB1P3NIMhknzyjdmU3t7S7WtVqkm5Twz 9 | lBUC1q+NHUHEgRQ4GNokExpSP4SU63sdlaQTmv0cBxmkNarS6ZuMBgDy4XoLvaYX 10 | MSUf/uukDLhg0ehFS3BteVFtdJyllhDdTenF1Nb1rAeN4egt8XLsE5NQDr1szFEG 11 | xH5lb+8EDtzgsGpeIddWR64xP0lDIKSZWst/toYKWiwjaY9uZCfAhvYQ1RsO7L/t 12 | sERmpYgh+rAZUh/Lr98EI8BPSPhzFcSHmtqzzejvC5zrZPHcUimz0CGA3YBiLoJX 13 | V1OrxmECgYEAxkd8gpmVP+LEWB3lqpSvJaXcGkbzcDb9m0OPzHUAJDZtiIIf0UmO 14 | nvL68/mzbCHSj+yFjZeG1rsrAVrOzrfDCuXjAv+JkEtEx0DIevU1u60lGnevOeky 15 | r8Be7pmymFB9/gzQAd5ezIlTv/COgoO986a3h1yfhzrrzbqSiivw308CgYEAwecI 16 | aZZwqH3GifR+0+Z1B48cezA5tC8LZt5yObGzUfxKTWy30d7lxe9N59t0KUVt/QL5 17 | qVkd7mqGzsUMyxUN2U2HVnFTWfUFMhkn/OnCnayhILs8UlCTD2Xxoy1KbQH/9FIr 18 | xf0pbMNJLXeGfyRt/8H+BzSZKBw9opJBWE4gqfECgYBp9FdvvryHuBkt8UQCRJPX 19 | rWsRy6pY47nf11mnazpZH5Cmqspv3zvMapF6AIxFk0leyYiQolFWvAv+HFV5F6+t 20 | Si1mM8GCDwbA5zh6pEBDewHhw+UqMBh63HSeUhmi1RiOwrAA36CO8i+D2Pt+eQHv 21 | ir52IiPJcs4BUNrv5Q1BdwKBgBHgVNw3LGe8QMOTMOYkRwHNZdjNl2RPOgPf2jQL 22 | d/bFBayhq0jD/fcDmvEXQFxVtFAxKAc+2g2S8J67d/R5Gm/AQAvuIrsWZcY6n38n 23 | pfOXaLt1x5fnKcevpFlg4Y2vM4O416RHNLx8PJDehh3Oo/2CSwMrDDuwbtZAGZok 24 | icphAoGBAI74Tisfn+aeCZMrO8KxaWS5r2CD1KVzddEMRKlJvSKTY+dOCtJ+XKj1 25 | OsZdcDvDC5GtgcywHsYeOWHldgDWY1S8Z/PUo4eK9qBXYBXp3JEZQ1dqzFdz+Txi 26 | rBn2WsFLsxV9j2/ugm0PqWVBcU2bPUCwvaRu3SOms2teaLwGCkhr 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cr-bot", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A Probot app", 6 | "author": "anc95", 7 | "license": "ISC", 8 | "type": "module", 9 | "homepage": "https://github.com//", 10 | "keywords": [ 11 | "probot", 12 | "github", 13 | "probot-app", 14 | "code review" 15 | ], 16 | "scripts": { 17 | "start": "node -r dotenv/config ./dist/index.js", 18 | "test": "jest", 19 | "build": "rm -rf dist && rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript && ncc build src/github-action.cjs -o action", 20 | "build:lambda": "ncc build src/aws-lambda.cjs -o lambda" 21 | }, 22 | "dependencies": { 23 | "@probot/adapter-aws-lambda-serverless": "^3.0.2", 24 | "@probot/adapter-github-actions": "^3.1.3", 25 | "@vercel/edge": "^0.2.7", 26 | "dotenv": "^16.0.3", 27 | "loglevel": "^1.9.2", 28 | "minimatch": "^10.0.1", 29 | "next": "^13.1.6", 30 | "node-fetch": "^3.3.0", 31 | "openai": "^4.71.0", 32 | "probot": "^12.2.4" 33 | }, 34 | "devDependencies": { 35 | "@rollup/plugin-commonjs": "^24.0.1", 36 | "@rollup/plugin-json": "^6.0.0", 37 | "@rollup/plugin-node-resolve": "^15.0.1", 38 | "@rollup/plugin-typescript": "^11.0.0", 39 | "@types/jest": "^29.0.0", 40 | "@types/node": "^18.0.0", 41 | "@types/pino-std-serializers": "^4.0.0", 42 | "@vercel/ncc": "^0.36.1", 43 | "esbuild": "^0.17.7", 44 | "jest": "^29.0.0", 45 | "nock": "^13.0.5", 46 | "rollup": "^3.15.0", 47 | "rollup-plugin-commonjs": "^10.1.0", 48 | "rollup-plugin-esbuild": "^5.0.0", 49 | "smee-client": "^1.2.2", 50 | "ts-jest": "^29.0.0", 51 | "typescript": "^4.1.3" 52 | }, 53 | "engines": { 54 | "node": ">= 18" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 10 | 11 | ## Issues and PRs 12 | 13 | If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them. 14 | 15 | We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR. 16 | 17 | ## Submitting a pull request 18 | 19 | 1. [Fork][fork] and clone the repository. 20 | 1. Configure and install the dependencies: `npm install`. 21 | 1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so there's no need to lint separately. 22 | 1. Create a new branch: `git checkout -b my-branch-name`. 23 | 1. Make your change, add tests, and make sure the tests still pass. 24 | 1. Push to your fork and [submit a pull request][pr]. 25 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged. 26 | 27 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 28 | 29 | - Write and update tests. 30 | - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 31 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 32 | 33 | Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you. 34 | 35 | ## Resources 36 | 37 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 38 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 39 | - [GitHub Help](https://help.github.com) 40 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | // You can import your modules 2 | // import index from '../src/index' 3 | 4 | import nock from "nock"; 5 | // Requiring our app implementation 6 | import myProbotApp from "../src"; 7 | import { Probot, ProbotOctokit } from "probot"; 8 | // Requiring our fixtures 9 | import payload from "./fixtures/issues.opened.json"; 10 | const issueCreatedBody = { body: "Thanks for opening this issue!" }; 11 | const fs = require("fs"); 12 | const path = require("path"); 13 | 14 | const privateKey = fs.readFileSync( 15 | path.join(__dirname, "fixtures/mock-cert.pem"), 16 | "utf-8" 17 | ); 18 | 19 | describe("My Probot app", () => { 20 | let probot: any; 21 | 22 | beforeEach(() => { 23 | nock.disableNetConnect(); 24 | probot = new Probot({ 25 | appId: 123, 26 | privateKey, 27 | // disable request throttling and retries for testing 28 | Octokit: ProbotOctokit.defaults({ 29 | retry: { enabled: false }, 30 | throttle: { enabled: false }, 31 | }), 32 | }); 33 | // Load our app into probot 34 | probot.load(myProbotApp); 35 | }); 36 | 37 | test("creates a comment when an issue is opened", async () => { 38 | const mock = nock("https://api.github.com") 39 | // Test that we correctly return a test token 40 | .post("/app/installations/2/access_tokens") 41 | .reply(200, { 42 | token: "test", 43 | permissions: { 44 | issues: "write", 45 | }, 46 | }) 47 | 48 | // Test that a comment is posted 49 | .post("/repos/hiimbex/testing-things/issues/1/comments", (body: any) => { 50 | expect(body).toMatchObject(issueCreatedBody); 51 | return true; 52 | }) 53 | .reply(200); 54 | 55 | // Receive a webhook event 56 | await probot.receive({ name: "issues", payload }); 57 | 58 | expect(mock.pendingMocks()).toStrictEqual([]); 59 | }); 60 | 61 | afterEach(() => { 62 | nock.cleanAll(); 63 | nock.enableNetConnect(); 64 | }); 65 | }); 66 | 67 | // For more information about testing with Jest see: 68 | // https://facebook.github.io/jest/ 69 | 70 | // For more information about using TypeScript in your tests, Jest recommends: 71 | // https://github.com/kulshekhar/ts-jest 72 | 73 | // For more information about testing with Nock see: 74 | // https://github.com/nock/nock 75 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 代码审查机器人 2 | 3 | > 由 ChatGPT 提供支持的代码审查机器人 4 | 5 | > 想在 Gitlab 使用? 6 | > 试试 https://github.com/nangongchengfeng/Chat-CodeReview.git 7 | 8 | 翻译版本:[英语](./README.md)\|[简体中文](./README.zh-CN.md)\|[繁體中文](./README.zh-TW.md) \| [한국어](./README.ko.md) \| [日本語](./README.ja.md) 9 | 10 | ## 用法 11 | 12 | ❗️⚠️ 鉴于成本考虑,BOT 仅用于测试目的,并目前在 AWS Lambda 上部署并受到速率限制。因此,不稳定的情况是完全正常的。建议自己部署应用程序。 13 | 14 | ### 安装 15 | 16 | 安装:[apps/cr-gpt](https://github.com/apps/cr-gpt); 17 | 18 | ### 配置 19 | 20 | 1. 转到你要集成此机器人的仓库首页 21 | 2. 点击`settings` 22 | 3. 点击`actions`在下面`secrets and variables` 23 | 4. 切换到`Variables`选项,创建一个新变量`OPENAI_API_KEY`,值为你的 open api 的 keyimage 24 | 25 | ### 开始使用 26 | 27 | 1. 当你创建一个新的 Pull request 时,机器人会自动进行代码审查,审查信息将显示在 pr timeline / file changes 部分。 28 | 2. 在`git push`更新 PR 之后,cr bot 将重新审查更改的文件 29 | 30 | 例子: 31 | 32 | [ChatGPT-CodeReview/pull/21](https://github.com/anc95/ChatGPT-CodeReview/pull/21) 33 | 34 | image 35 | 36 | ### 使用 Github Action 37 | 38 | > 这是推荐的方式,因为 github bot 在一个不起眼的 vps 上服务,我不能确保它总是稳定的 39 | 40 | [actions/chatgpt-codereviewer](https://github.com/marketplace/actions/chatgpt-codereviewer) 41 | 42 | 1. 添加`OPENAI_API_KEY`到你的 github action 密钥 43 | 2. 创建`.github/workflows/cr.yml`添加以下内容 44 | 45 | ```yml 46 | name: Code Review 47 | 48 | permissions: 49 | contents: read 50 | pull-requests: write 51 | 52 | on: 53 | pull_request: 54 | types: [opened, reopened] 55 | 56 | jobs: 57 | test: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: anc95/ChatGPT-CodeReview@main 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 64 | # optional 65 | LANGUAGE: Chinese 66 | PROMPT: 67 | IGNORE_PATTERNS: /node_modules,*.md # Regex pattern to ignore files, separated by comma 68 | ``` 69 | 70 | ## 自托管 71 | 72 | 1. 克隆代码 73 | 2. 复制`.env.example`到`.env`, 并填写环境变量 74 | 3. 安装 deps 并运行 75 | 76 | ```sh 77 | npm i 78 | npm -i g pm2 79 | npm run build 80 | pm2 start pm2.config.cjs 81 | ``` 82 | 83 | [机器人](https://probot.github.io/docs/development/)了解更多详情 84 | 85 | ## 开发 86 | 87 | ### 设置 88 | 89 | ```sh 90 | # Install dependencies 91 | npm install 92 | 93 | # Run the bot 94 | npm start 95 | ``` 96 | 97 | ### Docker 98 | 99 | ```sh 100 | # 1. Build container 101 | docker build -t cr-bot . 102 | 103 | # 2. Start container 104 | docker run -e APP_ID= -e PRIVATE_KEY= cr-bot 105 | ``` 106 | 107 | ## 贡献 108 | 109 | 如果您对如何改进 cr-bot 有建议,或者想报告错误,请打开一个问题!我们会喜欢所有的贡献。 110 | 111 | 有关更多信息,请查看[投稿指南](CONTRIBUTING.md). 112 | 113 | ## 灵感 114 | 115 | 这个项目的灵感来自[代码审查.gpt](https://github.com/sturdy-dev/codereview.gpt) 116 | 117 | ## License 118 | 119 | [ISC](LICENSE)© 2023 anc95 120 | -------------------------------------------------------------------------------- /README.ko.md: -------------------------------------------------------------------------------- 1 | # CodeReview BOT 2 | 3 | > cr bot은 ChatGPT를 활용한 코드리뷰 로봇입니다. 4 | 5 | Translation Versions: [ENGLISH](./README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [日本語](./README.ja.md) 6 | 7 | ## 사용법 8 | 9 | ❗️⚠️비용을 고려하여 BOT은 테스트 목적으로만 사용되며, 현재 AWS Lambda에 배포되어 속도 제한을 받고 있습니다. 따라서 불안정한 상황은 완전히 정상적입니다. 응용 프로그램을 직접 배포하는 것이 좋습니다. 10 | 11 | ### 설치 12 | 13 | 설치: [apps/cr-gpt](https://github.com/apps/cr-gpt); 14 | 15 | ### 설정 16 | 17 | 1. cr bot을 적용할 레포지토리 홈페이지로 이동합니다. 18 | 2. `settings` 클릭 19 | 3. `secrets and variables` 메뉴 밑의 `actions` 를 클릭 20 | 4. `Variables` 탭으로 변경합니다, `New repository variable` 버튼을 눌러서 새로운 `OPENAI_API_KEY` 변수를 생성합니다. 변수의 값으로 당신의 open api key 를 입력합니다. (OpenAI 홈페이지에서 api 키를 받을 수 있습니다.) 21 | image 22 | 23 | ### 사용 시작하기 24 | 25 | 1. 새로운 Pull request를 생성하면 로봇이 자동으로 코드 리뷰를 수행하며, 리뷰 정보는 Pull request 타임라인 / 파일 변경 부분에 표시됩니다. 26 | 2. `git push` 이후에 Pull request를 업데이트하면, cr bot은 변경된 파일을 다시 검토합니다. 27 | 28 | 예시: 29 | 30 | [ChatGPT-CodeReview/pull/21](https://github.com/anc95/ChatGPT-CodeReview/pull/21) 31 | 32 | image 33 | 34 | ### Github Actions 사용하기 35 | 36 | > 깃허브 봇이 humble vps에서 서비스되므로, 항상 안정적인 상태임을 보장할 수 없기 때문에 이 방법을 권장합니다. 37 | 38 | [actions/chatgpt-codereviewer](https://github.com/marketplace/actions/chatgpt-codereviewer) 39 | 40 | 1. `OPENAI_API_KEY` 를 당신의 github actions secrets 에 추가합니다. 41 | 2. `.github/workflows/cr.yml` 를 생성하고, 아래의 내용을 추가합니다. 42 | 43 | ```yml 44 | name: Code Review 45 | 46 | permissions: 47 | contents: read 48 | pull-requests: write 49 | 50 | on: 51 | pull_request: 52 | types: [opened, reopened, synchronize] 53 | 54 | jobs: 55 | test: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: anc95/ChatGPT-CodeReview@main 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 62 | # optional 63 | LANGUAGE: Korean 64 | PROMPT: 65 | IGNORE_PATTERNS: /node_modules,*.md # Regex pattern to ignore files, separated by comma 66 | ``` 67 | 68 | ## Self-hosting 69 | 70 | 1. 코드를 clone 합니다. 71 | 2. `.env.example` 을 `.env`로 복제하고, 환경변수(env variable)을 입력합니다. 72 | 3. 종속성(deps)들을 설치하고 실행합니다. 73 | 74 | ```sh 75 | npm i 76 | npm -i g pm2 77 | npm run build 78 | pm2 start pm2.config.cjs 79 | ``` 80 | 81 | [probot](https://probot.github.io/docs/development/) 더 자세한 정보 82 | 83 | ## Dev 84 | 85 | ### 설정 86 | 87 | ```sh 88 | # 종속성 설치 89 | npm install 90 | 91 | # 봇 실행 92 | npm start 93 | ``` 94 | 95 | ### Docker 96 | 97 | ```sh 98 | # 1. 컨테이너 빌드 99 | docker build -t cr-bot . 100 | 101 | # 2. 컨테이너 시작 102 | docker run -e APP_ID= -e PRIVATE_KEY= cr-bot 103 | ``` 104 | 105 | ## 기여하기 106 | 107 | 만약 당신이 cr-bot의 개선 제안이나 버그 신고가 있으면 issue를 열어주세요! 모든 당신의 기여를 환영합니다. 108 | 109 | 자세한 내용은 [기여 가이드](CONTRIBUTING.md)를 확인하세요. 110 | 111 | ## Credit 112 | 113 | 이 프로젝트는 [codereview.gpt](https://github.com/sturdy-dev/codereview.gpt)에서 영감을 얻었습니다. 114 | 115 | ## License 116 | 117 | [ISC](LICENSE) © 2023 anc95 118 | -------------------------------------------------------------------------------- /src/chat.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI, AzureOpenAI } from 'openai'; 2 | 3 | export class Chat { 4 | private openai: OpenAI | AzureOpenAI; 5 | private isAzure: boolean; 6 | private isGithubModels: boolean; 7 | 8 | constructor(apikey: string) { 9 | this.isAzure = Boolean( 10 | process.env.AZURE_API_VERSION && process.env.AZURE_DEPLOYMENT, 11 | ); 12 | 13 | this.isGithubModels = process.env.USE_GITHUB_MODELS === 'true'; 14 | 15 | if (this.isAzure) { 16 | // Azure OpenAI configuration 17 | this.openai = new AzureOpenAI({ 18 | apiKey: apikey, 19 | endpoint: process.env.OPENAI_API_ENDPOINT || '', 20 | apiVersion: process.env.AZURE_API_VERSION || '', 21 | deployment: process.env.AZURE_DEPLOYMENT || '', 22 | }); 23 | } else { 24 | // Standard OpenAI configuration 25 | this.openai = new OpenAI({ 26 | apiKey: apikey, 27 | baseURL: this.isGithubModels ? 'https://models.github.ai/inference' : process.env.OPENAI_API_ENDPOINT || 'https://api.openai.com/v1', 28 | }); 29 | } 30 | } 31 | 32 | private generatePrompt = (patch: string) => { 33 | const answerLanguage = process.env.LANGUAGE 34 | ? `Answer me in ${process.env.LANGUAGE},` 35 | : ''; 36 | 37 | const userPrompt = process.env.PROMPT || 'Please review the following code patch. Focus on potential bugs, risks, and improvement suggestions.'; 38 | 39 | const jsonFormatRequirement = '\nProvide your feedback in a strict JSON format with the following structure:\n' + 40 | '{\n' + 41 | ' "lgtm": boolean, // true if the code looks good to merge, false if there are concerns\n' + 42 | ' "review_comment": string // Your detailed review comments. You can use markdown syntax in this string, but the overall response must be a valid JSON\n' + 43 | '}\n' + 44 | 'Ensure your response is a valid JSON object.\n'; 45 | 46 | return `${userPrompt}${jsonFormatRequirement} ${answerLanguage}: 47 | ${patch} 48 | `; 49 | }; 50 | 51 | public codeReview = async (patch: string): Promise<{ lgtm: boolean, review_comment: string }> => { 52 | if (!patch) { 53 | return { 54 | lgtm: true, 55 | review_comment: "" 56 | }; 57 | } 58 | 59 | console.time('code-review cost'); 60 | const prompt = this.generatePrompt(patch); 61 | 62 | const res = await this.openai.chat.completions.create({ 63 | messages: [ 64 | { 65 | role: 'user', 66 | content: prompt, 67 | }, 68 | ], 69 | model: process.env.MODEL || (this.isGithubModels ? 'openai/gpt-4o-mini' : 'gpt-4o-mini'), 70 | temperature: +(process.env.temperature || 0) || 1, 71 | top_p: +(process.env.top_p || 0) || 1, 72 | max_tokens: process.env.max_tokens ? +process.env.max_tokens : undefined, 73 | response_format: { 74 | type: "json_object" 75 | }, 76 | }); 77 | 78 | console.timeEnd('code-review cost'); 79 | 80 | if (res.choices.length) { 81 | try { 82 | const json = JSON.parse(res.choices[0].message.content || ""); 83 | return json 84 | } catch (e) { 85 | return { 86 | lgtm: false, 87 | review_comment: res.choices[0].message.content || "" 88 | } 89 | } 90 | } 91 | 92 | return { 93 | lgtm: true, 94 | review_comment: "" 95 | } 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # CodeReview BOT 2 | 3 | > A code review robot powered by ChatGPT 4 | 5 | Translation Versions: [ENGLISH](./README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [日本語](./README.ja.md) 6 | 7 | ## Usage 8 | 9 | ❗️⚠️ コストを考慮して BOT はテスト目的でのみ使用され、現在 AWS Lambda に展開されて速度制限を受けています。そのため、不安定な状況は完全に正常です。アプリケーションを直接展開することをお勧めします。 10 | 11 | ## Install 12 | 13 | Install: [apps/cr-gpt](https://github.com/apps/cr-gpt); 14 | 15 | ### Configuration 16 | 17 | 1. リポジトリのホームページに移動します 18 | 2. `settings` をクリックします 19 | 3. `secrets and variables` メニューの下の `actions` をクリックします 20 | 4. `New repository variable` をクリックして OpenAI の API キーの登録を行います。変数名は `OPENAI_API_KEY` にしてください。変数の値には OpenAI の API キーを入力します。 (OpenAI のホームページから API キーを取得できます。) 21 | image 22 | 23 | ### Start using 24 | 25 | 1. この bot は新しいプルリクエストが作成されたときに自動的にコードレビューを行います。レビュー結果はプルリクエストのタイムラインやファイル変更部分に表示されます。 26 | 2. `git push` によりプルリクエストの更新が行われたときにも自動的にコードレビューを行います。 27 | 28 | example: 29 | 30 | [ChatGPT-CodeReview/pull/21](https://github.com/anc95/ChatGPT-CodeReview/pull/21) 31 | 32 | image 33 | 34 | ### Using Github Actions 35 | 36 | > 基本的には、Github Actions での利用を推奨します。 37 | 38 | [actions/chatgpt-codereviewer](https://github.com/marketplace/actions/chatgpt-codereviewer) 39 | 40 | 1. `OPENAI_API_KEY` を設定する 41 | 2. 以下の例のように `.github/workflows/cr.yml` を作成する 42 | 43 | ```yml 44 | name: Code Review 45 | 46 | permissions: 47 | contents: read 48 | pull-requests: write 49 | 50 | on: 51 | pull_request: 52 | types: [opened, reopened, synchronize] 53 | 54 | jobs: 55 | test: 56 | if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: anc95/ChatGPT-CodeReview@main 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 63 | # Optional 64 | LANGUAGE: Chinese 65 | MODEL: 66 | PROMPT: 67 | top_p: 1 68 | temperature: 1 69 | IGNORE_PATTERNS: /node_modules,*.md # Regex pattern to ignore files, separated by comma 70 | ``` 71 | 72 | ## Self-hosting 73 | 74 | 1. このリポジトリをクローンします 75 | 2. `.env.example` を `.env` にリネームし、必要な環境変数を設定します 76 | 3. 以下のコマンドを順番に実行することで依存関係をインストールし、bot を起動します 77 | 78 | ```sh 79 | npm i 80 | npm -i g pm2 81 | npm run build 82 | pm2 start pm2.config.cjs 83 | ``` 84 | 85 | 詳細は [probot](https://probot.github.io/docs/development/) を参照してください。 86 | 87 | ## Dev 88 | 89 | ### Setup 90 | 91 | ```sh 92 | # Install dependencies 93 | npm install 94 | 95 | # Run the bot 96 | npm start 97 | ``` 98 | 99 | ### Docker 100 | 101 | ```sh 102 | # 1. Build container 103 | docker build -t cr-bot . 104 | 105 | # 2. Start container 106 | docker run -e APP_ID= -e PRIVATE_KEY= cr-bot 107 | ``` 108 | 109 | ## Contributing 110 | 111 | cr-bot の改善に関する提案やバグ報告は、issue を作成してください。どのような貢献でも歓迎します!! 112 | 113 | より詳しい情報は [Contributing Guide](CONTRIBUTING.md) を参照してください。 114 | 115 | ## Credit 116 | 117 | this project is inpired by [codereview.gpt](https://github.com/sturdy-dev/codereview.gpt) 118 | 119 | ## License 120 | 121 | [ISC](LICENSE) © 2023 anc95 122 | -------------------------------------------------------------------------------- /README.zh-TW.md: -------------------------------------------------------------------------------- 1 | # 程式碼審查機器人 2 | 3 | > 由 ChatGPT 提供支援的程式碼審查機器人 4 | 5 | 翻譯版本:[English](./README.md) | [簡體中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [日本語](./README.ja.md) 6 | 7 | ## 機器人使用方式 8 | 9 | ❗️⚠️ `由於成本考量,BOT 目前僅用於測試目的,並部署在有限制的 AWS Lambda 上。因此,不穩定的情況是完全正常的。建議自行部署 app。` 10 | 11 | ### 安裝 12 | 13 | 安裝:[apps/cr-gpt](https://github.com/apps/cr-gpt); 14 | 15 | ### 設定 16 | 17 | 1. 轉到你要整合此機器人的倉庫首頁 18 | 2. 點選 `settings` 19 | 3. 點選 `actions` 在下面的 `secrets and variables` 20 | 4. 切換到 `Variables` 選項,建立一個新變數 `OPENAI_API_KEY`,值為你的 open api key (如果是 Github Action 整合,則設定在 secrets 中) 21 | image 22 | 23 | ### 開始使用 24 | 25 | 1. 當你建立一個新的 Pull request 時,機器人會自動進行程式碼審查,審查訊息將顯示在 pr timeline / file changes 部分。 26 | 2. 在 `git push` 更新 Pull request 之後,cr bot 將重新審查更改的文件 27 | 28 | 範例: 29 | 30 | [ChatGPT-CodeReview/pull/21](https://github.com/anc95/ChatGPT-CodeReview/pull/21) 31 | 32 | image 33 | 34 | ## 使用 Github Actions 35 | 36 | [actions/chatgpt-codereviewer](https://github.com/marketplace/actions/chatgpt-codereviewer) 37 | 38 | 1. 新增 `OPENAI_API_KEY` 到你的 github actions secrets 39 | 2. 建立 `.github/workflows/cr.yml` 新增以下內容 40 | 41 | ```yml 42 | name: Code Review 43 | 44 | permissions: 45 | contents: read 46 | pull-requests: write 47 | 48 | on: 49 | pull_request: 50 | types: [opened, reopened, synchronize] 51 | 52 | jobs: 53 | test: 54 | # if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: anc95/ChatGPT-CodeReview@main 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 61 | # Optional 62 | LANGUAGE: Chinese 63 | OPENAI_API_ENDPOINT: https://api.openai.com/v1 64 | MODEL: gpt-3.5-turbo # https://platform.openai.com/docs/models 65 | PROMPT: # example: Please check if there are any confusions or irregularities in the following code diff: 66 | top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p 67 | temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature 68 | max_tokens: 10000 69 | MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length. 70 | IGNORE_PATTERNS: /node_modules,*.md # Regex pattern to ignore files, separated by comma 71 | ``` 72 | 73 | ## 自我託管 74 | 75 | 1. 複製程式碼 76 | 2. 複製 `.env.example` 到 `.env`, 並填寫環境變數 77 | 3. 安裝相依性並執行 78 | 79 | ```sh 80 | npm i 81 | npm i -g pm2 82 | npm run build 83 | pm2 start pm2.config.cjs 84 | ``` 85 | 86 | [probot](https://probot.github.io/docs/development/) 了解更多詳情 87 | 88 | ## 開發 89 | 90 | ### 設定 91 | 92 | ```sh 93 | # Install dependencies 94 | npm install 95 | 96 | # Build code 97 | npm run build 98 | 99 | # Run the bot 100 | npm run start 101 | ``` 102 | 103 | ### Docker 104 | 105 | ```sh 106 | # 1. Build container 107 | docker build -t cr-bot . 108 | 109 | # 2. Start container 110 | docker run -e APP_ID= -e PRIVATE_KEY= cr-bot 111 | ``` 112 | 113 | ## 貢獻 114 | 115 | 如果您對如何改進 cr-bot 有建議,或者想報告錯誤,請開啟一個問題!我們喜歡所有的貢獻。 116 | 117 | 有關更多信息,請查看[貢獻指南](CONTRIBUTING.md). 118 | 119 | ## 靈感 120 | 121 | 這個項目的靈感來自[codereview.gpt](https://github.com/sturdy-dev/codereview.gpt) 122 | 123 | ## License 124 | 125 | [ISC](LICENSE)© 2023 anc95 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeReview BOT 2 | 3 | > A code review robot powered by ChatGPT 4 | 5 | Translation Versions: [ENGLISH](./README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [日本語](./README.ja.md) 6 | 7 | ## Bot Usage 8 | 9 | ❗️⚠️ `Due to cost considerations, BOT is only used for testing purposes and is currently deployed on AWS Lambda with ratelimit restrictions. Therefore, unstable situations are completely normal. It is recommended to deploy an app by yourself.` 10 | 11 | ### Install 12 | 13 | Install: [apps/cr-gpt](https://github.com/apps/cr-gpt); 14 | 15 | ### Configuration 16 | 17 | 1. Go to the repo homepage which you want integrate this bot 18 | 2. click `settings` 19 | 3. click `actions` under `secrets and variables` 20 | 4. Change to `Variables` tab, create a new variable `OPENAI_API_KEY` with the value of your open api key (For Github Action integration, set it in secrets) 21 | image 22 | 23 | ### Start using 24 | 25 | 1. The robot will automatically do the code review when you create a new Pull request, the review information will show in the pr timeline / file changes part. 26 | 2. After `git push` update the pull request, cr bot will re-review the changed files 27 | 28 | example: 29 | 30 | [ChatGPT-CodeReview/pull/21](https://github.com/anc95/ChatGPT-CodeReview/pull/21) 31 | 32 | image 33 | 34 | ## Using Github Actions 35 | 36 | [actions/chatgpt-codereviewer](https://github.com/marketplace/actions/chatgpt-codereviewer) 37 | 38 | 1. add the `OPENAI_API_KEY` to your github actions secrets 39 | 2. create `.github/workflows/cr.yml` add bellow content 40 | 41 | ```yml 42 | name: Code Review 43 | 44 | permissions: 45 | contents: read 46 | pull-requests: write 47 | models: true # if you choose use github models, set this to be true 48 | 49 | on: 50 | pull_request: 51 | types: [opened, reopened, synchronize] 52 | 53 | jobs: 54 | test: 55 | # if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: anc95/ChatGPT-CodeReview@main 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | # if use github models https://github.com/marketplace/models 63 | USE_GITHUB_MODELS: true 64 | MODEL: openai/gpt-4o 65 | 66 | # else if use azure deployment 67 | AZURE_API_VERSION: xx 68 | AZURE_DEPLOYMENT: xx 69 | 70 | # else use standard llm model 71 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 72 | OPENAI_API_ENDPOINT: https://api.openai.com/v1 73 | MODEL: gpt-3.5-turbo # https://platform.openai.com/docs/models 74 | 75 | # common 76 | LANGUAGE: Chinese 77 | PROMPT: # example: Please check if there are any confusions or irregularities in the following code diff: 78 | top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p 79 | temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature 80 | max_tokens: 10000 81 | MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length. 82 | IGNORE_PATTERNS: /node_modules/**/*,*.md # glob pattern or regex pattern to ignore files, separated by comma 83 | INCLUDE_PATTERNS: *.js,*.ts # glob pattern or regex pattern to include files, separated by comma 84 | ``` 85 | 86 | ## Self-hosting 87 | 88 | 1. clone code 89 | 2. copy `.env.example` to `.env`, and fill the env variables 90 | 3. install deps and run 91 | 92 | ```sh 93 | npm i 94 | npm i -g pm2 95 | npm run build 96 | pm2 start pm2.config.cjs 97 | ``` 98 | 99 | [probot](https://probot.github.io/docs/development/) for more detail 100 | 101 | ## Dev 102 | 103 | ### Setup 104 | 105 | ```sh 106 | # Install dependencies 107 | npm install 108 | 109 | # Build code 110 | npm run build 111 | 112 | # Run the bot 113 | npm run start 114 | ``` 115 | 116 | ### Docker 117 | 118 | ```sh 119 | # 1. Build container 120 | docker build -t cr-bot . 121 | 122 | # 2. Start container 123 | docker run -e APP_ID= -e PRIVATE_KEY= cr-bot 124 | ``` 125 | 126 | ## Contributing 127 | 128 | If you have suggestions for how cr-bot could be improved, or want to report a bug, open an issue! We'd love all and any contributions. 129 | 130 | For more, check out the [Contributing Guide](CONTRIBUTING.md). 131 | 132 | ## Credit 133 | 134 | this project is inpired by [codereview.gpt](https://github.com/sturdy-dev/codereview.gpt) 135 | 136 | ## License 137 | 138 | [ISC](LICENSE) © 2023 anc95 139 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | # This is a GitHub App Manifest. These settings will be used by default when 2 | # initially configuring your GitHub App. 3 | # 4 | # NOTE: changing this file will not update your GitHub App settings. 5 | # You must visit github.com/settings/apps/your-app-name to edit them. 6 | # 7 | # Read more about configuring your GitHub App: 8 | # https://probot.github.io/docs/development/#configuring-a-github-app 9 | # 10 | # Read more about GitHub App Manifests: 11 | # https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/ 12 | 13 | # The list of events the GitHub App subscribes to. 14 | # Uncomment the event names below to enable them. 15 | default_events: 16 | # - check_run 17 | # - check_suite 18 | # - commit_comment 19 | # - create 20 | # - delete 21 | # - deployment 22 | # - deployment_status 23 | # - fork 24 | # - gollum 25 | # - issue_comment 26 | - issues 27 | # - label 28 | # - milestone 29 | # - member 30 | # - membership 31 | # - org_block 32 | # - organization 33 | # - page_build 34 | # - project 35 | # - project_card 36 | # - project_column 37 | # - public 38 | # - pull_request 39 | # - pull_request_review 40 | # - pull_request_review_comment 41 | # - push 42 | # - release 43 | # - repository 44 | # - repository_import 45 | # - status 46 | # - team 47 | # - team_add 48 | # - watch 49 | 50 | # The set of permissions needed by the GitHub App. The format of the object uses 51 | # the permission name for the key (for example, issues) and the access type for 52 | # the value (for example, write). 53 | # Valid values are `read`, `write`, and `none` 54 | default_permissions: 55 | # Repository creation, deletion, settings, teams, and collaborators. 56 | # https://developer.github.com/v3/apps/permissions/#permission-on-administration 57 | # administration: read 58 | 59 | # Checks on code. 60 | # https://developer.github.com/v3/apps/permissions/#permission-on-checks 61 | # checks: read 62 | 63 | # Repository contents, commits, branches, downloads, releases, and merges. 64 | # https://developer.github.com/v3/apps/permissions/#permission-on-contents 65 | # contents: read 66 | 67 | # Deployments and deployment statuses. 68 | # https://developer.github.com/v3/apps/permissions/#permission-on-deployments 69 | # deployments: read 70 | 71 | # Issues and related comments, assignees, labels, and milestones. 72 | # https://developer.github.com/v3/apps/permissions/#permission-on-issues 73 | issues: write 74 | 75 | # Search repositories, list collaborators, and access repository metadata. 76 | # https://developer.github.com/v3/apps/permissions/#metadata-permissions 77 | metadata: read 78 | 79 | # Retrieve Pages statuses, configuration, and builds, as well as create new builds. 80 | # https://developer.github.com/v3/apps/permissions/#permission-on-pages 81 | # pages: read 82 | 83 | # Pull requests and related comments, assignees, labels, milestones, and merges. 84 | # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests 85 | # pull_requests: read 86 | 87 | # Manage the post-receive hooks for a repository. 88 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks 89 | # repository_hooks: read 90 | 91 | # Manage repository projects, columns, and cards. 92 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects 93 | # repository_projects: read 94 | 95 | # Retrieve security vulnerability alerts. 96 | # https://developer.github.com/v4/object/repositoryvulnerabilityalert/ 97 | # vulnerability_alerts: read 98 | 99 | # Commit statuses. 100 | # https://developer.github.com/v3/apps/permissions/#permission-on-statuses 101 | # statuses: read 102 | 103 | # Organization members and teams. 104 | # https://developer.github.com/v3/apps/permissions/#permission-on-members 105 | # members: read 106 | 107 | # View and manage users blocked by the organization. 108 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking 109 | # organization_user_blocking: read 110 | 111 | # Manage organization projects, columns, and cards. 112 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects 113 | # organization_projects: read 114 | 115 | # Manage team discussions and related comments. 116 | # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions 117 | # team_discussions: read 118 | 119 | # Manage the post-receive hooks for an organization. 120 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks 121 | # organization_hooks: read 122 | 123 | # Get notified of, and update, content references. 124 | # https://developer.github.com/v3/apps/permissions/ 125 | # organization_administration: read 126 | # The name of the GitHub App. Defaults to the name specified in package.json 127 | # name: My Probot App 128 | 129 | # The homepage of your GitHub App. 130 | # url: https://example.com/ 131 | 132 | # A description of the GitHub App. 133 | # description: A description of my awesome app 134 | 135 | # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app. 136 | # Default: true 137 | # public: false 138 | -------------------------------------------------------------------------------- /action/worker1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { realImport, realRequire } = require('real-require') 4 | const { workerData, parentPort } = require('worker_threads') 5 | const { WRITE_INDEX, READ_INDEX } = require('./indexes') 6 | const { waitDiff } = require('./wait') 7 | 8 | const { 9 | dataBuf, 10 | filename, 11 | stateBuf 12 | } = workerData 13 | 14 | let destination 15 | 16 | const state = new Int32Array(stateBuf) 17 | const data = Buffer.from(dataBuf) 18 | 19 | async function start () { 20 | let worker 21 | try { 22 | if (filename.endsWith('.ts') || filename.endsWith('.cts')) { 23 | // TODO: add support for the TSM modules loader ( https://github.com/lukeed/tsm ). 24 | if (!process[Symbol.for('ts-node.register.instance')]) { 25 | realRequire('ts-node/register') 26 | } else if (process.env.TS_NODE_DEV) { 27 | realRequire('ts-node-dev') 28 | } 29 | // TODO: Support ES imports once tsc, tap & ts-node provide better compatibility guarantees. 30 | // Remove extra forwardslash on Windows 31 | worker = realRequire(decodeURIComponent(filename.replace(process.platform === 'win32' ? 'file:///' : 'file://', ''))) 32 | } else { 33 | worker = (await realImport(filename)) 34 | } 35 | } catch (error) { 36 | // A yarn user that tries to start a ThreadStream for an external module 37 | // provides a filename pointing to a zip file. 38 | // eg. require.resolve('pino-elasticsearch') // returns /foo/pino-elasticsearch-npm-6.1.0-0c03079478-6915435172.zip/bar.js 39 | // The `import` will fail to try to load it. 40 | // This catch block executes the `require` fallback to load the module correctly. 41 | // In fact, yarn modifies the `require` function to manage the zipped path. 42 | // More details at https://github.com/pinojs/pino/pull/1113 43 | // The error codes may change based on the node.js version (ENOTDIR > 12, ERR_MODULE_NOT_FOUND <= 12 ) 44 | if ((error.code === 'ENOTDIR' || error.code === 'ERR_MODULE_NOT_FOUND') && 45 | filename.startsWith('file://')) { 46 | worker = realRequire(decodeURIComponent(filename.replace('file://', ''))) 47 | } else if (error.code === undefined) { 48 | // When bundled with pkg, an undefined error is thrown when called with realImport 49 | worker = realRequire(decodeURIComponent(filename.replace(process.platform === 'win32' ? 'file:///' : 'file://', ''))) 50 | } else { 51 | throw error 52 | } 53 | } 54 | 55 | // Depending on how the default export is performed, and on how the code is 56 | // transpiled, we may find cases of two nested "default" objects. 57 | // See https://github.com/pinojs/pino/issues/1243#issuecomment-982774762 58 | if (typeof worker === 'object') worker = worker.default 59 | if (typeof worker === 'object') worker = worker.default 60 | 61 | destination = await worker(workerData.workerData) 62 | 63 | destination.on('error', function (err) { 64 | Atomics.store(state, WRITE_INDEX, -2) 65 | Atomics.notify(state, WRITE_INDEX) 66 | 67 | Atomics.store(state, READ_INDEX, -2) 68 | Atomics.notify(state, READ_INDEX) 69 | 70 | parentPort.postMessage({ 71 | code: 'ERROR', 72 | err 73 | }) 74 | }) 75 | 76 | destination.on('close', function () { 77 | // process._rawDebug('worker close emitted') 78 | const end = Atomics.load(state, WRITE_INDEX) 79 | Atomics.store(state, READ_INDEX, end) 80 | Atomics.notify(state, READ_INDEX) 81 | setImmediate(() => { 82 | process.exit(0) 83 | }) 84 | }) 85 | } 86 | 87 | // No .catch() handler, 88 | // in case there is an error it goes 89 | // to unhandledRejection 90 | start().then(function () { 91 | parentPort.postMessage({ 92 | code: 'READY' 93 | }) 94 | 95 | process.nextTick(run) 96 | }) 97 | 98 | function run () { 99 | const current = Atomics.load(state, READ_INDEX) 100 | const end = Atomics.load(state, WRITE_INDEX) 101 | 102 | // process._rawDebug(`pre state ${current} ${end}`) 103 | 104 | if (end === current) { 105 | if (end === data.length) { 106 | waitDiff(state, READ_INDEX, end, Infinity, run) 107 | } else { 108 | waitDiff(state, WRITE_INDEX, end, Infinity, run) 109 | } 110 | return 111 | } 112 | 113 | // process._rawDebug(`post state ${current} ${end}`) 114 | 115 | if (end === -1) { 116 | // process._rawDebug('end') 117 | destination.end() 118 | return 119 | } 120 | 121 | const toWrite = data.toString('utf8', current, end) 122 | // process._rawDebug('worker writing: ' + toWrite) 123 | 124 | const res = destination.write(toWrite) 125 | 126 | if (res) { 127 | Atomics.store(state, READ_INDEX, end) 128 | Atomics.notify(state, READ_INDEX) 129 | setImmediate(run) 130 | } else { 131 | destination.once('drain', function () { 132 | Atomics.store(state, READ_INDEX, end) 133 | Atomics.notify(state, READ_INDEX) 134 | run() 135 | }) 136 | } 137 | } 138 | 139 | process.on('unhandledRejection', function (err) { 140 | parentPort.postMessage({ 141 | code: 'ERROR', 142 | err 143 | }) 144 | process.exit(1) 145 | }) 146 | 147 | process.on('uncaughtException', function (err) { 148 | parentPort.postMessage({ 149 | code: 'ERROR', 150 | err 151 | }) 152 | process.exit(1) 153 | }) 154 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "incremental": true /* Enable incremental compilation */, 5 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": [ 8 | "es2015", 9 | "es2017" 10 | ] /* Specify library files to be included in the compilation. */, 11 | "allowJs": true /* Allow javascript files to be compiled. */, 12 | "checkJs": true /* Report errors in .js files. */, 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | "declaration": true /* Generates corresponding '.d.ts' file. */, 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | "sourceMap": true /* Generates corresponding '.map' file. */, 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./lib" /* Redirect output structure to the directory. */, 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "tsBuildInfoFile": "./" /* Specify file to store incremental compilation information */, 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | /* Additional Checks */ 37 | "noUnusedLocals": true /* Report errors on unused locals. */, 38 | "noUnusedParameters": true /* Report errors on unused parameters. */, 39 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 40 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./src" /* Base directory to resolve non-absolute module names. */, 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just type checking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | /* Advanced Options */ 61 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 62 | "resolveJsonModule": true, 63 | "pretty": false, 64 | "skipLibCheck": true, 65 | "allowSyntheticDefaultImports": true 66 | }, 67 | "include": [ 68 | "src/", 69 | "*.ts", 70 | ], 71 | "compileOnSave": false 72 | } -------------------------------------------------------------------------------- /src/bot.ts: -------------------------------------------------------------------------------- 1 | import { Context, Probot } from 'probot'; 2 | import { minimatch } from 'minimatch' 3 | 4 | import { Chat } from './chat.js'; 5 | import log from 'loglevel'; 6 | 7 | const OPENAI_API_KEY = 'OPENAI_API_KEY'; 8 | const MAX_PATCH_COUNT = process.env.MAX_PATCH_LENGTH 9 | ? +process.env.MAX_PATCH_LENGTH 10 | : Infinity; 11 | 12 | export const robot = (app: Probot) => { 13 | const loadChat = async (context: Context) => { 14 | if (process.env.USE_GITHUB_MODELS === 'true' && process.env.GITHUB_TOKEN) { 15 | return new Chat(process.env.GITHUB_TOKEN); 16 | } 17 | 18 | if (process.env.OPENAI_API_KEY) { 19 | return new Chat(process.env.OPENAI_API_KEY); 20 | } 21 | 22 | const repo = context.repo(); 23 | 24 | try { 25 | const { data } = (await context.octokit.request( 26 | 'GET /repos/{owner}/{repo}/actions/variables/{name}', 27 | { 28 | owner: repo.owner, 29 | repo: repo.repo, 30 | name: OPENAI_API_KEY, 31 | } 32 | )) as any; 33 | 34 | if (!data?.value) { 35 | return null; 36 | } 37 | 38 | return new Chat(data.value); 39 | } catch { 40 | await context.octokit.issues.createComment({ 41 | repo: repo.repo, 42 | owner: repo.owner, 43 | issue_number: context.pullRequest().pull_number, 44 | body: `Seems you are using me but didn't get OPENAI_API_KEY seted in Variables/Secrets for this repo. you could follow [readme](https://github.com/anc95/ChatGPT-CodeReview) for more information`, 45 | }); 46 | return null; 47 | } 48 | }; 49 | 50 | app.on( 51 | ['pull_request.opened', 'pull_request.synchronize'], 52 | async (context) => { 53 | const repo = context.repo(); 54 | const chat = await loadChat(context); 55 | 56 | if (!chat) { 57 | log.info('Chat initialized failed'); 58 | return 'no chat'; 59 | } 60 | 61 | const pull_request = context.payload.pull_request; 62 | 63 | log.debug('pull_request:', pull_request); 64 | 65 | if ( 66 | pull_request.state === 'closed' || 67 | pull_request.locked 68 | ) { 69 | log.info('invalid event payload'); 70 | return 'invalid event payload'; 71 | } 72 | 73 | const target_label = process.env.TARGET_LABEL; 74 | if ( 75 | target_label && 76 | (!pull_request.labels?.length || 77 | pull_request.labels.every((label) => label.name !== target_label)) 78 | ) { 79 | log.info('no target label attached'); 80 | return 'no target label attached'; 81 | } 82 | 83 | const data = await context.octokit.repos.compareCommits({ 84 | owner: repo.owner, 85 | repo: repo.repo, 86 | base: context.payload.pull_request.base.sha, 87 | head: context.payload.pull_request.head.sha, 88 | }); 89 | 90 | let { files: changedFiles, commits } = data.data; 91 | 92 | log.debug("compareCommits, base:", context.payload.pull_request.base.sha, "head:", context.payload.pull_request.head.sha) 93 | log.debug("compareCommits.commits:", commits) 94 | log.debug("compareCommits.files", changedFiles) 95 | 96 | if (context.payload.action === 'synchronize' && commits.length >= 2) { 97 | const { 98 | data: { files }, 99 | } = await context.octokit.repos.compareCommits({ 100 | owner: repo.owner, 101 | repo: repo.repo, 102 | base: commits[commits.length - 2].sha, 103 | head: commits[commits.length - 1].sha, 104 | }); 105 | 106 | changedFiles = files 107 | } 108 | 109 | const ignoreList = (process.env.IGNORE || process.env.ignore || '') 110 | .split('\n') 111 | .filter((v) => v !== ''); 112 | const ignorePatterns = (process.env.IGNORE_PATTERNS || '').split(',').filter((v) => Boolean(v.trim())); 113 | const includePatterns = (process.env.INCLUDE_PATTERNS || '').split(',').filter((v) => Boolean(v.trim())); 114 | 115 | log.debug('ignoreList:', ignoreList); 116 | log.debug('ignorePatterns:', ignorePatterns); 117 | log.debug('includePatterns:', includePatterns); 118 | 119 | changedFiles = changedFiles?.filter( 120 | (file) => { 121 | const url = new URL(file.contents_url) 122 | const pathname = decodeURIComponent(url.pathname) 123 | // if includePatterns is not empty, only include files that match the pattern 124 | if (includePatterns.length) { 125 | return matchPatterns(includePatterns, pathname) 126 | } 127 | 128 | if (ignoreList.includes(file.filename)) { 129 | return false; 130 | } 131 | 132 | // if ignorePatterns is not empty, ignore files that match the pattern 133 | if (ignorePatterns.length) { 134 | return !matchPatterns(ignorePatterns, pathname) 135 | } 136 | 137 | return true 138 | }) 139 | 140 | if (!changedFiles?.length) { 141 | log.info('no change found'); 142 | return 'no change'; 143 | } 144 | 145 | console.time('gpt cost'); 146 | 147 | const ress = []; 148 | 149 | for (let i = 0; i < changedFiles.length; i++) { 150 | const file = changedFiles[i]; 151 | const patch = file.patch || ''; 152 | 153 | if (file.status !== 'modified' && file.status !== 'added') { 154 | continue; 155 | } 156 | 157 | if (!patch || patch.length > MAX_PATCH_COUNT) { 158 | log.info( 159 | `${file.filename} skipped caused by its diff is too large` 160 | ); 161 | continue; 162 | } 163 | try { 164 | const res = await chat?.codeReview(patch); 165 | if (!res.lgtm && !!res.review_comment) { 166 | ress.push({ 167 | path: file.filename, 168 | body: res.review_comment, 169 | position: patch.split('\n').length - 1, 170 | }) 171 | } 172 | } catch (e) { 173 | log.info(`review ${file.filename} failed`, e); 174 | throw e; 175 | } 176 | } 177 | try { 178 | await context.octokit.pulls.createReview({ 179 | repo: repo.repo, 180 | owner: repo.owner, 181 | pull_number: context.pullRequest().pull_number, 182 | body: ress.length ? "Code review by ChatGPT" : "LGTM 👍", 183 | event: 'COMMENT', 184 | commit_id: commits[commits.length - 1].sha, 185 | comments: ress, 186 | }); 187 | } catch (e) { 188 | log.info(`Failed to create review`, e); 189 | throw e; 190 | } 191 | 192 | console.timeEnd('gpt cost'); 193 | log.info( 194 | 'successfully reviewed', 195 | context.payload.pull_request.html_url 196 | ); 197 | 198 | return 'success'; 199 | } 200 | ); 201 | }; 202 | 203 | const matchPatterns = (patterns: string[], path: string) => { 204 | return patterns.some((pattern) => { 205 | try { 206 | return minimatch(path, pattern.startsWith('/') ? "**" + pattern : pattern.startsWith("**") ? pattern : "**/" + pattern); 207 | } catch { 208 | // if the pattern is not a valid glob pattern, try to match it as a regular expression 209 | try { 210 | return new RegExp(pattern).test(path); 211 | } catch (e) { 212 | return false; 213 | } 214 | } 215 | }) 216 | } -------------------------------------------------------------------------------- /action/37.index.cjs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.id = 37; 3 | exports.ids = [37]; 4 | exports.modules = { 5 | 6 | /***/ 94037: 7 | /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { 8 | 9 | __webpack_require__.r(__webpack_exports__); 10 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 11 | /* harmony export */ "toFormData": () => (/* binding */ toFormData) 12 | /* harmony export */ }); 13 | /* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7972); 14 | /* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(68010); 15 | 16 | 17 | 18 | let s = 0; 19 | const S = { 20 | START_BOUNDARY: s++, 21 | HEADER_FIELD_START: s++, 22 | HEADER_FIELD: s++, 23 | HEADER_VALUE_START: s++, 24 | HEADER_VALUE: s++, 25 | HEADER_VALUE_ALMOST_DONE: s++, 26 | HEADERS_ALMOST_DONE: s++, 27 | PART_DATA_START: s++, 28 | PART_DATA: s++, 29 | END: s++ 30 | }; 31 | 32 | let f = 1; 33 | const F = { 34 | PART_BOUNDARY: f, 35 | LAST_BOUNDARY: f *= 2 36 | }; 37 | 38 | const LF = 10; 39 | const CR = 13; 40 | const SPACE = 32; 41 | const HYPHEN = 45; 42 | const COLON = 58; 43 | const A = 97; 44 | const Z = 122; 45 | 46 | const lower = c => c | 0x20; 47 | 48 | const noop = () => {}; 49 | 50 | class MultipartParser { 51 | /** 52 | * @param {string} boundary 53 | */ 54 | constructor(boundary) { 55 | this.index = 0; 56 | this.flags = 0; 57 | 58 | this.onHeaderEnd = noop; 59 | this.onHeaderField = noop; 60 | this.onHeadersEnd = noop; 61 | this.onHeaderValue = noop; 62 | this.onPartBegin = noop; 63 | this.onPartData = noop; 64 | this.onPartEnd = noop; 65 | 66 | this.boundaryChars = {}; 67 | 68 | boundary = '\r\n--' + boundary; 69 | const ui8a = new Uint8Array(boundary.length); 70 | for (let i = 0; i < boundary.length; i++) { 71 | ui8a[i] = boundary.charCodeAt(i); 72 | this.boundaryChars[ui8a[i]] = true; 73 | } 74 | 75 | this.boundary = ui8a; 76 | this.lookbehind = new Uint8Array(this.boundary.length + 8); 77 | this.state = S.START_BOUNDARY; 78 | } 79 | 80 | /** 81 | * @param {Uint8Array} data 82 | */ 83 | write(data) { 84 | let i = 0; 85 | const length_ = data.length; 86 | let previousIndex = this.index; 87 | let {lookbehind, boundary, boundaryChars, index, state, flags} = this; 88 | const boundaryLength = this.boundary.length; 89 | const boundaryEnd = boundaryLength - 1; 90 | const bufferLength = data.length; 91 | let c; 92 | let cl; 93 | 94 | const mark = name => { 95 | this[name + 'Mark'] = i; 96 | }; 97 | 98 | const clear = name => { 99 | delete this[name + 'Mark']; 100 | }; 101 | 102 | const callback = (callbackSymbol, start, end, ui8a) => { 103 | if (start === undefined || start !== end) { 104 | this[callbackSymbol](ui8a && ui8a.subarray(start, end)); 105 | } 106 | }; 107 | 108 | const dataCallback = (name, clear) => { 109 | const markSymbol = name + 'Mark'; 110 | if (!(markSymbol in this)) { 111 | return; 112 | } 113 | 114 | if (clear) { 115 | callback(name, this[markSymbol], i, data); 116 | delete this[markSymbol]; 117 | } else { 118 | callback(name, this[markSymbol], data.length, data); 119 | this[markSymbol] = 0; 120 | } 121 | }; 122 | 123 | for (i = 0; i < length_; i++) { 124 | c = data[i]; 125 | 126 | switch (state) { 127 | case S.START_BOUNDARY: 128 | if (index === boundary.length - 2) { 129 | if (c === HYPHEN) { 130 | flags |= F.LAST_BOUNDARY; 131 | } else if (c !== CR) { 132 | return; 133 | } 134 | 135 | index++; 136 | break; 137 | } else if (index - 1 === boundary.length - 2) { 138 | if (flags & F.LAST_BOUNDARY && c === HYPHEN) { 139 | state = S.END; 140 | flags = 0; 141 | } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { 142 | index = 0; 143 | callback('onPartBegin'); 144 | state = S.HEADER_FIELD_START; 145 | } else { 146 | return; 147 | } 148 | 149 | break; 150 | } 151 | 152 | if (c !== boundary[index + 2]) { 153 | index = -2; 154 | } 155 | 156 | if (c === boundary[index + 2]) { 157 | index++; 158 | } 159 | 160 | break; 161 | case S.HEADER_FIELD_START: 162 | state = S.HEADER_FIELD; 163 | mark('onHeaderField'); 164 | index = 0; 165 | // falls through 166 | case S.HEADER_FIELD: 167 | if (c === CR) { 168 | clear('onHeaderField'); 169 | state = S.HEADERS_ALMOST_DONE; 170 | break; 171 | } 172 | 173 | index++; 174 | if (c === HYPHEN) { 175 | break; 176 | } 177 | 178 | if (c === COLON) { 179 | if (index === 1) { 180 | // empty header field 181 | return; 182 | } 183 | 184 | dataCallback('onHeaderField', true); 185 | state = S.HEADER_VALUE_START; 186 | break; 187 | } 188 | 189 | cl = lower(c); 190 | if (cl < A || cl > Z) { 191 | return; 192 | } 193 | 194 | break; 195 | case S.HEADER_VALUE_START: 196 | if (c === SPACE) { 197 | break; 198 | } 199 | 200 | mark('onHeaderValue'); 201 | state = S.HEADER_VALUE; 202 | // falls through 203 | case S.HEADER_VALUE: 204 | if (c === CR) { 205 | dataCallback('onHeaderValue', true); 206 | callback('onHeaderEnd'); 207 | state = S.HEADER_VALUE_ALMOST_DONE; 208 | } 209 | 210 | break; 211 | case S.HEADER_VALUE_ALMOST_DONE: 212 | if (c !== LF) { 213 | return; 214 | } 215 | 216 | state = S.HEADER_FIELD_START; 217 | break; 218 | case S.HEADERS_ALMOST_DONE: 219 | if (c !== LF) { 220 | return; 221 | } 222 | 223 | callback('onHeadersEnd'); 224 | state = S.PART_DATA_START; 225 | break; 226 | case S.PART_DATA_START: 227 | state = S.PART_DATA; 228 | mark('onPartData'); 229 | // falls through 230 | case S.PART_DATA: 231 | previousIndex = index; 232 | 233 | if (index === 0) { 234 | // boyer-moore derrived algorithm to safely skip non-boundary data 235 | i += boundaryEnd; 236 | while (i < bufferLength && !(data[i] in boundaryChars)) { 237 | i += boundaryLength; 238 | } 239 | 240 | i -= boundaryEnd; 241 | c = data[i]; 242 | } 243 | 244 | if (index < boundary.length) { 245 | if (boundary[index] === c) { 246 | if (index === 0) { 247 | dataCallback('onPartData', true); 248 | } 249 | 250 | index++; 251 | } else { 252 | index = 0; 253 | } 254 | } else if (index === boundary.length) { 255 | index++; 256 | if (c === CR) { 257 | // CR = part boundary 258 | flags |= F.PART_BOUNDARY; 259 | } else if (c === HYPHEN) { 260 | // HYPHEN = end boundary 261 | flags |= F.LAST_BOUNDARY; 262 | } else { 263 | index = 0; 264 | } 265 | } else if (index - 1 === boundary.length) { 266 | if (flags & F.PART_BOUNDARY) { 267 | index = 0; 268 | if (c === LF) { 269 | // unset the PART_BOUNDARY flag 270 | flags &= ~F.PART_BOUNDARY; 271 | callback('onPartEnd'); 272 | callback('onPartBegin'); 273 | state = S.HEADER_FIELD_START; 274 | break; 275 | } 276 | } else if (flags & F.LAST_BOUNDARY) { 277 | if (c === HYPHEN) { 278 | callback('onPartEnd'); 279 | state = S.END; 280 | flags = 0; 281 | } else { 282 | index = 0; 283 | } 284 | } else { 285 | index = 0; 286 | } 287 | } 288 | 289 | if (index > 0) { 290 | // when matching a possible boundary, keep a lookbehind reference 291 | // in case it turns out to be a false lead 292 | lookbehind[index - 1] = c; 293 | } else if (previousIndex > 0) { 294 | // if our boundary turned out to be rubbish, the captured lookbehind 295 | // belongs to partData 296 | const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); 297 | callback('onPartData', 0, previousIndex, _lookbehind); 298 | previousIndex = 0; 299 | mark('onPartData'); 300 | 301 | // reconsider the current character even so it interrupted the sequence 302 | // it could be the beginning of a new sequence 303 | i--; 304 | } 305 | 306 | break; 307 | case S.END: 308 | break; 309 | default: 310 | throw new Error(`Unexpected state entered: ${state}`); 311 | } 312 | } 313 | 314 | dataCallback('onHeaderField'); 315 | dataCallback('onHeaderValue'); 316 | dataCallback('onPartData'); 317 | 318 | // Update properties for the next call 319 | this.index = index; 320 | this.state = state; 321 | this.flags = flags; 322 | } 323 | 324 | end() { 325 | if ((this.state === S.HEADER_FIELD_START && this.index === 0) || 326 | (this.state === S.PART_DATA && this.index === this.boundary.length)) { 327 | this.onPartEnd(); 328 | } else if (this.state !== S.END) { 329 | throw new Error('MultipartParser.end(): stream ended unexpectedly'); 330 | } 331 | } 332 | } 333 | 334 | function _fileName(headerValue) { 335 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 336 | const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); 337 | if (!m) { 338 | return; 339 | } 340 | 341 | const match = m[2] || m[3] || ''; 342 | let filename = match.slice(match.lastIndexOf('\\') + 1); 343 | filename = filename.replace(/%22/g, '"'); 344 | filename = filename.replace(/&#(\d{4});/g, (m, code) => { 345 | return String.fromCharCode(code); 346 | }); 347 | return filename; 348 | } 349 | 350 | async function toFormData(Body, ct) { 351 | if (!/multipart/i.test(ct)) { 352 | throw new TypeError('Failed to fetch'); 353 | } 354 | 355 | const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); 356 | 357 | if (!m) { 358 | throw new TypeError('no or bad content-type header, no multipart boundary'); 359 | } 360 | 361 | const parser = new MultipartParser(m[1] || m[2]); 362 | 363 | let headerField; 364 | let headerValue; 365 | let entryValue; 366 | let entryName; 367 | let contentType; 368 | let filename; 369 | const entryChunks = []; 370 | const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct(); 371 | 372 | const onPartData = ui8a => { 373 | entryValue += decoder.decode(ui8a, {stream: true}); 374 | }; 375 | 376 | const appendToFile = ui8a => { 377 | entryChunks.push(ui8a); 378 | }; 379 | 380 | const appendFileToFormData = () => { 381 | const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType}); 382 | formData.append(entryName, file); 383 | }; 384 | 385 | const appendEntryToFormData = () => { 386 | formData.append(entryName, entryValue); 387 | }; 388 | 389 | const decoder = new TextDecoder('utf-8'); 390 | decoder.decode(); 391 | 392 | parser.onPartBegin = function () { 393 | parser.onPartData = onPartData; 394 | parser.onPartEnd = appendEntryToFormData; 395 | 396 | headerField = ''; 397 | headerValue = ''; 398 | entryValue = ''; 399 | entryName = ''; 400 | contentType = ''; 401 | filename = null; 402 | entryChunks.length = 0; 403 | }; 404 | 405 | parser.onHeaderField = function (ui8a) { 406 | headerField += decoder.decode(ui8a, {stream: true}); 407 | }; 408 | 409 | parser.onHeaderValue = function (ui8a) { 410 | headerValue += decoder.decode(ui8a, {stream: true}); 411 | }; 412 | 413 | parser.onHeaderEnd = function () { 414 | headerValue += decoder.decode(); 415 | headerField = headerField.toLowerCase(); 416 | 417 | if (headerField === 'content-disposition') { 418 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 419 | const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); 420 | 421 | if (m) { 422 | entryName = m[2] || m[3] || ''; 423 | } 424 | 425 | filename = _fileName(headerValue); 426 | 427 | if (filename) { 428 | parser.onPartData = appendToFile; 429 | parser.onPartEnd = appendFileToFormData; 430 | } 431 | } else if (headerField === 'content-type') { 432 | contentType = headerValue; 433 | } 434 | 435 | headerValue = ''; 436 | headerField = ''; 437 | }; 438 | 439 | for await (const chunk of Body) { 440 | parser.write(chunk); 441 | } 442 | 443 | parser.end(); 444 | 445 | return formData; 446 | } 447 | 448 | 449 | /***/ }) 450 | 451 | }; 452 | ; -------------------------------------------------------------------------------- /action/multipart-parser-d1d13d05.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('node:fs'); 4 | require('node:path'); 5 | var githubAction = require('./github-action.js'); 6 | require('path'); 7 | require('os'); 8 | require('vm'); 9 | require('events'); 10 | require('fs'); 11 | require('util'); 12 | require('tty'); 13 | require('stream'); 14 | require('buffer'); 15 | require('url'); 16 | require('http'); 17 | require('https'); 18 | require('domain'); 19 | require('crypto'); 20 | require('assert'); 21 | require('string_decoder'); 22 | require('net'); 23 | require('tls'); 24 | require('dns'); 25 | require('punycode'); 26 | require('zlib'); 27 | require('querystring'); 28 | require('process'); 29 | require('http2'); 30 | require('constants'); 31 | require('child_process'); 32 | require('worker_threads'); 33 | require('module'); 34 | require('node:http'); 35 | require('node:https'); 36 | require('node:zlib'); 37 | require('node:stream'); 38 | require('node:buffer'); 39 | require('node:util'); 40 | require('node:url'); 41 | require('node:net'); 42 | 43 | let s = 0; 44 | const S = { 45 | START_BOUNDARY: s++, 46 | HEADER_FIELD_START: s++, 47 | HEADER_FIELD: s++, 48 | HEADER_VALUE_START: s++, 49 | HEADER_VALUE: s++, 50 | HEADER_VALUE_ALMOST_DONE: s++, 51 | HEADERS_ALMOST_DONE: s++, 52 | PART_DATA_START: s++, 53 | PART_DATA: s++, 54 | END: s++ 55 | }; 56 | 57 | let f = 1; 58 | const F = { 59 | PART_BOUNDARY: f, 60 | LAST_BOUNDARY: f *= 2 61 | }; 62 | 63 | const LF = 10; 64 | const CR = 13; 65 | const SPACE = 32; 66 | const HYPHEN = 45; 67 | const COLON = 58; 68 | const A = 97; 69 | const Z = 122; 70 | 71 | const lower = c => c | 0x20; 72 | 73 | const noop = () => {}; 74 | 75 | class MultipartParser { 76 | /** 77 | * @param {string} boundary 78 | */ 79 | constructor(boundary) { 80 | this.index = 0; 81 | this.flags = 0; 82 | 83 | this.onHeaderEnd = noop; 84 | this.onHeaderField = noop; 85 | this.onHeadersEnd = noop; 86 | this.onHeaderValue = noop; 87 | this.onPartBegin = noop; 88 | this.onPartData = noop; 89 | this.onPartEnd = noop; 90 | 91 | this.boundaryChars = {}; 92 | 93 | boundary = '\r\n--' + boundary; 94 | const ui8a = new Uint8Array(boundary.length); 95 | for (let i = 0; i < boundary.length; i++) { 96 | ui8a[i] = boundary.charCodeAt(i); 97 | this.boundaryChars[ui8a[i]] = true; 98 | } 99 | 100 | this.boundary = ui8a; 101 | this.lookbehind = new Uint8Array(this.boundary.length + 8); 102 | this.state = S.START_BOUNDARY; 103 | } 104 | 105 | /** 106 | * @param {Uint8Array} data 107 | */ 108 | write(data) { 109 | let i = 0; 110 | const length_ = data.length; 111 | let previousIndex = this.index; 112 | let {lookbehind, boundary, boundaryChars, index, state, flags} = this; 113 | const boundaryLength = this.boundary.length; 114 | const boundaryEnd = boundaryLength - 1; 115 | const bufferLength = data.length; 116 | let c; 117 | let cl; 118 | 119 | const mark = name => { 120 | this[name + 'Mark'] = i; 121 | }; 122 | 123 | const clear = name => { 124 | delete this[name + 'Mark']; 125 | }; 126 | 127 | const callback = (callbackSymbol, start, end, ui8a) => { 128 | if (start === undefined || start !== end) { 129 | this[callbackSymbol](ui8a && ui8a.subarray(start, end)); 130 | } 131 | }; 132 | 133 | const dataCallback = (name, clear) => { 134 | const markSymbol = name + 'Mark'; 135 | if (!(markSymbol in this)) { 136 | return; 137 | } 138 | 139 | if (clear) { 140 | callback(name, this[markSymbol], i, data); 141 | delete this[markSymbol]; 142 | } else { 143 | callback(name, this[markSymbol], data.length, data); 144 | this[markSymbol] = 0; 145 | } 146 | }; 147 | 148 | for (i = 0; i < length_; i++) { 149 | c = data[i]; 150 | 151 | switch (state) { 152 | case S.START_BOUNDARY: 153 | if (index === boundary.length - 2) { 154 | if (c === HYPHEN) { 155 | flags |= F.LAST_BOUNDARY; 156 | } else if (c !== CR) { 157 | return; 158 | } 159 | 160 | index++; 161 | break; 162 | } else if (index - 1 === boundary.length - 2) { 163 | if (flags & F.LAST_BOUNDARY && c === HYPHEN) { 164 | state = S.END; 165 | flags = 0; 166 | } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { 167 | index = 0; 168 | callback('onPartBegin'); 169 | state = S.HEADER_FIELD_START; 170 | } else { 171 | return; 172 | } 173 | 174 | break; 175 | } 176 | 177 | if (c !== boundary[index + 2]) { 178 | index = -2; 179 | } 180 | 181 | if (c === boundary[index + 2]) { 182 | index++; 183 | } 184 | 185 | break; 186 | case S.HEADER_FIELD_START: 187 | state = S.HEADER_FIELD; 188 | mark('onHeaderField'); 189 | index = 0; 190 | // falls through 191 | case S.HEADER_FIELD: 192 | if (c === CR) { 193 | clear('onHeaderField'); 194 | state = S.HEADERS_ALMOST_DONE; 195 | break; 196 | } 197 | 198 | index++; 199 | if (c === HYPHEN) { 200 | break; 201 | } 202 | 203 | if (c === COLON) { 204 | if (index === 1) { 205 | // empty header field 206 | return; 207 | } 208 | 209 | dataCallback('onHeaderField', true); 210 | state = S.HEADER_VALUE_START; 211 | break; 212 | } 213 | 214 | cl = lower(c); 215 | if (cl < A || cl > Z) { 216 | return; 217 | } 218 | 219 | break; 220 | case S.HEADER_VALUE_START: 221 | if (c === SPACE) { 222 | break; 223 | } 224 | 225 | mark('onHeaderValue'); 226 | state = S.HEADER_VALUE; 227 | // falls through 228 | case S.HEADER_VALUE: 229 | if (c === CR) { 230 | dataCallback('onHeaderValue', true); 231 | callback('onHeaderEnd'); 232 | state = S.HEADER_VALUE_ALMOST_DONE; 233 | } 234 | 235 | break; 236 | case S.HEADER_VALUE_ALMOST_DONE: 237 | if (c !== LF) { 238 | return; 239 | } 240 | 241 | state = S.HEADER_FIELD_START; 242 | break; 243 | case S.HEADERS_ALMOST_DONE: 244 | if (c !== LF) { 245 | return; 246 | } 247 | 248 | callback('onHeadersEnd'); 249 | state = S.PART_DATA_START; 250 | break; 251 | case S.PART_DATA_START: 252 | state = S.PART_DATA; 253 | mark('onPartData'); 254 | // falls through 255 | case S.PART_DATA: 256 | previousIndex = index; 257 | 258 | if (index === 0) { 259 | // boyer-moore derrived algorithm to safely skip non-boundary data 260 | i += boundaryEnd; 261 | while (i < bufferLength && !(data[i] in boundaryChars)) { 262 | i += boundaryLength; 263 | } 264 | 265 | i -= boundaryEnd; 266 | c = data[i]; 267 | } 268 | 269 | if (index < boundary.length) { 270 | if (boundary[index] === c) { 271 | if (index === 0) { 272 | dataCallback('onPartData', true); 273 | } 274 | 275 | index++; 276 | } else { 277 | index = 0; 278 | } 279 | } else if (index === boundary.length) { 280 | index++; 281 | if (c === CR) { 282 | // CR = part boundary 283 | flags |= F.PART_BOUNDARY; 284 | } else if (c === HYPHEN) { 285 | // HYPHEN = end boundary 286 | flags |= F.LAST_BOUNDARY; 287 | } else { 288 | index = 0; 289 | } 290 | } else if (index - 1 === boundary.length) { 291 | if (flags & F.PART_BOUNDARY) { 292 | index = 0; 293 | if (c === LF) { 294 | // unset the PART_BOUNDARY flag 295 | flags &= ~F.PART_BOUNDARY; 296 | callback('onPartEnd'); 297 | callback('onPartBegin'); 298 | state = S.HEADER_FIELD_START; 299 | break; 300 | } 301 | } else if (flags & F.LAST_BOUNDARY) { 302 | if (c === HYPHEN) { 303 | callback('onPartEnd'); 304 | state = S.END; 305 | flags = 0; 306 | } else { 307 | index = 0; 308 | } 309 | } else { 310 | index = 0; 311 | } 312 | } 313 | 314 | if (index > 0) { 315 | // when matching a possible boundary, keep a lookbehind reference 316 | // in case it turns out to be a false lead 317 | lookbehind[index - 1] = c; 318 | } else if (previousIndex > 0) { 319 | // if our boundary turned out to be rubbish, the captured lookbehind 320 | // belongs to partData 321 | const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); 322 | callback('onPartData', 0, previousIndex, _lookbehind); 323 | previousIndex = 0; 324 | mark('onPartData'); 325 | 326 | // reconsider the current character even so it interrupted the sequence 327 | // it could be the beginning of a new sequence 328 | i--; 329 | } 330 | 331 | break; 332 | case S.END: 333 | break; 334 | default: 335 | throw new Error(`Unexpected state entered: ${state}`); 336 | } 337 | } 338 | 339 | dataCallback('onHeaderField'); 340 | dataCallback('onHeaderValue'); 341 | dataCallback('onPartData'); 342 | 343 | // Update properties for the next call 344 | this.index = index; 345 | this.state = state; 346 | this.flags = flags; 347 | } 348 | 349 | end() { 350 | if ((this.state === S.HEADER_FIELD_START && this.index === 0) || 351 | (this.state === S.PART_DATA && this.index === this.boundary.length)) { 352 | this.onPartEnd(); 353 | } else if (this.state !== S.END) { 354 | throw new Error('MultipartParser.end(): stream ended unexpectedly'); 355 | } 356 | } 357 | } 358 | 359 | function _fileName(headerValue) { 360 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 361 | const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); 362 | if (!m) { 363 | return; 364 | } 365 | 366 | const match = m[2] || m[3] || ''; 367 | let filename = match.slice(match.lastIndexOf('\\') + 1); 368 | filename = filename.replace(/%22/g, '"'); 369 | filename = filename.replace(/&#(\d{4});/g, (m, code) => { 370 | return String.fromCharCode(code); 371 | }); 372 | return filename; 373 | } 374 | 375 | async function toFormData(Body, ct) { 376 | if (!/multipart/i.test(ct)) { 377 | throw new TypeError('Failed to fetch'); 378 | } 379 | 380 | const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); 381 | 382 | if (!m) { 383 | throw new TypeError('no or bad content-type header, no multipart boundary'); 384 | } 385 | 386 | const parser = new MultipartParser(m[1] || m[2]); 387 | 388 | let headerField; 389 | let headerValue; 390 | let entryValue; 391 | let entryName; 392 | let contentType; 393 | let filename; 394 | const entryChunks = []; 395 | const formData = new githubAction.FormData(); 396 | 397 | const onPartData = ui8a => { 398 | entryValue += decoder.decode(ui8a, {stream: true}); 399 | }; 400 | 401 | const appendToFile = ui8a => { 402 | entryChunks.push(ui8a); 403 | }; 404 | 405 | const appendFileToFormData = () => { 406 | const file = new githubAction.File(entryChunks, filename, {type: contentType}); 407 | formData.append(entryName, file); 408 | }; 409 | 410 | const appendEntryToFormData = () => { 411 | formData.append(entryName, entryValue); 412 | }; 413 | 414 | const decoder = new TextDecoder('utf-8'); 415 | decoder.decode(); 416 | 417 | parser.onPartBegin = function () { 418 | parser.onPartData = onPartData; 419 | parser.onPartEnd = appendEntryToFormData; 420 | 421 | headerField = ''; 422 | headerValue = ''; 423 | entryValue = ''; 424 | entryName = ''; 425 | contentType = ''; 426 | filename = null; 427 | entryChunks.length = 0; 428 | }; 429 | 430 | parser.onHeaderField = function (ui8a) { 431 | headerField += decoder.decode(ui8a, {stream: true}); 432 | }; 433 | 434 | parser.onHeaderValue = function (ui8a) { 435 | headerValue += decoder.decode(ui8a, {stream: true}); 436 | }; 437 | 438 | parser.onHeaderEnd = function () { 439 | headerValue += decoder.decode(); 440 | headerField = headerField.toLowerCase(); 441 | 442 | if (headerField === 'content-disposition') { 443 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 444 | const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); 445 | 446 | if (m) { 447 | entryName = m[2] || m[3] || ''; 448 | } 449 | 450 | filename = _fileName(headerValue); 451 | 452 | if (filename) { 453 | parser.onPartData = appendToFile; 454 | parser.onPartEnd = appendFileToFormData; 455 | } 456 | } else if (headerField === 'content-type') { 457 | contentType = headerValue; 458 | } 459 | 460 | headerValue = ''; 461 | headerField = ''; 462 | }; 463 | 464 | for await (const chunk of Body) { 465 | parser.write(chunk); 466 | } 467 | 468 | parser.end(); 469 | 470 | return formData; 471 | } 472 | 473 | exports.toFormData = toFormData; 474 | --------------------------------------------------------------------------------