├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ ├── question.md │ └── support_request.md ├── PULL_REQUEST_TEMPLATE.md ├── auto-approve.yml ├── auto-label.yaml ├── blunderbuss.yml ├── generated-files-bot.yml ├── release-please.yml ├── release-trigger.yml ├── sync-repo-settings.yaml └── workflows │ ├── build-action.yaml │ ├── ci-action.yaml │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .jsdoc.js ├── .kokoro ├── .gitattributes ├── common.cfg ├── continuous │ ├── node12 │ │ ├── common.cfg │ │ ├── lint.cfg │ │ ├── samples-test.cfg │ │ ├── system-test.cfg │ │ └── test.cfg │ └── node14 │ │ ├── common.cfg │ │ ├── lint.cfg │ │ ├── samples-test.cfg │ │ ├── system-test.cfg │ │ └── test.cfg ├── docs.sh ├── lint.sh ├── populate-secrets.sh ├── presubmit │ ├── node12 │ │ ├── common.cfg │ │ ├── samples-test.cfg │ │ ├── system-test.cfg │ │ └── test.cfg │ ├── node14 │ │ ├── common.cfg │ │ ├── samples-test.cfg │ │ ├── system-test.cfg │ │ └── test.cfg │ └── windows │ │ ├── common.cfg │ │ └── test.cfg ├── publish.sh ├── release │ ├── docs-devsite.cfg │ ├── docs-devsite.sh │ ├── docs.cfg │ ├── docs.sh │ └── publish.cfg ├── samples-test.sh ├── system-test.sh ├── test.bat ├── test.sh ├── trampoline.sh └── trampoline_v2.sh ├── .mocharc.js ├── .nycrc ├── .prettierignore ├── .prettierrc.js ├── .repo-metadata.json ├── .trampolinerc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── action.yaml ├── action ├── .eslintignore ├── dist │ ├── index.js │ ├── index.js.map │ ├── licenses.txt │ └── sourcemap-register.js ├── package-lock.json ├── package.json ├── src │ └── main.ts ├── test │ └── main.ts └── tsconfig.json ├── linkinator.config.json ├── owlbot.py ├── package-lock.json ├── package.json ├── src ├── bin │ ├── code-suggester.ts │ ├── handle-git-dir-change.ts │ └── workflow.ts ├── default-options-handler.ts ├── errors.ts ├── github │ ├── branch.ts │ ├── commit-and-push.ts │ ├── create-commit.ts │ ├── fork.ts │ ├── labels.ts │ ├── open-pull-request.ts │ └── review-pull-request.ts ├── index.ts ├── logger.ts ├── types.ts └── utils │ ├── diff-utils.ts │ └── hunk-utils.ts ├── system-test └── main.test.ts ├── test ├── branch.ts ├── cli.ts ├── commit-and-push.ts ├── diff-utils.ts ├── fixtures │ ├── add-labels-response.json │ ├── create-commit-response.json │ ├── create-fork-response.json │ ├── create-pr-response.json │ ├── create-ref-response.json │ ├── create-tree-response.json │ ├── diffs │ │ ├── addition.diff │ │ ├── deletion.diff │ │ ├── many-to-many.diff │ │ ├── many-to-one.diff │ │ ├── one-line-to-many-newline.diff │ │ ├── one-line-to-many.diff │ │ └── one-line-to-one.diff │ ├── get-branch-response.json │ ├── get-commit-response.json │ ├── get-pull-request-response.json │ ├── get-ref-response.json │ └── list-pulls-response.json ├── fork.ts ├── git-dir-handler.ts ├── helper-review-pull-request.ts ├── hunk-utils.ts ├── inline-suggest.ts ├── invalid-hunks.ts ├── issues.ts ├── main-make-pr.ts ├── main-pr-option-defaults.ts ├── main-review-pull-request.ts ├── parse-text-files.ts ├── pr.ts ├── pull-request-hunks.ts ├── remote-github-patch-text.ts ├── scope-handler.ts ├── suggestion-hunk.ts └── util.ts ├── tsconfig.json └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/node_modules 3 | .coverage 4 | coverage 5 | .nyc_output 6 | docs/ 7 | out/ 8 | build/ 9 | system-test/secrets.js 10 | system-test/*key.json 11 | *.lock 12 | .DS_Store 13 | # package-lock.json 14 | __pycache__ 15 | .env 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | test/fixtures 4 | build/ 5 | docs/ 6 | protos/ 7 | samples/generated/ 8 | dist/ 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts", 3 | "root": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts text eol=lf 2 | *.js text eol=lf 3 | protos/* linguist-generated 4 | **/api-extractor.json linguist-language=JSON-with-Comments 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | # 4 | # For syntax help see: 5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 6 | 7 | 8 | # The yoshi-nodejs team is the default owner for nodejs repositories. 9 | * @googleapis/yoshi-nodejs @googleapis/github-automation 10 | # The github automation team is the default owner for the auto-approve file. 11 | .github/auto-approve.yml @googleapis/github-automation -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: 'type: bug, priority: p2' 5 | --- 6 | 7 | Thanks for stopping by to let us know something could be better! 8 | 9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 10 | 11 | 1) Is this a client library issue or a product issue? 12 | This is the client library for . We will only be able to assist with issues that pertain to the behaviors of this library. If the issue you're experiencing is due to the behavior of the product itself, please visit the [ Support page]() to reach the most relevant engineers. 13 | 14 | 2) Did someone already solve this? 15 | - Search the issues already opened: https://github.com/googleapis/code-suggester/issues 16 | - Search the issues on our "catch-all" repository: https://github.com/googleapis/google-cloud-node 17 | - Search or ask on StackOverflow (engineers monitor these tags): http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js 18 | 19 | 3) Do you have a support contract? 20 | Please create an issue in the [support console](https://cloud.google.com/support/) to ensure a timely response. 21 | 22 | If the support paths suggested above still do not result in a resolution, please provide the following details. 23 | 24 | #### Environment details 25 | 26 | - OS: 27 | - Node.js version: 28 | - npm version: 29 | - `code-suggester` version: 30 | 31 | #### Steps to reproduce 32 | 33 | 1. ? 34 | 2. ? 35 | 36 | Making sure to follow these steps will guarantee the quickest resolution possible. 37 | 38 | Thanks! 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Google Cloud Support 3 | url: https://cloud.google.com/support/ 4 | about: If you have a support contract with Google, please use the Google Cloud Support portal. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this library 4 | labels: 'type: feature request, priority: p3' 5 | --- 6 | 7 | Thanks for stopping by to let us know something could be better! 8 | 9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | labels: 'type: question, priority: p3' 5 | --- 6 | 7 | Thanks for stopping by to ask us a question! Please make sure to include: 8 | - What you're trying to do 9 | - What code you've already tried 10 | - Any error messages you're getting 11 | 12 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support request 3 | about: If you have a support contract with Google, please create an issue in the Google Cloud Support console. 4 | 5 | --- 6 | 7 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: 2 | - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/code-suggester/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea 3 | - [ ] Ensure the tests and linter pass 4 | - [ ] Code coverage does not decrease (if any source code was changed) 5 | - [ ] Appropriate docs were updated (if necessary) 6 | 7 | Fixes # 🦕 8 | -------------------------------------------------------------------------------- /.github/auto-approve.yml: -------------------------------------------------------------------------------- 1 | processes: 2 | - "NodeDependency" 3 | - "OwlBotTemplateChangesNode" 4 | - "OwlBotPRsNode" -------------------------------------------------------------------------------- /.github/auto-label.yaml: -------------------------------------------------------------------------------- 1 | requestsize: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | assign_issues: 2 | - chingor13 3 | - SurferJeffAtGoogle 4 | assign_prs: 5 | - chingor13 6 | - SurferJeffAtGoogle 7 | -------------------------------------------------------------------------------- /.github/generated-files-bot.yml: -------------------------------------------------------------------------------- 1 | generatedFiles: 2 | - path: '.kokoro/**' 3 | message: '`.kokoro` files are templated and should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' 4 | - path: '.github/CODEOWNERS' 5 | message: 'CODEOWNERS should instead be modified via the `codeowner_team` property in .repo-metadata.json' 6 | - path: '.github/workflows/ci.yaml' 7 | message: '`.github/workflows/ci.yaml` (GitHub Actions) should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' 8 | - path: '.github/generated-files-bot.+(yml|yaml)' 9 | message: '`.github/generated-files-bot.(yml|yaml)` should be updated in [`synthtool`](https://github.com/googleapis/synthtool)' 10 | - path: 'README.md' 11 | message: '`README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/main/.readme-partials.yaml' 12 | - path: 'samples/README.md' 13 | message: '`samples/README.md` is managed by [`synthtool`](https://github.com/googleapis/synthtool). However, a partials file can be used to update the README, e.g.: https://github.com/googleapis/nodejs-storage/blob/main/.readme-partials.yaml' 14 | ignoreAuthors: 15 | - 'gcf-owl-bot[bot]' 16 | - 'yoshi-automation' 17 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | releaseType: node 2 | handleGHRelease: true 3 | -------------------------------------------------------------------------------- /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | multiScmName: code-suggester 3 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | branchProtectionRules: 2 | - pattern: main 3 | isAdminEnforced: true 4 | requiredApprovingReviewCount: 1 5 | requiresCodeOwnerReviews: true 6 | requiresStrictStatusChecks: true 7 | requiredStatusCheckContexts: 8 | - lint 9 | - test (14) 10 | - test (16) 11 | - test (18) 12 | - cla/google 13 | - windows 14 | permissionRules: 15 | - team: yoshi-admins 16 | permission: admin 17 | - team: jsteam-admins 18 | permission: admin 19 | - team: jsteam 20 | permission: push 21 | -------------------------------------------------------------------------------- /.github/workflows/build-action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | on: 16 | push: 17 | branches: 18 | - main 19 | name: build-action 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 25 | - name: build 26 | run: |- 27 | set -e 28 | npm ci 29 | npm run compile 30 | pushd action 31 | npm ci 32 | npm run package 33 | - name: open-pull-request 34 | uses: googleapis/code-suggester@bdc0d58d4f62704eaf3d83e2be2255a5d8a41cee # v4 35 | env: 36 | ACCESS_TOKEN: ${{ secrets.YOSHI_CODE_BOT_TOKEN }} 37 | with: 38 | command: pr 39 | upstream_owner: googleapis 40 | upstream_repo: code-suggester 41 | description: 'Compiles the code-suggester action dist' 42 | title: 'build(action): build dist' 43 | message: 'build(action): build dist' 44 | branch: build-dist 45 | primary: main 46 | fork: true 47 | force: true 48 | git_dir: './action/dist' 49 | -------------------------------------------------------------------------------- /.github/workflows/ci-action.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: ci-action 7 | jobs: 8 | test-action: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 12 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 13 | with: 14 | node-version: 20 15 | - name: npm 8 16 | run: npm i -g npm@8 --registry=https://registry.npmjs.org 17 | - run: |- 18 | set -ex 19 | npm --version 20 | npm ci 21 | npm run compile 22 | pushd action 23 | npm ci 24 | npm test 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: ci 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [18, 20] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - run: node --version 19 | # The first installation step ensures that all of our production 20 | # dependencies work on the given Node.js version, this helps us find 21 | # dependencies that don't match our engines field: 22 | - run: npm install --production --engine-strict --ignore-scripts --no-package-lock 23 | # Clean up the production install, before installing dev/production: 24 | - run: rm -rf node_modules 25 | - run: npm install --engine-strict 26 | - run: npm test 27 | env: 28 | MOCHA_THROW_DEPRECATION: false 29 | windows: 30 | runs-on: windows-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: 18 36 | - run: npm install --engine-strict 37 | - run: npm test 38 | env: 39 | MOCHA_THROW_DEPRECATION: false 40 | lint: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v3 44 | - uses: actions/setup-node@v3 45 | with: 46 | node-version: 18 47 | - run: npm install 48 | - run: npm run lint 49 | docs: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: actions/setup-node@v3 54 | with: 55 | node-version: 18 56 | - run: npm install 57 | - run: npm run docs 58 | - uses: JustinBeckwith/linkinator-action@v1 59 | with: 60 | paths: docs/ 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | name: release 5 | jobs: 6 | update-major: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/github-script@98814c53be79b1d30f795b907e553d8679345975 # v6 10 | id: parse-major 11 | with: 12 | script: | 13 | const ref = context.payload.release.tag_name; 14 | const versionRegex = new RegExp('v?(\\d+)\\.\\d+\\.\\d+'); 15 | const match = ref.match(versionRegex); 16 | if (match) { 17 | return "v" + match[1]; 18 | } 19 | return ""; 20 | result-encoding: string 21 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 22 | with: 23 | fetch-depth: 0 24 | if: ${{ steps.parse-major.outputs.result }} 25 | - name: delete existing major tag 26 | run: | 27 | (git tag -d ${{ steps.parse-major.outputs.result }} && git push origin :${{ steps.parse-major.outputs.result }}) || true 28 | if: ${{ steps.parse-major.outputs.result }} 29 | - name: create major tag 30 | run: | 31 | git tag ${{ steps.parse-major.outputs.result }} ${{ github.event.GITHUB_REF }} 32 | git push origin --tags 33 | if: ${{ steps.parse-major.outputs.result }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/node_modules 3 | .coverage 4 | coverage 5 | .nyc_output 6 | docs/ 7 | out/ 8 | build/ 9 | system-test/secrets.js 10 | system-test/*key.json 11 | *.lock 12 | .DS_Store 13 | __pycache__ 14 | .env 15 | -------------------------------------------------------------------------------- /.jsdoc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // ** This file is automatically generated by gapic-generator-typescript. ** 16 | // ** https://github.com/googleapis/gapic-generator-typescript ** 17 | // ** All changes to this file may be overwritten. ** 18 | 19 | 'use strict'; 20 | 21 | module.exports = { 22 | opts: { 23 | readme: './README.md', 24 | package: './package.json', 25 | recurse: true, 26 | verbose: true, 27 | destination: './docs/' 28 | }, 29 | plugins: [ 30 | 'plugins/markdown', 31 | 'jsdoc-region-tag' 32 | ], 33 | source: { 34 | excludePattern: '(^|\\/|\\\\)[._]', 35 | include: [ 36 | 'build/src' 37 | ], 38 | includePattern: '\\.js$' 39 | }, 40 | templates: { 41 | copyright: 'Copyright 2020 Google, LLC.', 42 | includeDate: false, 43 | sourceFiles: false, 44 | systemName: '@google-cloud/code-suggester', 45 | theme: 'lumen', 46 | default: { 47 | outputSourceFiles: false 48 | } 49 | }, 50 | markdown: { 51 | idInHeadings: true 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /.kokoro/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-generated=true 2 | -------------------------------------------------------------------------------- /.kokoro/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Download trampoline resources. 11 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 12 | 13 | # Use the trampoline script to run in docker. 14 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 15 | 16 | # Configure the docker image for kokoro-trampoline. 17 | env_vars: { 18 | key: "TRAMPOLINE_IMAGE" 19 | value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" 20 | } 21 | env_vars: { 22 | key: "TRAMPOLINE_BUILD_FILE" 23 | value: "github/code-suggester/.kokoro/test.sh" 24 | } 25 | -------------------------------------------------------------------------------- /.kokoro/continuous/node12/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Download trampoline resources. 11 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 12 | 13 | # Use the trampoline script to run in docker. 14 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 15 | 16 | # Configure the docker image for kokoro-trampoline. 17 | env_vars: { 18 | key: "TRAMPOLINE_IMAGE" 19 | value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" 20 | } 21 | env_vars: { 22 | key: "TRAMPOLINE_BUILD_FILE" 23 | value: "github/code-suggester/.kokoro/test.sh" 24 | } 25 | -------------------------------------------------------------------------------- /.kokoro/continuous/node12/lint.cfg: -------------------------------------------------------------------------------- 1 | env_vars: { 2 | key: "TRAMPOLINE_BUILD_FILE" 3 | value: "github/code-suggester/.kokoro/lint.sh" 4 | } 5 | -------------------------------------------------------------------------------- /.kokoro/continuous/node12/samples-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/samples-test.sh" 7 | } 8 | 9 | env_vars: { 10 | key: "SECRET_MANAGER_KEYS" 11 | value: "long-door-651-kokoro-system-test-service-account" 12 | } -------------------------------------------------------------------------------- /.kokoro/continuous/node12/system-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/system-test.sh" 7 | } 8 | 9 | env_vars: { 10 | key: "SECRET_MANAGER_KEYS" 11 | value: "long-door-651-kokoro-system-test-service-account" 12 | } -------------------------------------------------------------------------------- /.kokoro/continuous/node12/test.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/code-suggester/6600a47461f0f027bf7b49671139456dfa4a5b4d/.kokoro/continuous/node12/test.cfg -------------------------------------------------------------------------------- /.kokoro/continuous/node14/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Download trampoline resources. 11 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 12 | 13 | # Use the trampoline script to run in docker. 14 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 15 | 16 | # Configure the docker image for kokoro-trampoline. 17 | env_vars: { 18 | key: "TRAMPOLINE_IMAGE" 19 | value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" 20 | } 21 | env_vars: { 22 | key: "TRAMPOLINE_BUILD_FILE" 23 | value: "github/code-suggester/.kokoro/test.sh" 24 | } 25 | -------------------------------------------------------------------------------- /.kokoro/continuous/node14/lint.cfg: -------------------------------------------------------------------------------- 1 | env_vars: { 2 | key: "TRAMPOLINE_BUILD_FILE" 3 | value: "github/code-suggester/.kokoro/lint.sh" 4 | } 5 | -------------------------------------------------------------------------------- /.kokoro/continuous/node14/samples-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/samples-test.sh" 7 | } 8 | 9 | env_vars: { 10 | key: "SECRET_MANAGER_KEYS" 11 | value: "long-door-651-kokoro-system-test-service-account" 12 | } -------------------------------------------------------------------------------- /.kokoro/continuous/node14/system-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/system-test.sh" 7 | } 8 | 9 | env_vars: { 10 | key: "SECRET_MANAGER_KEYS" 11 | value: "long-door-651-kokoro-system-test-service-account" 12 | } -------------------------------------------------------------------------------- /.kokoro/continuous/node14/test.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/code-suggester/6600a47461f0f027bf7b49671139456dfa4a5b4d/.kokoro/continuous/node14/test.cfg -------------------------------------------------------------------------------- /.kokoro/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | cd $(dirname $0)/.. 22 | 23 | npm install 24 | 25 | npm run docs-test 26 | -------------------------------------------------------------------------------- /.kokoro/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | cd $(dirname $0)/.. 22 | 23 | npm install 24 | 25 | # Install and link samples 26 | if [ -f samples/package.json ]; then 27 | cd samples/ 28 | npm link ../ 29 | npm install 30 | cd .. 31 | fi 32 | 33 | npm run lint 34 | -------------------------------------------------------------------------------- /.kokoro/populate-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2020 Google LLC. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This file is called in the early stage of `trampoline_v2.sh` to 17 | # populate secrets needed for the CI builds. 18 | 19 | set -eo pipefail 20 | 21 | function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} 22 | function msg { println "$*" >&2 ;} 23 | function println { printf '%s\n' "$(now) $*" ;} 24 | 25 | SECRET_PROJECT=cloud-sdk-release-custom-pool 26 | 27 | # Populates requested secrets set in SECRET_MANAGER_KEYS 28 | 29 | # In Kokoro CI builds, we use the service account attached to the 30 | # Kokoro VM. This means we need to setup auth on other CI systems. 31 | # For local run, we just use the gcloud command for retrieving the 32 | # secrets. 33 | 34 | if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then 35 | GCLOUD_COMMANDS=( 36 | "docker" 37 | "run" 38 | "--entrypoint=gcloud" 39 | "--volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR}" 40 | "gcr.io/google.com/cloudsdktool/cloud-sdk" 41 | ) 42 | if [[ "${TRAMPOLINE_CI:-}" == "kokoro" ]]; then 43 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" 44 | else 45 | echo "Authentication for this CI system is not implemented yet." 46 | exit 2 47 | # TODO: Determine appropriate SECRET_LOCATION and the GCLOUD_COMMANDS. 48 | fi 49 | else 50 | # For local run, use /dev/shm or temporary directory for 51 | # KOKORO_GFILE_DIR. 52 | if [[ -d "/dev/shm" ]]; then 53 | export KOKORO_GFILE_DIR=/dev/shm 54 | else 55 | export KOKORO_GFILE_DIR=$(mktemp -d -t ci-XXXXXXXX) 56 | fi 57 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" 58 | GCLOUD_COMMANDS=("gcloud") 59 | fi 60 | 61 | msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" 62 | mkdir -p ${SECRET_LOCATION} 63 | 64 | for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") 65 | do 66 | msg "Retrieving secret ${key}" 67 | "${GCLOUD_COMMANDS[@]}" \ 68 | secrets versions access latest \ 69 | --project "${SECRET_PROJECT}" \ 70 | --secret $key > \ 71 | "$SECRET_LOCATION/$key" 72 | if [[ $? == 0 ]]; then 73 | msg "Secret written to ${SECRET_LOCATION}/${key}" 74 | else 75 | msg "Error retrieving secret ${key}" 76 | exit 2 77 | fi 78 | done 79 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node12/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Use the trampoline script to run in docker. 11 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 12 | 13 | # Configure the docker image for kokoro-trampoline. 14 | env_vars: { 15 | key: "TRAMPOLINE_IMAGE" 16 | value: "gcr.io/cloud-devrel-kokoro-resources/node:12-user" 17 | } 18 | env_vars: { 19 | key: "TRAMPOLINE_BUILD_FILE" 20 | value: "github/code-suggester/.kokoro/test.sh" 21 | } 22 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node12/samples-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/samples-test.sh" 7 | } 8 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node12/system-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/system-test.sh" 7 | } 8 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node12/test.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/code-suggester/6600a47461f0f027bf7b49671139456dfa4a5b4d/.kokoro/presubmit/node12/test.cfg -------------------------------------------------------------------------------- /.kokoro/presubmit/node14/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Use the trampoline script to run in docker. 11 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 12 | 13 | # Configure the docker image for kokoro-trampoline. 14 | env_vars: { 15 | key: "TRAMPOLINE_IMAGE" 16 | value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" 17 | } 18 | env_vars: { 19 | key: "TRAMPOLINE_BUILD_FILE" 20 | value: "github/code-suggester/.kokoro/test.sh" 21 | } 22 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node14/samples-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/samples-test.sh" 7 | } 8 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node14/system-test.cfg: -------------------------------------------------------------------------------- 1 | # Download resources for system tests (service account key, etc.) 2 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" 3 | 4 | env_vars: { 5 | key: "TRAMPOLINE_BUILD_FILE" 6 | value: "github/code-suggester/.kokoro/system-test.sh" 7 | } 8 | -------------------------------------------------------------------------------- /.kokoro/presubmit/node14/test.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/code-suggester/6600a47461f0f027bf7b49671139456dfa4a5b4d/.kokoro/presubmit/node14/test.cfg -------------------------------------------------------------------------------- /.kokoro/presubmit/windows/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | -------------------------------------------------------------------------------- /.kokoro/presubmit/windows/test.cfg: -------------------------------------------------------------------------------- 1 | # Use the test file directly 2 | build_file: "code-suggester/.kokoro/test.bat" 3 | -------------------------------------------------------------------------------- /.kokoro/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | # Start the releasetool reporter 22 | python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script 23 | 24 | cd $(dirname $0)/.. 25 | 26 | NPM_TOKEN=$(cat $KOKORO_KEYSTORE_DIR/73713_google-cloud-npm-token-1) 27 | echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc 28 | 29 | npm install 30 | npm pack . 31 | # npm provides no way to specify, observe, or predict the name of the tarball 32 | # file it generates. We have to look in the current directory for the freshest 33 | # .tgz file. 34 | TARBALL=$(ls -1 -t *.tgz | head -1) 35 | 36 | npm publish --access=public --registry=https://wombat-dressing-room.appspot.com "$TARBALL" 37 | 38 | # Kokoro collects *.tgz and package-lock.json files and stores them in Placer 39 | # so we can generate SBOMs and attestations. 40 | # However, we *don't* want Kokoro to collect package-lock.json and *.tgz files 41 | # that happened to be installed with dependencies. 42 | find node_modules -name package-lock.json -o -name "*.tgz" | xargs rm -f -------------------------------------------------------------------------------- /.kokoro/release/docs-devsite.cfg: -------------------------------------------------------------------------------- 1 | # service account used to publish up-to-date docs. 2 | before_action { 3 | fetch_keystore { 4 | keystore_resource { 5 | keystore_config_id: 73713 6 | keyname: "docuploader_service_account" 7 | } 8 | } 9 | } 10 | 11 | # doc publications use a Python image. 12 | env_vars: { 13 | key: "TRAMPOLINE_IMAGE" 14 | value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" 15 | } 16 | 17 | # Download trampoline resources. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 19 | 20 | # Use the trampoline script to run in docker. 21 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 22 | 23 | env_vars: { 24 | key: "TRAMPOLINE_BUILD_FILE" 25 | value: "github/code-suggester/.kokoro/release/docs-devsite.sh" 26 | } 27 | -------------------------------------------------------------------------------- /.kokoro/release/docs-devsite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | if [[ -z "$CREDENTIALS" ]]; then 20 | # if CREDENTIALS are explicitly set, assume we're testing locally 21 | # and don't set NPM_CONFIG_PREFIX. 22 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 23 | export PATH="$PATH:${NPM_CONFIG_PREFIX}/bin" 24 | cd $(dirname $0)/../.. 25 | fi 26 | 27 | npm install 28 | npm install --no-save @google-cloud/cloud-rad@^0.4.0 29 | # publish docs to devsite 30 | npx @google-cloud/cloud-rad . cloud-rad 31 | -------------------------------------------------------------------------------- /.kokoro/release/docs.cfg: -------------------------------------------------------------------------------- 1 | # service account used to publish up-to-date docs. 2 | before_action { 3 | fetch_keystore { 4 | keystore_resource { 5 | keystore_config_id: 73713 6 | keyname: "docuploader_service_account" 7 | } 8 | } 9 | } 10 | 11 | # doc publications use a Python image. 12 | env_vars: { 13 | key: "TRAMPOLINE_IMAGE" 14 | value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" 15 | } 16 | 17 | # Download trampoline resources. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 19 | 20 | # Use the trampoline script to run in docker. 21 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 22 | 23 | env_vars: { 24 | key: "TRAMPOLINE_BUILD_FILE" 25 | value: "github/code-suggester/.kokoro/release/docs.sh" 26 | } 27 | -------------------------------------------------------------------------------- /.kokoro/release/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | # build jsdocs (Python is installed on the Node 10 docker image). 20 | if [[ -z "$CREDENTIALS" ]]; then 21 | # if CREDENTIALS are explicitly set, assume we're testing locally 22 | # and don't set NPM_CONFIG_PREFIX. 23 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 24 | export PATH="$PATH:${NPM_CONFIG_PREFIX}/bin" 25 | cd $(dirname $0)/../.. 26 | fi 27 | npm install 28 | npm run docs 29 | 30 | # create docs.metadata, based on package.json and .repo-metadata.json. 31 | npm i json@9.0.6 -g 32 | python3 -m docuploader create-metadata \ 33 | --name=$(cat .repo-metadata.json | json name) \ 34 | --version=$(cat package.json | json version) \ 35 | --language=$(cat .repo-metadata.json | json language) \ 36 | --distribution-name=$(cat .repo-metadata.json | json distribution_name) \ 37 | --product-page=$(cat .repo-metadata.json | json product_documentation) \ 38 | --github-repository=$(cat .repo-metadata.json | json repo) \ 39 | --issue-tracker=$(cat .repo-metadata.json | json issue_tracker) 40 | cp docs.metadata ./docs/docs.metadata 41 | 42 | # deploy the docs. 43 | if [[ -z "$CREDENTIALS" ]]; then 44 | CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account 45 | fi 46 | if [[ -z "$BUCKET" ]]; then 47 | BUCKET=docs-staging 48 | fi 49 | python3 -m docuploader upload ./docs --credentials $CREDENTIALS --staging-bucket $BUCKET 50 | -------------------------------------------------------------------------------- /.kokoro/release/publish.cfg: -------------------------------------------------------------------------------- 1 | before_action { 2 | fetch_keystore { 3 | keystore_resource { 4 | keystore_config_id: 73713 5 | keyname: "docuploader_service_account" 6 | } 7 | } 8 | } 9 | 10 | before_action { 11 | fetch_keystore { 12 | keystore_resource { 13 | keystore_config_id: 73713 14 | keyname: "google-cloud-npm-token-1" 15 | } 16 | } 17 | } 18 | 19 | env_vars: { 20 | key: "SECRET_MANAGER_KEYS" 21 | value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" 22 | } 23 | 24 | # Use the trampoline script to run in docker. 25 | build_file: "code-suggester/.kokoro/trampoline_v2.sh" 26 | 27 | # Configure the docker image for kokoro-trampoline. 28 | env_vars: { 29 | key: "TRAMPOLINE_IMAGE" 30 | value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/node14" 31 | } 32 | 33 | env_vars: { 34 | key: "TRAMPOLINE_BUILD_FILE" 35 | value: "github/code-suggester/.kokoro/publish.sh" 36 | } 37 | 38 | # Store the packages we uploaded to npmjs.org and their corresponding 39 | # package-lock.jsons in Placer. That way, we have a record of exactly 40 | # what we published, and which version of which tools we used to publish 41 | # it, which we can use to generate SBOMs and attestations. 42 | action { 43 | define_artifacts { 44 | regex: "github/**/*.tgz" 45 | regex: "github/**/package-lock.json" 46 | strip_prefix: "github" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.kokoro/samples-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | # Setup service account credentials. 22 | export GCLOUD_PROJECT=long-door-651 23 | 24 | cd $(dirname $0)/.. 25 | 26 | # Run a pre-test hook, if a pre-samples-test.sh is in the project 27 | if [ -f .kokoro/pre-samples-test.sh ]; then 28 | set +x 29 | . .kokoro/pre-samples-test.sh 30 | set -x 31 | fi 32 | 33 | if [ -f samples/package.json ]; then 34 | npm install 35 | 36 | # Install and link samples 37 | cd samples/ 38 | npm link ../ 39 | npm install 40 | cd .. 41 | # If tests are running against main branch, configure flakybot 42 | # to open issues on failures: 43 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then 44 | export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml 45 | export MOCHA_REPORTER=xunit 46 | cleanup() { 47 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot 48 | $KOKORO_GFILE_DIR/linux_amd64/flakybot 49 | } 50 | trap cleanup EXIT HUP 51 | fi 52 | 53 | npm run samples-test 54 | fi 55 | 56 | # codecov combines coverage across integration and unit tests. Include 57 | # the logic below for any environment you wish to collect coverage for: 58 | COVERAGE_NODE=14 59 | if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then 60 | NYC_BIN=./node_modules/nyc/bin/nyc.js 61 | if [ -f "$NYC_BIN" ]; then 62 | $NYC_BIN report || true 63 | fi 64 | bash $KOKORO_GFILE_DIR/codecov.sh 65 | else 66 | echo "coverage is only reported for Node $COVERAGE_NODE" 67 | fi 68 | -------------------------------------------------------------------------------- /.kokoro/system-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | # Setup service account credentials. 22 | export GCLOUD_PROJECT=long-door-651 23 | 24 | cd $(dirname $0)/.. 25 | 26 | # Run a pre-test hook, if a pre-system-test.sh is in the project 27 | if [ -f .kokoro/pre-system-test.sh ]; then 28 | set +x 29 | . .kokoro/pre-system-test.sh 30 | set -x 31 | fi 32 | 33 | npm install 34 | 35 | # If tests are running against main branch, configure flakybot 36 | # to open issues on failures: 37 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then 38 | export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml 39 | export MOCHA_REPORTER=xunit 40 | cleanup() { 41 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot 42 | $KOKORO_GFILE_DIR/linux_amd64/flakybot 43 | } 44 | trap cleanup EXIT HUP 45 | fi 46 | 47 | npm run system-test 48 | 49 | # codecov combines coverage across integration and unit tests. Include 50 | # the logic below for any environment you wish to collect coverage for: 51 | COVERAGE_NODE=14 52 | if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then 53 | NYC_BIN=./node_modules/nyc/bin/nyc.js 54 | if [ -f "$NYC_BIN" ]; then 55 | $NYC_BIN report || true 56 | fi 57 | bash $KOKORO_GFILE_DIR/codecov.sh 58 | else 59 | echo "coverage is only reported for Node $COVERAGE_NODE" 60 | fi 61 | -------------------------------------------------------------------------------- /.kokoro/test.bat: -------------------------------------------------------------------------------- 1 | @rem Copyright 2018 Google LLC. All rights reserved. 2 | @rem 3 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 4 | @rem you may not use this file except in compliance with the License. 5 | @rem You may obtain a copy of the License at 6 | @rem 7 | @rem http://www.apache.org/licenses/LICENSE-2.0 8 | @rem 9 | @rem Unless required by applicable law or agreed to in writing, software 10 | @rem distributed under the License is distributed on an "AS IS" BASIS, 11 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | @rem See the License for the specific language governing permissions and 13 | @rem limitations under the License. 14 | 15 | @echo "Starting Windows build" 16 | 17 | cd /d %~dp0 18 | cd .. 19 | 20 | @rem npm path is not currently set in our image, we should fix this next time 21 | @rem we upgrade Node.js in the image: 22 | SET PATH=%PATH%;/cygdrive/c/Program Files/nodejs/npm 23 | 24 | call nvm use v14.17.3 25 | call which node 26 | 27 | call npm install || goto :error 28 | call npm run test || goto :error 29 | 30 | goto :EOF 31 | 32 | :error 33 | exit /b 1 34 | -------------------------------------------------------------------------------- /.kokoro/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | export NPM_CONFIG_PREFIX=${HOME}/.npm-global 20 | 21 | cd $(dirname $0)/.. 22 | 23 | npm install 24 | # If tests are running against main branch, configure flakybot 25 | # to open issues on failures: 26 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]] || [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"nightly"* ]]; then 27 | export MOCHA_REPORTER_OUTPUT=test_output_sponge_log.xml 28 | export MOCHA_REPORTER=xunit 29 | cleanup() { 30 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot 31 | $KOKORO_GFILE_DIR/linux_amd64/flakybot 32 | } 33 | trap cleanup EXIT HUP 34 | fi 35 | # Unit tests exercise the entire API surface, which may include 36 | # deprecation warnings: 37 | export MOCHA_THROW_DEPRECATION=false 38 | npm test 39 | 40 | # codecov combines coverage across integration and unit tests. Include 41 | # the logic below for any environment you wish to collect coverage for: 42 | COVERAGE_NODE=14 43 | if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then 44 | NYC_BIN=./node_modules/nyc/bin/nyc.js 45 | if [ -f "$NYC_BIN" ]; then 46 | $NYC_BIN report || true 47 | fi 48 | bash $KOKORO_GFILE_DIR/codecov.sh 49 | else 50 | echo "coverage is only reported for Node $COVERAGE_NODE" 51 | fi 52 | -------------------------------------------------------------------------------- /.kokoro/trampoline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2017 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This file is not used any more, but we keep this file for making it 17 | # easy to roll back. 18 | # TODO: Remove this file from the template. 19 | 20 | set -eo pipefail 21 | 22 | # Always run the cleanup script, regardless of the success of bouncing into 23 | # the container. 24 | function cleanup() { 25 | chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh 26 | ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh 27 | echo "cleanup"; 28 | } 29 | trap cleanup EXIT 30 | 31 | $(dirname $0)/populate-secrets.sh # Secret Manager secrets. 32 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" 33 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | const config = { 15 | "enable-source-maps": true, 16 | "throw-deprecation": true, 17 | "timeout": 10000, 18 | "recursive": true 19 | } 20 | if (process.env.MOCHA_THROW_DEPRECATION === 'false') { 21 | delete config['throw-deprecation']; 22 | } 23 | if (process.env.MOCHA_REPORTER) { 24 | config.reporter = process.env.MOCHA_REPORTER; 25 | } 26 | if (process.env.MOCHA_REPORTER_OUTPUT) { 27 | config['reporter-option'] = `output=${process.env.MOCHA_REPORTER_OUTPUT}`; 28 | } 29 | module.exports = config 30 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "report-dir": "./.coverage", 3 | "reporter": ["text", "lcov"], 4 | "exclude": [ 5 | "**/*-test", 6 | "**/.coverage", 7 | "**/apis", 8 | "**/benchmark", 9 | "**/conformance", 10 | "**/docs", 11 | "**/samples", 12 | "**/scripts", 13 | "**/protos", 14 | "**/test", 15 | "**/*.d.ts", 16 | ".jsdoc.js", 17 | "**/.jsdoc.js", 18 | "karma.conf.js", 19 | "webpack-tests.config.js", 20 | "webpack.config.js" 21 | ], 22 | "exclude-after-remap": false, 23 | "all": true 24 | } 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | test/fixtures 4 | build/ 5 | docs/ 6 | protos/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports = { 16 | ...require('gts/.prettierrc.json') 17 | } 18 | -------------------------------------------------------------------------------- /.repo-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-suggester", 3 | "name_pretty": "GitHub Code Suggester", 4 | "release_level": "alpha", 5 | "language": "nodejs", 6 | "repo": "googleapis/code-suggester", 7 | "distribution_name": "@google-cloud/code-suggester", 8 | "codeowner_team": "@googleapis/github-automation", 9 | "client_documentation": "https://cloud.google.com/nodejs/docs/reference/code-suggester/latest" 10 | } 11 | -------------------------------------------------------------------------------- /.trampolinerc: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Template for .trampolinerc 16 | 17 | # Add required env vars here. 18 | required_envvars+=( 19 | ) 20 | 21 | # Add env vars which are passed down into the container here. 22 | pass_down_envvars+=( 23 | "AUTORELEASE_PR" 24 | "VERSION" 25 | ) 26 | 27 | # Prevent unintentional override on the default image. 28 | if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \ 29 | [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 30 | echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." 31 | exit 1 32 | fi 33 | 34 | # Define the default value if it makes sense. 35 | if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then 36 | TRAMPOLINE_IMAGE_UPLOAD="" 37 | fi 38 | 39 | if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 40 | TRAMPOLINE_IMAGE="" 41 | fi 42 | 43 | if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then 44 | TRAMPOLINE_DOCKERFILE="" 45 | fi 46 | 47 | if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then 48 | TRAMPOLINE_BUILD_FILE="" 49 | fi 50 | 51 | # Secret Manager secrets. 52 | TRAMPOLINE_HOST=$(echo "${TRAMPOLINE_IMAGE}" | cut -d/ -f1) 53 | if [[ ! "${TRAMPOLINE_HOST}" =~ "gcr.io" ]] 54 | then 55 | echo "TRAMPOLINE_HOST: ${TRAMPOLINE_HOST}" 56 | gcloud components update 57 | gcloud auth configure-docker "${TRAMPOLINE_HOST}" 58 | fi 59 | source ${PROJECT_ROOT}/.kokoro/populate-secrets.sh 60 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to making participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, gender identity and expression, level of 10 | experience, education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 44 | contributor for other behaviors that they deem inappropriate, threatening, 45 | offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. Representation of a project may be 54 | further defined and clarified by project maintainers. 55 | 56 | This Code of Conduct also applies outside the project spaces when the Project 57 | Steward has a reasonable belief that an individual's behavior may have a 58 | negative impact on the project or its community. 59 | 60 | ## Conflict Resolution 61 | 62 | We do not believe that all conflict is bad; healthy debate and disagreement 63 | often yield positive results. However, it is never okay to be disrespectful or 64 | to engage in behavior that violates the project’s code of conduct. 65 | 66 | If you see someone violating the code of conduct, you are encouraged to address 67 | the behavior directly with those involved. Many issues can be resolved quickly 68 | and easily, and this gives people more control over the outcome of their 69 | dispute. If you are unable to resolve the matter for any reason, or if the 70 | behavior is threatening or harassing, report it. We are dedicated to providing 71 | an environment where participants feel welcome and safe. 72 | 73 | Reports should be directed to *googleapis-stewards@google.com*, the 74 | Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to 75 | receive and address reported violations of the code of conduct. They will then 76 | work with a committee consisting of representatives from the Open Source 77 | Programs Office and the Google Open Source Strategy team. If for any reason you 78 | are uncomfortable reaching out to the Project Steward, please email 79 | opensource@google.com. 80 | 81 | We will investigate every complaint, but you may not receive a direct response. 82 | We will use our discretion in determining when and how to follow up on reported 83 | incidents, which may range from not taking action to permanent expulsion from 84 | the project and project-sponsored spaces. We will notify the accused of the 85 | report and provide them an opportunity to discuss it before any action is taken. 86 | The identity of the reporter will be omitted from the details of the report 87 | supplied to the accused. In potentially harmful situations, such as ongoing 88 | harassment or threats to anyone's safety, we may take action without notice. 89 | 90 | ## Attribution 91 | 92 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 93 | available at 94 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | **Table of contents** 4 | 5 | * [Contributor License Agreements](#contributor-license-agreements) 6 | * [Contributing a patch](#contributing-a-patch) 7 | * [Running the tests](#running-the-tests) 8 | * [Releasing the library](#releasing-the-library) 9 | 10 | ## Contributor License Agreements 11 | 12 | We'd love to accept your sample apps and patches! Before we can take them, we 13 | have to jump a couple of legal hurdles. 14 | 15 | Please fill out either the individual or corporate Contributor License Agreement 16 | (CLA). 17 | 18 | * If you are an individual writing original source code and you're sure you 19 | own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). 20 | * If you work for a company that wants to allow you to contribute your work, 21 | then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). 22 | 23 | Follow either of the two links above to access the appropriate CLA and 24 | instructions for how to sign and return it. Once we receive it, we'll be able to 25 | accept your pull requests. 26 | 27 | ## Contributing A Patch 28 | 29 | 1. Submit an issue describing your proposed change to the repo in question. 30 | 1. The repo owner will respond to your issue promptly. 31 | 1. If your proposed change is accepted, and you haven't already done so, sign a 32 | Contributor License Agreement (see details above). 33 | 1. Fork the desired repo, develop and test your code changes. 34 | 1. Ensure that your code adheres to the existing style in the code to which 35 | you are contributing. 36 | 1. Ensure that your code has an appropriate set of tests which all pass. 37 | 1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. 38 | 1. Submit a pull request. 39 | 40 | ### Before you begin 41 | 42 | 1. [Select or create a Cloud Platform project][projects]. 43 | 1. [Set up authentication with a service account][auth] so you can access the 44 | API from your local workstation. 45 | 46 | 47 | ## Running the tests 48 | 49 | 1. [Prepare your environment for Node.js setup][setup]. 50 | 51 | 1. Install dependencies: 52 | 53 | npm install 54 | 55 | 1. Run the tests: 56 | 57 | # Run unit tests. 58 | npm test 59 | 60 | # Run sample integration tests. 61 | npm run samples-test 62 | 63 | # Run all system tests. 64 | npm run system-test 65 | 66 | 1. Lint (and maybe fix) any changes: 67 | 68 | npm run fix 69 | 70 | [setup]: https://cloud.google.com/nodejs/docs/setup 71 | [projects]: https://console.cloud.google.com/project 72 | [billing]: https://support.google.com/cloud/answer/6293499#enable-billing 73 | 74 | [auth]: https://cloud.google.com/docs/authentication/getting-started -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. 8 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Code-Suggester 16 | author: googleapis 17 | description: An action-wrapper for the npm cli code-suggester. It suggests code to your repository 18 | inputs: 19 | command: 20 | description: 'The command for code-suggester to run. Can be "pr" or "review"' 21 | required: true 22 | upstream_repo: 23 | description: 'The repository to create the fork off of.' 24 | upstream_owner: 25 | description: 'The owner of the upstream repository.' 26 | description: 27 | description: 'The GitHub Pull Request description. Required for "pr" command.' 28 | title: 29 | description: 'The GitHub Pull Request title. Required for "pr" command.' 30 | message: 31 | description: 'The GitHub commit message. Required for "pr" command.' 32 | branch: 33 | description: 'The GitHub working branch name. Required for "pr" command.' 34 | primary: 35 | description: 'The primary upstream branch to open a PR against.' 36 | default: main 37 | force: 38 | description: >- 39 | Whether or not to force push a reference with different commit history 40 | before the remote reference HEAD. Default is false 41 | default: false 42 | maintainers_can_modify: 43 | description: 'Whether or not maintainers can modify the pull request.' 44 | default: true 45 | git_dir: 46 | description: 'The path of a git directory' 47 | default: . 48 | fork: 49 | description: >- 50 | Whether or not to attempt forking to a separate repository. Default is true. 51 | default: true 52 | pull_number: 53 | description: Pull Request number to review. Required for "review" command. 54 | labels: 55 | description: Labels to add to the pull request being opened. 56 | runs: 57 | using: 'node20' 58 | main: 'action/dist/index.js' 59 | -------------------------------------------------------------------------------- /action/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | test/fixtures 4 | build/ 5 | docs/ 6 | protos/ 7 | samples/generated/ 8 | dist/ 9 | -------------------------------------------------------------------------------- /action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-suggester-action", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Code Suggester action", 6 | "main": "build/src/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "build-dist": "ncc build --source-map --license licenses.txt", 10 | "fix": "gts fix", 11 | "lint": "gts check", 12 | "package": "npm run build && npm run build-dist", 13 | "test": "c8 mocha build/test", 14 | "pretest": "npm run build", 15 | "all": "npm run build && npm run fix && npm run lint && npm run package && npm test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/googleapis/code-suggester.git" 20 | }, 21 | "keywords": [ 22 | "actions", 23 | "node", 24 | "setup" 25 | ], 26 | "author": "Google LLC", 27 | "license": "Apache-2.0", 28 | "dependencies": { 29 | "@actions/core": "^1.10.0", 30 | "@octokit/rest": "^20.1.2", 31 | "code-suggester": "file:.." 32 | }, 33 | "devDependencies": { 34 | "@types/mocha": "^10.0.0", 35 | "@types/node": "^18.11.9", 36 | "@types/sinon": "^10.0.13", 37 | "@vercel/ncc": "^0.34.0", 38 | "c8": "^7.12.0", 39 | "gts": "^3.1.1", 40 | "js-yaml": "^4.1.0", 41 | "mocha": "^10.1.0", 42 | "sinon": "^15.0.0", 43 | "ts-jest": "^29.2.6", 44 | "typescript": "^5.7.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /action/src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as core from '@actions/core'; 16 | import { 17 | createPullRequest, 18 | reviewPullRequest, 19 | getChanges, 20 | getDiffString, 21 | CreatePullRequestUserOptions, 22 | CreateReviewCommentUserOptions, 23 | } from 'code-suggester'; 24 | import {Octokit} from '@octokit/rest'; 25 | 26 | export async function run(): Promise { 27 | try { 28 | const command = core.getInput('command', {required: true}); 29 | const gitDir = core.getInput('git_dir'); 30 | const octokit = new Octokit({auth: process.env.ACCESS_TOKEN}); 31 | switch (command) { 32 | case 'pr': { 33 | const options = parsePullRequestOptions(); 34 | const changes = await getChanges(gitDir); 35 | const pullRequestNumber = await createPullRequest( 36 | octokit, 37 | changes, 38 | options 39 | ); 40 | if (pullRequestNumber) { 41 | core.setOutput('pull', pullRequestNumber); 42 | } 43 | break; 44 | } 45 | case 'review': { 46 | const options = parseReviewOptions(); 47 | const changes = await getDiffString(gitDir); 48 | const reviewNumber = await reviewPullRequest(octokit, changes, options); 49 | if (reviewNumber) { 50 | core.setOutput('review', reviewNumber); 51 | } 52 | break; 53 | } 54 | default: { 55 | throw new Error(`Unsupported command '${command}'`); 56 | } 57 | } 58 | } catch (error) { 59 | console.log(error); 60 | if (error instanceof Error) core.setFailed(error.message); 61 | } 62 | } 63 | 64 | function parseRepository(): {owner: string; repo: string} { 65 | let owner: string = core.getInput('upstream_owner'); 66 | if (!owner) { 67 | owner = process.env['GITHUB_REPOSITORY']?.split('/')[0] || ''; 68 | } 69 | let repo = core.getInput('upstream_repo'); 70 | if (!repo) { 71 | repo = process.env['GITHUB_REPOSITORY']?.split('/')[1] || ''; 72 | } 73 | return {owner, repo}; 74 | } 75 | 76 | function parsePullRequestOptions(): CreatePullRequestUserOptions { 77 | const {owner, repo} = parseRepository(); 78 | return { 79 | upstreamOwner: owner, 80 | upstreamRepo: repo, 81 | message: core.getInput('message', {required: true}), 82 | description: core.getInput('description', {required: true}), 83 | title: core.getInput('title', {required: true}), 84 | branch: core.getInput('branch', {required: true}), 85 | force: core.getBooleanInput('force'), 86 | primary: core.getInput('primary'), 87 | maintainersCanModify: core.getBooleanInput('maintainers_can_modify'), 88 | fork: core.getBooleanInput('fork'), 89 | labels: core.getMultilineInput('labels'), 90 | logger: console, 91 | }; 92 | } 93 | 94 | function parseReviewOptions(): CreateReviewCommentUserOptions { 95 | const {owner, repo} = parseRepository(); 96 | return { 97 | owner, 98 | repo, 99 | pullNumber: parseInt(core.getInput('pull_number', {required: true})), 100 | logger: console, 101 | }; 102 | } 103 | 104 | if (require.main === module) { 105 | run(); 106 | } 107 | -------------------------------------------------------------------------------- /action/test/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unpublished-import */ 16 | import {describe, it, beforeEach, afterEach} from 'mocha'; 17 | import {run as main} from '../src/main'; 18 | import * as assert from 'assert'; 19 | import * as sinon from 'sinon'; 20 | import * as core from '@actions/core'; 21 | import * as codeSuggesterModule from 'code-suggester'; 22 | 23 | const sandbox = sinon.createSandbox(); 24 | 25 | describe('pr', () => { 26 | let createPullRequestStub: sinon.SinonStub; 27 | let setOutputStub: sinon.SinonStub; 28 | beforeEach(() => { 29 | createPullRequestStub = sandbox.stub( 30 | codeSuggesterModule, 31 | 'createPullRequest' 32 | ); 33 | setOutputStub = sandbox.stub(core, 'setOutput'); 34 | }); 35 | afterEach(() => { 36 | sandbox.restore(); 37 | }); 38 | it('parses inputs', async () => { 39 | createPullRequestStub.resolves(123); 40 | await withInputs( 41 | { 42 | command: 'pr', 43 | upstream_owner: 'testOwner', 44 | upstream_repo: 'testRepo', 45 | message: 'test message', 46 | title: 'test title', 47 | description: 'test description', 48 | branch: 'test-branch', 49 | force: 'true', 50 | fork: 'true', 51 | maintainers_can_modify: 'true', 52 | labels: 'abc', 53 | }, 54 | main 55 | ); 56 | 57 | sinon.assert.calledOnceWithMatch(createPullRequestStub); 58 | sinon.assert.calledOnceWithExactly(setOutputStub, 'pull', 123); 59 | const options = createPullRequestStub.firstCall 60 | .args[2] as codeSuggesterModule.CreatePullRequestUserOptions; 61 | assert.strictEqual(options.upstreamOwner, 'testOwner'); 62 | assert.strictEqual(options.upstreamRepo, 'testRepo'); 63 | assert.strictEqual(options.message, 'test message'); 64 | assert.strictEqual(options.title, 'test title'); 65 | assert.strictEqual(options.branch, 'test-branch'); 66 | assert.ok(options.force); 67 | assert.ok(options.fork); 68 | assert.ok(options.maintainersCanModify); 69 | assert.deepStrictEqual(options.labels, ['abc']); 70 | }); 71 | it('parses multiple labels', async () => { 72 | createPullRequestStub.resolves(123); 73 | await withInputs( 74 | { 75 | command: 'pr', 76 | upstream_owner: 'testOwner', 77 | upstream_repo: 'testRepo', 78 | message: 'test message', 79 | title: 'test title', 80 | description: 'test description', 81 | branch: 'test-branch', 82 | force: 'true', 83 | fork: 'true', 84 | maintainers_can_modify: 'true', 85 | labels: 'foo\nbar', 86 | }, 87 | main 88 | ); 89 | 90 | sinon.assert.calledOnceWithMatch(createPullRequestStub); 91 | sinon.assert.calledOnceWithExactly(setOutputStub, 'pull', 123); 92 | const options = createPullRequestStub.firstCall 93 | .args[2] as codeSuggesterModule.CreatePullRequestUserOptions; 94 | assert.deepStrictEqual(options.labels, ['foo', 'bar']); 95 | }); 96 | it('parses default repository', async () => { 97 | createPullRequestStub.resolves(123); 98 | await withEnv( 99 | { 100 | GITHUB_REPOSITORY: 'testOwner/testRepo', 101 | }, 102 | async () => { 103 | await withInputs( 104 | { 105 | command: 'pr', 106 | message: 'test message', 107 | title: 'test title', 108 | description: 'test description', 109 | branch: 'test-branch', 110 | force: 'true', 111 | fork: 'true', 112 | maintainers_can_modify: 'true', 113 | }, 114 | main 115 | ); 116 | } 117 | ); 118 | 119 | sinon.assert.calledOnceWithMatch(createPullRequestStub); 120 | sinon.assert.calledOnceWithExactly(setOutputStub, 'pull', 123); 121 | const options = createPullRequestStub.firstCall 122 | .args[2] as codeSuggesterModule.CreatePullRequestUserOptions; 123 | assert.strictEqual(options.upstreamOwner, 'testOwner'); 124 | assert.strictEqual(options.upstreamRepo, 'testRepo'); 125 | }); 126 | }); 127 | 128 | describe('review', () => { 129 | let reviewPullRequestStub: sinon.SinonStub; 130 | let setOutputStub: sinon.SinonStub; 131 | beforeEach(() => { 132 | reviewPullRequestStub = sandbox.stub( 133 | codeSuggesterModule, 134 | 'reviewPullRequest' 135 | ); 136 | setOutputStub = sandbox.stub(core, 'setOutput'); 137 | }); 138 | afterEach(() => { 139 | sandbox.restore(); 140 | }); 141 | it('parses inputs', async () => { 142 | reviewPullRequestStub.resolves(234); 143 | await withInputs( 144 | { 145 | command: 'review', 146 | upstream_owner: 'testOwner', 147 | upstream_repo: 'testRepo', 148 | pull_number: '123', 149 | }, 150 | main 151 | ); 152 | 153 | sinon.assert.calledOnceWithMatch(reviewPullRequestStub); 154 | sinon.assert.calledOnceWithExactly(setOutputStub, 'review', 234); 155 | const options = reviewPullRequestStub.firstCall 156 | .args[2] as codeSuggesterModule.CreateReviewCommentUserOptions; 157 | assert.strictEqual(options.owner, 'testOwner'); 158 | assert.strictEqual(options.repo, 'testRepo'); 159 | assert.strictEqual(options.pullNumber, 123); 160 | }); 161 | it('parses default repository', async () => { 162 | reviewPullRequestStub.resolves(234); 163 | await withEnv( 164 | { 165 | GITHUB_REPOSITORY: 'testOwner/testRepo', 166 | }, 167 | async () => { 168 | await withInputs( 169 | { 170 | command: 'review', 171 | pull_number: '123', 172 | }, 173 | main 174 | ); 175 | } 176 | ); 177 | 178 | sinon.assert.calledOnceWithMatch(reviewPullRequestStub); 179 | sinon.assert.calledOnceWithExactly(setOutputStub, 'review', 234); 180 | const options = reviewPullRequestStub.firstCall 181 | .args[2] as codeSuggesterModule.CreateReviewCommentUserOptions; 182 | assert.strictEqual(options.owner, 'testOwner'); 183 | assert.strictEqual(options.repo, 'testRepo'); 184 | assert.strictEqual(options.pullNumber, 123); 185 | }); 186 | }); 187 | 188 | async function withEnv( 189 | variables: Record, 190 | callback: () => Promise 191 | ) { 192 | const originalEnv = Object.assign({}, process.env); 193 | try { 194 | for (const key in variables) { 195 | process.env[key] = variables[key]; 196 | } 197 | await callback(); 198 | } finally { 199 | process.env = originalEnv; 200 | } 201 | } 202 | 203 | async function withInputs( 204 | inputs: Record, 205 | callback: () => Promise 206 | ) { 207 | const variables: Record = {}; 208 | for (const key in inputs) { 209 | variables['INPUT_' + key.toUpperCase()] = inputs[key]; 210 | } 211 | await withEnv(variables, callback); 212 | } 213 | -------------------------------------------------------------------------------- /action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "build", 6 | "skipLibCheck": true, 7 | "resolveJsonModule": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ] 12 | }, 13 | "include": [ 14 | "src/*.ts", 15 | "src/**/*.ts", 16 | "test/*.ts", 17 | "test/**/*.ts", 18 | "system-test/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /linkinator.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "recurse": true, 3 | "skip": [ 4 | "https://codecov.io/gh/googleapis/", 5 | "www.googleapis.com", 6 | "img.shields.io" 7 | ], 8 | "silent": true, 9 | "concurrency": 10 10 | } 11 | -------------------------------------------------------------------------------- /owlbot.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import synthtool.languages.node as node 16 | 17 | node.owlbot_main(templates_excludes=[ 18 | 'README.md', 19 | '.github/CODEOWNERS', 20 | '.github/release-please.yml', 21 | 'renovate.json', 22 | '.eslintignore', 23 | '.eslintrc.json', 24 | '.prettierignore', 25 | ]) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-suggester", 3 | "description": "Library to propose code changes", 4 | "version": "5.0.0", 5 | "license": "Apache-2.0", 6 | "author": "Google LLC", 7 | "engines": { 8 | "node": ">=18.0.0" 9 | }, 10 | "bin": { 11 | "code-suggester": "./build/src/bin/code-suggester.js" 12 | }, 13 | "repository": "googleapis/code-suggester", 14 | "main": "build/src/index.js", 15 | "module": "build/src/index.js", 16 | "source": "build/src/index.js", 17 | "types": "build/src/index.d.ts", 18 | "files": [ 19 | "build/src" 20 | ], 21 | "keywords": [ 22 | "google", 23 | "google cloud platform", 24 | "google cloud" 25 | ], 26 | "scripts": { 27 | "samples-test": "echo 😱", 28 | "clean": "gts clean", 29 | "compile": "tsc -p .", 30 | "docs": "jsdoc -c .jsdoc.js", 31 | "docs-test": "linkinator docs", 32 | "fix": "gts fix", 33 | "lint": "gts check", 34 | "predocs-test": "npm run docs", 35 | "prepare": "npm run compile", 36 | "presystem-test": "npm run compile", 37 | "system-test": "c8 mocha build/system-test", 38 | "test": "c8 mocha build/test", 39 | "pretest": "npm run compile" 40 | }, 41 | "dependencies": { 42 | "@octokit/rest": "^20.1.2", 43 | "@types/yargs": "^16.0.0", 44 | "async-retry": "^1.3.1", 45 | "diff": "^5.0.0", 46 | "glob": "^7.1.6", 47 | "parse-diff": "^0.11.0", 48 | "yargs": "^16.0.0" 49 | }, 50 | "devDependencies": { 51 | "@octokit/types": "^9.0.0", 52 | "@types/async-retry": "^1.4.2", 53 | "@types/diff": "^5.0.0", 54 | "@types/mocha": "^9.0.0", 55 | "@types/node": "^18.0.0", 56 | "@types/sinon": "^10.0.0", 57 | "c8": "^7.0.1", 58 | "gts": "^3.1.0", 59 | "jsdoc": "^4.0.0", 60 | "jsdoc-region-tag": "^2.0.0", 61 | "linkinator": "^4.0.0", 62 | "mocha": "^11.1.0", 63 | "nock": "^13.0.2", 64 | "null-loader": "^4.0.0", 65 | "sinon": "^15.0.0", 66 | "ts-loader": "^8.0.0", 67 | "typescript": "^5.7.3", 68 | "webpack": "^5.0.0", 69 | "webpack-cli": "^5.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/bin/code-suggester.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright 2020 Google LLC 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // https://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import * as yargs from 'yargs'; 18 | import {CREATE_PR_COMMAND, REVIEW_PR_COMMAND, main} from './workflow'; 19 | import {logger} from '../logger'; 20 | 21 | yargs 22 | .scriptName('code-suggester') 23 | .usage('$0 [args]') 24 | .command(CREATE_PR_COMMAND, 'Create a new pull request', { 25 | 'upstream-repo': { 26 | alias: 'r', 27 | demandOption: true, 28 | describe: 'Required. The repository to create the fork off of.', 29 | type: 'string', 30 | }, 31 | 'upstream-owner': { 32 | alias: 'o', 33 | demandOption: true, 34 | describe: 'Required. The owner of the upstream repository.', 35 | type: 'string', 36 | }, 37 | description: { 38 | alias: 'd', 39 | demandOption: true, 40 | describe: 'Required. The GitHub Pull Request description', 41 | type: 'string', 42 | }, 43 | title: { 44 | alias: 't', 45 | demandOption: true, 46 | describe: 'Required. The title of the Pull Request.', 47 | type: 'string', 48 | }, 49 | branch: { 50 | alias: 'b', 51 | describe: 'The name of the working branch to apply changes to.', 52 | default: 'code-suggestion', 53 | type: 'string', 54 | }, 55 | message: { 56 | alias: 'm', 57 | demandOption: true, 58 | describe: 'Required. The GitHub commit message.', 59 | type: 'string', 60 | }, 61 | primary: { 62 | alias: 'p', 63 | describe: 64 | "The primary upstream branch to open a Pull Request against. Default is 'main'.", 65 | default: 'main', 66 | type: 'string', 67 | }, 68 | force: { 69 | alias: 'f', 70 | describe: 71 | 'Whether or not to force push the current reference HEAD against the remote reference HEAD. Default is false.', 72 | default: false, 73 | type: 'boolean', 74 | }, 75 | 'maintainers-can-modify': { 76 | alias: 'modify', 77 | describe: 78 | 'Whether or not maintainers can modify the pull request. Default is true.', 79 | default: true, 80 | type: 'boolean', 81 | }, 82 | 'git-dir': { 83 | describe: 84 | 'Required. The location of any un-tracked changes that should be made into a pull request. Files in the .gitignore are ignored.', 85 | type: 'string', 86 | demandOption: true, 87 | }, 88 | fork: { 89 | describe: 90 | 'Whether or not to attempt forking to a separate repository. Default is true.', 91 | default: true, 92 | type: 'boolean', 93 | }, 94 | labels: { 95 | describe: 96 | 'The list of labels to add to the pull request. Default is none.', 97 | default: [], 98 | type: 'array', 99 | }, 100 | 'files-per-commit': { 101 | describe: 'Number of files per commit. Defaults to 100', 102 | default: 100, 103 | type: 'number', 104 | }, 105 | }) 106 | .command(REVIEW_PR_COMMAND, 'Review an open pull request', { 107 | 'upstream-repo': { 108 | alias: 'r', 109 | demandOption: true, 110 | describe: 'Required. The repository to create the fork off of.', 111 | type: 'string', 112 | }, 113 | 'upstream-owner': { 114 | alias: 'o', 115 | demandOption: true, 116 | describe: 'Required. The owner of the upstream repository.', 117 | type: 'string', 118 | }, 119 | 'pull-number': { 120 | alias: 'p', 121 | demandOption: true, 122 | describe: 'Required. The pull request number to comment on.', 123 | type: 'number', 124 | }, 125 | 'git-dir': { 126 | describe: 127 | 'Required. The location of any un-tracked changes that should be made into pull request comments. Files in the .gitignore are ignored.', 128 | type: 'string', 129 | demandOption: true, 130 | }, 131 | }) 132 | .check(argv => { 133 | for (const key in argv) { 134 | if (typeof argv[key] === 'string' && !argv[key]) { 135 | throw Error( 136 | `String parameters cannot be provided empty values. Parameter ${key} was given an empty value` 137 | ); 138 | } 139 | } 140 | return true; 141 | }) 142 | .demandCommand(1, 'A minimum of 1 command must be specified') 143 | .help().argv; 144 | 145 | /** 146 | * Parse yargs, get change object, invoke framework-core library! 147 | */ 148 | main().catch(err => { 149 | logger.error(err); 150 | /* eslint-disable no-process-exit */ 151 | // If just rethrow, the application exists with code 0. 152 | // Need exit code 1 to fail GitHub Actions step if the process fails. 153 | process.exit(1); 154 | }); 155 | -------------------------------------------------------------------------------- /src/bin/workflow.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | CreatePullRequestUserOptions, 17 | CreateReviewCommentUserOptions, 18 | } from '../types'; 19 | import {Octokit} from '@octokit/rest'; 20 | import * as git from './handle-git-dir-change'; 21 | import {createPullRequest, reviewPullRequest} from '../'; 22 | import {logger, setupLogger} from '../logger'; 23 | import * as yargs from 'yargs'; 24 | 25 | export const CREATE_PR_COMMAND = 'pr'; 26 | export const REVIEW_PR_COMMAND = 'review'; 27 | 28 | /** 29 | * map yargs to user pull request otions 30 | */ 31 | export function coerceUserCreatePullRequestOptions(): CreatePullRequestUserOptions { 32 | return { 33 | upstreamRepo: yargs.argv.upstreamRepo as string, 34 | upstreamOwner: yargs.argv.upstreamOwner as string, 35 | message: yargs.argv.message as string, 36 | description: yargs.argv.description as string, 37 | title: yargs.argv.title as string, 38 | branch: yargs.argv.branch as string, 39 | force: yargs.argv.force as boolean, 40 | primary: yargs.argv.primary as string, 41 | maintainersCanModify: yargs.argv.maintainersCanModify as boolean, 42 | fork: yargs.argv.fork as boolean, 43 | labels: yargs.argv.labels as string[], 44 | logger, 45 | filesPerCommit: yargs.argv.filesPerCommit as number, 46 | }; 47 | } 48 | 49 | /** 50 | * map yargs to user pull request otions 51 | */ 52 | export function coerceUserCreateReviewRequestOptions(): CreateReviewCommentUserOptions { 53 | return { 54 | repo: yargs.argv.upstreamRepo as string, 55 | owner: yargs.argv.upstreamOwner as string, 56 | pullNumber: yargs.argv.pullNumber as number, 57 | logger, 58 | }; 59 | } 60 | 61 | async function createCommand() { 62 | const options = coerceUserCreatePullRequestOptions(); 63 | const changes = await git.getChanges(yargs.argv['git-dir'] as string); 64 | const octokit = new Octokit({auth: process.env.ACCESS_TOKEN}); 65 | await createPullRequest(octokit, changes, options); 66 | } 67 | 68 | async function reviewCommand() { 69 | const reviewOptions = coerceUserCreateReviewRequestOptions(); 70 | const diffContents = await git.getDiffString(yargs.argv['git-dir'] as string); 71 | const octokit = new Octokit({auth: process.env.ACCESS_TOKEN}); 72 | await reviewPullRequest(octokit, diffContents, reviewOptions); 73 | } 74 | 75 | /** 76 | * main workflow entrance 77 | */ 78 | export async function main() { 79 | try { 80 | setupLogger(console); 81 | if (!process.env.ACCESS_TOKEN) { 82 | throw Error('The ACCESS_TOKEN should not be undefined'); 83 | } 84 | switch (yargs.argv._[0]) { 85 | case CREATE_PR_COMMAND: 86 | await createCommand(); 87 | break; 88 | case REVIEW_PR_COMMAND: 89 | await reviewCommand(); 90 | break; 91 | default: 92 | // yargs should have caught this. 93 | throw Error(`Unhandled command detected: ${yargs.argv._[0]}`); 94 | } 95 | } catch (err) { 96 | logger.error('Workflow failed'); 97 | throw err; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/default-options-handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | CreatePullRequest, 17 | CreatePullRequestUserOptions, 18 | CreateReviewComment, 19 | CreateReviewCommentUserOptions, 20 | } from './types'; 21 | 22 | const DEFAULT_BRANCH_NAME = 'code-suggestions'; 23 | const DEFAULT_PRIMARY_BRANCH = 'main'; 24 | const DEFAULT_PAGE_SIZE = 100; 25 | 26 | /** 27 | * Add defaults to GitHub Pull Request options. 28 | * Preserves the empty string. 29 | * For ESCMAScript, null/undefined values are preserved for required fields. 30 | * Recommended with an object validation function to check empty strings and incorrect types. 31 | * @param {PullRequestUserOptions} options the user-provided github pull request options 32 | * @returns {CreatePullRequest} git hub context with defaults applied 33 | */ 34 | export function addPullRequestDefaults( 35 | options: CreatePullRequestUserOptions 36 | ): CreatePullRequest { 37 | const pullRequestSettings: CreatePullRequest = { 38 | upstreamOwner: options.upstreamOwner, 39 | upstreamRepo: options.upstreamRepo, 40 | description: options.description, 41 | title: options.title, 42 | message: options.message, 43 | force: options.force || false, 44 | branch: 45 | typeof options.branch === 'string' ? options.branch : DEFAULT_BRANCH_NAME, 46 | primary: 47 | typeof options.primary === 'string' 48 | ? options.primary 49 | : DEFAULT_PRIMARY_BRANCH, 50 | maintainersCanModify: options.maintainersCanModify === false ? false : true, 51 | filesPerCommit: options.filesPerCommit, 52 | }; 53 | return pullRequestSettings; 54 | } 55 | 56 | /** 57 | * Format user input for pull request review comments 58 | * @param options The user's options input for review comments 59 | * @returns the formatted version of user input for pull request review comments 60 | */ 61 | export function addReviewCommentsDefaults( 62 | options: CreateReviewCommentUserOptions 63 | ): CreateReviewComment { 64 | const createReviewComment: CreateReviewComment = { 65 | repo: options.repo, 66 | owner: options.owner, 67 | pullNumber: options.pullNumber, 68 | // if zero set as 0 69 | pageSize: 70 | options.pageSize === null || options.pageSize === undefined 71 | ? DEFAULT_PAGE_SIZE 72 | : options.pageSize, 73 | }; 74 | return createReviewComment; 75 | } 76 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export class CommitError extends Error { 16 | cause: Error; 17 | constructor(message: string, cause: Error) { 18 | super(message); 19 | this.cause = cause; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/github/branch.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {logger} from '../logger'; 16 | import {RepoDomain} from '../types'; 17 | import {Octokit} from '@octokit/rest'; 18 | 19 | const REF_PREFIX = 'refs/heads/'; 20 | const DEFAULT_PRIMARY_BRANCH = 'main'; 21 | 22 | /** 23 | * Create a new branch reference with the ref prefix 24 | * @param {string} branchName name of the branch 25 | */ 26 | export function createRef(branchName: string) { 27 | return REF_PREFIX + branchName; 28 | } 29 | 30 | /** 31 | * get branch commit HEAD SHA of a repository 32 | * Throws an error if the branch cannot be found 33 | * @param {Octokit} octokit The authenticated octokit instance 34 | * @param {RepoDomain} origin The domain information of the remote origin repository 35 | * @param {string} branch the name of the branch 36 | * @returns {Promise} branch commit HEAD SHA 37 | */ 38 | export async function getBranchHead( 39 | octokit: Octokit, 40 | origin: RepoDomain, 41 | branch: string 42 | ): Promise { 43 | const branchData = ( 44 | await octokit.repos.getBranch({ 45 | owner: origin.owner, 46 | repo: origin.repo, 47 | branch, 48 | }) 49 | ).data; 50 | logger.info(`Successfully found branch HEAD sha "${branchData.commit.sha}".`); 51 | return branchData.commit.sha; 52 | } 53 | 54 | /** 55 | * Determine if there is a branch with the provided name in the remote GitHub repository 56 | * @param {Octokit} octokit The authenticated octokit instance 57 | * @param {RepoDomain} remote The domain information of the remote repository 58 | * @param {string} name The branch name to create on the repository 59 | * @returns {Promise} if there is a branch already existing in the remote GitHub repository 60 | */ 61 | export async function existsBranchWithName( 62 | octokit: Octokit, 63 | remote: RepoDomain, 64 | name: string 65 | ): Promise { 66 | try { 67 | const data = ( 68 | await octokit.git.getRef({ 69 | owner: remote.owner, 70 | repo: remote.repo, 71 | ref: `heads/${name}`, 72 | }) 73 | ).data; 74 | return data.ref ? true : false; 75 | } catch (err) { 76 | if ((err as {status: number}).status === 404) return false; 77 | else throw err; 78 | } 79 | } 80 | 81 | /** 82 | * Create a branch on the remote repository if there is not an existing branch 83 | * @param {Octokit} octokit The authenticated octokit instance 84 | * @param {RepoDomain} remote The domain information of the remote origin repository 85 | * @param {string} name The branch name to create on the origin repository 86 | * @param {string} baseSha the sha that the base of the reference points to 87 | * @param {boolean} duplicate whether there is an existing branch or not 88 | * @returns {Promise} 89 | */ 90 | export async function createBranch( 91 | octokit: Octokit, 92 | remote: RepoDomain, 93 | name: string, 94 | baseSha: string, 95 | duplicate: boolean 96 | ): Promise { 97 | if (!duplicate) { 98 | const refData = ( 99 | await octokit.git.createRef({ 100 | owner: remote.owner, 101 | repo: remote.repo, 102 | ref: createRef(name), 103 | sha: baseSha, 104 | }) 105 | ).data; 106 | logger.info(`Successfully created branch at ${refData.url}`); 107 | } else { 108 | logger.info('Skipping branch creation step...'); 109 | } 110 | } 111 | 112 | /** 113 | * Create a GitHub branch given a remote origin. 114 | * Throws an exception if octokit fails, or if the base branch is invalid 115 | * @param {Octokit} octokit The authenticated octokit instance 116 | * @param {RepoDomain} origin The domain information of the remote origin repository 117 | * @param {RepoDomain} upstream The domain information of the remote upstream repository 118 | * @param {string} name The branch name to create on the origin repository 119 | * @param {string} baseBranch the name of the branch to base the new branch off of. Default is main 120 | * @returns {Promise} the base SHA for subsequent commits to be based off for the origin branch 121 | */ 122 | export async function branch( 123 | octokit: Octokit, 124 | origin: RepoDomain, 125 | upstream: RepoDomain, 126 | name: string, 127 | baseBranch: string = DEFAULT_PRIMARY_BRANCH 128 | ): Promise { 129 | // create branch from primary branch HEAD SHA 130 | try { 131 | const baseSha = await getBranchHead(octokit, upstream, baseBranch); 132 | const duplicate: boolean = await existsBranchWithName( 133 | octokit, 134 | origin, 135 | name 136 | ); 137 | await createBranch(octokit, origin, name, baseSha, duplicate); 138 | return baseSha; 139 | } catch (err) { 140 | logger.error('Error when creating branch'); 141 | throw err; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/github/commit-and-push.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { 16 | Changes, 17 | FileData, 18 | TreeObject, 19 | RepoDomain, 20 | BranchDomain, 21 | } from '../types'; 22 | import {Octokit} from '@octokit/rest'; 23 | import {logger} from '../logger'; 24 | import {createCommit, CreateCommitOptions} from './create-commit'; 25 | import {CommitError} from '../errors'; 26 | 27 | const DEFAULT_FILES_PER_COMMIT = 100; 28 | 29 | /** 30 | * Generate and return a GitHub tree object structure 31 | * containing the target change data 32 | * See https://developer.github.com/v3/git/trees/#tree-object 33 | * @param {Changes} changes the set of repository changes 34 | * @returns {TreeObject[]} The new GitHub changes 35 | */ 36 | export function generateTreeObjects(changes: Changes): TreeObject[] { 37 | const tree: TreeObject[] = []; 38 | changes.forEach((fileData: FileData, path: string) => { 39 | if (fileData.content === null) { 40 | // if no file content then file is deleted 41 | tree.push({ 42 | path, 43 | mode: fileData.mode, 44 | type: 'blob', 45 | sha: null, 46 | }); 47 | } else { 48 | // update file with its content 49 | tree.push({ 50 | path, 51 | mode: fileData.mode, 52 | type: 'blob', 53 | content: fileData.content, 54 | }); 55 | } 56 | }); 57 | return tree; 58 | } 59 | 60 | function* inGroupsOf( 61 | all: T[], 62 | groupSize: number 63 | ): Generator { 64 | for (let i = 0; i < all.length; i += groupSize) { 65 | yield all.slice(i, i + groupSize); 66 | } 67 | } 68 | 69 | /** 70 | * Upload and create a remote GitHub tree 71 | * and resolves with the new tree SHA. 72 | * Rejects if GitHub V3 API fails with the GitHub error response 73 | * @param {Octokit} octokit The authenticated octokit instance 74 | * @param {RepoDomain} origin the the remote repository to push changes to 75 | * @param {string} refHead the base of the new commit(s) 76 | * @param {TreeObject[]} tree the set of GitHub changes to upload 77 | * @returns {Promise} the GitHub tree SHA 78 | * @throws {CommitError} 79 | */ 80 | export async function createTree( 81 | octokit: Octokit, 82 | origin: RepoDomain, 83 | refHead: string, 84 | tree: TreeObject[] 85 | ): Promise { 86 | const oldTreeSha = ( 87 | await octokit.git.getCommit({ 88 | owner: origin.owner, 89 | repo: origin.repo, 90 | commit_sha: refHead, 91 | }) 92 | ).data.tree.sha; 93 | logger.info('Got the latest commit tree'); 94 | try { 95 | const treeSha = ( 96 | await octokit.git.createTree({ 97 | owner: origin.owner, 98 | repo: origin.repo, 99 | tree, 100 | base_tree: oldTreeSha, 101 | }) 102 | ).data.sha; 103 | logger.info( 104 | `Successfully created a tree with the desired changes with SHA ${treeSha}` 105 | ); 106 | return treeSha; 107 | } catch (e) { 108 | throw new CommitError(`Error adding to tree: ${refHead}`, e as Error); 109 | } 110 | } 111 | 112 | /** 113 | * Update a reference to a SHA 114 | * Rejects if GitHub V3 API fails with the GitHub error response 115 | * @param {Octokit} octokit The authenticated octokit instance 116 | * @param {BranchDomain} origin the the remote branch to push changes to 117 | * @param {string} newSha the ref to update the commit HEAD to 118 | * @param {boolean} force to force the commit changes given refHead 119 | * @returns {Promise} 120 | */ 121 | export async function updateRef( 122 | octokit: Octokit, 123 | origin: BranchDomain, 124 | newSha: string, 125 | force: boolean 126 | ): Promise { 127 | logger.info(`Updating reference heads/${origin.branch} to ${newSha}`); 128 | try { 129 | await octokit.git.updateRef({ 130 | owner: origin.owner, 131 | repo: origin.repo, 132 | ref: `heads/${origin.branch}`, 133 | sha: newSha, 134 | force, 135 | }); 136 | logger.info(`Successfully updated reference ${origin.branch} to ${newSha}`); 137 | } catch (e) { 138 | throw new CommitError( 139 | `Error updating ref heads/${origin.branch} to ${newSha}`, 140 | e as Error 141 | ); 142 | } 143 | } 144 | 145 | interface CommitAndPushOptions extends CreateCommitOptions { 146 | filesPerCommit?: number; 147 | } 148 | 149 | /** 150 | * Given a set of changes, apply the commit(s) on top of the given branch's head and upload it to GitHub 151 | * Rejects if GitHub V3 API fails with the GitHub error response 152 | * @param {Octokit} octokit The authenticated octokit instance 153 | * @param {string} refHead the base of the new commit(s) 154 | * @param {Changes} changes the set of repository changes 155 | * @param {RepoDomain} origin the the remote repository to push changes to 156 | * @param {string} originBranchName the remote branch that will contain the new changes 157 | * @param {string} commitMessage the message of the new commit 158 | * @param {boolean} force to force the commit changes given refHead 159 | * @returns {Promise} 160 | * @throws {CommitError} 161 | */ 162 | export async function commitAndPush( 163 | octokit: Octokit, 164 | refHead: string, 165 | changes: Changes, 166 | originBranch: BranchDomain, 167 | commitMessage: string, 168 | force: boolean, 169 | options?: CommitAndPushOptions 170 | ) { 171 | const filesPerCommit = options?.filesPerCommit ?? DEFAULT_FILES_PER_COMMIT; 172 | const tree = generateTreeObjects(changes); 173 | for (const treeGroup of inGroupsOf(tree, filesPerCommit)) { 174 | const treeSha = await createTree(octokit, originBranch, refHead, treeGroup); 175 | refHead = await createCommit( 176 | octokit, 177 | originBranch, 178 | refHead, 179 | treeSha, 180 | commitMessage, 181 | options 182 | ); 183 | } 184 | await updateRef(octokit, originBranch, refHead, force); 185 | } 186 | -------------------------------------------------------------------------------- /src/github/create-commit.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Octokit} from '@octokit/rest'; 16 | import {RepoDomain, CommitSigner, UserData} from '../types'; 17 | import {logger} from '../logger'; 18 | import {CommitError} from '../errors'; 19 | 20 | export interface CreateCommitOptions { 21 | signer?: CommitSigner; 22 | author?: UserData; 23 | committer?: UserData; 24 | } 25 | 26 | /** 27 | * Create a commit with a repo snapshot SHA on top of the reference HEAD 28 | * and resolves with the SHA of the commit. 29 | * Rejects if GitHub V3 API fails with the GitHub error response 30 | * @param {Octokit} octokit The authenticated octokit instance 31 | * @param {RepoDomain} origin the the remote repository to push changes to 32 | * @param {string} refHead the base of the new commit(s) 33 | * @param {string} treeSha the tree SHA that this commit will point to 34 | * @param {string} message the message of the new commit 35 | * @returns {Promise} the new commit SHA 36 | * @see https://docs.github.com/en/rest/git/commits?apiVersion=2022-11-28#create-a-commit 37 | */ 38 | export async function createCommit( 39 | octokit: Octokit, 40 | origin: RepoDomain, 41 | refHead: string, 42 | treeSha: string, 43 | message: string, 44 | options: CreateCommitOptions = {} 45 | ): Promise { 46 | try { 47 | const signature = options.signer 48 | ? await options.signer.generateSignature({ 49 | message, 50 | tree: treeSha, 51 | parents: [refHead], 52 | author: options.author, 53 | committer: options.committer, 54 | }) 55 | : undefined; 56 | const { 57 | data: {sha, url}, 58 | } = await octokit.git.createCommit({ 59 | owner: origin.owner, 60 | repo: origin.repo, 61 | message, 62 | tree: treeSha, 63 | parents: [refHead], 64 | signature, 65 | author: options.author, 66 | committer: options.committer, 67 | }); 68 | logger.info(`Successfully created commit. See commit at ${url}`); 69 | return sha; 70 | } catch (e) { 71 | throw new CommitError(`Error creating commit for: ${treeSha}`, e as Error); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/github/fork.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {RepoDomain} from '../types'; 16 | import {Octokit} from '@octokit/rest'; 17 | import {logger} from '../logger'; 18 | 19 | /** 20 | * Fork the GitHub owner's repository. 21 | * Returns the fork owner and fork repo when the fork creation request to GitHub succeeds. 22 | * Otherwise throws error. 23 | * 24 | * If fork already exists no new fork is created, no error occurs, and the existing Fork data is returned 25 | * with the `updated_at` + any historical repo changes. 26 | * @param {Octokit} octokit The authenticated octokit instance 27 | * @param {RepoDomain} upstream upstream repository information 28 | * @returns {Promise} the forked repository name, as well as the owner of that fork 29 | */ 30 | async function fork( 31 | octokit: Octokit, 32 | upstream: RepoDomain 33 | ): Promise { 34 | try { 35 | const forkedRepo = ( 36 | await octokit.repos.createFork({ 37 | owner: upstream.owner, 38 | repo: upstream.repo, 39 | }) 40 | ).data; 41 | const origin: RepoDomain = { 42 | repo: forkedRepo.name, 43 | owner: forkedRepo.owner!.login, 44 | }; 45 | logger.info( 46 | `Create fork request was successful for ${origin.owner}/${origin.repo}` 47 | ); 48 | return origin; 49 | } catch (err) { 50 | logger.error('Error when forking'); 51 | throw err; 52 | } 53 | } 54 | 55 | export {fork}; 56 | -------------------------------------------------------------------------------- /src/github/labels.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {BranchDomain, RepoDomain} from '../types'; 16 | import {Octokit} from '@octokit/rest'; 17 | import {logger} from '../logger'; 18 | 19 | /** 20 | * Create a GitHub PR on the upstream organization's repo 21 | * Throws an error if the GitHub API fails 22 | * @param {Octokit} octokit The authenticated octokit instance 23 | * @param {RepoDomain} upstream The upstream repository 24 | * @param {BranchDomain} origin The remote origin information that contains the origin branch 25 | * @param {number} issue_number The issue number to add labels to. Can also be a PR number 26 | * @param {string[]} labels The list of labels to apply to the issue/pull request. Default is []. the funciton will no-op. 27 | * @returns {Promise} The list of resulting labels after the addition of the given labels 28 | */ 29 | async function addLabels( 30 | octokit: Octokit, 31 | upstream: RepoDomain, 32 | origin: BranchDomain, 33 | issue_number: number, 34 | labels?: string[] 35 | ): Promise { 36 | if (!labels || labels.length === 0) { 37 | return []; 38 | } 39 | 40 | const labelsResponseData = ( 41 | await octokit.issues.addLabels({ 42 | owner: upstream.owner, 43 | repo: origin.repo, 44 | issue_number: issue_number, 45 | labels: labels, 46 | }) 47 | ).data; 48 | logger.info(`Successfully added labels ${labels} to issue: ${issue_number}`); 49 | return labelsResponseData.map(l => l.name); 50 | } 51 | 52 | export {addLabels}; 53 | -------------------------------------------------------------------------------- /src/github/open-pull-request.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {BranchDomain, Description, RepoDomain} from '../types'; 16 | import {Octokit} from '@octokit/rest'; 17 | import {logger} from '../logger'; 18 | 19 | const DEFAULT_PRIMARY = 'main'; 20 | 21 | /** 22 | * Create a GitHub PR on the upstream organization's repo 23 | * Throws an error if the GitHub API fails 24 | * @param {Octokit} octokit The authenticated octokit instance 25 | * @param {RepoDomain} upstream The upstream repository 26 | * @param {BranchDomain} origin The remote origin information that contains the origin branch 27 | * @param {Description} description The pull request title and detailed description 28 | * @param {boolean} maintainersCanModify Whether or not maintainers can modify the pull request. Default is true 29 | * @param {string} upstreamPrimary The upstream repository's primary branch. Default is main. 30 | * @param draft Open a DRAFT pull request. Defaults to false. 31 | * @returns {Promise} 32 | */ 33 | async function openPullRequest( 34 | octokit: Octokit, 35 | upstream: RepoDomain, 36 | origin: BranchDomain, 37 | description: Description, 38 | maintainersCanModify = true, 39 | upstreamPrimary: string = DEFAULT_PRIMARY, 40 | draft = false 41 | ): Promise { 42 | const head = `${origin.owner}:${origin.branch}`; 43 | const existingPullRequest = ( 44 | await octokit.pulls.list({ 45 | owner: upstream.owner, 46 | repo: origin.repo, 47 | head, 48 | }) 49 | ).data.find(pr => pr.head.label === head); 50 | if (existingPullRequest) { 51 | logger.info( 52 | `Found existing pull request for reference ${origin.owner}:${origin.branch}. Skipping creating a new pull request.` 53 | ); 54 | return existingPullRequest.number; 55 | } 56 | const pullResponseData = ( 57 | await octokit.pulls.create({ 58 | owner: upstream.owner, 59 | repo: origin.repo, 60 | title: description.title, 61 | head: `${origin.owner}:${origin.branch}`, 62 | base: upstreamPrimary, 63 | body: description.body, 64 | maintainer_can_modify: maintainersCanModify, 65 | draft: draft, 66 | }) 67 | ).data; 68 | logger.info( 69 | `Successfully opened pull request available at url: ${pullResponseData.url}.` 70 | ); 71 | return pullResponseData.number; 72 | } 73 | 74 | export {openPullRequest}; 75 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Logger} from './types'; 16 | 17 | class NullLogger implements Logger { 18 | error = () => {}; 19 | warn = () => {}; 20 | info = () => {}; 21 | debug = () => {}; 22 | trace = () => {}; 23 | } 24 | 25 | let logger: Logger = new NullLogger(); 26 | 27 | function setupLogger(userLogger?: Logger) { 28 | if (userLogger) { 29 | logger = userLogger; 30 | } else { 31 | logger = new NullLogger(); 32 | } 33 | } 34 | 35 | export {logger, setupLogger}; 36 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {CreateCommitOptions} from './github/create-commit'; 16 | 17 | export type FileMode = '100644' | '100755' | '040000' | '160000' | '120000'; 18 | 19 | /** 20 | * GitHub definition of tree 21 | */ 22 | export interface TreeObject { 23 | path: string; 24 | mode: FileMode; 25 | type: 'blob' | 'tree' | 'commit'; 26 | sha?: string | null; 27 | content?: string; 28 | } 29 | 30 | /** 31 | * The content and the mode of a file. 32 | * Default file mode is a text file which has code '100644'. 33 | * If `content` is not null, then `content` must be the entire file content. 34 | * See https://developer.github.com/v3/git/trees/#tree-object for details on mode. 35 | */ 36 | export class FileData { 37 | readonly mode: FileMode; 38 | readonly content: string | null; 39 | constructor(content: string | null, mode: FileMode = '100644') { 40 | this.mode = mode; 41 | this.content = content; 42 | } 43 | } 44 | 45 | /** 46 | * The map of a path to its content data. 47 | * The content must be the entire file content. 48 | */ 49 | export type Changes = Map; 50 | 51 | /** 52 | * The domain of a repository 53 | */ 54 | export interface RepoDomain { 55 | repo: string; 56 | owner: string; 57 | } 58 | 59 | /** 60 | * The domain for a branch 61 | */ 62 | export interface BranchDomain extends RepoDomain { 63 | branch: string; 64 | } 65 | 66 | /** 67 | * The descriptive properties for any entity 68 | */ 69 | export interface Description { 70 | title: string; 71 | body: string; 72 | } 73 | 74 | /** 75 | * The user options for creating GitHub PRs 76 | */ 77 | export interface CreatePullRequestUserOptions extends CreateCommitOptions { 78 | // the owner of the target fork repository 79 | upstreamOwner: string; 80 | // the name of the target fork repository 81 | upstreamRepo: string; 82 | // The message of any commits made. 83 | message: string; 84 | // The description of the pull request. 85 | description: string; 86 | // The title of the pull request. 87 | title: string; 88 | // the name of the branch to push changes to. Default is 'code-suggestions'. (optional) 89 | branch?: string; 90 | // Whether or not to force branch reference updates. Default is false. (optional) 91 | force?: boolean; 92 | // Should a fork be used when creating pull request 93 | fork?: boolean; 94 | // Primary upstream branch to open PRs against. Default is 'main' (optional) 95 | primary?: string; 96 | // Whether or not maintainers can modify the PR. Default is true. (optional) 97 | maintainersCanModify?: boolean; 98 | // The list of labels to apply to the newly created PR. Default is empty. (optional) 99 | labels?: string[]; 100 | // Number of times to retry if the request fails. Defaults to 5. 101 | retry?: number; 102 | // Create a DRAFT pull request. 103 | draft?: boolean; 104 | // Optional logger to set 105 | logger?: Logger; 106 | // Optional number of files per commit 107 | filesPerCommit?: number; 108 | } 109 | 110 | /** 111 | * GitHub data needed for creating a PR 112 | */ 113 | export interface CreatePullRequest { 114 | // the owner of the target fork repository 115 | upstreamOwner: string; 116 | // the name of the target fork repository 117 | upstreamRepo: string; 118 | // The message of any commits made. 119 | message: string; 120 | // The description of the pull request. 121 | description: string; 122 | // The title of the pull request 123 | title: string; 124 | // the name of the branch to push changes to. 125 | branch: string; 126 | // Whether or not to force branch reference updates. 127 | force: boolean; 128 | // Primary upstream branch to open PRs against. 129 | primary: string; 130 | // Whether or not maintainers can modify the PR. 131 | maintainersCanModify: boolean; 132 | // Optional number of files per commit 133 | filesPerCommit?: number; 134 | } 135 | 136 | /** 137 | * The user options for creating GitHub PR review comment 138 | */ 139 | export interface CreateReviewCommentUserOptions { 140 | // the owner of the target fork repository 141 | owner: string; 142 | // the name of the target fork repository 143 | repo: string; 144 | // The pull request number 145 | pullNumber: number; 146 | // The number of files to return per pull request list files query. Used when getting data on the remote PR's files. 147 | pageSize?: number; 148 | // Optional logger to set 149 | logger?: Logger; 150 | } 151 | 152 | /** 153 | * The user options for creating GitHub PR review comment 154 | */ 155 | export interface CreateReviewComment { 156 | // the owner of the target fork repository 157 | owner: string; 158 | // the name of the target fork repository 159 | repo: string; 160 | // The pull request number 161 | pullNumber: number; 162 | // The number of files to return per pull request list files query. Used when getting data on the remote PR's files. 163 | pageSize: number; 164 | } 165 | 166 | export class PatchSyntaxError extends Error { 167 | constructor(message: string) { 168 | super(message); 169 | this.name = 'PatchSyntaxError'; 170 | } 171 | } 172 | 173 | /** 174 | * The file content of the original content and the patched content 175 | */ 176 | export interface FileDiffContent { 177 | readonly oldContent: string; 178 | readonly newContent: string; 179 | } 180 | 181 | export interface Hunk { 182 | readonly oldStart: number; 183 | readonly oldEnd: number; 184 | readonly newStart: number; 185 | readonly newEnd: number; 186 | readonly newContent: string[]; 187 | readonly previousLine?: string; 188 | readonly nextLine?: string; 189 | } 190 | 191 | interface LogFn { 192 | /* eslint-disable @typescript-eslint/no-explicit-any */ 193 | (obj: T, msg?: string, ...args: any[]): void; 194 | (msg: string, ...args: any[]): void; 195 | /* eslint-enable @typescript-eslint/no-explicit-any */ 196 | } 197 | 198 | export interface Logger { 199 | error: LogFn; 200 | warn: LogFn; 201 | info: LogFn; 202 | debug: LogFn; 203 | trace: LogFn; 204 | } 205 | 206 | export interface UserData { 207 | name: string; 208 | email: string; 209 | } 210 | 211 | export interface CommitData { 212 | message: string; 213 | tree: string; 214 | parents: string[]; 215 | author?: UserData; 216 | committer?: UserData; 217 | } 218 | 219 | export interface CommitSigner { 220 | generateSignature(commit: CommitData): Promise; 221 | } 222 | -------------------------------------------------------------------------------- /src/utils/diff-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Hunk} from '../types'; 16 | import * as parseDiff from 'parse-diff'; 17 | import {createPatch} from 'diff'; 18 | 19 | // This header is ignored for calculating patch ranges, but is neccessary 20 | // for parsing a diff 21 | const _DIFF_HEADER = `diff --git a/file.ext b/file.ext 22 | index cac8fbc..87f387c 100644 23 | --- a/file.ext 24 | +++ b/file.ext 25 | `; 26 | 27 | /** 28 | * Given a patch expressed in GNU diff format, return the range of lines 29 | * from the original content that are changed. 30 | * @param diff Diff expressed in GNU diff format. 31 | * @returns Hunk[] 32 | */ 33 | export function parsePatch(patch: string): Hunk[] { 34 | return parseAllHunks(_DIFF_HEADER + patch).get('file.ext') || []; 35 | } 36 | 37 | /** 38 | * Given a diff expressed in GNU diff format, return the range of lines 39 | * from the original content that are changed. 40 | * @param diff Diff expressed in GNU diff format. 41 | * @returns Map 42 | */ 43 | export function parseAllHunks(diff: string): Map { 44 | const hunksByFile: Map = new Map(); 45 | parseDiff(diff).forEach(file => { 46 | const filename = file.to ? file.to : file.from!; 47 | const chunks = file.chunks.map(chunk => { 48 | let oldStart = chunk.oldStart; 49 | let newStart = chunk.newStart; 50 | let normalLines = 0; 51 | let changeSeen = false; 52 | const newLines: string[] = []; 53 | let previousLine: string | null = null; 54 | let nextLine: string | null = null; 55 | 56 | chunk.changes.forEach(change => { 57 | // strip off leading '+', '-', or ' ' and trailing carriage return 58 | const content = change.content.substring(1).replace(/[\n\r]+$/g, ''); 59 | if (change.type === 'normal') { 60 | normalLines++; 61 | if (changeSeen) { 62 | if (nextLine === null) { 63 | nextLine = content; 64 | } 65 | } else { 66 | previousLine = content; 67 | } 68 | } else { 69 | if (change.type === 'add') { 70 | // strip off leading '+' and trailing carriage return 71 | newLines.push(content); 72 | } 73 | if (!changeSeen) { 74 | oldStart += normalLines; 75 | newStart += normalLines; 76 | changeSeen = true; 77 | } 78 | } 79 | }); 80 | const newEnd = newStart + chunk.newLines - normalLines - 1; 81 | const oldEnd = oldStart + chunk.oldLines - normalLines - 1; 82 | let hunk: Hunk = { 83 | oldStart: oldStart, 84 | oldEnd: oldEnd, 85 | newStart: newStart, 86 | newEnd: newEnd, 87 | newContent: newLines, 88 | }; 89 | if (previousLine) { 90 | hunk = {...hunk, previousLine: previousLine}; 91 | } 92 | if (nextLine) { 93 | hunk = {...hunk, nextLine: nextLine}; 94 | } 95 | return hunk; 96 | }); 97 | hunksByFile.set(filename, chunks); 98 | }); 99 | return hunksByFile; 100 | } 101 | 102 | /** 103 | * Given two texts, return the range of lines that are changed. 104 | * @param oldContent The original content. 105 | * @param newContent The new content. 106 | * @returns Hunk[] 107 | */ 108 | export function getSuggestedHunks( 109 | oldContent: string, 110 | newContent: string 111 | ): Hunk[] { 112 | const diff = createPatch('unused', oldContent, newContent); 113 | return parseAllHunks(diff).get('unused') || []; 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/hunk-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {Hunk, FileDiffContent} from '../types'; 16 | import {getSuggestedHunks} from './diff-utils'; 17 | import {logger} from '../logger'; 18 | 19 | /** 20 | * Shift a Hunk up one line so it starts one line earlier. 21 | * @param {Hunk} hunk 22 | * @returns {Hunk | null} the adjusted Hunk or null if there is no preceeding line. 23 | */ 24 | export function adjustHunkUp(hunk: Hunk): Hunk | null { 25 | if (!hunk.previousLine) { 26 | return null; 27 | } 28 | return { 29 | oldStart: hunk.oldStart - 1, 30 | oldEnd: hunk.oldEnd, 31 | newStart: hunk.newStart - 1, 32 | newEnd: hunk.newEnd, 33 | newContent: [hunk.previousLine, ...hunk.newContent], 34 | }; 35 | } 36 | 37 | /** 38 | * Shift a Hunk up one line so it ends one line later. 39 | * @param {Hunk} hunk 40 | * @returns {Hunk | null} the adjusted Hunk or null if there is no following line. 41 | */ 42 | export function adjustHunkDown(hunk: Hunk): Hunk | null { 43 | if (!hunk.nextLine) { 44 | return null; 45 | } 46 | return { 47 | oldStart: hunk.oldStart, 48 | oldEnd: hunk.oldEnd + 1, 49 | newStart: hunk.newStart, 50 | newEnd: hunk.newEnd + 1, 51 | newContent: hunk.newContent.concat(hunk.nextLine), 52 | }; 53 | } 54 | 55 | /** 56 | * Given a map where the key is the file name and the value is the 57 | * old content and new content of the file 58 | * compute the hunk for each file whose old and new contents differ. 59 | * Do not compute the hunk if the old content is the same as the new content. 60 | * The hunk list is sorted and each interval is disjoint. 61 | * @param {Map} diffContents a map of the original file contents and the new file contents 62 | * @returns the hunks for each file whose old and new contents differ 63 | */ 64 | export function getRawSuggestionHunks( 65 | diffContents: Map 66 | ): Map { 67 | const fileHunks: Map = new Map(); 68 | diffContents.forEach((fileDiffContent: FileDiffContent, fileName: string) => { 69 | // if identical don't calculate the hunk and continue in the loop 70 | if (fileDiffContent.oldContent === fileDiffContent.newContent) { 71 | return; 72 | } 73 | const hunks = getSuggestedHunks( 74 | fileDiffContent.oldContent, 75 | fileDiffContent.newContent 76 | ); 77 | fileHunks.set(fileName, hunks); 78 | }); 79 | logger.info('Parsed ranges of old and new patch'); 80 | return fileHunks; 81 | } 82 | interface PartitionedHunks { 83 | validHunks: Map; 84 | invalidHunks: Map; 85 | } 86 | 87 | interface PartitionedFileHunks { 88 | validFileHunks: Hunk[]; 89 | invalidFileHunks: Hunk[]; 90 | } 91 | 92 | function hunkOverlaps(validHunk: Hunk, suggestedHunk: Hunk): boolean { 93 | return ( 94 | suggestedHunk.oldStart >= validHunk.newStart && 95 | suggestedHunk.oldEnd <= validHunk.newEnd 96 | ); 97 | } 98 | 99 | function partitionFileHunks( 100 | pullRequestHunks: Hunk[], 101 | suggestedHunks: Hunk[] 102 | ): PartitionedFileHunks { 103 | // check ranges: the entirety of the old range of the suggested 104 | // hunk must fit inside the new range of the valid Hunks 105 | let i = 0; 106 | let candidateHunk = pullRequestHunks[i]; 107 | const validFileHunks: Hunk[] = []; 108 | const invalidFileHunks: Hunk[] = []; 109 | suggestedHunks.forEach(suggestedHunk => { 110 | while (candidateHunk && suggestedHunk.oldStart > candidateHunk.newEnd) { 111 | i++; 112 | candidateHunk = pullRequestHunks[i]; 113 | } 114 | if (!candidateHunk) { 115 | invalidFileHunks.push(suggestedHunk); 116 | return; 117 | } 118 | // if deletion only or addition only 119 | if ( 120 | suggestedHunk.newEnd < suggestedHunk.newStart || 121 | suggestedHunk.oldEnd < suggestedHunk.oldStart 122 | ) { 123 | // try using previous line 124 | let adjustedHunk = adjustHunkUp(suggestedHunk); 125 | if (adjustedHunk && hunkOverlaps(candidateHunk, adjustedHunk)) { 126 | validFileHunks.push(adjustedHunk); 127 | return; 128 | } 129 | 130 | // try using next line 131 | adjustedHunk = adjustHunkDown(suggestedHunk); 132 | if (adjustedHunk && hunkOverlaps(candidateHunk, adjustedHunk)) { 133 | validFileHunks.push(adjustedHunk); 134 | return; 135 | } 136 | } else if (hunkOverlaps(candidateHunk, suggestedHunk)) { 137 | validFileHunks.push(suggestedHunk); 138 | return; 139 | } 140 | 141 | invalidFileHunks.push(suggestedHunk); 142 | }); 143 | return {validFileHunks, invalidFileHunks}; 144 | } 145 | 146 | /** 147 | * Split suggested hunks into commentable and non-commentable hunks. Compares the new line ranges 148 | * from pullRequestHunks against the old line ranges from allSuggestedHunks. 149 | * @param pullRequestHunks {Map} The parsed hunks from that represents the valid lines to comment. 150 | * @param allSuggestedHunks {Map} The hunks that represent suggested changes. 151 | * @returns {PartitionedHunks} split hunks 152 | */ 153 | export function partitionSuggestedHunksByScope( 154 | pullRequestHunks: Map, 155 | allSuggestedHunks: Map 156 | ): PartitionedHunks { 157 | const validHunks: Map = new Map(); 158 | const invalidHunks: Map = new Map(); 159 | allSuggestedHunks.forEach((suggestedHunks, filename) => { 160 | const pullRequestFileHunks = pullRequestHunks.get(filename); 161 | if (!pullRequestFileHunks) { 162 | // file is not the original PR 163 | invalidHunks.set(filename, suggestedHunks); 164 | return; 165 | } 166 | 167 | const {validFileHunks, invalidFileHunks} = partitionFileHunks( 168 | pullRequestFileHunks, 169 | suggestedHunks 170 | ); 171 | if (validFileHunks.length > 0) { 172 | validHunks.set(filename, validFileHunks); 173 | } 174 | if (invalidFileHunks.length > 0) { 175 | invalidHunks.set(filename, invalidFileHunks); 176 | } 177 | }); 178 | return {validHunks, invalidHunks}; 179 | } 180 | -------------------------------------------------------------------------------- /system-test/main.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as assert from 'assert'; 16 | import {describe, it} from 'mocha'; 17 | 18 | describe('System test', () => { 19 | it('has a test case', () => { 20 | assert.ok(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/cli.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | import {describe, it, afterEach} from 'mocha'; 17 | import * as assert from 'assert'; 18 | import {main, coerceUserCreatePullRequestOptions} from '../src/bin/workflow'; 19 | import * as gitChangeHander from '../src/bin/handle-git-dir-change'; 20 | import * as sinon from 'sinon'; 21 | import * as yargs from 'yargs'; 22 | 23 | describe('main', () => { 24 | const sandbox = sinon.createSandbox(); 25 | 26 | afterEach(() => { 27 | sandbox.restore(); 28 | }); 29 | 30 | it('Does not error', async () => { 31 | // setup 32 | sandbox 33 | .stub(process, 'env') 34 | .value({...process.env, ACCESS_TOKEN: '123121312'}); 35 | sandbox 36 | .stub(yargs, 'argv') 37 | .value({...yargs.argv, _: ['pr'], 'git-dir': 'some/dir'}); 38 | const getChangesStub = sandbox.stub(gitChangeHander, 'getChanges'); 39 | getChangesStub.resolves(new Map()); 40 | await main(); 41 | }); 42 | it('fails when there is no env variable', async () => { 43 | sandbox 44 | .stub(process, 'env') 45 | .value({...process.env, ACCESS_TOKEN: undefined}); 46 | await assert.rejects(main()); 47 | }); 48 | 49 | it('Fails when unrecognized command is given', async () => { 50 | // setup 51 | sandbox 52 | .stub(process, 'env') 53 | .value({...process.env, ACCESS_TOKEN: '123121312'}); 54 | sandbox 55 | .stub(yargs, 'argv') 56 | .value({...yargs.argv, _: ['unknown-command'], 'git-dir': 'some/dir'}); 57 | 58 | const getChangesStub = sandbox.stub(gitChangeHander, 'getChanges'); 59 | getChangesStub.rejects(new Error('error getting changes')); 60 | await assert.rejects(main()); 61 | }); 62 | 63 | it('Passes up the error message when fetching change failed', async () => { 64 | // setup 65 | sandbox 66 | .stub(process, 'env') 67 | .value({...process.env, ACCESS_TOKEN: '123121312'}); 68 | sandbox.stub(yargs, 'argv').value({...yargs.argv, _: ['unrecognized']}); 69 | await assert.rejects(main()); 70 | }); 71 | }); 72 | 73 | describe('Mapping pr yargs to create PR options', () => { 74 | const sandbox = sinon.createSandbox(); 75 | 76 | afterEach(() => { 77 | sandbox.restore(); 78 | }); 79 | 80 | it('Creates', () => { 81 | const options = { 82 | upstreamRepo: 'foo', 83 | upstreamOwner: 'bar', 84 | message: 'message', 85 | description: 'description', 86 | title: 'title', 87 | branch: 'branch', 88 | force: false, 89 | primary: 'primary', 90 | maintainersCanModify: true, 91 | fork: true, 92 | labels: ['automerge'], 93 | logger: console, 94 | filesPerCommit: 100, 95 | }; 96 | sandbox.stub(yargs, 'argv').value({_: ['pr'], ...options}); 97 | assert.deepStrictEqual(coerceUserCreatePullRequestOptions(), options); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/diff-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {describe, it, before} from 'mocha'; 16 | import {readFileSync} from 'fs'; 17 | import {setup} from './util'; 18 | import {resolve} from 'path'; 19 | import {parseAllHunks} from '../src/utils/diff-utils'; 20 | import * as assert from 'assert'; 21 | 22 | const fixturePath = 'test/fixtures/diffs'; 23 | 24 | before(() => { 25 | setup(); 26 | }); 27 | 28 | describe('parseAllHunks', () => { 29 | it('parses one-to-one hunks', () => { 30 | const diff = readFileSync( 31 | resolve(fixturePath, 'one-line-to-one.diff') 32 | ).toString(); 33 | const allHunks = parseAllHunks(diff); 34 | assert.strictEqual(allHunks.size, 1); 35 | const hunks = allHunks.get('cloudbuild.yaml'); 36 | assert.deepStrictEqual(hunks, [ 37 | { 38 | oldStart: 5, 39 | oldEnd: 5, 40 | newStart: 5, 41 | newEnd: 5, 42 | newContent: [" args: ['sleep', '301']"], 43 | nextLine: "- name: 'ubuntu'", 44 | previousLine: "- name: 'ubuntu'", 45 | }, 46 | ]); 47 | }); 48 | it('parses one-to-many hunks', () => { 49 | const diff = readFileSync( 50 | resolve(fixturePath, 'one-line-to-many.diff') 51 | ).toString(); 52 | const allHunks = parseAllHunks(diff); 53 | assert.strictEqual(allHunks.size, 1); 54 | const hunks = allHunks.get('cloudbuild.yaml'); 55 | assert.deepStrictEqual(hunks, [ 56 | { 57 | oldStart: 7, 58 | oldEnd: 7, 59 | newStart: 7, 60 | newEnd: 8, 61 | newContent: [" args: ['foobar']", ' id: asdf'], 62 | previousLine: "- name: 'ubuntu'", 63 | }, 64 | ]); 65 | }); 66 | it('parses one-to-many hunks with a newline added', () => { 67 | const diff = readFileSync( 68 | resolve(fixturePath, 'one-line-to-many-newline.diff') 69 | ).toString(); 70 | const allHunks = parseAllHunks(diff); 71 | assert.strictEqual(allHunks.size, 1); 72 | const hunks = allHunks.get('cloudbuild.yaml'); 73 | assert.deepStrictEqual(hunks, [ 74 | { 75 | oldStart: 5, 76 | oldEnd: 5, 77 | newStart: 5, 78 | newEnd: 6, 79 | newContent: [" args: ['sleep', '30']", " id: 'foobar'"], 80 | previousLine: "- name: 'ubuntu'", 81 | }, 82 | ]); 83 | }); 84 | it('parses many-to-many-hunks', () => { 85 | const diff = readFileSync( 86 | resolve(fixturePath, 'many-to-many.diff') 87 | ).toString(); 88 | const allHunks = parseAllHunks(diff); 89 | assert.strictEqual(allHunks.size, 1); 90 | const hunks = allHunks.get('cloudbuild.yaml'); 91 | assert.deepStrictEqual(hunks, [ 92 | { 93 | oldStart: 2, 94 | oldEnd: 5, 95 | newStart: 2, 96 | newEnd: 3, 97 | newContent: ["- name: 'foo'", " args: ['sleep 1']"], 98 | nextLine: "- name: 'ubuntu'", 99 | previousLine: 'steps:', 100 | }, 101 | ]); 102 | }); 103 | 104 | it('parses many-to-one-hunks', () => { 105 | const diff = readFileSync( 106 | resolve(fixturePath, 'many-to-one.diff') 107 | ).toString(); 108 | const allHunks = parseAllHunks(diff); 109 | assert.strictEqual(allHunks.size, 1); 110 | const hunks = allHunks.get('cloudbuild.yaml'); 111 | assert.deepStrictEqual(hunks, [ 112 | { 113 | oldStart: 2, 114 | oldEnd: 5, 115 | newStart: 2, 116 | newEnd: 2, 117 | newContent: ["- name: 'foo'"], 118 | nextLine: "- name: 'ubuntu'", 119 | previousLine: 'steps:', 120 | }, 121 | ]); 122 | }); 123 | it('parses deletions', () => { 124 | const diff = readFileSync(resolve(fixturePath, 'deletion.diff')).toString(); 125 | const allHunks = parseAllHunks(diff); 126 | assert.strictEqual(allHunks.size, 1); 127 | const hunks = allHunks.get('cloudbuild.yaml'); 128 | assert.deepStrictEqual(hunks, [ 129 | { 130 | oldStart: 4, 131 | oldEnd: 5, 132 | newStart: 4, 133 | newEnd: 3, 134 | newContent: [], 135 | nextLine: "- name: 'ubuntu'", 136 | previousLine: " args: ['echo', 'foobar']", 137 | }, 138 | ]); 139 | }); 140 | it('parses additions', () => { 141 | const diff = readFileSync(resolve(fixturePath, 'addition.diff')).toString(); 142 | const allHunks = parseAllHunks(diff); 143 | assert.strictEqual(allHunks.size, 1); 144 | const hunks = allHunks.get('cloudbuild.yaml'); 145 | assert.deepStrictEqual(hunks, [ 146 | { 147 | oldStart: 6, 148 | oldEnd: 5, 149 | newStart: 6, 150 | newEnd: 6, 151 | newContent: [" id: 'added'"], 152 | nextLine: "- name: 'ubuntu'", 153 | previousLine: " args: ['sleep', '30']", 154 | }, 155 | ]); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/fixtures/add-labels-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 208045946, 4 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 5 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 6 | "name": "bug", 7 | "description": "Something isn't working", 8 | "color": "f29513", 9 | "default": true 10 | }, 11 | { 12 | "id": 208045947, 13 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDc=", 14 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement", 15 | "name": "enhancement", 16 | "description": "New feature or request", 17 | "color": "a2eeef", 18 | "default": false 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /test/fixtures/create-commit-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "7638417db6d59f3c431d3e1f261cc637155684cd", 3 | "node_id": "MDY6Q29tbWl0NzYzODQxN2RiNmQ1OWYzYzQzMWQzZTFmMjYxY2M2MzcxNTU2ODRjZA==", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd", 5 | "author": { 6 | "date": "2014-11-07T22:01:45Z", 7 | "name": "Monalisa Octocat", 8 | "email": "octocat@github.com" 9 | }, 10 | "committer": { 11 | "date": "2014-11-07T22:01:45Z", 12 | "name": "Monalisa Octocat", 13 | "email": "octocat@github.com" 14 | }, 15 | "message": "my commit message", 16 | "tree": { 17 | "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/827efc6d56897b048c772eb4087f854f46256132", 18 | "sha": "827efc6d56897b048c772eb4087f854f46256132" 19 | }, 20 | "parents": [ 21 | { 22 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7d1b31e74ee336d15cbd21741bc88a537ed063a0", 23 | "sha": "7d1b31e74ee336d15cbd21741bc88a537ed063a0" 24 | } 25 | ], 26 | "verification": { 27 | "verified": false, 28 | "reason": "unsigned", 29 | "signature": "null", 30 | "payload": "null" 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/create-fork-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1296269, 3 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 4 | "name": "Hello-World", 5 | "full_name": "octocat/Hello-World", 6 | "owner": { 7 | "login": "octocat", 8 | "id": 1, 9 | "node_id": "MDQ6VXNlcjE=", 10 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 11 | "gravatar_id": "", 12 | "url": "https://api.github.com/users/octocat", 13 | "html_url": "https://github.com/octocat", 14 | "followers_url": "https://api.github.com/users/octocat/followers", 15 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 16 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 17 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 18 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 19 | "organizations_url": "https://api.github.com/users/octocat/orgs", 20 | "repos_url": "https://api.github.com/users/octocat/repos", 21 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 22 | "received_events_url": "https://api.github.com/users/octocat/received_events", 23 | "type": "User", 24 | "site_admin": false 25 | }, 26 | "private": false, 27 | "html_url": "https://github.com/octocat/Hello-World", 28 | "description": "This your first repo!", 29 | "fork": true, 30 | "url": "https://api.github.com/repos/octocat/Hello-World", 31 | "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", 32 | "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}", 33 | "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", 34 | "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}", 35 | "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", 36 | "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}", 37 | "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}", 38 | "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", 39 | "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}", 40 | "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors", 41 | "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments", 42 | "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads", 43 | "events_url": "http://api.github.com/repos/octocat/Hello-World/events", 44 | "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks", 45 | "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", 46 | "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", 47 | "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", 48 | "git_url": "git:github.com/octocat/Hello-World.git", 49 | "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", 50 | "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}", 51 | "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}", 52 | "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}", 53 | "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}", 54 | "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages", 55 | "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges", 56 | "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}", 57 | "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", 58 | "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}", 59 | "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}", 60 | "ssh_url": "git@github.com:octocat/Hello-World.git", 61 | "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers", 62 | "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}", 63 | "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers", 64 | "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription", 65 | "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags", 66 | "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams", 67 | "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", 68 | "clone_url": "https://github.com/octocat/Hello-World.git", 69 | "mirror_url": "git:git.example.com/octocat/Hello-World", 70 | "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks", 71 | "svn_url": "https://svn.github.com/octocat/Hello-World", 72 | "homepage": "https://github.com", 73 | "language": "null", 74 | "forks_count": 9, 75 | "stargazers_count": 80, 76 | "watchers_count": 80, 77 | "size": 108, 78 | "default_branch": "main", 79 | "open_issues_count": 0, 80 | "is_template": true, 81 | "topics": [ 82 | "octocat", 83 | "atom", 84 | "electron", 85 | "api" 86 | ], 87 | "has_issues": true, 88 | "has_projects": true, 89 | "has_wiki": true, 90 | "has_pages": false, 91 | "has_downloads": true, 92 | "archived": false, 93 | "disabled": false, 94 | "visibility": "public", 95 | "pushed_at": "2011-01-26T19:06:43Z", 96 | "created_at": "2011-01-26T19:01:12Z", 97 | "updated_at": "2011-01-26T19:14:43Z", 98 | "permissions": { 99 | "admin": false, 100 | "push": false, 101 | "pull": true 102 | }, 103 | "allow_rebase_merge": true, 104 | "template_repository": {}, 105 | "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", 106 | "allow_squash_merge": true, 107 | "delete_branch_on_merge": true, 108 | "allow_merge_commit": true, 109 | "subscribers_count": 42, 110 | "network_count": 0 111 | } 112 | -------------------------------------------------------------------------------- /test/fixtures/create-ref-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/featureA", 3 | "node_id": "MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlQQ==", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/featureA", 5 | "object": { 6 | "type": "commit", 7 | "sha": "aa218f56b14c9653891f9e74264a383fa43fefbd", 8 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/aa218f56b14c9653891f9e74264a383fa43fefbd" 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/create-tree-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "cd8274d15fa3ae2ab983129fb037999f264ba9a7", 3 | "url": "https://api.github.com/repos/octocat/Hello-World/trees/cd8274d15fa3ae2ab983129fb037999f264ba9a7", 4 | "tree": [ 5 | { 6 | "path": "file.rb", 7 | "mode": "100644", 8 | "type": "blob", 9 | "size": 132, 10 | "sha": "7c258a9869f33c1e1e1f74fbb32f07c86cb5a75b", 11 | "url": "https://api.github.com/repos/octocat/Hello-World/git/blobs/7c258a9869f33c1e1e1f74fbb32f07c86cb5a75b" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /test/fixtures/diffs/addition.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..f4cbead 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -3,5 +3,6 @@ steps: 6 | args: ['echo', 'foobar'] 7 | - name: 'ubuntu' 8 | args: ['sleep', '30'] 9 | + id: 'added' 10 | - name: 'ubuntu' 11 | args: ['echo', 'done'] 12 | -------------------------------------------------------------------------------- /test/fixtures/diffs/deletion.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..bc796a0 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -1,7 +1,5 @@ 6 | steps: 7 | - name: 'ubuntu' 8 | args: ['echo', 'foobar'] 9 | -- name: 'ubuntu' 10 | - args: ['sleep', '30'] 11 | - name: 'ubuntu' 12 | args: ['echo', 'done'] 13 | -------------------------------------------------------------------------------- /test/fixtures/diffs/many-to-many.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..87f387c 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -1,7 +1,5 @@ 6 | steps: 7 | -- name: 'ubuntu' 8 | - args: ['echo', 'foobar'] 9 | -- name: 'ubuntu' 10 | - args: ['sleep', '30'] 11 | +- name: 'foo' 12 | + args: ['sleep 1'] 13 | - name: 'ubuntu' 14 | args: ['echo', 'done'] 15 | -------------------------------------------------------------------------------- /test/fixtures/diffs/many-to-one.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..71fda1b 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -1,7 +1,4 @@ 6 | steps: 7 | -- name: 'ubuntu' 8 | - args: ['echo', 'foobar'] 9 | -- name: 'ubuntu' 10 | - args: ['sleep', '30'] 11 | +- name: 'foo' 12 | - name: 'ubuntu' 13 | args: ['echo', 'done'] 14 | -------------------------------------------------------------------------------- /test/fixtures/diffs/one-line-to-many-newline.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index 07e5def..ca56e25 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -2,4 +2,5 @@ steps: 6 | - name: 'ubuntu' 7 | args: ['echo', 'foobar'] 8 | - name: 'ubuntu' 9 | - args: ['sleep', '30'] 10 | \ No newline at end of file 11 | + args: ['sleep', '30'] 12 | + id: 'foobar' 13 | -------------------------------------------------------------------------------- /test/fixtures/diffs/one-line-to-many.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..c51c1cd 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -4,4 +4,5 @@ steps: 6 | - name: 'ubuntu' 7 | args: ['sleep', '30'] 8 | - name: 'ubuntu' 9 | - args: ['echo', 'done'] 10 | + args: ['foobar'] 11 | + id: asdf 12 | -------------------------------------------------------------------------------- /test/fixtures/diffs/one-line-to-one.diff: -------------------------------------------------------------------------------- 1 | diff --git a/cloudbuild.yaml b/cloudbuild.yaml 2 | index cac8fbc..199bf56 100644 3 | --- a/cloudbuild.yaml 4 | +++ b/cloudbuild.yaml 5 | @@ -2,6 +2,6 @@ steps: 6 | - name: 'ubuntu' 7 | args: ['echo', 'foobar'] 8 | - name: 'ubuntu' 9 | - args: ['sleep', '30'] 10 | + args: ['sleep', '301'] 11 | - name: 'ubuntu' 12 | args: ['echo', 'done'] 13 | -------------------------------------------------------------------------------- /test/fixtures/get-branch-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "main", 3 | "commit": { 4 | "sha": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 5 | "node_id": "MDY6Q29tbWl0N2ZkMWE2MGIwMWY5MWIzMTRmNTk5NTVhNGU0ZDRlODBkOGVkZjExZA==", 6 | "commit": { 7 | "author": { 8 | "name": "The Octocat", 9 | "date": "2012-03-06T15:06:50-08:00", 10 | "email": "octocat@nowhere.com" 11 | }, 12 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 13 | "message": "Merge pull request #6 from Spaceghost/patch-1\n\nNew line at end of file.", 14 | "tree": { 15 | "sha": "b4eecafa9be2f2006ce1b709d6857b07069b4608", 16 | "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/b4eecafa9be2f2006ce1b709d6857b07069b4608" 17 | }, 18 | "committer": { 19 | "name": "The Octocat", 20 | "date": "2012-03-06T15:06:50-08:00", 21 | "email": "octocat@nowhere.com" 22 | }, 23 | "verification": { 24 | "verified": false, 25 | "reason": "unsigned", 26 | "signature": "null", 27 | "payload": "null" 28 | } 29 | }, 30 | "author": { 31 | "gravatar_id": "", 32 | "avatar_url": "https://secure.gravatar.com/avatar/7ad39074b0584bc555d0417ae3e7d974?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", 33 | "url": "https://api.github.com/users/octocat", 34 | "id": 583231, 35 | "login": "octocat" 36 | }, 37 | "parents": [ 38 | { 39 | "sha": "553c2077f0edc3d5dc5d17262f6aa498e69d6f8e", 40 | "url": "https://api.github.com/repos/octocat/Hello-World/commits/553c2077f0edc3d5dc5d17262f6aa498e69d6f8e" 41 | }, 42 | { 43 | "sha": "762941318ee16e59dabbacb1b4049eec22f0d303", 44 | "url": "https://api.github.com/repos/octocat/Hello-World/commits/762941318ee16e59dabbacb1b4049eec22f0d303" 45 | } 46 | ], 47 | "url": "https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 48 | "committer": { 49 | "gravatar_id": "", 50 | "avatar_url": "https://secure.gravatar.com/avatar/7ad39074b0584bc555d0417ae3e7d974?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", 51 | "url": "https://api.github.com/users/octocat", 52 | "id": 583231, 53 | "login": "octocat" 54 | } 55 | }, 56 | "_links": { 57 | "html": "https://github.com/octocat/Hello-World/tree/main", 58 | "self": "https://api.github.com/repos/octocat/Hello-World/branches/main" 59 | }, 60 | "protected": true, 61 | "protection": { 62 | "enabled": true, 63 | "required_status_checks": { 64 | "enforcement_level": "non_admins", 65 | "contexts": [ 66 | "ci-test", 67 | "linter" 68 | ] 69 | } 70 | }, 71 | "protection_url": "https://api.github.com/repos/octocat/hello-world/branches/main/protection" 72 | } -------------------------------------------------------------------------------- /test/fixtures/get-commit-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "7638417db6d59f3c431d3e1f261cc637155684cd", 3 | "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==", 4 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd", 5 | "author": { 6 | "date": "2014-11-07T22:01:45Z", 7 | "name": "Monalisa Octocat", 8 | "email": "octocat@github.com" 9 | }, 10 | "committer": { 11 | "date": "2014-11-07T22:01:45Z", 12 | "name": "Monalisa Octocat", 13 | "email": "octocat@github.com" 14 | }, 15 | "message": "added readme, because im a good github citizen", 16 | "tree": { 17 | "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/691272480426f78a0138979dd3ce63b77f706feb", 18 | "sha": "691272480426f78a0138979dd3ce63b77f706feb" 19 | }, 20 | "parents": [ 21 | { 22 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5", 23 | "sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5" 24 | } 25 | ], 26 | "verification": { 27 | "verified": false, 28 | "reason": "unsigned", 29 | "signature": "null", 30 | "payload": "null" 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/get-ref-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/release-v5.0.0", 3 | "node_id": "abc123=", 4 | "url": "https://api.github.com/repos/bcoe/test-release-please/git/refs/heads/release-v5.0.0", 5 | "object": { 6 | "sha": "7fe1012cf4a9f9ada0fde9e6a6111ae2bf390c31", 7 | "type": "commit", 8 | "url": "https://api.github.com/repos/bcoe/test-release-please/git/commits/7fe1012cf4a9f9ada0fde9e6a6111ae2bf390c31" 9 | } 10 | } -------------------------------------------------------------------------------- /test/fork.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before, afterEach} from 'mocha'; 19 | import {octokit, setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; 22 | import {fork} from '../src/github/fork'; 23 | 24 | type CreateRefResponse = GetResponseTypeFromEndpointMethod< 25 | typeof octokit.repos.createFork 26 | >; 27 | 28 | before(() => { 29 | setup(); 30 | }); 31 | 32 | describe('Forking function', () => { 33 | const sandbox = sinon.createSandbox(); 34 | afterEach(() => { 35 | sandbox.restore(); 36 | }); 37 | const upstream = {owner: 'upstream-owner', repo: 'upstream-repo'}; 38 | it('Calls Octokit with the correct values', async () => { 39 | const responseData = await import('./fixtures/create-fork-response.json'); 40 | const createRefResponse = { 41 | headers: {}, 42 | status: 202, 43 | url: 'http://fake-url.com', 44 | data: responseData, 45 | } as unknown as CreateRefResponse; 46 | // setup 47 | const stub = sandbox 48 | .stub(octokit.repos, 'createFork') 49 | .resolves(createRefResponse); 50 | // tests 51 | await fork(octokit, upstream); 52 | sandbox.assert.calledOnceWithExactly(stub, { 53 | owner: upstream.owner, 54 | repo: upstream.repo, 55 | }); 56 | }); 57 | it('Returns correct values on success', async () => { 58 | const responseData = await import('./fixtures/create-fork-response.json'); 59 | const createRefResponse = { 60 | headers: {}, 61 | status: 202, 62 | url: 'http://fake-url.com', 63 | data: responseData, 64 | } as unknown as CreateRefResponse; 65 | // setup 66 | sandbox.stub(octokit.repos, 'createFork').resolves(createRefResponse); 67 | // tests 68 | const res = await fork(octokit, upstream); 69 | assert.strictEqual(res.owner, responseData.owner.login); 70 | assert.strictEqual(res.repo, responseData.name); 71 | }); 72 | it('Passes the error message with a throw when octokit fails', async () => { 73 | // setup 74 | const errorMsg = 'Error message'; 75 | sandbox.stub(octokit.repos, 'createFork').rejects(errorMsg); 76 | await assert.rejects( 77 | fork(octokit, upstream), 78 | 'The fork function should have failed because Octokit failed.' 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/git-dir-handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import {describe, it, before, afterEach} from 'mocha'; 18 | import * as assert from 'assert'; 19 | import {setup} from './util'; 20 | import { 21 | getGitFileData, 22 | getAllDiffs, 23 | parseChanges, 24 | findRepoRoot, 25 | resolvePath, 26 | } from '../src/bin/handle-git-dir-change'; 27 | import * as fs from 'fs'; 28 | import * as sinon from 'sinon'; 29 | import * as child_process from 'child_process'; 30 | import * as path from 'path'; 31 | 32 | before(() => { 33 | setup(); 34 | }); 35 | 36 | describe('git directory diff output to git file data + content', () => { 37 | const relativeGitDir = '/fixtures/some/git/dir'; 38 | const absoluteGitDir = process.cwd() + relativeGitDir; 39 | const sandbox = sinon.createSandbox(); 40 | afterEach(() => { 41 | // undo all changes 42 | sandbox.restore(); 43 | }); 44 | it('does not read from a file if the file status is deleted and uses the old mode', async () => { 45 | // setup 46 | const stubReadFile = sandbox.stub(fs, 'readFile').resolves('Text'); 47 | const gitDiffTxt = ':100644 000000 8e6c063 0000000 D\tReadme.md'; 48 | const gitFileData = await getGitFileData(absoluteGitDir, gitDiffTxt); 49 | assert.strictEqual(gitFileData.path, 'Readme.md'); 50 | assert.strictEqual(gitFileData.fileData.mode, '100644'); 51 | assert.strictEqual(gitFileData.fileData.content, null); 52 | sinon.assert.notCalled(stubReadFile); 53 | }); 54 | it('gets the new file mode, content and path for created files', async () => { 55 | // setup 56 | const stubReadFile = sandbox.stub(fs, 'readFile').yields(null, 'Text'); 57 | const gitDiffTxtAdd = ':000000 100644 0000000 8e6c063 A\tReadme.md'; 58 | const gitFileDataAdd = await getGitFileData(absoluteGitDir, gitDiffTxtAdd); 59 | assert.strictEqual(gitFileDataAdd.fileData.content, 'Text'); 60 | assert.strictEqual(gitFileDataAdd.fileData.mode, '100644'); 61 | assert.strictEqual(gitFileDataAdd.path, 'Readme.md'); 62 | sinon.assert.calledOnce(stubReadFile); 63 | }); 64 | it('gets the new file mode, content and path for content modified files', async () => { 65 | // setup 66 | const stubReadFile = sandbox.stub(fs, 'readFile').yields(null, 'new text'); 67 | const gitDiffTxtModified = 68 | ':100644 100644 0000000 8e6c063 M\tmodified/test.txt'; 69 | const gitFileDataModified = await getGitFileData( 70 | absoluteGitDir, 71 | gitDiffTxtModified 72 | ); 73 | assert.strictEqual(gitFileDataModified.fileData.content, 'new text'); 74 | assert.strictEqual(gitFileDataModified.fileData.mode, '100644'); 75 | assert.strictEqual(gitFileDataModified.path, 'modified/test.txt'); 76 | sinon.assert.calledOnce(stubReadFile); 77 | }); 78 | 79 | it('gets the new file mode, content and path for mode modified files', async () => { 80 | // setup 81 | const stubReadFile = sandbox 82 | .stub(fs, 'readFile') 83 | .yields(null, '#!/bin/bash'); 84 | const gitDiffTxtToExecutable = 85 | ':100644 100755 3b18e51 3b18e51 M\tbin/main/test.exe'; 86 | const gitFileDataTxtToExecutable = await getGitFileData( 87 | absoluteGitDir, 88 | gitDiffTxtToExecutable 89 | ); 90 | assert.strictEqual( 91 | gitFileDataTxtToExecutable.fileData.content, 92 | '#!/bin/bash' 93 | ); 94 | assert.strictEqual(gitFileDataTxtToExecutable.fileData.mode, '100755'); 95 | assert.strictEqual(gitFileDataTxtToExecutable.path, 'bin/main/test.exe'); 96 | sinon.assert.calledOnce(stubReadFile); 97 | }); 98 | }); 99 | 100 | describe('Repository root', () => { 101 | const dir = '/some/dir'; 102 | const sandbox = sinon.createSandbox(); 103 | afterEach(() => { 104 | // undo all changes 105 | sandbox.restore(); 106 | }); 107 | it('Executes the git find root bash command', () => { 108 | const stubGitDiff = sandbox 109 | .stub(child_process, 'execSync') 110 | .returns(Buffer.from(dir)); 111 | findRepoRoot(dir); 112 | sinon.assert.calledOnceWithExactly( 113 | stubGitDiff, 114 | 'git rev-parse --show-toplevel', 115 | {cwd: dir} 116 | ); 117 | }); 118 | }); 119 | 120 | describe('Path resolving', () => { 121 | it("Resolves to absolute path when './' is a prefix", () => { 122 | const relativeGitDir = './test/fixtures'; 123 | const testingPath = resolvePath(relativeGitDir); 124 | assert.strictEqual(path.isAbsolute(testingPath), true); 125 | }); 126 | 127 | it('Resolves to absolute path when the leading chars are letters', () => { 128 | const relativeGitDir = 'test/fixtures'; 129 | const testingPath = resolvePath(relativeGitDir); 130 | assert.strictEqual(path.isAbsolute(testingPath), true); 131 | }); 132 | }); 133 | 134 | describe('Finding repository root', () => { 135 | const sandbox = sinon.createSandbox(); 136 | afterEach(() => { 137 | sandbox.restore(); 138 | }); 139 | 140 | it('Removes the \\n character', () => { 141 | sandbox 142 | .stub(child_process, 'execSync') 143 | .returns(Buffer.from('/home/user/work\n')); 144 | assert.strictEqual( 145 | findRepoRoot('home/user/work/subdir'), 146 | '/home/user/work' 147 | ); 148 | }); 149 | 150 | it('Rethrows execsync error', () => { 151 | const error = new Error('Execsync error'); 152 | sandbox.stub(child_process, 'execSync').throws(error); 153 | assert.throws(() => findRepoRoot('home/user/work/subdir'), error); 154 | }); 155 | }); 156 | 157 | describe('parse all git diff output', () => { 158 | const testDir = process.cwd() + '/fixtures'; 159 | const sandbox = sinon.createSandbox(); 160 | afterEach(() => { 161 | // undo all changes 162 | sandbox.restore(); 163 | }); 164 | it('splits file diff into a list and removed \\n', async () => { 165 | sandbox 166 | .stub(child_process, 'execSync') 167 | .returns( 168 | Buffer.from( 169 | ':000000 100644 0000000 8e6c063 A\tadded.txt\n:100644 000000 8e6c063 0000000 D\tdeleted.txt\n' 170 | ) 171 | ); 172 | const diffs = getAllDiffs(testDir); 173 | assert.strictEqual(diffs[0], ':000000 100644 0000000 8e6c063 A\tadded.txt'); 174 | assert.strictEqual( 175 | diffs[1], 176 | ':100644 000000 8e6c063 0000000 D\tdeleted.txt' 177 | ); 178 | assert.strictEqual(diffs.length, 2); 179 | }); 180 | }); 181 | 182 | describe('parse changes', () => { 183 | const testDir = '/test/dir'; 184 | const sandbox = sinon.createSandbox(); 185 | const diffs = [ 186 | ':000000 100644 0000000 8e6c063 A\tadded.txt', 187 | ':100644 000000 8e6c063 0000000 D\tdeleted.txt', 188 | ]; 189 | afterEach(() => { 190 | // undo all changes 191 | sandbox.restore(); 192 | }); 193 | it('populates change object with everything from a diff output', async () => { 194 | sandbox.stub(fs, 'readFile').yields(null, 'new text'); 195 | const changes = await parseChanges(diffs, testDir); 196 | assert.strictEqual(changes.get('added.txt')?.mode, '100644'); 197 | assert.strictEqual(changes.get('added.txt')?.content, 'new text'); 198 | assert.strictEqual(changes.get('deleted.txt')?.mode, '100644'); 199 | assert.strictEqual(changes.get('deleted.txt')?.content, null); 200 | }); 201 | it('Passes up the error message with a throw when it is ', async () => { 202 | // setup 203 | sandbox.stub(fs, 'readFile').throws(Error()); 204 | await assert.rejects(parseChanges(diffs, '')); 205 | }); 206 | it('Passes up the error message with a throw when reading the file fails', async () => { 207 | // setup 208 | sandbox.stub(fs, 'readFile').yields(Error(), ''); 209 | await assert.rejects(parseChanges(diffs, '')); 210 | }); 211 | it('Passes up the error message with a throw when parsing the diff fails', async () => { 212 | // setup 213 | const badDiff = [':000000 100644 0000000 8e6c063 Aadded.txt']; 214 | await assert.rejects(parseChanges(badDiff, '')); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /test/helper-review-pull-request.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before} from 'mocha'; 19 | import {octokit, setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {RepoDomain, FileDiffContent, Hunk} from '../src/types'; 22 | import {Octokit} from '@octokit/rest'; 23 | import {readFileSync} from 'fs'; 24 | import {resolve} from 'path'; 25 | import * as reviewPullRequestHandler from '../src/github/review-pull-request'; 26 | import * as hunkHandler from '../src/utils/hunk-utils'; 27 | 28 | const fixturePath = 'test/fixtures/diffs'; 29 | 30 | const sandbox = sinon.createSandbox(); 31 | 32 | before(() => { 33 | setup(); 34 | }); 35 | 36 | describe('createPullRequestReview', () => { 37 | const diffContents: Map = new Map(); 38 | diffContents.set('src/index.ts', { 39 | newContent: 'hello world', 40 | oldContent: 'hello', 41 | }); 42 | const pageSize = 3; 43 | const pullNumber = 100; 44 | const repo = 'helper-comment-review-repo'; 45 | const owner = 'helper-comment-review-owner'; 46 | const remote: RepoDomain = {repo, owner}; 47 | 48 | let getPullRequestHunksStub: sinon.SinonStub; 49 | let makeInlineSuggestionsStub: sinon.SinonStub; 50 | 51 | beforeEach(() => { 52 | getPullRequestHunksStub = sandbox.stub( 53 | reviewPullRequestHandler, 54 | 'getPullRequestHunks' 55 | ); 56 | makeInlineSuggestionsStub = sandbox.stub( 57 | reviewPullRequestHandler, 58 | 'makeInlineSuggestions' 59 | ); 60 | }); 61 | 62 | afterEach(() => { 63 | sandbox.restore(); 64 | }); 65 | 66 | it('Succeeds when all values are passed as expected', async () => { 67 | const validFileHunks = new Map(); 68 | const invalidFileHunks = new Map(); 69 | const suggestionHunks = new Map(); 70 | getPullRequestHunksStub.resolves(validFileHunks); 71 | const getRawSuggestionHunksStub = sandbox.stub( 72 | hunkHandler, 73 | 'getRawSuggestionHunks' 74 | ); 75 | getRawSuggestionHunksStub.returns(suggestionHunks); 76 | const partitionHunksStub = sandbox.stub( 77 | hunkHandler, 78 | 'partitionSuggestedHunksByScope' 79 | ); 80 | partitionHunksStub.returns({ 81 | validHunks: validFileHunks, 82 | invalidHunks: invalidFileHunks, 83 | }); 84 | makeInlineSuggestionsStub.resolves(234); 85 | 86 | await reviewPullRequestHandler.createPullRequestReview( 87 | octokit, 88 | remote, 89 | pullNumber, 90 | pageSize, 91 | diffContents 92 | ); 93 | 94 | sinon.assert.calledWith( 95 | getPullRequestHunksStub, 96 | sinon.match.instanceOf(Octokit), 97 | { 98 | owner, 99 | repo, 100 | }, 101 | pullNumber, 102 | pageSize 103 | ); 104 | sinon.assert.calledWith(getRawSuggestionHunksStub, diffContents); 105 | sinon.assert.calledWith( 106 | partitionHunksStub, 107 | validFileHunks, 108 | suggestionHunks 109 | ); 110 | sinon.assert.calledWith( 111 | makeInlineSuggestionsStub, 112 | sinon.match.instanceOf(Octokit), 113 | validFileHunks, 114 | invalidFileHunks, 115 | remote, 116 | pullNumber 117 | ); 118 | }); 119 | 120 | it('Succeeds when diff string provided', async () => { 121 | const diffString = readFileSync( 122 | resolve(fixturePath, 'many-to-many.diff') 123 | ).toString(); 124 | const validFileHunks = new Map(); 125 | validFileHunks.set('cloudbuild.yaml', [ 126 | { 127 | newStart: 0, 128 | newEnd: 10, 129 | oldStart: 0, 130 | oldEnd: 10, 131 | newContent: [], 132 | }, 133 | ]); 134 | 135 | getPullRequestHunksStub.resolves(validFileHunks); 136 | makeInlineSuggestionsStub.resolves(234); 137 | 138 | await reviewPullRequestHandler.createPullRequestReview( 139 | octokit, 140 | remote, 141 | pullNumber, 142 | pageSize, 143 | diffString 144 | ); 145 | 146 | sinon.assert.calledWith( 147 | getPullRequestHunksStub, 148 | sinon.match.instanceOf(Octokit), 149 | {owner, repo}, 150 | pullNumber, 151 | pageSize 152 | ); 153 | sinon.assert.calledWith( 154 | makeInlineSuggestionsStub, 155 | sinon.match.instanceOf(Octokit), 156 | sinon.match.map, 157 | sinon.match.map, 158 | remote, 159 | pullNumber 160 | ); 161 | }); 162 | 163 | it('Passes up the error message when getPullRequestHunks helper fails', async () => { 164 | const error = new Error('getPullRequestHunks failed'); 165 | getPullRequestHunksStub.rejects(error); 166 | 167 | await assert.rejects( 168 | reviewPullRequestHandler.createPullRequestReview( 169 | octokit, 170 | remote, 171 | pullNumber, 172 | pageSize, 173 | diffContents 174 | ), 175 | error 176 | ); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/hunk-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {describe, it, before} from 'mocha'; 16 | import {setup} from './util'; 17 | import * as assert from 'assert'; 18 | import {adjustHunkUp, adjustHunkDown} from '../src/utils/hunk-utils'; 19 | 20 | before(() => { 21 | setup(); 22 | }); 23 | 24 | describe('adjustHunkUp', () => { 25 | it('returns a new hunk if there is a previous line', () => { 26 | const hunk = { 27 | oldStart: 5, 28 | oldEnd: 5, 29 | newStart: 5, 30 | newEnd: 5, 31 | newContent: [" args: ['sleep', '301']"], 32 | nextLine: "- name: 'ubuntu'", 33 | previousLine: "- name: 'ubuntu'", 34 | }; 35 | const adjustedHunk = adjustHunkUp(hunk); 36 | assert.deepStrictEqual(adjustedHunk, { 37 | oldStart: 4, 38 | oldEnd: 5, 39 | newStart: 4, 40 | newEnd: 5, 41 | newContent: ["- name: 'ubuntu'", " args: ['sleep', '301']"], 42 | }); 43 | }); 44 | it('returns null if there is no previous line', () => { 45 | const hunk = { 46 | oldStart: 5, 47 | oldEnd: 5, 48 | newStart: 5, 49 | newEnd: 5, 50 | newContent: [" args: ['sleep', '301']"], 51 | }; 52 | const adjustedHunk = adjustHunkUp(hunk); 53 | assert.strictEqual(adjustedHunk, null); 54 | }); 55 | }); 56 | 57 | describe('adjustHunkDown', () => { 58 | it('returns a new hunk if there is a next line', () => { 59 | const hunk = { 60 | oldStart: 5, 61 | oldEnd: 5, 62 | newStart: 5, 63 | newEnd: 5, 64 | newContent: [" args: ['sleep', '301']"], 65 | nextLine: "- name: 'ubuntu'", 66 | previousLine: "- name: 'ubuntu'", 67 | }; 68 | const adjustedHunk = adjustHunkDown(hunk); 69 | assert.deepStrictEqual(adjustedHunk, { 70 | oldStart: 5, 71 | oldEnd: 6, 72 | newStart: 5, 73 | newEnd: 6, 74 | newContent: [" args: ['sleep', '301']", "- name: 'ubuntu'"], 75 | }); 76 | }); 77 | it('returns null if there is no previous line', () => { 78 | const hunk = { 79 | oldStart: 5, 80 | oldEnd: 5, 81 | newStart: 5, 82 | newEnd: 5, 83 | newContent: [" args: ['sleep', '301']"], 84 | }; 85 | const adjustedHunk = adjustHunkDown(hunk); 86 | assert.deepStrictEqual(adjustedHunk, null); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/invalid-hunks.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as assert from 'assert'; 16 | import {describe, it, before} from 'mocha'; 17 | import {setup} from './util'; 18 | import {buildSummaryComment} from '../src/github/review-pull-request'; 19 | 20 | before(() => { 21 | setup(); 22 | }); 23 | 24 | describe('buildErrorMessage', () => { 25 | it('should handle an empty list of failures', () => { 26 | const invalidHunks = new Map(); 27 | const expectedMessage = ''; 28 | 29 | const errorMessage = buildSummaryComment(invalidHunks); 30 | assert.strictEqual(errorMessage, expectedMessage); 31 | }); 32 | 33 | it('should handle multiple file entries', () => { 34 | const invalidHunks = new Map(); 35 | invalidHunks.set('foo.txt', [ 36 | {oldStart: 1, oldEnd: 2, newStart: 1, newEnd: 2}, 37 | ]); 38 | invalidHunks.set('bar.txt', [ 39 | {oldStart: 3, oldEnd: 4, newStart: 3, newEnd: 4}, 40 | ]); 41 | const expectedMessage = `Some suggestions could not be made: 42 | * foo.txt 43 | * lines 1-2 44 | * bar.txt 45 | * lines 3-4`; 46 | 47 | const errorMessage = buildSummaryComment(invalidHunks); 48 | assert.strictEqual(errorMessage, expectedMessage); 49 | }); 50 | 51 | it('should handle multiple entries for a file', () => { 52 | const invalidHunks = new Map(); 53 | invalidHunks.set('foo.txt', [ 54 | {oldStart: 1, oldEnd: 2, newStart: 1, newEnd: 2}, 55 | {oldStart: 3, oldEnd: 4, newStart: 3, newEnd: 4}, 56 | ]); 57 | const expectedMessage = `Some suggestions could not be made: 58 | * foo.txt 59 | * lines 1-2 60 | * lines 3-4`; 61 | 62 | const errorMessage = buildSummaryComment(invalidHunks); 63 | assert.strictEqual(errorMessage, expectedMessage); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/issues.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before, afterEach} from 'mocha'; 19 | import {octokit, setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; 22 | import {addLabels} from '../src/github/labels'; 23 | 24 | type AddLabelsResponse = GetResponseTypeFromEndpointMethod< 25 | typeof octokit.issues.addLabels 26 | >; 27 | 28 | before(() => { 29 | setup(); 30 | }); 31 | 32 | describe('Adding labels', async () => { 33 | const sandbox = sinon.createSandbox(); 34 | const upstream = {owner: 'upstream-owner', repo: 'upstream-repo'}; 35 | const origin = { 36 | owner: 'origin-owner', 37 | repo: 'origin-repo', 38 | branch: 'issues-test-branch', 39 | }; 40 | const issue_number = 1; 41 | const labels = ['enhancement']; 42 | afterEach(() => { 43 | sandbox.restore(); 44 | }); 45 | 46 | it('Invokes octokit issues add labels on an existing pull request', async () => { 47 | // setup 48 | const responseAddLabelsData = await import( 49 | './fixtures/add-labels-response.json' 50 | ); 51 | const addLabelsResponse: AddLabelsResponse = { 52 | headers: {}, 53 | status: 200, 54 | url: 'http://fake-url.com', 55 | data: responseAddLabelsData, 56 | }; 57 | const stub = sandbox 58 | .stub(octokit.issues, 'addLabels') 59 | .resolves(addLabelsResponse); 60 | // tests 61 | const resultingLabels = await addLabels( 62 | octokit, 63 | upstream, 64 | origin, 65 | issue_number, 66 | labels 67 | ); 68 | sandbox.assert.calledOnceWithExactly(stub, { 69 | owner: upstream.owner, 70 | repo: origin.repo, 71 | issue_number: issue_number, 72 | labels: labels, 73 | }); 74 | assert.deepStrictEqual(resultingLabels, ['bug', 'enhancement']); 75 | }); 76 | 77 | it('No-op undefined labels', async () => { 78 | // setup 79 | const stub = sandbox.stub(octokit.issues, 'addLabels').resolves(); 80 | // tests 81 | const resultingLabels = await addLabels( 82 | octokit, 83 | upstream, 84 | origin, 85 | issue_number 86 | ); 87 | sandbox.assert.neverCalledWith(stub, sinon.match.any); 88 | assert.deepStrictEqual(resultingLabels, []); 89 | }); 90 | 91 | it('No-op with empty labels', async () => { 92 | // setup 93 | const stub = sandbox.stub(octokit.issues, 'addLabels').resolves(); 94 | // tests 95 | const resultingLabels = await addLabels( 96 | octokit, 97 | upstream, 98 | origin, 99 | issue_number, 100 | [] 101 | ); 102 | sandbox.assert.neverCalledWith(stub, sinon.match.any); 103 | assert.deepStrictEqual(resultingLabels, []); 104 | }); 105 | 106 | it('Passes up the error message with a throw when octokit issues add labels fails', async () => { 107 | // setup 108 | const error = new Error('Error message'); 109 | sandbox.stub(octokit.issues, 'addLabels').rejects(error); 110 | await assert.rejects( 111 | addLabels(octokit, upstream, origin, issue_number, labels), 112 | error 113 | ); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/main-pr-option-defaults.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as assert from 'assert'; 16 | import {describe, it, before} from 'mocha'; 17 | import {setup} from './util'; 18 | import { 19 | CreatePullRequestUserOptions, 20 | CreateReviewCommentUserOptions, 21 | } from '../src/types'; 22 | import { 23 | addPullRequestDefaults, 24 | addReviewCommentsDefaults, 25 | } from '../src/default-options-handler'; 26 | 27 | before(() => { 28 | setup(); 29 | }); 30 | 31 | describe('addPullRequestDefaults', () => { 32 | it('Populates all un-specified parameters with a default', () => { 33 | const upstreamOnly: CreatePullRequestUserOptions = { 34 | upstreamOwner: 'owner', 35 | upstreamRepo: 'repo', 36 | description: 'custom description', 37 | title: 'chore: custom title', 38 | message: 'chore: custom description', 39 | }; 40 | const gitHubPr1 = addPullRequestDefaults(upstreamOnly); 41 | assert.deepStrictEqual(gitHubPr1, { 42 | upstreamOwner: 'owner', 43 | upstreamRepo: 'repo', 44 | branch: 'code-suggestions', 45 | force: false, 46 | description: 'custom description', 47 | title: 'chore: custom title', 48 | message: 'chore: custom description', 49 | primary: 'main', 50 | maintainersCanModify: true, 51 | filesPerCommit: undefined, 52 | }); 53 | const upstreamAndPrimary: CreatePullRequestUserOptions = { 54 | upstreamOwner: 'owner', 55 | upstreamRepo: 'repo', 56 | primary: 'non-default-primary-branch', 57 | description: 'custom description', 58 | title: 'chore: custom title', 59 | message: 'chore: custom description', 60 | }; 61 | const gitHubPr2 = addPullRequestDefaults(upstreamAndPrimary); 62 | assert.deepStrictEqual(gitHubPr2, { 63 | upstreamOwner: 'owner', 64 | upstreamRepo: 'repo', 65 | branch: 'code-suggestions', 66 | force: false, 67 | description: 'custom description', 68 | title: 'chore: custom title', 69 | message: 'chore: custom description', 70 | primary: 'non-default-primary-branch', 71 | maintainersCanModify: true, 72 | filesPerCommit: undefined, 73 | }); 74 | const upstreamAndPrDescription: CreatePullRequestUserOptions = { 75 | upstreamOwner: 'owner', 76 | upstreamRepo: 'repo', 77 | description: 'Non-default PR description', 78 | title: 'chore: code suggestions non-default PR ttile', 79 | message: 'chore: custom code suggestions message', 80 | }; 81 | const gitHubPr3 = addPullRequestDefaults(upstreamAndPrDescription); 82 | assert.deepStrictEqual(gitHubPr3, { 83 | upstreamOwner: 'owner', 84 | upstreamRepo: 'repo', 85 | branch: 'code-suggestions', 86 | description: 'Non-default PR description', 87 | title: 'chore: code suggestions non-default PR ttile', 88 | force: false, 89 | message: 'chore: custom code suggestions message', 90 | primary: 'main', 91 | maintainersCanModify: true, 92 | filesPerCommit: undefined, 93 | }); 94 | }); 95 | it("Uses all of user's provided options", () => { 96 | const options: CreatePullRequestUserOptions = { 97 | upstreamOwner: 'owner', 98 | upstreamRepo: 'repo', 99 | branch: 'custom-code-suggestion-branch', 100 | description: 'The PR will use this description', 101 | title: 'chore: code suggestions custom PR title', 102 | force: true, 103 | message: 'chore: code suggestions custom commit message', 104 | primary: 'non-default-primary-branch', 105 | maintainersCanModify: false, 106 | filesPerCommit: 10, 107 | }; 108 | const gitHubPr = addPullRequestDefaults(options); 109 | assert.deepStrictEqual(gitHubPr, options); 110 | }); 111 | }); 112 | 113 | describe('addReviewCommentsDefaults', () => { 114 | it('Populates all un-specified parameters with a default', () => { 115 | const reviewOptionsWithDefaultPageSize: CreateReviewCommentUserOptions = { 116 | owner: 'owner', 117 | repo: 'repo', 118 | pullNumber: 12345678, 119 | }; 120 | const gitHubPrReview = addReviewCommentsDefaults( 121 | reviewOptionsWithDefaultPageSize 122 | ); 123 | assert.deepStrictEqual(gitHubPrReview, { 124 | owner: 'owner', 125 | repo: 'repo', 126 | pullNumber: 12345678, 127 | pageSize: 100, 128 | }); 129 | }); 130 | it("Uses all of user's provided options", () => { 131 | const reviewOptions: CreateReviewCommentUserOptions = { 132 | owner: 'owner', 133 | repo: 'repo', 134 | pullNumber: 12345678, 135 | pageSize: 4321, 136 | }; 137 | const gitHubPrReview = addReviewCommentsDefaults(reviewOptions); 138 | assert.deepStrictEqual(gitHubPrReview, reviewOptions); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/main-review-pull-request.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before} from 'mocha'; 19 | import {octokit, setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {CreateReviewComment, FileDiffContent} from '../src/types'; 22 | import {Octokit} from '@octokit/rest'; 23 | import * as reviewPullRequestHandler from '../src/github/review-pull-request'; 24 | import {reviewPullRequest} from '../src/index'; 25 | 26 | const sandbox = sinon.createSandbox(); 27 | 28 | before(() => { 29 | setup(); 30 | }); 31 | 32 | /* eslint-disable @typescript-eslint/no-unused-vars */ 33 | describe('reviewPullRequest', () => { 34 | const diffContents: Map = new Map(); 35 | diffContents.set('src/index.ts', { 36 | newContent: 'hello world', 37 | oldContent: 'hello', 38 | }); 39 | const repo = 'main-comment-review-repo'; 40 | const owner = 'main-comment-review-owner'; 41 | const pullNumber = 1232143242; 42 | const pageSize = 321; 43 | const options: CreateReviewComment = { 44 | repo, 45 | owner, 46 | pullNumber, 47 | pageSize, 48 | }; 49 | let reviewPullRequestStub: sinon.SinonStub; 50 | 51 | beforeEach(() => { 52 | reviewPullRequestStub = sandbox.stub( 53 | reviewPullRequestHandler, 54 | 'createPullRequestReview' 55 | ); 56 | }); 57 | 58 | afterEach(() => { 59 | sandbox.restore(); 60 | }); 61 | 62 | it('Does not error on success', async () => { 63 | reviewPullRequestStub.resolves(); 64 | await reviewPullRequest(octokit, diffContents, options); 65 | sinon.assert.calledOnceWithMatch( 66 | reviewPullRequestStub, 67 | sinon.match.instanceOf(Octokit), 68 | { 69 | owner, 70 | repo, 71 | }, 72 | pullNumber, 73 | pageSize, 74 | diffContents 75 | ); 76 | }); 77 | 78 | it('Does not call the github handlers when there are no changes to make because user passed an empty changeset', async () => { 79 | await reviewPullRequest(octokit, new Map(), options); 80 | sinon.assert.notCalled(reviewPullRequestStub); 81 | }); 82 | 83 | it('Passes up the error message when the create review comment helper fails', async () => { 84 | const error = new Error('Review pull request helper failed'); 85 | reviewPullRequestStub.rejects(error); 86 | await assert.rejects( 87 | reviewPullRequest(octokit, diffContents, options), 88 | error 89 | ); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/parse-text-files.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as assert from 'assert'; 16 | import {describe, it, before} from 'mocha'; 17 | import {setup} from './util'; 18 | import {parseTextFiles} from '../src/'; 19 | import {FileData} from '../src/types'; 20 | 21 | before(() => { 22 | setup(); 23 | }); 24 | 25 | describe('Parse text files function', () => { 26 | it('should parse map objects into Change object', () => { 27 | const userFilesMap = new Map(); 28 | userFilesMap.set('src/index.js', "console.log('hello index!'"); 29 | userFilesMap.set('src/foo.js', "console.log('hello foo!'"); 30 | userFilesMap.set('src/deleted.js', null); 31 | 32 | const parsedMap = new Map(); 33 | parsedMap.set( 34 | 'src/index.js', 35 | new FileData("console.log('hello index!'", '100644') 36 | ); 37 | parsedMap.set( 38 | 'src/foo.js', 39 | new FileData("console.log('hello foo!'", '100644') 40 | ); 41 | parsedMap.set('src/deleted.js', new FileData(null, '100644')); 42 | // tests 43 | const changes = parseTextFiles(userFilesMap); 44 | console.log(changes); 45 | assert.deepStrictEqual(changes, parsedMap); 46 | }); 47 | it('Parses objects of string property value into Change object', () => { 48 | const userFilesObject = { 49 | 'src/index.js': "console.log('hello index!'", 50 | 'src/foo.js': "console.log('hello foo!'", 51 | 'src/deleted.js': null, 52 | }; 53 | const parsedMap = new Map(); 54 | parsedMap.set( 55 | 'src/index.js', 56 | new FileData("console.log('hello index!'", '100644') 57 | ); 58 | parsedMap.set( 59 | 'src/foo.js', 60 | new FileData("console.log('hello foo!'", '100644') 61 | ); 62 | parsedMap.set('src/deleted.js', new FileData(null, '100644')); 63 | // tests 64 | const changes = parseTextFiles(userFilesObject); 65 | assert.deepStrictEqual(changes, parsedMap); 66 | }); 67 | it('Rejects invalid file data for objects', () => { 68 | // tests 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | let userFilesObject: any = { 71 | 'src/index.js': [], 72 | }; 73 | assert.throws( 74 | () => parseTextFiles(userFilesObject), 75 | err => { 76 | assert.ok(err instanceof TypeError); 77 | return true; 78 | } 79 | ); 80 | userFilesObject = { 81 | 'src/foo.js': 1234, 82 | }; 83 | assert.throws( 84 | () => parseTextFiles(userFilesObject), 85 | err => { 86 | assert.ok(err instanceof TypeError); 87 | return true; 88 | } 89 | ); 90 | userFilesObject = { 91 | 'src/deleted.js': undefined, 92 | }; 93 | assert.throws( 94 | () => parseTextFiles(userFilesObject), 95 | err => { 96 | assert.ok(err instanceof TypeError); 97 | return true; 98 | } 99 | ); 100 | }); 101 | it('Rejects invalid file data for map', () => { 102 | const userFilesMap = new Map(); 103 | userFilesMap.set('asfd', 1); 104 | assert.throws( 105 | () => parseTextFiles(userFilesMap), 106 | err => { 107 | assert.ok(err instanceof TypeError); 108 | return true; 109 | } 110 | ); 111 | 112 | userFilesMap.set('asfd', undefined); 113 | assert.throws( 114 | () => parseTextFiles(userFilesMap), 115 | err => { 116 | assert.ok(err instanceof TypeError); 117 | return true; 118 | } 119 | ); 120 | 121 | userFilesMap.set('asfd', []); 122 | assert.throws( 123 | () => parseTextFiles(userFilesMap), 124 | err => { 125 | assert.ok(err instanceof TypeError); 126 | return true; 127 | } 128 | ); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/pull-request-hunks.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before, afterEach} from 'mocha'; 19 | import {setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {Octokit} from '@octokit/rest'; 22 | import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; 23 | import {getPullRequestHunks} from '../src/github/review-pull-request'; 24 | 25 | const octokit = new Octokit(); 26 | type ListFilesResponse = GetResponseTypeFromEndpointMethod< 27 | typeof octokit.pulls.listFiles 28 | >; 29 | 30 | before(() => { 31 | setup(); 32 | }); 33 | 34 | describe('getPullRequestHunks', () => { 35 | const upstream = {owner: 'upstream-owner', repo: 'upstream-repo'}; 36 | const pullNumber = 10; 37 | const pageSize = 80; 38 | const sandbox = sinon.createSandbox(); 39 | afterEach(() => { 40 | sandbox.restore(); 41 | }); 42 | 43 | it('Returns the correct values when octokit and patch text parsing function execute properly', async () => { 44 | // setup 45 | const patch = `@@ -1,2 +1,5 @@ 46 | Hello world 47 | -! 48 | +Goodbye World 49 | +gOodBYE world 50 | + 51 | +Goodbye World`; 52 | const listFilesOfPRResult: ListFilesResponse = { 53 | headers: {}, 54 | status: 200, 55 | url: 'http://fake-url.com', 56 | data: [ 57 | { 58 | sha: 'a1d470fa4d7b04450715e3e02d240a34517cd988', 59 | filename: 'Readme.md', 60 | status: 'modified', 61 | additions: 4, 62 | deletions: 1, 63 | changes: 5, 64 | blob_url: 65 | 'https://github.com/TomKristie/HelloWorld/blob/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 66 | raw_url: 67 | 'https://github.com/TomKristie/HelloWorld/raw/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 68 | contents_url: 69 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/Readme.md?ref=eb53f3871f56e8dd6321e44621fe6ac2da1bc120', 70 | patch: patch, 71 | }, 72 | ], 73 | }; 74 | const stub = sandbox 75 | .stub(octokit.pulls, 'listFiles') 76 | .resolves(listFilesOfPRResult); 77 | 78 | // tests 79 | const pullRequestHunks = await getPullRequestHunks( 80 | octokit, 81 | upstream, 82 | pullNumber, 83 | pageSize 84 | ); 85 | sandbox.assert.calledOnceWithExactly(stub, { 86 | owner: upstream.owner, 87 | repo: upstream.repo, 88 | pull_number: pullNumber, 89 | per_page: pageSize, 90 | }); 91 | const hunks = pullRequestHunks.get('Readme.md'); 92 | assert.notStrictEqual(hunks, null); 93 | assert.strictEqual(hunks!.length, 1); 94 | assert.strictEqual(hunks![0].newStart, 2); 95 | assert.strictEqual(hunks![0].newEnd, 5); 96 | }); 97 | 98 | it('Passes up the error when a sub-method fails', async () => { 99 | // setup 100 | const error = new Error('Test error for list files'); 101 | sandbox.stub(octokit.pulls, 'listFiles').rejects(error); 102 | 103 | // tests 104 | await assert.rejects( 105 | getPullRequestHunks(octokit, upstream, pullNumber, pageSize), 106 | error 107 | ); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/remote-github-patch-text.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable node/no-unsupported-features/node-builtins */ 16 | 17 | import * as assert from 'assert'; 18 | import {describe, it, before, afterEach} from 'mocha'; 19 | import {setup} from './util'; 20 | import * as sinon from 'sinon'; 21 | import {getCurrentPullRequestPatches} from '../src/github/review-pull-request'; 22 | import {Octokit} from '@octokit/rest'; 23 | import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; 24 | import {logger} from '../src/logger'; 25 | 26 | const octokit = new Octokit({}); 27 | type ListFilesResponse = GetResponseTypeFromEndpointMethod< 28 | typeof octokit.pulls.listFiles 29 | >; 30 | 31 | before(() => { 32 | setup(); 33 | }); 34 | 35 | describe('getCurrentPullRequestPatches', () => { 36 | const sandbox = sinon.createSandbox(); 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | const upstream = {owner: 'upstream-owner', repo: 'upstream-repo'}; 41 | const pullNumber = 10; 42 | const pageSize = 80; 43 | 44 | it('Calls Octokit with the correct values', async () => { 45 | // setup 46 | const listFilesOfPRResult: ListFilesResponse = { 47 | headers: {}, 48 | status: 200, 49 | url: 'http://fake-url.com', 50 | data: [ 51 | { 52 | sha: 'a1d470fa4d7b04450715e3e02d240a34517cd988', 53 | filename: 'Readme.md', 54 | status: 'modified', 55 | additions: 4, 56 | deletions: 1, 57 | changes: 5, 58 | blob_url: 59 | 'https://github.com/TomKristie/HelloWorld/blob/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 60 | raw_url: 61 | 'https://github.com/TomKristie/HelloWorld/raw/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 62 | contents_url: 63 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/Readme.md?ref=eb53f3871f56e8dd6321e44621fe6ac2da1bc120', 64 | patch: 65 | '@@ -1,2 +1,5 @@\n Hello world\n-!\n+Goodbye World\n+gOodBYE world\n+\n+Goodbye World', 66 | }, 67 | ], 68 | }; 69 | const stub = sandbox 70 | .stub(octokit.pulls, 'listFiles') 71 | .resolves(listFilesOfPRResult); 72 | 73 | // tests 74 | await getCurrentPullRequestPatches(octokit, upstream, pullNumber, pageSize); 75 | sandbox.assert.calledOnceWithExactly(stub, { 76 | owner: upstream.owner, 77 | repo: upstream.repo, 78 | pull_number: pullNumber, 79 | per_page: pageSize, 80 | }); 81 | }); 82 | it('Returns all the valid patches', async () => { 83 | // setup 84 | const listFilesOfPRResult: ListFilesResponse = { 85 | headers: {}, 86 | status: 200, 87 | url: 'http://fake-url.com', 88 | data: [ 89 | { 90 | sha: 'a1d470fa4d7b04450715e3e02d240a34517cd988', 91 | filename: 'Readme.md', 92 | status: 'modified', 93 | additions: 4, 94 | deletions: 1, 95 | changes: 5, 96 | blob_url: 97 | 'https://github.com/TomKristie/HelloWorld/blob/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 98 | raw_url: 99 | 'https://github.com/TomKristie/HelloWorld/raw/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 100 | contents_url: 101 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/Readme.md?ref=eb53f3871f56e8dd6321e44621fe6ac2da1bc120', 102 | patch: 103 | '@@ -1,2 +1,5 @@\n Hello world\n-!\n+Goodbye World\n+gOodBYE world\n+\n+Goodbye World', 104 | }, 105 | { 106 | sha: '8b137891791fe96927ad78e64b0aad7bded08bdc', 107 | filename: 'foo/foo.txt', 108 | status: 'modified', 109 | additions: 1, 110 | deletions: 1, 111 | changes: 2, 112 | blob_url: 113 | 'https://github.com/TomKristie/HelloWorld/blob/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/foo/foo.txt', 114 | raw_url: 115 | 'https://github.com/TomKristie/HelloWorld/raw/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/foo/foo.txt', 116 | contents_url: 117 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/foo/foo.txt?ref=eb53f3871f56e8dd6321e44621fe6ac2da1bc120', 118 | patch: '@@ -1 +1 @@\n-Hello foo\n+', 119 | }, 120 | { 121 | sha: '3b18e512dba79e4c8300dd08aeb37f8e728b8dad', 122 | filename: 'helloworld.txt', 123 | status: 'removed', 124 | additions: 0, 125 | deletions: 1, 126 | changes: 1, 127 | blob_url: 128 | 'https://github.com/TomKristie/HelloWorld/blob/f5da827a725a701302da7db2da16b1678f52fdcc/helloworld.txt', 129 | raw_url: 130 | 'https://github.com/TomKristie/HelloWorld/raw/f5da827a725a701302da7db2da16b1678f52fdcc/helloworld.txt', 131 | contents_url: 132 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/helloworld.txt?ref=f5da827a725a701302da7db2da16b1678f52fdcc', 133 | patch: '@@ -1 +0,0 @@\n-hello world', 134 | }, 135 | ], 136 | }; 137 | sandbox.stub(octokit.pulls, 'listFiles').resolves(listFilesOfPRResult); 138 | 139 | // tests 140 | const {patches, filesMissingPatch} = await getCurrentPullRequestPatches( 141 | octokit, 142 | upstream, 143 | pullNumber, 144 | pageSize 145 | ); 146 | assert.strictEqual(patches.size, 3); 147 | assert.strictEqual( 148 | patches.get(listFilesOfPRResult.data[0].filename), 149 | '@@ -1,2 +1,5 @@\n Hello world\n-!\n+Goodbye World\n+gOodBYE world\n+\n+Goodbye World' 150 | ); 151 | assert.strictEqual( 152 | patches.get(listFilesOfPRResult.data[1].filename), 153 | '@@ -1 +1 @@\n-Hello foo\n+' 154 | ); 155 | assert.strictEqual( 156 | patches.get(listFilesOfPRResult.data[2].filename), 157 | '@@ -1 +0,0 @@\n-hello world' 158 | ); 159 | assert.strictEqual(filesMissingPatch.length, 0); 160 | }); 161 | it('Passes the error message up from octokit when octokit fails', async () => { 162 | // setup 163 | const error = new Error('Error message'); 164 | sandbox.stub(octokit.pulls, 'listFiles').rejects(error); 165 | await assert.rejects( 166 | getCurrentPullRequestPatches(octokit, upstream, pullNumber, pageSize), 167 | error 168 | ); 169 | }); 170 | it('Throws when there is no list file data returned from octokit', async () => { 171 | // setup 172 | const listFilesOfPRResult: ListFilesResponse = { 173 | headers: {}, 174 | status: 200, 175 | url: 'http://fake-url.com', 176 | data: [], 177 | }; 178 | sandbox.stub(octokit.pulls, 'listFiles').resolves(listFilesOfPRResult); 179 | await assert.rejects( 180 | getCurrentPullRequestPatches(octokit, upstream, pullNumber, pageSize), 181 | /Empty Pull Request/ 182 | ); 183 | }); 184 | it('Does not error when there is list file data but no patch data', async () => { 185 | // setup 186 | const listFilesOfPRResult = { 187 | headers: {}, 188 | status: 200, 189 | url: 'http://fake-url.com', 190 | data: [ 191 | { 192 | sha: 'a1d470fa4d7b04450715e3e02d240a34517cd988', 193 | filename: 'Readme.md', 194 | status: 'modified', 195 | additions: 4, 196 | deletions: 1, 197 | changes: 5, 198 | blob_url: 199 | 'https://github.com/TomKristie/HelloWorld/blob/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 200 | raw_url: 201 | 'https://github.com/TomKristie/HelloWorld/raw/eb53f3871f56e8dd6321e44621fe6ac2da1bc120/Readme.md', 202 | contents_url: 203 | 'https://api.github.com/repos/TomKristie/HelloWorld/contents/Readme.md?ref=eb53f3871f56e8dd6321e44621fe6ac2da1bc120', 204 | }, 205 | ], 206 | }; 207 | 208 | /* eslint-disable @typescript-eslint/no-explicit-any */ 209 | // these are real results from calling listFiles API and are a valid GitHub return type, but octokit type definition says otherwise 210 | // cannot force another type cast since the GitHub API return types are not importable 211 | // unknown type cast not allowed 212 | const stub = sandbox 213 | .stub(logger, 'warn') 214 | .resolves(listFilesOfPRResult as any); 215 | sandbox 216 | .stub(octokit.pulls, 'listFiles') 217 | .resolves(listFilesOfPRResult as any); 218 | 219 | // tests 220 | const {filesMissingPatch} = await getCurrentPullRequestPatches( 221 | octokit, 222 | upstream, 223 | pullNumber, 224 | pageSize 225 | ); 226 | sandbox.assert.called(stub); 227 | assert.strictEqual(filesMissingPatch.length, 1); 228 | assert.strictEqual(filesMissingPatch[0], 'Readme.md'); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/suggestion-hunk.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as assert from 'assert'; 16 | import {describe, it, before} from 'mocha'; 17 | import {setup} from './util'; 18 | import {getRawSuggestionHunks} from '../src/utils/hunk-utils'; 19 | import {FileDiffContent} from '../src/types'; 20 | 21 | before(() => { 22 | setup(); 23 | }); 24 | 25 | describe('getRawSuggestionHunks', () => { 26 | const diffContents: Map = new Map(); 27 | const fileDiffContent1: FileDiffContent = { 28 | oldContent: 'foo', 29 | newContent: 'FOO', 30 | }; 31 | const fileDiffContent2: FileDiffContent = { 32 | oldContent: 33 | 'bar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar', 34 | newContent: 35 | 'foo\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nfoo', 36 | }; 37 | const fileName1 = 'README.md'; 38 | const fileName2 = 'bars.txt'; 39 | diffContents.set(fileName1, fileDiffContent1); 40 | diffContents.set(fileName2, fileDiffContent2); 41 | 42 | it("Does not update the user's input of text file diff contents", () => { 43 | getRawSuggestionHunks(diffContents); 44 | assert.strictEqual(fileDiffContent1.oldContent, 'foo'); 45 | assert.strictEqual(fileDiffContent1.newContent, 'FOO'); 46 | assert.strictEqual(diffContents.get(fileName1)!.oldContent, 'foo'); 47 | assert.strictEqual(diffContents.get(fileName1)!.newContent, 'FOO'); 48 | assert.strictEqual( 49 | fileDiffContent2.oldContent, 50 | 'bar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar' 51 | ); 52 | assert.strictEqual( 53 | fileDiffContent2.newContent, 54 | 'foo\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nfoo' 55 | ); 56 | assert.strictEqual( 57 | diffContents.get(fileName2)!.oldContent, 58 | 'bar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar' 59 | ); 60 | assert.strictEqual( 61 | diffContents.get(fileName2)!.newContent, 62 | 'foo\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nbar\nfoo' 63 | ); 64 | }); 65 | 66 | it('Generates the hunks that are produced by the diff library for all files that are updated', () => { 67 | const fileHunks = getRawSuggestionHunks(diffContents); 68 | assert.strictEqual(fileHunks.size, 2); 69 | assert.strictEqual(fileHunks.get(fileName1)!.length, 1); 70 | assert.strictEqual(fileHunks.get(fileName1)![0].oldStart, 1); 71 | assert.strictEqual(fileHunks.get(fileName1)![0].oldEnd, 1); 72 | assert.strictEqual(fileHunks.get(fileName1)![0].newStart, 1); 73 | assert.strictEqual(fileHunks.get(fileName1)![0].newEnd, 1); 74 | assert.strictEqual(fileHunks.get(fileName2)!.length, 2); 75 | assert.strictEqual(fileHunks.get(fileName2)![0].oldStart, 1); 76 | // FIXME: See #126 77 | assert.strictEqual(fileHunks.get(fileName2)![0].oldEnd, 0); 78 | assert.strictEqual(fileHunks.get(fileName2)![0].newStart, 1); 79 | assert.strictEqual(fileHunks.get(fileName2)![0].newEnd, 1); 80 | assert.strictEqual(fileHunks.get(fileName2)![1].oldStart, 16); 81 | assert.strictEqual(fileHunks.get(fileName2)![1].oldEnd, 16); 82 | assert.strictEqual(fileHunks.get(fileName2)![1].newStart, 17); 83 | assert.strictEqual(fileHunks.get(fileName2)![1].newEnd, 18); 84 | }); 85 | 86 | it('Does not generate hunks for changes that contain no updates', () => { 87 | const samediffContents = new Map(); 88 | samediffContents.set('unchanged-1.txt', {oldContent: '', newContent: ''}); 89 | samediffContents.set('unchanged-2.txt', { 90 | oldContent: 'same', 91 | newContent: 'same', 92 | }); 93 | const fileHunks = getRawSuggestionHunks(samediffContents); 94 | assert.strictEqual(fileHunks.size, 0); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import {setupLogger, logger} from '../src/logger'; 16 | import {Octokit} from '@octokit/rest'; 17 | import {disableNetConnect} from 'nock'; 18 | 19 | const octokit: Octokit = new Octokit(); 20 | 21 | /** 22 | * setup tests 23 | */ 24 | function setup() { 25 | disableNetConnect(); 26 | setupLogger(console); 27 | } 28 | 29 | export {logger, octokit, setup}; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "build", 6 | "skipLibCheck": true, 7 | "resolveJsonModule": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ] 12 | }, 13 | "include": [ 14 | "src/*.ts", 15 | "src/**/*.ts", 16 | "test/*.ts", 17 | "test/**/*.ts", 18 | "system-test/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const path = require('path'); 16 | 17 | module.exports = { 18 | entry: './src/index.ts', 19 | output: { 20 | library: 'CodeSuggester', 21 | filename: './code-suggester.js', 22 | }, 23 | node: { 24 | child_process: 'empty', 25 | fs: 'empty', 26 | crypto: 'empty', 27 | }, 28 | resolve: { 29 | alias: { 30 | '../../../package.json': path.resolve(__dirname, 'package.json'), 31 | }, 32 | extensions: ['.js', '.json', '.ts'], 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.tsx?$/, 38 | use: 'ts-loader', 39 | exclude: /node_modules/, 40 | }, 41 | { 42 | test: /node_modules[\\/]@grpc[\\/]grpc-js/, 43 | use: 'null-loader', 44 | }, 45 | { 46 | test: /node_modules[\\/]grpc/, 47 | use: 'null-loader', 48 | }, 49 | { 50 | test: /node_modules[\\/]retry-request/, 51 | use: 'null-loader', 52 | }, 53 | { 54 | test: /node_modules[\\/]https?-proxy-agent/, 55 | use: 'null-loader', 56 | }, 57 | { 58 | test: /node_modules[\\/]gtoken/, 59 | use: 'null-loader', 60 | }, 61 | ], 62 | }, 63 | mode: 'production', 64 | }; 65 | --------------------------------------------------------------------------------