├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── question.yml ├── config │ └── exclude.txt ├── dependabot.yml └── workflows │ ├── actions-config-validation.yml │ ├── codeql-analysis.yml │ ├── lint.yml │ ├── package-check.yml │ ├── test.yml │ └── update-latest-release-tag.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── functions │ ├── actions-status.test.js │ ├── allowlist.test.js │ ├── context-check.test.js │ ├── post-reactions.test.js │ ├── post.test.js │ ├── prechecks.test.js │ ├── react-emote.test.js │ ├── string-to-array.test.js │ ├── trigger-check.test.js │ └── valid-permissions.test.js ├── main.test.js ├── schemas │ └── action.schema.yml └── version.test.js ├── action.yml ├── badges └── coverage.svg ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── docs └── assets │ ├── parameters.md │ └── ship-it.jpg ├── package-lock.json ├── package.json ├── script └── release └── src ├── functions ├── action-status.js ├── allowlist.js ├── colors.js ├── context-check.js ├── parameters.js ├── post-reactions.js ├── post.js ├── prechecks.js ├── react-emote.js ├── string-to-array.js ├── trigger-check.js └── valid-permissions.js ├── main.js └── version.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "@babel/plugin-transform-modules-commonjs" 6 | ] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "sourceType": "module" 16 | }, 17 | "rules": {} 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @GrantBirki 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug/issue report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Bug Report 🐛 9 | 10 | Thanks for taking the time to fill out this bug report! 11 | 12 | Please answer each question below to your best ability. It is okay to leave questions blank if you have to! 13 | 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: Describe the Issue 18 | description: Please describe the bug/issue in detail 19 | placeholder: Something is wrong with X when trying to do Y 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: config 25 | attributes: 26 | label: Action Configuration 27 | description: Please copy and paste your Action's configuration. Please omit any sensitive information if your configuration is not public. 28 | placeholder: | 29 | ```yaml 30 | - name: command 31 | id: command 32 | uses: github/command@vX.X.X 33 | with: 34 | trigger: ".xyz" 35 | ``` 36 | 37 | - type: textarea 38 | id: logs 39 | attributes: 40 | label: Relevant Actions Log Output 41 | description: Please copy and paste any relevant log output. If your Action's workflow is public, please provide a direct link to the logs 42 | 43 | - type: textarea 44 | id: extra 45 | attributes: 46 | label: Extra Information 47 | description: Any extra information, links to issues, screenshots, etc 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Open an issue to ask a question or start a discussion 3 | labels: ["question"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Question ❓ 9 | 10 | Thanks for taking the time to open an issue! 11 | 12 | This issue template is just for asking questions or starting a discussion. If you have a bug report, please use the bug template instead. 13 | 14 | - type: textarea 15 | id: question 16 | attributes: 17 | label: Details 18 | description: Add details about your question here 19 | -------------------------------------------------------------------------------- /.github/config/exclude.txt: -------------------------------------------------------------------------------- 1 | # gitignore style exclude file for the GrantBirki/json-yaml-validate Action 2 | .github/ 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | groups: 7 | github-actions: 8 | patterns: 9 | - "*" 10 | schedule: 11 | interval: monthly 12 | - package-ecosystem: npm 13 | directory: "/" 14 | groups: 15 | npm-dependencies: 16 | patterns: 17 | - "*" 18 | schedule: 19 | interval: monthly 20 | -------------------------------------------------------------------------------- /.github/workflows/actions-config-validation.yml: -------------------------------------------------------------------------------- 1 | name: actions-config-validation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: write # enable write permissions for pull request comments 12 | 13 | jobs: 14 | actions-config-validation: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: actions-config-validation 20 | uses: GrantBirki/json-yaml-validate@947ae8ac60c83cf78e4e00b3170ff8bee61f5248 # pin@v3.3.0 21 | with: 22 | comment: "true" # enable comment mode 23 | yaml_schema: "__tests__/schemas/action.schema.yml" 24 | exclude_file: ".github/config/exclude.txt" 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | schedule: 7 | - cron: '45 3 * * 5' 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'javascript' ] 22 | 23 | steps: 24 | - name: checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: ${{ matrix.language }} 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v3 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v3 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/*' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: setup node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version-file: .node-version 22 | cache: 'npm' 23 | 24 | - name: install dependencies 25 | run: npm ci 26 | 27 | - name: lint 28 | run: | 29 | npm run format-check 30 | npm run lint 31 | -------------------------------------------------------------------------------- /.github/workflows/package-check.yml: -------------------------------------------------------------------------------- 1 | name: package-check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | package-check: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version-file: .node-version 24 | cache: 'npm' 25 | 26 | - name: install dependencies 27 | run: npm ci 28 | 29 | - name: rebuild the dist/ directory 30 | run: npm run bundle 31 | 32 | - name: compare the expected and actual dist/ directories 33 | run: | 34 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 35 | echo "Detected uncommitted changes after build. See status below:" 36 | git diff 37 | exit 1 38 | fi 39 | id: diff 40 | 41 | # If index.js was different than expected, upload the expected version as an artifact 42 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # pin@v4.6.2 43 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 44 | with: 45 | name: dist 46 | path: dist/ 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/*' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: setup node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version-file: .node-version 22 | cache: 'npm' 23 | 24 | - name: install dependencies 25 | run: npm ci 26 | 27 | - name: test 28 | run: npm run ci-test 29 | -------------------------------------------------------------------------------- /.github/workflows/update-latest-release-tag.yml: -------------------------------------------------------------------------------- 1 | name: Update Latest Release Tag 2 | run-name: Update ${{ github.event.inputs.major_version_tag }} with ${{ github.event.inputs.source_tag }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | source_tag: 8 | description: 'The tag or reference to use as the source (example: v8.0.0)' 9 | required: true 10 | default: vX.X.X 11 | major_version_tag: 12 | description: 'The major release tag to update with the source (example: v8)' 13 | required: true 14 | default: vX 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | tag: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: git config 28 | run: | 29 | git config user.name github-actions 30 | git config user.email github-actions@github.com 31 | 32 | - name: tag new target 33 | run: git tag -f ${{ github.event.inputs.major_version_tag }} ${{ github.event.inputs.source_tag }} 34 | 35 | - name: push new tag 36 | run: git push origin ${{ github.event.inputs.major_version_tag }} --force 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Dependency directory 5 | node_modules/ 6 | 7 | # Coverage directory used by tools like istanbul 8 | coverage 9 | 10 | # Optional npm cache directory 11 | .npm 12 | 13 | # Optional eslint cache 14 | .eslintcache 15 | 16 | # OS metadata 17 | .DS_Store 18 | Thumbs.db 19 | 20 | # Extra 21 | tmp/ 22 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.13.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 💻 2 | 3 | All contributions are welcome and greatly appreciated! 4 | 5 | ## Steps to Contribute 💡 6 | 7 | > Check the `.node-version` file in the root of this repo so see what version of Node.js is required for local development - note, this can be different from the version of Node.js which runs the Action on GitHub runners. It is suggested to download [nodenv](https://github.com/nodenv/nodenv) which uses this file and manages your Node.js versions for you 8 | 9 | 1. Fork this repository 10 | 2. Commit your changes 11 | 3. Test your changes (learn how to test below) 12 | 4. Open a pull request back to this repository 13 | > Make sure to run `npm run all` as your final commit! 14 | 5. Notify the maintainers of this repository for peer review and approval 15 | 6. Merge! 16 | 17 | The maintainers of this repository will create a new release with your changes so that everyone can use the new release and enjoy the awesome features of this Action 18 | 19 | ## Testing 🧪 20 | 21 | This project requires **100%** test coverage 22 | 23 | ### Running the test suite (required) 24 | 25 | Simply run the following command to execute the entire test suite: 26 | 27 | ```bash 28 | npm run test 29 | ``` 30 | 31 | > Note: this requires that you have already run `npm install` 32 | 33 | ### Testing FAQs 🤔 34 | 35 | Answers to questions you might have around testing 36 | 37 | Q: Why do I have to commit my changes to `main`? 38 | 39 | A: The `on: issue_comment` workflow only uses workflow files from the `main` branch by design - [learn more in the branch-deploy repo](https://github.com/github/branch-deploy#security-) 40 | 41 | Q: How can I test my changes once my PR is merged and *before* a new release is created? 42 | 43 | A: You should create a repo like [this one](https://github.com/GrantBirki/actions-sandbox) that uses `github/command@main` as the Action version and test your changes there 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # command 2 | 3 | [![CodeQL](https://github.com/github/command/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/github/command/actions/workflows/codeql-analysis.yml) [![test](https://github.com/github/command/actions/workflows/test.yml/badge.svg)](https://github.com/github/command/actions/workflows/test.yml) [![package-check](https://github.com/github/command/actions/workflows/package-check.yml/badge.svg)](https://github.com/github/command/actions/workflows/package-check.yml) [![lint](https://github.com/github/command/actions/workflows/lint.yml/badge.svg)](https://github.com/github/command/actions/workflows/lint.yml) [![actions-config-validation](https://github.com/github/command/actions/workflows/actions-config-validation.yml/badge.svg)](https://github.com/github/command/actions/workflows/actions-config-validation.yml) [![coverage](./badges/coverage.svg)](./badges/coverage.svg) 4 | 5 | IssueOps commands in GitHub Actions! 6 | 7 | > _Like ChatOps but for GitHub Issues and Pull Requests_ 🤩 8 | 9 | ![ship-it](docs/assets/ship-it.jpg) 10 | 11 | This project is based off the [github/branch-deploy](https://github.com/github/branch-deploy) Action. There are **many** similarities between the two, but there is a key difference. The [github/branch-deploy](https://github.com/github/branch-deploy) Action is designed specifically for deployments via IssueOps where this project (`command`) can be used for **any** IssueOps command. This Action allows you to tailor your IssueOps command exactly how you want. 12 | 13 | This Action does the heavy lifting for you to enabled customized IssueOps commands: 14 | 15 | - 🔍 Detects when IssueOps commands are used on a pull request or an issue 16 | - ✏️ Configurable - Choose your command syntax, optional parameters, who can run the commands, what GitHub permissions are required, and much more 17 | - ✔️ Respects your branch protection settings configured for the repo - if commands are run on pull requests 18 | - 🗨️ Reacts to your IssueOps commands 19 | - 🚀 Can be enabled with simple configuration 20 | - 🧶 This Action can be tied into your existing workflows 21 | 22 | ## Turbo Quickstart ⚡ 23 | 24 | A quick section to get you started with this Action 25 | 26 | ### Usage 📝 27 | 28 | Basic usage assuming all defaults: 29 | 30 | ```yaml 31 | - name: command 32 | id: command 33 | uses: github/command@vX.X.X 34 | with: 35 | command: .lint # can be anything you want (example) 36 | ``` 37 | 38 | Advanced usage with some custom configuration: 39 | 40 | ```yaml 41 | - name: command 42 | id: command 43 | uses: github/command@vX.X.X 44 | with: 45 | command: .restart # can be anything you want (example) 46 | reaction: "eyes" 47 | allowed_contexts: "pull_request,issue" 48 | permissions: "write,admin" 49 | allowlist: monalisa 50 | ``` 51 | 52 | For configuration details, see the **inputs** section below 53 | 54 | ### Example 📚 55 | 56 | Check out a super simple workflow example using this Action to quickly get up and running with `github/command`: 57 | 58 | ```yaml 59 | name: "command demo" 60 | 61 | # the workflow to execute on is comments that are newly created 62 | on: 63 | issue_comment: 64 | types: [created] 65 | 66 | # permissions needed for reacting to IssueOps commands on issues and PRs 67 | permissions: 68 | pull-requests: write 69 | issues: write 70 | checks: read 71 | contents: read # useful if your workflows call actions/checkout@vX.X.X 72 | 73 | jobs: 74 | demo: 75 | runs-on: ubuntu-latest 76 | steps: 77 | # execute IssueOps command logic, hooray! 78 | # this will be used to "gate" all future steps below 79 | - uses: github/command@vX.X.X 80 | id: command 81 | with: 82 | command: ".ping" 83 | allowed_contexts: issue,pull_request # run on issues AND pull requests 84 | 85 | # run your custom logic for your project here - example seen below 86 | 87 | # conditionally run some logic here 88 | - name: ping 89 | if: ${{ steps.command.outputs.continue == 'true' }} 90 | run: echo "I am going to ping some cool website now!" 91 | ``` 92 | 93 | > Keep reading to learn more about this Action! Even further details about how this Action works can be found below as well 94 | 95 | ## About 💡 96 | 97 | Before we get into details, let's first define a few key terms below: 98 | 99 | - **IssueOps** - Its like ChatOps but instead of using a chat bot, commands are invoked by commenting on a pull request (PRs are issues under the hood) - Example: commenting `.restart` on a pull request 100 | - **PR** - Short for pull request 101 | 102 | ### IssueOps 🗨️ 103 | 104 | The best way to define IssueOps is to compare it to something similar, ChatOps. You may be familiar with the concept ChatOps already but in case you aren't here is a quick definition below: 105 | 106 | > ChatOps is the process of interacting with a chat bot to execute commands directly in a chat platform. For example, with ChatOps you might do something like `.ping example.org` to check the status of a website 107 | 108 | IssueOps adopts the same mindset but through a different medium. Rather than using a chat service to invoke the commands we use comments on a GitHub Issue or Pull Request. GitHub Actions is the runtime which executes our desired logic 109 | 110 | ## How does it work? 📚 111 | 112 | > This section will go into detail about how this Action works and hopefully inspire you on ways you can leverage it in your own projects 113 | 114 | Let's walk through a GitHub Action workflow using this Action line by line: 115 | 116 | ```yaml 117 | # The name of the workflow, it can be anything you wish 118 | name: "IssueOps github/command demo" 119 | 120 | # The workflow to execute on is comments that are newly created 121 | on: 122 | issue_comment: 123 | types: [created] 124 | ``` 125 | 126 | It is important to note that the workflow we want to run IssueOps on is `issue_comment` and `created`. This means we will not run under any other contexts for this workflow. You can edit this as you wish but it does change how this model ultimately works. For example, `issue_comment` workflows **only** use files found on `main` to run. If you do something like `on: pull_request` you could open yourself up to issues as a user could alter a file in a PR and exfil your secrets for example. Only using `issue_comment` is the suggested workflow type. It should also be noted that comments on pull requests, **and** issues will trigger the `issue_comment` workflow event. 127 | 128 | ```yaml 129 | # permissions definitions 130 | permissions: 131 | pull-requests: write # required for adding reactions to command comments on PRs 132 | issues: write # required for adding reactions to command comments on issues 133 | checks: read # required for checking if the CI checks have passed on a pull request (if using this Action in the context of PR comments) 134 | ``` 135 | 136 | These are the minimum permissions you need to run this Action (this assumes you are running this Action on pull requests and issues) 137 | 138 | ```yaml 139 | jobs: 140 | demo: 141 | runs-on: ubuntu-latest 142 | steps: 143 | # Checkout your projects repository 144 | - uses: actions/checkout@v4 145 | ``` 146 | 147 | Sets up your `demo` job, uses an ubuntu runner, and checks out your repo - Just some standard setup for a general Action. We also add an `if:` statement here to only run this workflow on pull request comments to make it a little more specific (if necessary) 148 | 149 | > Note: The Action will check the context for us anyways but this can save us a bit of CI time by using the `if:` condition 150 | 151 | ```yaml 152 | # Execute IssueOps command logic, hooray! 153 | - uses: github/command@vX.X.X 154 | id: command 155 | with: 156 | command: ".ping" 157 | ``` 158 | 159 | > Note: It is important to set an `id:` for this job so we can reference its outputs in subsequent steps 160 | 161 | The core of this Action takes place here. This block of code will trigger the `github/command` action to run. It will do the following: 162 | 163 | 1. Check the comment which invoked the workflow for the `command:` phrase (`.ping`) defined here 164 | 2. If the command trigger phrase is found, it will proceed 165 | 3. It will start by reacting to your message to let you know it is running 166 | 4. The Action will check to ensure the user that invoked the operation has the correct permissions to run the command, collect any parameters used in the command, check CI / reviews (if run on a PR), etc 167 | 5. Outputs will be exported by this job for later reference in other jobs as well 168 | 169 | ```yaml 170 | # conditionally run further steps if the command Action was successful 171 | - name: ping 172 | if: ${{ steps.command.outputs.continue == 'true' }} 173 | run: echo "Do your custom logic here to ping your site!" 174 | ``` 175 | 176 | As seen above, we have a single example step. Perhaps you would actually use a real utility to ping a website, but for this example, we just echo out some text. This step is conditionally gated by the `continue` variable: 177 | 178 | - `steps.command.outputs.continue == 'true'` - The `continue` variable is only set to true when a workflow should continue - This is set by logic in the `github/command` Action 179 | 180 | > Example: You comment `.ping` on a pull request. A workflow is kicked off and the `github/command` Action begins to check the comment body of the message you just typed on the pull request. If you have the correct permissions to execute the IssueOps command, the action outputs the `continue` variable to `true`. This will allow the "ping" step seen above to run. 181 | 182 | ## Inputs 📥 183 | 184 | | Input | Required? | Default | Description | 185 | | ----- | --------- | ------- | ----------- | 186 | | `command` | `true` | - | The string to look for in comments as an IssueOps trigger/command. Example: `".lint"` - You must provide a value for this option | 187 | | `github_token` | `true` | `${{ github.token }}` | The GitHub token used to create an authenticated client - Provided for you by default! | 188 | | `status` | `true` | `${{ job.status }}` | The status of the GitHub Actions - For use in the post run workflow - Provided for you by default! | 189 | | `reaction` | `true` | `eyes` | If set, the specified emoji "reaction" is put on the comment to indicate that the trigger was detected. For example, "rocket" or "eyes" | 190 | | `success_reaction` | `true` | `+1` | The reaction to add to the comment that triggered the Action if its execution was successful | 191 | | `failure_reaction` | `true` | `-1` | The reaction to add to the comment that triggered the Action if its execution failed | 192 | | `allowed_contexts` | `true` | `pull_request` | A comma separated list of comment contexts that are allowed to trigger this IssueOps command. Pull requests and issues are the only currently supported contexts. To allow IssueOps commands to be invoked from both PRs and issues, set this option to the following: `"pull_request,issue"`. By default, the only place this Action will allow IssueOps commands from is pull requests | 193 | | `permissions` | `true` | `"write,admin"` | The allowed GitHub permissions an actor can have to invoke IssueOps commands | 194 | | `allow_drafts` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on draft pull requests | 195 | | `allow_forks` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on forked pull requests | 196 | | `skip_ci` | `true` | `"false"` | Whether or not to require passing CI checks before this IssueOps command can be run | 197 | | `skip_reviews` | `true` | `"false"` | Whether or not to require reviews before this IssueOps command can be run | 198 | | `param_separator` | `true` | `"\|"` | The separator to use for parsing parameters in comments in IssueOps commands. Parameters will are saved as outputs and can be used in subsequent steps. The default value for this input is the pipe character (`\|`) | 199 | | `allowlist` | `false` | `"false"` | A comma separated list of GitHub usernames or teams that should be allowed to use the IssueOps commands configured in this Action. If unset, then all users meeting the "permissions" requirement will be able to run commands. Example: `"monalisa,octocat,my-org/my-team"` | 200 | | `allowlist_pat` | `false` | `"false"` | A GitHub personal access token with "read:org" scopes. This is only needed if you are using the "allowlist" option with a GitHub org team. For example: `"my-org/my-team"` | 201 | | `skip_completing` | `true` | `"false"` | If set to `"true"`, skip the process of completing the Action. This is useful if you want to customize the way this Action completes - For example, custom reactions, comments, etc | 202 | | `fork_review_bypass` | `true` | `"false"` | If set to "true", allow forks to bypass the review requirement if the operation is being made on a pull request from a fork. This option is potentially dangerous if you are checking out code in your workflow as a result of invoking this Action. If the code you are checking out has not been reviewed, then you might open yourself up to a TOCTOU vulnerability. You should always ensure that the code you are checking out has been reviewed, and that you checkout an exact commit sha rather than a ref. | 203 | 204 | ## Outputs 📤 205 | 206 | | Output | Description | 207 | | ------ | ----------- | 208 | | `triggered` | The string "true" if the trigger was found, otherwise the string "false" - Just because the workflow was triggered does not mean it should continue. This is a step 1/2 check | 209 | | `continue` | ⭐ The string "true" if the workflow should continue, otherwise empty - Use this to conditionally control if your workflow should proceed or not. This is a step 2/2 check. This is the output you will want to use to determine if your IssueOps flow should _continue_ after this Action completes | 210 | | `comment_body` | The comment body | 211 | | `actor` | The GitHub handle of the actor that invoked the IssueOps command | 212 | | `params` | The raw parameters that were passed into the IssueOps command (see param_separator) - Further [documentation](docs/assets/parameters.md) | 213 | | `comment_id` | The comment id which triggered this action | 214 | | `issue_number` | The issue number of the pull request (or issue) that was commented on | 215 | | `initial_reaction_id` | The reaction id for the initial reaction on the trigger comment | 216 | | `fork` | The string "true" if the pull request is a fork, otherwise "false" | 217 | | `fork_ref` | The true ref of the fork | 218 | | `fork_label` | The API label field returned for the fork | 219 | | `fork_checkout` | The console command presented in the GitHub UI to checkout a given fork locally | 220 | | `fork_full_name` | The full name of the fork in "org/repo" format | 221 | | `sha` | The commit sha if being used in the context of a pull request | 222 | | `ref` | The ref if being used in the context of a pull request | 223 | | `base_ref` | The base ref that the pull request is merging into (if available and run in the context of a pull request) | 224 | 225 | ## Allowlist 👩‍🔬 226 | 227 | This Action supports a configurable input called `allowlist` which can be used to specify a list of individual GitHub users or teams that should have permission to use this Action. By default, this input option's value is set to `"false"` which means that anyone how has the proper `permissions` (see the **inputs** section above) "permissions", will be able to invoke IssueOps commands. You can actually use both the `allowlist` and `permissions` input together to help control who can invoke IssueOps commands. For example, you could use these two options together to only allow people in the GitHub `octoawesome` team with `admin` permissions to run your commands. 228 | 229 | The `allowlist` input option takes a comma separated list of GitHub handles or GitHub org teams. For example, if you give the option `allowlist: monalisa`, the `monalisa` user will be the only user allowed to invoke IssueOps commands (assuming they also have the correct `permissions`) 230 | 231 | Here is a simple example using only handles below (the monalisa and octocat users will be allowlisted): 232 | 233 | ```yaml 234 | - uses: github/command@vX.X.X 235 | id: command 236 | with: 237 | allowlist: monalisa,octocat 238 | ``` 239 | 240 | Here is an example using a mix of GitHub handles and a GitHub org team below: 241 | 242 | ```yaml 243 | - uses: github/command@vX.X.X 244 | id: command 245 | with: 246 | allowlist: monalisa,octocat,octo-awesome-org/octo-awesome-team 247 | allowlist_pat: ${{ secrets.ALLOWLIST_PAT }} 248 | ``` 249 | 250 | In this case, all users (and future users) in the `octo-awesome-org/octo-awesome-team` team will be treated as admins in addition to the monalisa and octocat users 251 | 252 | It should be noted if you choose to use GitHub org teams for allowlist definitions, you **will** need a [GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with the `read:org` scope. This is because the Action will need to make API calls on behalf of an authenticated user in the org to retrieve team memberships. If you choose to only use GitHub handles for admin definitions, then the `allowlist_pat` input is not required 253 | 254 | > Note: You can read more about the `allowlist` option under the **inputs** section in this readme 255 | 256 | ## Live Examples 📸 257 | 258 | Check out some of the links below to see how others are using this Action in their projects: 259 | 260 | - coming soon! 261 | 262 | ## Actions Stability 🔧 263 | 264 | In order to ensure your usage of this action is stable, it is highly recommended that you use either pin your action to a SHA or use a specific release tag 265 | 266 | ### Actions Tag Pinning 267 | 268 | You can easily select the exact version you want on the GitHub Actions marketplace seen in the screenshot below: 269 | 270 | ![Screenshot from 2022-05-09 12-12-06](https://user-images.githubusercontent.com/23362539/167471509-71ca2cf9-7b8f-4709-acee-67a679869fa6.png) 271 | 272 | ### Actions SHA Pinning 273 | 274 | You can also pin to an exact commit SHA as well using a third party tool such as [mheap/pin-github-action](https://github.com/mheap/pin-github-action) 275 | 276 | > GitHub Actions security hardening and stability docs available here: [docs](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions) 277 | 278 | ## Suggestions 🌟 279 | 280 | This section will cover a few suggestions that will help you when using this Action 281 | 282 | 1. Suggest Updating Pull Request Branches - A great setting to enable for pull request hygiene. This option can be found in your repository's `/settings` page 283 | 284 | ![branch-setting](https://user-images.githubusercontent.com/23362539/172939811-a8816db8-8e7c-404a-b12a-11ec5bc6e93d.png) 285 | 286 | 2. Enable Branch Protection Settings - It is always a good idea to enable branch protection settings for your repo, especially when using this Action 287 | 288 | ## Security 🔒 289 | 290 | The security aspects of this Action have already been well documented in the `branch-deploy` repo. Please see the following [docs](https://github.com/github/branch-deploy/tree/ccff97cdddb9dc6f43748c6d17416ce66a4abff6#security-) to learn more. 291 | 292 | --- 293 | 294 | ## Contributing 💻 295 | 296 | All contributions are welcome from all! 297 | 298 | Check out the [contributing guide](CONTRIBUTING.md) to learn more 299 | -------------------------------------------------------------------------------- /__tests__/functions/actions-status.test.js: -------------------------------------------------------------------------------- 1 | import {actionStatus} from '../../src/functions/action-status' 2 | 3 | var context 4 | var octokit 5 | beforeEach(() => { 6 | jest.clearAllMocks() 7 | process.env.GITHUB_SERVER_URL = 'https://github.com' 8 | process.env.GITHUB_RUN_ID = '12345' 9 | 10 | context = { 11 | repo: { 12 | owner: 'corp', 13 | repo: 'test' 14 | }, 15 | issue: { 16 | number: 1 17 | }, 18 | payload: { 19 | comment: { 20 | id: '1' 21 | } 22 | } 23 | } 24 | 25 | octokit = { 26 | rest: { 27 | reactions: { 28 | createForIssueComment: jest.fn().mockReturnValueOnce({ 29 | data: {} 30 | }), 31 | deleteForIssueComment: jest.fn().mockReturnValueOnce({ 32 | data: {} 33 | }) 34 | }, 35 | issues: { 36 | createComment: jest.fn().mockReturnValueOnce({ 37 | data: {} 38 | }) 39 | } 40 | } 41 | } 42 | }) 43 | 44 | test('adds a successful status message for an operation', async () => { 45 | expect( 46 | await actionStatus(context, octokit, 123, 'Everything worked!', true) 47 | ).toBe(undefined) 48 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ 49 | body: 'Everything worked!', 50 | issue_number: 1, 51 | owner: 'corp', 52 | repo: 'test' 53 | }) 54 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 55 | comment_id: '1', 56 | content: 'rocket', 57 | owner: 'corp', 58 | repo: 'test' 59 | }) 60 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 61 | comment_id: '1', 62 | owner: 'corp', 63 | reaction_id: 123, 64 | repo: 'test' 65 | }) 66 | }) 67 | 68 | test('adds a successful status message for an operation (with alt message)', async () => { 69 | expect( 70 | await actionStatus(context, octokit, 123, 'Everything worked!', true, true) 71 | ).toBe(undefined) 72 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ 73 | body: 'Everything worked!', 74 | issue_number: 1, 75 | owner: 'corp', 76 | repo: 'test' 77 | }) 78 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 79 | comment_id: '1', 80 | content: '+1', 81 | owner: 'corp', 82 | repo: 'test' 83 | }) 84 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 85 | comment_id: '1', 86 | owner: 'corp', 87 | reaction_id: 123, 88 | repo: 'test' 89 | }) 90 | }) 91 | 92 | test('adds a failure status message for an operation', async () => { 93 | expect( 94 | await actionStatus(context, octokit, 123, 'Everything failed!', false) 95 | ).toBe(undefined) 96 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ 97 | body: 'Everything failed!', 98 | issue_number: 1, 99 | owner: 'corp', 100 | repo: 'test' 101 | }) 102 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 103 | comment_id: '1', 104 | content: '-1', 105 | owner: 'corp', 106 | repo: 'test' 107 | }) 108 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 109 | comment_id: '1', 110 | owner: 'corp', 111 | reaction_id: 123, 112 | repo: 'test' 113 | }) 114 | }) 115 | 116 | test('uses default log url when the "message" variable is empty for failures', async () => { 117 | expect(await actionStatus(context, octokit, 123, '', false)).toBe(undefined) 118 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ 119 | body: 'Unknown error, [check logs](https://github.com/corp/test/actions/runs/12345) for more details.', 120 | issue_number: 1, 121 | owner: 'corp', 122 | repo: 'test' 123 | }) 124 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 125 | comment_id: '1', 126 | content: '-1', 127 | owner: 'corp', 128 | repo: 'test' 129 | }) 130 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 131 | comment_id: '1', 132 | owner: 'corp', 133 | reaction_id: 123, 134 | repo: 'test' 135 | }) 136 | }) 137 | 138 | test('uses default log url when the "message" variable is empty for a success', async () => { 139 | expect(await actionStatus(context, octokit, 123, '', true)).toBe(undefined) 140 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ 141 | body: 'Unknown error, [check logs](https://github.com/corp/test/actions/runs/12345) for more details.', 142 | issue_number: 1, 143 | owner: 'corp', 144 | repo: 'test' 145 | }) 146 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 147 | comment_id: '1', 148 | content: 'rocket', 149 | owner: 'corp', 150 | repo: 'test' 151 | }) 152 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 153 | comment_id: '1', 154 | owner: 'corp', 155 | reaction_id: 123, 156 | repo: 'test' 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /__tests__/functions/allowlist.test.js: -------------------------------------------------------------------------------- 1 | import {isAllowed} from '../../src/functions/allowlist' 2 | import * as github from '@actions/github' 3 | import * as core from '@actions/core' 4 | 5 | const debugMock = jest.spyOn(core, 'debug').mockImplementation(() => {}) 6 | const warningMock = jest.spyOn(core, 'warning').mockImplementation(() => {}) 7 | 8 | class NotFoundError extends Error { 9 | constructor(message) { 10 | super(message) 11 | this.status = 404 12 | } 13 | } 14 | 15 | class WildError extends Error { 16 | constructor(message) { 17 | super(message) 18 | this.status = 500 19 | } 20 | } 21 | 22 | var context 23 | var octokit 24 | beforeEach(() => { 25 | jest.clearAllMocks() 26 | process.env.INPUT_ALLOWLIST_PAT = 'faketoken' 27 | process.env.INPUT_ALLOWLIST = 28 | 'MoNaLiSa,@lisamona,octoawesome/octo-awEsome-team,bad$user' 29 | 30 | context = { 31 | actor: 'monalisa' 32 | } 33 | 34 | octokit = { 35 | request: jest.fn().mockReturnValueOnce({ 36 | status: 204 37 | }), 38 | rest: { 39 | orgs: { 40 | get: jest.fn().mockReturnValueOnce({ 41 | data: {id: '12345'} 42 | }) 43 | }, 44 | teams: { 45 | getByName: jest.fn().mockReturnValueOnce({ 46 | data: {id: '567890'} 47 | }) 48 | } 49 | } 50 | } 51 | 52 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 53 | return octokit 54 | }) 55 | }) 56 | 57 | test('runs isAllowed checks and finds a valid admin via handle reference', async () => { 58 | expect(await isAllowed(context)).toStrictEqual(true) 59 | expect(debugMock).toHaveBeenCalledWith( 60 | 'monalisa is an allowlisted operator via handle reference' 61 | ) 62 | }) 63 | 64 | test('runs isAllowed checks and finds a valid handle that is a GitHub EMU', async () => { 65 | process.env.INPUT_ALLOWLIST = 'username_company' 66 | const contextNoAdmin = { 67 | actor: 'username_company' 68 | } 69 | expect(await isAllowed(contextNoAdmin)).toStrictEqual(true) 70 | expect(debugMock).toHaveBeenCalledWith( 71 | 'username_company is an allowlisted operator via handle reference' 72 | ) 73 | }) 74 | 75 | test('runs isAllowed checks and does not find a valid admin', async () => { 76 | process.env.INPUT_ALLOWLIST = 'monalisa' 77 | const contextNoAdmin = { 78 | actor: 'eviluser' 79 | } 80 | expect(await isAllowed(contextNoAdmin)).toStrictEqual(false) 81 | expect(debugMock).toHaveBeenCalledWith( 82 | 'eviluser is not an allowed operator for this command' 83 | ) 84 | }) 85 | 86 | test('runs isAllowed checks and does not find a valid admin due to a bad GitHub handle', async () => { 87 | process.env.INPUT_ALLOWLIST = 'mona%lisa-' 88 | const contextNoAdmin = { 89 | actor: 'mona%lisa-' 90 | } 91 | expect(await isAllowed(contextNoAdmin)).toStrictEqual(false) 92 | expect(debugMock).toHaveBeenCalledWith( 93 | 'mona%lisa- is not a valid GitHub username... skipping allowlist check' 94 | ) 95 | }) 96 | 97 | test('runs isAllowed checks and determines that all users are allowed because it is unset', async () => { 98 | process.env.INPUT_ALLOWLIST = 'false' 99 | expect(await isAllowed(context)).toStrictEqual(true) 100 | expect(debugMock).toHaveBeenCalledWith( 101 | 'no allowlist provided, all users are allowed' 102 | ) 103 | }) 104 | 105 | test('runs isAllowed checks for an org team and fails due to no admins_pat', async () => { 106 | process.env.INPUT_ALLOWLIST_PAT = 'false' 107 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome' 108 | expect(await isAllowed(context)).toStrictEqual(false) 109 | expect(warningMock).toHaveBeenCalledWith( 110 | 'no allowlist_pat provided, skipping allowlist check for org team membership' 111 | ) 112 | }) 113 | 114 | test('runs isAllowed checks for an org team and finds a valid user', async () => { 115 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 116 | expect(await isAllowed(context)).toStrictEqual(true) 117 | expect(debugMock).toHaveBeenCalledWith( 118 | 'monalisa is in octoawesome/octo-awesome-team' 119 | ) 120 | expect(debugMock).toHaveBeenCalledWith( 121 | 'monalisa is an allowlisted operator via org team reference' 122 | ) 123 | }) 124 | 125 | // This only handles the global failure case of any 404 in the admin.js file 126 | test('runs isAllowed checks for an org team and does not find the org', async () => { 127 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 128 | return { 129 | rest: { 130 | orgs: { 131 | get: jest 132 | .fn() 133 | .mockRejectedValueOnce( 134 | new NotFoundError('Reference does not exist') 135 | ) 136 | } 137 | } 138 | } 139 | }) 140 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 141 | expect(await isAllowed(context)).toStrictEqual(false) 142 | expect(debugMock).toHaveBeenCalledWith( 143 | 'monalisa is not a member of the octoawesome/octo-awesome-team team' 144 | ) 145 | }) 146 | 147 | // This only handles the global failure case of any 404 in the admin.js file 148 | test('runs isAllowed checks for an org team and does not find the team', async () => { 149 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 150 | return { 151 | rest: { 152 | orgs: { 153 | get: jest.fn().mockReturnValueOnce({ 154 | data: {id: '12345'} 155 | }) 156 | }, 157 | teams: { 158 | getByName: jest 159 | .fn() 160 | .mockRejectedValueOnce( 161 | new NotFoundError('Reference does not exist') 162 | ) 163 | } 164 | } 165 | } 166 | }) 167 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 168 | expect(await isAllowed(context)).toStrictEqual(false) 169 | expect(debugMock).toHaveBeenCalledWith( 170 | 'monalisa is not a member of the octoawesome/octo-awesome-team team' 171 | ) 172 | }) 173 | 174 | // This test correctly tests if a user is a member of a team or not. If they are in a team a 204 is returned. If they are not a 404 is returned like in this test example 175 | test('runs isAllowed checks for an org team and does not find the user in the team', async () => { 176 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 177 | return { 178 | request: jest 179 | .fn() 180 | .mockRejectedValueOnce(new NotFoundError('Reference does not exist')), 181 | rest: { 182 | orgs: { 183 | get: jest.fn().mockReturnValueOnce({ 184 | data: {id: '12345'} 185 | }) 186 | }, 187 | teams: { 188 | getByName: jest.fn().mockReturnValueOnce({ 189 | data: {id: '567890'} 190 | }) 191 | } 192 | } 193 | } 194 | }) 195 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 196 | expect(await isAllowed(context)).toStrictEqual(false) 197 | expect(debugMock).toHaveBeenCalledWith( 198 | 'monalisa is not a member of the octoawesome/octo-awesome-team team' 199 | ) 200 | }) 201 | 202 | test('runs isAllowed checks for an org team and an unexpected status code is received from the request method with octokit', async () => { 203 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 204 | return { 205 | request: jest.fn().mockReturnValueOnce({ 206 | status: 500 207 | }), 208 | rest: { 209 | orgs: { 210 | get: jest.fn().mockReturnValueOnce({ 211 | data: {id: '12345'} 212 | }) 213 | }, 214 | teams: { 215 | getByName: jest.fn().mockReturnValueOnce({ 216 | data: {id: '567890'} 217 | }) 218 | } 219 | } 220 | } 221 | }) 222 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 223 | expect(await isAllowed(context)).toStrictEqual(false) 224 | expect(debugMock).toHaveBeenCalledWith( 225 | 'monalisa is not an allowed operator for this command' 226 | ) 227 | expect(warningMock).toHaveBeenCalledWith( 228 | 'non 204 response from org team check: 500' 229 | ) 230 | }) 231 | 232 | test('runs isAllowed checks for an org team and an unexpected error is thrown from any API call', async () => { 233 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 234 | return { 235 | request: jest 236 | .fn() 237 | .mockRejectedValueOnce(new WildError('something went boom')), 238 | rest: { 239 | orgs: { 240 | get: jest.fn().mockReturnValueOnce({ 241 | data: {id: '12345'} 242 | }) 243 | }, 244 | teams: { 245 | getByName: jest.fn().mockReturnValueOnce({ 246 | data: {id: '567890'} 247 | }) 248 | } 249 | } 250 | } 251 | }) 252 | process.env.INPUT_ALLOWLIST = 'octoawesome/octo-awesome-team' 253 | expect(await isAllowed(context)).toStrictEqual(false) 254 | expect(debugMock).toHaveBeenCalledWith( 255 | 'monalisa is not an allowed operator for this command' 256 | ) 257 | expect(warningMock).toHaveBeenCalledWith( 258 | 'Error checking org team membership: Error: something went boom' 259 | ) 260 | }) 261 | -------------------------------------------------------------------------------- /__tests__/functions/context-check.test.js: -------------------------------------------------------------------------------- 1 | import {contextCheck} from '../../src/functions/context-check' 2 | import * as core from '@actions/core' 3 | 4 | const warningMock = jest.spyOn(core, 'warning') 5 | const saveStateMock = jest.spyOn(core, 'saveState') 6 | 7 | var context 8 | beforeEach(() => { 9 | jest.clearAllMocks() 10 | process.env.INPUT_ALLOWED_CONTEXTS = 'pull_request,issue' 11 | jest.spyOn(core, 'warning').mockImplementation(() => {}) 12 | jest.spyOn(core, 'saveState').mockImplementation(() => {}) 13 | jest.spyOn(core, 'debug').mockImplementation(() => {}) 14 | 15 | context = { 16 | eventName: 'issue_comment', 17 | payload: { 18 | issue: { 19 | pull_request: {} 20 | } 21 | }, 22 | pull_request: { 23 | number: 1 24 | } 25 | } 26 | }) 27 | 28 | test('checks the event context and finds that it is valid', async () => { 29 | expect(await contextCheck(context)).toStrictEqual({ 30 | valid: true, 31 | context: 'pull_request' 32 | }) 33 | }) 34 | 35 | test('checks the event context for an issue comment and finds that it is valid', async () => { 36 | context.payload.issue = {} 37 | expect(await contextCheck(context)).toStrictEqual({ 38 | valid: true, 39 | context: 'issue' 40 | }) 41 | }) 42 | 43 | test('checks the event context for an issue comment and finds that it is valid - when only issue comments are allowed', async () => { 44 | process.env.INPUT_ALLOWED_CONTEXTS = 'issue' 45 | context.payload.issue = {} 46 | expect(await contextCheck(context)).toStrictEqual({ 47 | valid: true, 48 | context: 'issue' 49 | }) 50 | }) 51 | 52 | test('checks the event context and exits because bad input was used', async () => { 53 | process.env.INPUT_ALLOWED_CONTEXTS = 'bad' 54 | context.payload.issue = {} 55 | expect(await contextCheck(context)).toStrictEqual({ 56 | valid: false, 57 | context: 'issue_comment' 58 | }) 59 | }) 60 | 61 | test('checks the event context for a pr comment and finds that it is valid - when only pr comments are allowed', async () => { 62 | process.env.INPUT_ALLOWED_CONTEXTS = 'pull_request' 63 | expect(await contextCheck(context)).toStrictEqual({ 64 | valid: true, 65 | context: 'pull_request' 66 | }) 67 | }) 68 | 69 | test('checks the event context for a pr comment and finds that it is invalid - when only pr comments are allowed', async () => { 70 | process.env.INPUT_ALLOWED_CONTEXTS = 'pull_request' 71 | context.payload.issue = {} 72 | expect(await contextCheck(context)).toStrictEqual({ 73 | valid: false, 74 | context: 'issue_comment' 75 | }) 76 | 77 | expect(warningMock).toHaveBeenCalledWith( 78 | 'this Action can only be run in the context of a pull request comment' 79 | ) 80 | }) 81 | 82 | test('checks the event context for an issue comment and finds that it is invalid - when only issue comments are allowed', async () => { 83 | process.env.INPUT_ALLOWED_CONTEXTS = 'issue' 84 | expect(await contextCheck(context)).toStrictEqual({ 85 | valid: false, 86 | context: 'issue_comment' 87 | }) 88 | 89 | expect(warningMock).toHaveBeenCalledWith( 90 | 'this Action can only be run in the context of an issue comment' 91 | ) 92 | }) 93 | 94 | test('checks the event context and finds that it is invalid', async () => { 95 | context.eventName = 'push' 96 | expect(await contextCheck(context)).toStrictEqual({ 97 | valid: false, 98 | context: 'push' 99 | }) 100 | expect(warningMock).toHaveBeenCalledWith( 101 | 'this Action can only be run in the context of an issue_comment' 102 | ) 103 | expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true') 104 | }) 105 | 106 | test('checks the event context and throws an error', async () => { 107 | try { 108 | await contextCheck('evil') 109 | } catch (e) { 110 | expect(e.message).toBe( 111 | "Could not get PR event context: TypeError: Cannot read properties of undefined (reading 'issue')" 112 | ) 113 | } 114 | }) 115 | -------------------------------------------------------------------------------- /__tests__/functions/post-reactions.test.js: -------------------------------------------------------------------------------- 1 | import {postReactions} from '../../src/functions/post-reactions' 2 | 3 | var context 4 | var octokit 5 | beforeEach(() => { 6 | jest.clearAllMocks() 7 | 8 | context = { 9 | repo: { 10 | owner: 'corp', 11 | repo: 'test' 12 | }, 13 | payload: { 14 | comment: { 15 | id: '1' 16 | } 17 | } 18 | } 19 | 20 | octokit = { 21 | rest: { 22 | reactions: { 23 | createForIssueComment: jest.fn().mockReturnValueOnce({ 24 | data: {} 25 | }), 26 | deleteForIssueComment: jest.fn().mockReturnValueOnce({ 27 | data: {} 28 | }) 29 | } 30 | } 31 | } 32 | }) 33 | 34 | test('adds a successful reaction', async () => { 35 | expect(await postReactions(octokit, context, '+1', '123')).toBe(undefined) 36 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 37 | comment_id: '1', 38 | content: '+1', 39 | owner: 'corp', 40 | repo: 'test' 41 | }) 42 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 43 | comment_id: '1', 44 | owner: 'corp', 45 | reaction_id: 123, 46 | repo: 'test' 47 | }) 48 | }) 49 | 50 | test('adds a failure reaction', async () => { 51 | expect(await postReactions(octokit, context, '-1', '123')).toBe(undefined) 52 | expect(octokit.rest.reactions.createForIssueComment).toHaveBeenCalledWith({ 53 | comment_id: '1', 54 | content: '-1', 55 | owner: 'corp', 56 | repo: 'test' 57 | }) 58 | expect(octokit.rest.reactions.deleteForIssueComment).toHaveBeenCalledWith({ 59 | comment_id: '1', 60 | owner: 'corp', 61 | reaction_id: 123, 62 | repo: 'test' 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /__tests__/functions/post.test.js: -------------------------------------------------------------------------------- 1 | import {post} from '../../src/functions/post' 2 | import * as core from '@actions/core' 3 | import * as contextCheck from '../../src/functions/context-check' 4 | import * as github from '@actions/github' 5 | import * as postReactions from '../../src/functions/post-reactions' 6 | 7 | const validBooleanInputs = { 8 | skip_completing: false 9 | } 10 | const validInputs = { 11 | status: 'success' 12 | } 13 | 14 | const validStates = { 15 | comment_id: '123', 16 | token: 'test-token' 17 | } 18 | 19 | const setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation(() => {}) 20 | const setWarningMock = jest.spyOn(core, 'warning').mockImplementation(() => {}) 21 | const infoMock = jest.spyOn(core, 'info').mockImplementation(() => {}) 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks() 25 | process.env.INPUT_GITHUB_REPOSITORY = 'test-owner/test-repo' 26 | jest.spyOn(core, 'error').mockImplementation(() => {}) 27 | jest.spyOn(core, 'debug').mockImplementation(() => {}) 28 | jest.spyOn(core, 'getBooleanInput').mockImplementation(name => { 29 | return validBooleanInputs[name] 30 | }) 31 | jest.spyOn(core, 'getInput').mockImplementation(name => { 32 | return validInputs[name] 33 | }) 34 | jest.spyOn(core, 'getState').mockImplementation(name => { 35 | return validStates[name] 36 | }) 37 | jest.spyOn(contextCheck, 'contextCheck').mockImplementation(() => { 38 | return {valid: true, context: 'pull_request'} 39 | }) 40 | 41 | jest.spyOn(postReactions, 'postReactions').mockImplementation(() => { 42 | return true 43 | }) 44 | 45 | // spy and return a mock octokit object with methods 46 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 47 | return true 48 | }) 49 | }) 50 | 51 | test('successfully runs post() Action logic', async () => { 52 | expect(await post()).toBeUndefined() 53 | }) 54 | 55 | test('successfully runs post() Action logic when "success" is false', async () => { 56 | validInputs.status = 'failure' 57 | expect(await post()).toBeUndefined() 58 | }) 59 | 60 | test('exits due to an invalid Actions context', async () => { 61 | jest.spyOn(contextCheck, 'contextCheck').mockImplementation(() => { 62 | return {valid: false, context: 'pull_request'} 63 | }) 64 | expect(await post()).toBeUndefined() 65 | }) 66 | 67 | test('exits due to a bypass being set', async () => { 68 | const bypassed = { 69 | bypass: 'true' 70 | } 71 | jest.spyOn(core, 'getState').mockImplementation(name => { 72 | return bypassed[name] 73 | }) 74 | expect(await post()).toBeUndefined() 75 | expect(setWarningMock).toHaveBeenCalledWith('bypass set, exiting') 76 | }) 77 | 78 | test('skips the process of completing the run', async () => { 79 | const skipped = { 80 | skip_completing: 'true' 81 | } 82 | jest.spyOn(core, 'getBooleanInput').mockImplementation(name => { 83 | return skipped[name] 84 | }) 85 | expect(await post()).toBeUndefined() 86 | expect(infoMock).toHaveBeenCalledWith('⏩ skip_completing set, exiting') 87 | }) 88 | 89 | test('throws an error', async () => { 90 | try { 91 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 92 | throw new Error('test error') 93 | }) 94 | await post() 95 | } catch (e) { 96 | expect(e.message).toBe('test error') 97 | expect(setFailedMock).toHaveBeenCalledWith('test error') 98 | } 99 | }) 100 | 101 | test('use reaction specified in input on success', async () => { 102 | const reactionInput = { 103 | status: 'success', 104 | success_reaction: 'rocket' 105 | } 106 | jest.spyOn(core, 'getInput').mockImplementation(name => { 107 | return reactionInput[name] 108 | }) 109 | const postReactionsMock = jest.spyOn(postReactions, 'postReactions') 110 | 111 | expect(await post()).toBeUndefined() 112 | expect(postReactionsMock).toHaveBeenCalled() 113 | expect(postReactionsMock.mock.calls[0][2]).toBe('rocket') 114 | }) 115 | 116 | test('use reaction specified in input on failure', async () => { 117 | const reactionInput = { 118 | status: 'failure', 119 | failure_reaction: 'confused' 120 | } 121 | jest.spyOn(core, 'getInput').mockImplementation(name => { 122 | return reactionInput[name] 123 | }) 124 | const postReactionsMock = jest.spyOn(postReactions, 'postReactions') 125 | 126 | expect(await post()).toBeUndefined() 127 | expect(postReactionsMock).toHaveBeenCalled() 128 | expect(postReactionsMock.mock.calls[0][2]).toBe('confused') 129 | }) 130 | -------------------------------------------------------------------------------- /__tests__/functions/react-emote.test.js: -------------------------------------------------------------------------------- 1 | import {reactEmote} from '../../src/functions/react-emote' 2 | 3 | const context = { 4 | repo: { 5 | owner: 'corp', 6 | repo: 'test' 7 | }, 8 | payload: { 9 | comment: { 10 | id: '1' 11 | } 12 | } 13 | } 14 | 15 | const octokit = { 16 | rest: { 17 | reactions: { 18 | createForIssueComment: jest.fn().mockReturnValueOnce({ 19 | data: { 20 | id: '1' 21 | } 22 | }) 23 | } 24 | } 25 | } 26 | 27 | test('adds a reaction emote to a comment', async () => { 28 | expect(await reactEmote('eyes', context, octokit)).toStrictEqual({ 29 | data: {id: '1'} 30 | }) 31 | }) 32 | 33 | test('returns if no reaction is specified', async () => { 34 | expect(await reactEmote('', context, octokit)).toBe(undefined) 35 | expect(await reactEmote(null, context, octokit)).toBe(undefined) 36 | }) 37 | 38 | test('throws an error if a bad emote is used', async () => { 39 | try { 40 | await reactEmote('bad', context, octokit) 41 | } catch (e) { 42 | expect(e.message).toBe('Reaction "bad" is not a valid preset') 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /__tests__/functions/string-to-array.test.js: -------------------------------------------------------------------------------- 1 | import {stringToArray} from '../../src/functions/string-to-array' 2 | import * as core from '@actions/core' 3 | 4 | const debugMock = jest.spyOn(core, 'debug') 5 | 6 | beforeEach(() => { 7 | jest.clearAllMocks() 8 | jest.spyOn(core, 'debug').mockImplementation(() => {}) 9 | }) 10 | 11 | test('successfully converts a string to an array', async () => { 12 | expect(await stringToArray('production,staging,development')).toStrictEqual([ 13 | 'production', 14 | 'staging', 15 | 'development' 16 | ]) 17 | }) 18 | 19 | test('successfully converts a single string item string to an array', async () => { 20 | expect(await stringToArray('production,')).toStrictEqual(['production']) 21 | 22 | expect(await stringToArray('production')).toStrictEqual(['production']) 23 | }) 24 | 25 | test('successfully converts an empty string to an empty array', async () => { 26 | expect(await stringToArray('')).toStrictEqual([]) 27 | 28 | expect(debugMock).toHaveBeenCalledWith( 29 | 'in stringToArray(), an empty String was found so an empty Array was returned' 30 | ) 31 | }) 32 | 33 | test('successfully converts garbage to an empty array', async () => { 34 | expect(await stringToArray(',,,')).toStrictEqual([]) 35 | }) 36 | -------------------------------------------------------------------------------- /__tests__/functions/trigger-check.test.js: -------------------------------------------------------------------------------- 1 | import {triggerCheck} from '../../src/functions/trigger-check' 2 | import {COLORS} from '../../src/functions/colors' 3 | import * as core from '@actions/core' 4 | 5 | const setOutputMock = jest.spyOn(core, 'setOutput') 6 | const debugMock = jest.spyOn(core, 'debug') 7 | 8 | beforeEach(() => { 9 | jest.clearAllMocks() 10 | jest.spyOn(core, 'setOutput').mockImplementation(() => {}) 11 | jest.spyOn(core, 'saveState').mockImplementation(() => {}) 12 | jest.spyOn(core, 'debug').mockImplementation(() => {}) 13 | }) 14 | 15 | test('checks a message and finds a standard trigger', async () => { 16 | const body = '.test' 17 | const trigger = '.test' 18 | expect(await triggerCheck(body, trigger)).toBe(true) 19 | expect(setOutputMock).toHaveBeenCalledWith('comment_body', '.test') 20 | }) 21 | 22 | test('checks a message and does not find trigger', async () => { 23 | const body = '.bad' 24 | const trigger = '.test' 25 | expect(await triggerCheck(body, trigger)).toBe(false) 26 | expect(setOutputMock).toHaveBeenCalledWith('comment_body', '.bad') 27 | expect(debugMock).toHaveBeenCalledWith( 28 | `trigger ${COLORS.highlight}${trigger}${COLORS.reset} not found in the comment body` 29 | ) 30 | }) 31 | 32 | test('checks a message and finds a trigger with extra text', async () => { 33 | const trigger = '.test' 34 | expect(await triggerCheck('.test dev something', trigger)).toBe(true) 35 | expect(setOutputMock).toHaveBeenCalledWith( 36 | 'comment_body', 37 | '.test dev something' 38 | ) 39 | 40 | expect(await triggerCheck('.test something', trigger)).toBe(true) 41 | expect(setOutputMock).toHaveBeenCalledWith( 42 | 'comment_body', 43 | '.test dev something' 44 | ) 45 | 46 | expect(await triggerCheck('.test dev something', trigger)).toBe(true) 47 | expect(setOutputMock).toHaveBeenCalledWith( 48 | 'comment_body', 49 | '.test dev something' 50 | ) 51 | 52 | expect(await triggerCheck('.test dev something', trigger)).toBe(true) 53 | expect(setOutputMock).toHaveBeenCalledWith( 54 | 'comment_body', 55 | '.test dev something' 56 | ) 57 | }) 58 | 59 | test('checks a message and does not find a trigger', async () => { 60 | const body = 'I want to .ping a website' 61 | const trigger = '.test' 62 | expect(await triggerCheck(body, trigger)).toBe(false) 63 | expect(setOutputMock).toHaveBeenCalledWith( 64 | 'comment_body', 65 | 'I want to .ping a website' 66 | ) 67 | expect(debugMock).toHaveBeenCalledWith( 68 | `trigger ${COLORS.highlight}${trigger}${COLORS.reset} not found in the comment body` 69 | ) 70 | }) 71 | -------------------------------------------------------------------------------- /__tests__/functions/valid-permissions.test.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {validPermissions} from '../../src/functions/valid-permissions' 3 | 4 | const setOutputMock = jest.spyOn(core, 'setOutput') 5 | 6 | var octokit 7 | var context 8 | beforeEach(() => { 9 | jest.clearAllMocks() 10 | jest.spyOn(core, 'setOutput').mockImplementation(() => {}) 11 | process.env.INPUT_PERMISSIONS = 'write,admin' 12 | 13 | context = { 14 | actor: 'monalisa' 15 | } 16 | 17 | octokit = { 18 | rest: { 19 | repos: { 20 | getCollaboratorPermissionLevel: jest.fn().mockReturnValueOnce({ 21 | status: 200, 22 | data: { 23 | permission: 'write' 24 | } 25 | }) 26 | } 27 | } 28 | } 29 | }) 30 | 31 | test('determines that a user has valid permissions to invoke the Action', async () => { 32 | expect(await validPermissions(octokit, context)).toEqual(true) 33 | expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa') 34 | }) 35 | 36 | test('determines that a user has does not valid permissions to invoke the Action', async () => { 37 | octokit.rest.repos.getCollaboratorPermissionLevel = jest 38 | .fn() 39 | .mockReturnValue({ 40 | status: 200, 41 | data: { 42 | permission: 'read' 43 | } 44 | }) 45 | 46 | expect(await validPermissions(octokit, context)).toEqual( 47 | '👋 __monalisa__, seems as if you have not write/admin permissions in this repo, permissions: read' 48 | ) 49 | expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa') 50 | }) 51 | 52 | test('fails to get actor permissions due to a bad status code', async () => { 53 | octokit.rest.repos.getCollaboratorPermissionLevel = jest 54 | .fn() 55 | .mockReturnValue({ 56 | status: 500 57 | }) 58 | 59 | expect(await validPermissions(octokit, context)).toEqual( 60 | 'Permission check returns non-200 status: 500' 61 | ) 62 | expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa') 63 | }) 64 | -------------------------------------------------------------------------------- /__tests__/main.test.js: -------------------------------------------------------------------------------- 1 | import {run} from '../src/main' 2 | import * as reactEmote from '../src/functions/react-emote' 3 | import * as contextCheck from '../src/functions/context-check' 4 | import * as prechecks from '../src/functions/prechecks' 5 | import * as actionStatus from '../src/functions/action-status' 6 | import * as github from '@actions/github' 7 | import * as core from '@actions/core' 8 | 9 | const setOutputMock = jest.spyOn(core, 'setOutput') 10 | const saveStateMock = jest.spyOn(core, 'saveState') 11 | const setFailedMock = jest.spyOn(core, 'setFailed') 12 | const infoMock = jest.spyOn(core, 'info') 13 | // const debugMock = jest.spyOn(core, 'debug') 14 | 15 | beforeEach(() => { 16 | jest.clearAllMocks() 17 | jest.spyOn(core, 'setOutput').mockImplementation(() => {}) 18 | jest.spyOn(core, 'setFailed').mockImplementation(() => {}) 19 | jest.spyOn(core, 'saveState').mockImplementation(() => {}) 20 | jest.spyOn(core, 'info').mockImplementation(() => {}) 21 | jest.spyOn(core, 'debug').mockImplementation(() => {}) 22 | jest.spyOn(core, 'warning').mockImplementation(() => {}) 23 | jest.spyOn(core, 'error').mockImplementation(() => {}) 24 | process.env.INPUT_GITHUB_TOKEN = 'faketoken' 25 | process.env.INPUT_COMMAND = '.test' 26 | process.env.INPUT_REACTION = 'eyes' 27 | process.env.INPUT_PARAM_SEPARATOR = '|' 28 | process.env.INPUT_REQUIRED_CONTEXTS = 'false' 29 | process.env.INPUT_ALLOW_FORKS = 'false' 30 | process.env.INPUT_SKIP_CI = 'false' 31 | process.env.INPUT_ALLOW_DRAFTS = 'false' 32 | process.env.INPUT_SKIP_REVIEWS = 'false' 33 | process.env.INPUT_FORK_REVIEW_BYPASS = 'false' 34 | process.env.GITHUB_REPOSITORY = 'corp/test' 35 | github.context.payload = { 36 | issue: { 37 | number: 123 38 | }, 39 | comment: { 40 | body: '.test', 41 | id: 123, 42 | user: { 43 | login: 'monalisa' 44 | } 45 | } 46 | } 47 | 48 | jest.spyOn(github, 'getOctokit').mockImplementation(() => { 49 | return { 50 | rest: { 51 | issues: { 52 | createComment: jest.fn().mockReturnValueOnce({ 53 | data: {} 54 | }) 55 | }, 56 | pulls: { 57 | get: jest.fn().mockImplementation(() => { 58 | return {data: {head: {ref: 'test-ref'}}, status: 200} 59 | }) 60 | } 61 | } 62 | } 63 | }) 64 | jest.spyOn(contextCheck, 'contextCheck').mockImplementation(() => { 65 | return {valid: true, context: 'pull_request'} 66 | }) 67 | jest.spyOn(reactEmote, 'reactEmote').mockImplementation(() => { 68 | return {data: {id: '123'}} 69 | }) 70 | jest.spyOn(prechecks, 'prechecks').mockImplementation(() => { 71 | return { 72 | ref: 'test-ref', 73 | status: true, 74 | message: '✔️ PR is approved and all CI checks passed - OK', 75 | noopMode: false 76 | } 77 | }) 78 | }) 79 | 80 | test('successfully runs the action', async () => { 81 | expect(await run()).toBe('success') 82 | expect(setOutputMock).toHaveBeenCalledWith('comment_body', '.test') 83 | expect(setOutputMock).toHaveBeenCalledWith('triggered', 'true') 84 | expect(setOutputMock).toHaveBeenCalledWith('comment_id', 123) 85 | expect(setOutputMock).toHaveBeenCalledWith('ref', 'test-ref') 86 | expect(setOutputMock).toHaveBeenCalledWith('continue', 'true') 87 | expect(setOutputMock).toHaveBeenCalledWith('params', '') 88 | expect(saveStateMock).toHaveBeenCalledWith('isPost', 'true') 89 | expect(saveStateMock).toHaveBeenCalledWith('actionsToken', 'faketoken') 90 | expect(saveStateMock).toHaveBeenCalledWith('comment_id', 123) 91 | }) 92 | 93 | test('successfully runs the action with parameters', async () => { 94 | const body = '.test | test1 test2 --vm-size=chonky' 95 | github.context.payload.comment.body = body 96 | process.env.INPUT_COMMAND = body 97 | expect(await run()).toBe('success') 98 | expect(setOutputMock).toHaveBeenCalledWith('comment_body', body) 99 | expect(setOutputMock).toHaveBeenCalledWith('triggered', 'true') 100 | expect(setOutputMock).toHaveBeenCalledWith('comment_id', 123) 101 | expect(setOutputMock).toHaveBeenCalledWith('ref', 'test-ref') 102 | expect(setOutputMock).toHaveBeenCalledWith('continue', 'true') 103 | expect(setOutputMock).toHaveBeenCalledWith( 104 | 'params', 105 | 'test1 test2 --vm-size=chonky' 106 | ) 107 | expect(saveStateMock).toHaveBeenCalledWith('isPost', 'true') 108 | expect(saveStateMock).toHaveBeenCalledWith('actionsToken', 'faketoken') 109 | expect(saveStateMock).toHaveBeenCalledWith('comment_id', 123) 110 | }) 111 | 112 | test('successfully runs the action after trimming the body', async () => { 113 | const body = '.test \n\t\n ' 114 | github.context.payload.comment.body = body 115 | process.env.INPUT_COMMAND = body 116 | expect(await run()).toBe('success') 117 | expect(setOutputMock).toHaveBeenCalledWith('comment_body', '.test') 118 | expect(setOutputMock).toHaveBeenCalledWith('triggered', 'true') 119 | expect(setOutputMock).toHaveBeenCalledWith('comment_id', 123) 120 | expect(setOutputMock).toHaveBeenCalledWith('ref', 'test-ref') 121 | expect(setOutputMock).toHaveBeenCalledWith('continue', 'true') 122 | expect(setOutputMock).toHaveBeenCalledWith('params', '') 123 | expect(saveStateMock).toHaveBeenCalledWith('isPost', 'true') 124 | expect(saveStateMock).toHaveBeenCalledWith('actionsToken', 'faketoken') 125 | expect(saveStateMock).toHaveBeenCalledWith('comment_id', 123) 126 | }) 127 | 128 | test('fails due to a bad context', async () => { 129 | jest.spyOn(contextCheck, 'contextCheck').mockImplementation(() => { 130 | return {valid: false, context: 'pull_request'} 131 | }) 132 | expect(await run()).toBe('safe-exit') 133 | }) 134 | 135 | test('fails due to no trigger being found', async () => { 136 | process.env.INPUT_COMMAND = '.shipit' 137 | expect(await run()).toBe('safe-exit') 138 | expect(infoMock).toHaveBeenCalledWith('⛔ no command detected in comment') 139 | }) 140 | 141 | test('fails prechecks', async () => { 142 | jest.spyOn(prechecks, 'prechecks').mockImplementation(() => { 143 | return { 144 | ref: 'test-ref', 145 | status: false, 146 | message: '### ⚠️ Cannot proceed with operation... something went wrong', 147 | noopMode: false 148 | } 149 | }) 150 | jest.spyOn(actionStatus, 'actionStatus').mockImplementation(() => { 151 | return undefined 152 | }) 153 | expect(await run()).toBe('failure') 154 | expect(saveStateMock).toHaveBeenCalledWith('bypass', 'true') 155 | expect(setFailedMock).toHaveBeenCalledWith( 156 | '### ⚠️ Cannot proceed with operation... something went wrong' 157 | ) 158 | }) 159 | -------------------------------------------------------------------------------- /__tests__/schemas/action.schema.yml: -------------------------------------------------------------------------------- 1 | # Action base info 2 | name: 3 | type: string 4 | required: true 5 | description: 6 | type: string 7 | required: true 8 | author: 9 | type: string 10 | required: true 11 | branding: 12 | icon: 13 | type: string 14 | required: true 15 | color: 16 | type: string 17 | required: true 18 | 19 | # runs section 20 | runs: 21 | using: 22 | type: string 23 | required: true 24 | main: 25 | type: string 26 | required: true 27 | post: 28 | type: string 29 | required: true 30 | 31 | # inputs section 32 | inputs: 33 | github_token: 34 | description: 35 | type: string 36 | required: true 37 | default: 38 | type: string 39 | required: true 40 | required: 41 | type: boolean 42 | required: true 43 | status: 44 | description: 45 | type: string 46 | required: true 47 | default: 48 | type: string 49 | required: true 50 | required: 51 | type: boolean 52 | required: true 53 | command: 54 | description: 55 | type: string 56 | required: true 57 | required: 58 | type: boolean 59 | required: true 60 | allow_drafts: 61 | description: 62 | type: string 63 | required: true 64 | required: 65 | type: boolean 66 | required: true 67 | default: 68 | type: string 69 | required: false 70 | reaction: 71 | description: 72 | type: string 73 | required: true 74 | required: 75 | type: boolean 76 | required: true 77 | default: 78 | type: string 79 | required: true 80 | success_reaction: 81 | description: 82 | type: string 83 | required: true 84 | required: 85 | type: boolean 86 | required: true 87 | default: 88 | type: string 89 | required: true 90 | failure_reaction: 91 | description: 92 | type: string 93 | required: true 94 | required: 95 | type: boolean 96 | required: true 97 | default: 98 | type: string 99 | required: true 100 | allowed_contexts: 101 | description: 102 | type: string 103 | required: true 104 | required: 105 | type: boolean 106 | required: true 107 | default: 108 | type: string 109 | required: true 110 | permissions: 111 | description: 112 | type: string 113 | required: true 114 | default: 115 | type: string 116 | required: true 117 | required: 118 | type: boolean 119 | required: true 120 | param_separator: 121 | description: 122 | type: string 123 | required: true 124 | default: 125 | type: string 126 | required: true 127 | required: 128 | type: boolean 129 | required: true 130 | skip_ci: 131 | description: 132 | type: string 133 | required: true 134 | required: 135 | type: boolean 136 | required: true 137 | default: 138 | type: string 139 | required: false 140 | skip_reviews: 141 | description: 142 | type: string 143 | required: true 144 | required: 145 | type: boolean 146 | required: true 147 | default: 148 | type: string 149 | required: false 150 | skip_completing: 151 | description: 152 | type: string 153 | required: true 154 | required: 155 | type: boolean 156 | required: true 157 | default: 158 | type: string 159 | required: false 160 | allow_forks: 161 | description: 162 | type: string 163 | required: true 164 | required: 165 | type: boolean 166 | required: true 167 | default: 168 | type: string 169 | required: true 170 | allowlist: 171 | description: 172 | type: string 173 | required: true 174 | required: 175 | type: boolean 176 | required: true 177 | default: 178 | type: string 179 | required: true 180 | allowlist_pat: 181 | description: 182 | type: string 183 | required: true 184 | required: 185 | type: boolean 186 | required: true 187 | default: 188 | type: string 189 | required: true 190 | fork_review_bypass: 191 | description: 192 | type: string 193 | required: true 194 | required: 195 | type: boolean 196 | required: true 197 | default: 198 | type: string 199 | required: true 200 | 201 | # outputs section 202 | outputs: 203 | triggered: 204 | description: 205 | type: string 206 | required: true 207 | comment_body: 208 | description: 209 | type: string 210 | required: true 211 | actor: 212 | description: 213 | type: string 214 | required: true 215 | params: 216 | description: 217 | type: string 218 | required: true 219 | comment_id: 220 | description: 221 | type: string 222 | required: true 223 | issue_number: 224 | description: 225 | type: string 226 | required: true 227 | continue: 228 | description: 229 | type: string 230 | required: true 231 | fork: 232 | description: 233 | type: string 234 | required: true 235 | fork_ref: 236 | description: 237 | type: string 238 | required: true 239 | fork_label: 240 | description: 241 | type: string 242 | required: true 243 | fork_checkout: 244 | description: 245 | type: string 246 | required: true 247 | fork_full_name: 248 | description: 249 | type: string 250 | required: true 251 | ref: 252 | description: 253 | type: string 254 | required: true 255 | sha: 256 | description: 257 | type: string 258 | required: true 259 | initial_reaction_id: 260 | description: 261 | type: string 262 | required: true 263 | base_ref: 264 | description: 265 | type: string 266 | required: true 267 | -------------------------------------------------------------------------------- /__tests__/version.test.js: -------------------------------------------------------------------------------- 1 | import {VERSION} from '../src/version' 2 | 3 | describe('VERSION constant', () => { 4 | const versionRegex = /^v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/ 5 | 6 | it('should match the version pattern', () => { 7 | expect(VERSION).toMatch(versionRegex) 8 | }) 9 | 10 | it('should validate v1.0.0', () => { 11 | const version = 'v1.0.0' 12 | expect(version).toMatch(versionRegex) 13 | }) 14 | 15 | it('should validate v4.5.1', () => { 16 | const version = 'v4.5.1' 17 | expect(version).toMatch(versionRegex) 18 | }) 19 | 20 | it('should validate v10.123.44', () => { 21 | const version = 'v10.123.44' 22 | expect(version).toMatch(versionRegex) 23 | }) 24 | 25 | it('should validate v1.1.1-rc.1', () => { 26 | const version = 'v1.1.1-rc.1' 27 | expect(version).toMatch(versionRegex) 28 | }) 29 | 30 | it('should validate v15.19.4-rc.35', () => { 31 | const version = 'v15.19.4-rc.35' 32 | expect(version).toMatch(versionRegex) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "command-action" 2 | description: "IssueOps commands in GitHub Actions" 3 | author: "Grant Birkinbine" 4 | branding: 5 | icon: 'play' 6 | color: 'gray-dark' 7 | inputs: 8 | github_token: 9 | description: The GitHub token used to create an authenticated client - Provided for you by default! 10 | default: ${{ github.token }} 11 | required: true 12 | status: 13 | description: The status of the GitHub Actions - For use in the post run workflow - Provided for you by default! 14 | default: ${{ job.status }} 15 | required: true 16 | command: 17 | description: 'The string to look for in comments as an IssueOps trigger/command. Example: ".lint"' 18 | required: true 19 | reaction: 20 | description: 'If set, the specified emoji "reaction" is put on the comment to indicate that the trigger was detected. For example, "rocket" or "eyes"' 21 | required: true 22 | default: "eyes" 23 | success_reaction: 24 | description: 'The reaction to add to the comment that triggered the Action if its execution was successful' 25 | required: true 26 | default: "+1" 27 | failure_reaction: 28 | description: 'The reaction to add to the comment that triggered the Action if its execution failed' 29 | required: true 30 | default: "-1" 31 | allowed_contexts: 32 | description: 'A comma separated list of comment contexts that are allowed to trigger this IssueOps command. Pull requests and issues are the only currently supported contexts' 33 | required: true 34 | default: "pull_request" 35 | permissions: 36 | description: 'The allowed GitHub permissions an actor can have to invoke IssueOps commands - Example: "write,admin"' 37 | required: true 38 | default: "write,admin" 39 | allow_drafts: 40 | description: 'Whether or not to allow this IssueOps command to be run on draft pull requests' 41 | required: true 42 | default: "false" 43 | allow_forks: 44 | description: 'Whether or not to allow this IssueOps command to be run on forked pull requests' 45 | required: true 46 | default: "false" 47 | skip_ci: 48 | description: 'Whether or not to require passing CI checks before this IssueOps command can be run' 49 | required: true 50 | default: "false" 51 | skip_reviews: 52 | description: 'Whether or not to require reviews before this IssueOps command can be run' 53 | required: true 54 | default: "false" 55 | param_separator: 56 | description: 'The separator to use for parsing parameters in comments in IssueOps commands. Parameters will are saved as outputs and can be used in subsequent steps' 57 | required: true 58 | default: "|" 59 | allowlist: 60 | description: 'A comma separated list of GitHub usernames or teams that should be allowed to use the IssueOps commands configured in this Action. If unset, then all users meeting the "permissions" requirement will be able to run commands. Example: "monalisa,octocat,my-org/my-team"' 61 | required: false 62 | default: "false" 63 | allowlist_pat: 64 | description: 'A GitHub personal access token with "read:org" scopes. This is only needed if you are using the "allowlist" option with a GitHub org team. For example: "my-org/my-team"' 65 | required: false 66 | default: "false" 67 | skip_completing: 68 | description: 'If set to "true", skip the process of completing the Action. This is useful if you want to customize the way this Action completes - For example, custom reactions, comments, etc' 69 | required: true 70 | default: "false" 71 | fork_review_bypass: 72 | description: 'If set to "true", allow forks to bypass the review requirement if the operation is being made on a pull request from a fork. This option is potentially dangerous if you are checking out code in your workflow as a result of invoking this Action. If the code you are checking out has not been reviewed, then you might open yourself up to a TOCTOU vulnerability. You should always ensure that the code you are checking out has been reviewed, and that you checkout an exact commit sha rather than a ref.' 73 | required: true 74 | default: "false" 75 | outputs: 76 | triggered: 77 | description: 'The string "true" if the trigger was found, otherwise the string "false" - Just because the workflow was triggered does not mean it should continue. This is a step 1/2 check' 78 | continue: 79 | description: 'The string "true" if the workflow should continue, otherwise empty - Use this to conditionally control if your workflow should proceed or not. This is a step 2/2 check' 80 | comment_body: 81 | description: The comment body 82 | actor: 83 | description: The GitHub handle of the actor that invoked the IssueOps command 84 | params: 85 | description: The raw parameters that were passed into the IssueOps command 86 | comment_id: 87 | description: The comment id which triggered this action 88 | issue_number: 89 | description: The issue number which this Action was triggered on 90 | initial_reaction_id: 91 | description: The reaction id for the initial reaction on the trigger comment 92 | fork: 93 | description: 'The string "true" if the pull request is a fork, otherwise "false"' 94 | fork_ref: 95 | description: 'The true ref of the fork' 96 | fork_label: 97 | description: 'The API label field returned for the fork' 98 | fork_checkout: 99 | description: 'The console command presented in the GitHub UI to checkout a given fork locally' 100 | fork_full_name: 101 | description: 'The full name of the fork in "org/repo" format' 102 | sha: 103 | description: 'The commit sha if being used in the context of a pull request' 104 | ref: 105 | description: 'The ref if being used in the context of a pull request' 106 | base_ref: 107 | description: The base ref that the pull request is merging into (if available and run in the context of a pull request) 108 | runs: 109 | using: "node20" 110 | main: "dist/index.js" 111 | post: "dist/index.js" 112 | -------------------------------------------------------------------------------- /badges/coverage.svg: -------------------------------------------------------------------------------- 1 | Coverage: 100%Coverage100% -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/github 26 | MIT 27 | The MIT License (MIT) 28 | 29 | Copyright 2019 GitHub 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | 37 | @actions/http-client 38 | MIT 39 | Actions Http Client for Node.js 40 | 41 | Copyright (c) GitHub, Inc. 42 | 43 | All rights reserved. 44 | 45 | MIT License 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 48 | associated documentation files (the "Software"), to deal in the Software without restriction, 49 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 50 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 51 | subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 56 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 57 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 58 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 59 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 60 | 61 | 62 | @actions/io 63 | MIT 64 | The MIT License (MIT) 65 | 66 | Copyright 2019 GitHub 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 73 | 74 | @fastify/busboy 75 | MIT 76 | Copyright Brian White. All rights reserved. 77 | 78 | Permission is hereby granted, free of charge, to any person obtaining a copy 79 | of this software and associated documentation files (the "Software"), to 80 | deal in the Software without restriction, including without limitation the 81 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 82 | sell copies of the Software, and to permit persons to whom the Software is 83 | furnished to do so, subject to the following conditions: 84 | 85 | The above copyright notice and this permission notice shall be included in 86 | all copies or substantial portions of the Software. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 89 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 90 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 91 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 92 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 94 | IN THE SOFTWARE. 95 | 96 | @octokit/auth-token 97 | MIT 98 | The MIT License 99 | 100 | Copyright (c) 2019 Octokit contributors 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to deal 104 | in the Software without restriction, including without limitation the rights 105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 106 | copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in 110 | all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 118 | THE SOFTWARE. 119 | 120 | 121 | @octokit/core 122 | MIT 123 | The MIT License 124 | 125 | Copyright (c) 2019 Octokit contributors 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in 135 | all copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 143 | THE SOFTWARE. 144 | 145 | 146 | @octokit/endpoint 147 | MIT 148 | The MIT License 149 | 150 | Copyright (c) 2018 Octokit contributors 151 | 152 | Permission is hereby granted, free of charge, to any person obtaining a copy 153 | of this software and associated documentation files (the "Software"), to deal 154 | in the Software without restriction, including without limitation the rights 155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | copies of the Software, and to permit persons to whom the Software is 157 | furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in 160 | all copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 168 | THE SOFTWARE. 169 | 170 | 171 | @octokit/graphql 172 | MIT 173 | The MIT License 174 | 175 | Copyright (c) 2018 Octokit contributors 176 | 177 | Permission is hereby granted, free of charge, to any person obtaining a copy 178 | of this software and associated documentation files (the "Software"), to deal 179 | in the Software without restriction, including without limitation the rights 180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 181 | copies of the Software, and to permit persons to whom the Software is 182 | furnished to do so, subject to the following conditions: 183 | 184 | The above copyright notice and this permission notice shall be included in 185 | all copies or substantial portions of the Software. 186 | 187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 193 | THE SOFTWARE. 194 | 195 | 196 | @octokit/plugin-paginate-rest 197 | MIT 198 | MIT License Copyright (c) 2019 Octokit contributors 199 | 200 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 201 | 202 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 203 | 204 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 205 | 206 | 207 | @octokit/plugin-rest-endpoint-methods 208 | MIT 209 | MIT License Copyright (c) 2019 Octokit contributors 210 | 211 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 212 | 213 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 214 | 215 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 216 | 217 | 218 | @octokit/plugin-retry 219 | MIT 220 | MIT License 221 | 222 | Copyright (c) 2018 Octokit contributors 223 | 224 | Permission is hereby granted, free of charge, to any person obtaining a copy 225 | of this software and associated documentation files (the "Software"), to deal 226 | in the Software without restriction, including without limitation the rights 227 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 228 | copies of the Software, and to permit persons to whom the Software is 229 | furnished to do so, subject to the following conditions: 230 | 231 | The above copyright notice and this permission notice shall be included in all 232 | copies or substantial portions of the Software. 233 | 234 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 235 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 236 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 237 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 238 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 239 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 240 | SOFTWARE. 241 | 242 | 243 | @octokit/request 244 | MIT 245 | The MIT License 246 | 247 | Copyright (c) 2018 Octokit contributors 248 | 249 | Permission is hereby granted, free of charge, to any person obtaining a copy 250 | of this software and associated documentation files (the "Software"), to deal 251 | in the Software without restriction, including without limitation the rights 252 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 253 | copies of the Software, and to permit persons to whom the Software is 254 | furnished to do so, subject to the following conditions: 255 | 256 | The above copyright notice and this permission notice shall be included in 257 | all copies or substantial portions of the Software. 258 | 259 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 260 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 261 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 262 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 263 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 264 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 265 | THE SOFTWARE. 266 | 267 | 268 | @octokit/request-error 269 | MIT 270 | The MIT License 271 | 272 | Copyright (c) 2019 Octokit contributors 273 | 274 | Permission is hereby granted, free of charge, to any person obtaining a copy 275 | of this software and associated documentation files (the "Software"), to deal 276 | in the Software without restriction, including without limitation the rights 277 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 278 | copies of the Software, and to permit persons to whom the Software is 279 | furnished to do so, subject to the following conditions: 280 | 281 | The above copyright notice and this permission notice shall be included in 282 | all copies or substantial portions of the Software. 283 | 284 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 285 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 286 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 287 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 288 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 289 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 290 | THE SOFTWARE. 291 | 292 | 293 | before-after-hook 294 | Apache-2.0 295 | Apache License 296 | Version 2.0, January 2004 297 | http://www.apache.org/licenses/ 298 | 299 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 300 | 301 | 1. Definitions. 302 | 303 | "License" shall mean the terms and conditions for use, reproduction, 304 | and distribution as defined by Sections 1 through 9 of this document. 305 | 306 | "Licensor" shall mean the copyright owner or entity authorized by 307 | the copyright owner that is granting the License. 308 | 309 | "Legal Entity" shall mean the union of the acting entity and all 310 | other entities that control, are controlled by, or are under common 311 | control with that entity. For the purposes of this definition, 312 | "control" means (i) the power, direct or indirect, to cause the 313 | direction or management of such entity, whether by contract or 314 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 315 | outstanding shares, or (iii) beneficial ownership of such entity. 316 | 317 | "You" (or "Your") shall mean an individual or Legal Entity 318 | exercising permissions granted by this License. 319 | 320 | "Source" form shall mean the preferred form for making modifications, 321 | including but not limited to software source code, documentation 322 | source, and configuration files. 323 | 324 | "Object" form shall mean any form resulting from mechanical 325 | transformation or translation of a Source form, including but 326 | not limited to compiled object code, generated documentation, 327 | and conversions to other media types. 328 | 329 | "Work" shall mean the work of authorship, whether in Source or 330 | Object form, made available under the License, as indicated by a 331 | copyright notice that is included in or attached to the work 332 | (an example is provided in the Appendix below). 333 | 334 | "Derivative Works" shall mean any work, whether in Source or Object 335 | form, that is based on (or derived from) the Work and for which the 336 | editorial revisions, annotations, elaborations, or other modifications 337 | represent, as a whole, an original work of authorship. For the purposes 338 | of this License, Derivative Works shall not include works that remain 339 | separable from, or merely link (or bind by name) to the interfaces of, 340 | the Work and Derivative Works thereof. 341 | 342 | "Contribution" shall mean any work of authorship, including 343 | the original version of the Work and any modifications or additions 344 | to that Work or Derivative Works thereof, that is intentionally 345 | submitted to Licensor for inclusion in the Work by the copyright owner 346 | or by an individual or Legal Entity authorized to submit on behalf of 347 | the copyright owner. For the purposes of this definition, "submitted" 348 | means any form of electronic, verbal, or written communication sent 349 | to the Licensor or its representatives, including but not limited to 350 | communication on electronic mailing lists, source code control systems, 351 | and issue tracking systems that are managed by, or on behalf of, the 352 | Licensor for the purpose of discussing and improving the Work, but 353 | excluding communication that is conspicuously marked or otherwise 354 | designated in writing by the copyright owner as "Not a Contribution." 355 | 356 | "Contributor" shall mean Licensor and any individual or Legal Entity 357 | on behalf of whom a Contribution has been received by Licensor and 358 | subsequently incorporated within the Work. 359 | 360 | 2. Grant of Copyright License. Subject to the terms and conditions of 361 | this License, each Contributor hereby grants to You a perpetual, 362 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 363 | copyright license to reproduce, prepare Derivative Works of, 364 | publicly display, publicly perform, sublicense, and distribute the 365 | Work and such Derivative Works in Source or Object form. 366 | 367 | 3. Grant of Patent License. Subject to the terms and conditions of 368 | this License, each Contributor hereby grants to You a perpetual, 369 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 370 | (except as stated in this section) patent license to make, have made, 371 | use, offer to sell, sell, import, and otherwise transfer the Work, 372 | where such license applies only to those patent claims licensable 373 | by such Contributor that are necessarily infringed by their 374 | Contribution(s) alone or by combination of their Contribution(s) 375 | with the Work to which such Contribution(s) was submitted. If You 376 | institute patent litigation against any entity (including a 377 | cross-claim or counterclaim in a lawsuit) alleging that the Work 378 | or a Contribution incorporated within the Work constitutes direct 379 | or contributory patent infringement, then any patent licenses 380 | granted to You under this License for that Work shall terminate 381 | as of the date such litigation is filed. 382 | 383 | 4. Redistribution. You may reproduce and distribute copies of the 384 | Work or Derivative Works thereof in any medium, with or without 385 | modifications, and in Source or Object form, provided that You 386 | meet the following conditions: 387 | 388 | (a) You must give any other recipients of the Work or 389 | Derivative Works a copy of this License; and 390 | 391 | (b) You must cause any modified files to carry prominent notices 392 | stating that You changed the files; and 393 | 394 | (c) You must retain, in the Source form of any Derivative Works 395 | that You distribute, all copyright, patent, trademark, and 396 | attribution notices from the Source form of the Work, 397 | excluding those notices that do not pertain to any part of 398 | the Derivative Works; and 399 | 400 | (d) If the Work includes a "NOTICE" text file as part of its 401 | distribution, then any Derivative Works that You distribute must 402 | include a readable copy of the attribution notices contained 403 | within such NOTICE file, excluding those notices that do not 404 | pertain to any part of the Derivative Works, in at least one 405 | of the following places: within a NOTICE text file distributed 406 | as part of the Derivative Works; within the Source form or 407 | documentation, if provided along with the Derivative Works; or, 408 | within a display generated by the Derivative Works, if and 409 | wherever such third-party notices normally appear. The contents 410 | of the NOTICE file are for informational purposes only and 411 | do not modify the License. You may add Your own attribution 412 | notices within Derivative Works that You distribute, alongside 413 | or as an addendum to the NOTICE text from the Work, provided 414 | that such additional attribution notices cannot be construed 415 | as modifying the License. 416 | 417 | You may add Your own copyright statement to Your modifications and 418 | may provide additional or different license terms and conditions 419 | for use, reproduction, or distribution of Your modifications, or 420 | for any such Derivative Works as a whole, provided Your use, 421 | reproduction, and distribution of the Work otherwise complies with 422 | the conditions stated in this License. 423 | 424 | 5. Submission of Contributions. Unless You explicitly state otherwise, 425 | any Contribution intentionally submitted for inclusion in the Work 426 | by You to the Licensor shall be under the terms and conditions of 427 | this License, without any additional terms or conditions. 428 | Notwithstanding the above, nothing herein shall supersede or modify 429 | the terms of any separate license agreement you may have executed 430 | with Licensor regarding such Contributions. 431 | 432 | 6. Trademarks. This License does not grant permission to use the trade 433 | names, trademarks, service marks, or product names of the Licensor, 434 | except as required for reasonable and customary use in describing the 435 | origin of the Work and reproducing the content of the NOTICE file. 436 | 437 | 7. Disclaimer of Warranty. Unless required by applicable law or 438 | agreed to in writing, Licensor provides the Work (and each 439 | Contributor provides its Contributions) on an "AS IS" BASIS, 440 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 441 | implied, including, without limitation, any warranties or conditions 442 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 443 | PARTICULAR PURPOSE. You are solely responsible for determining the 444 | appropriateness of using or redistributing the Work and assume any 445 | risks associated with Your exercise of permissions under this License. 446 | 447 | 8. Limitation of Liability. In no event and under no legal theory, 448 | whether in tort (including negligence), contract, or otherwise, 449 | unless required by applicable law (such as deliberate and grossly 450 | negligent acts) or agreed to in writing, shall any Contributor be 451 | liable to You for damages, including any direct, indirect, special, 452 | incidental, or consequential damages of any character arising as a 453 | result of this License or out of the use or inability to use the 454 | Work (including but not limited to damages for loss of goodwill, 455 | work stoppage, computer failure or malfunction, or any and all 456 | other commercial damages or losses), even if such Contributor 457 | has been advised of the possibility of such damages. 458 | 459 | 9. Accepting Warranty or Additional Liability. While redistributing 460 | the Work or Derivative Works thereof, You may choose to offer, 461 | and charge a fee for, acceptance of support, warranty, indemnity, 462 | or other liability obligations and/or rights consistent with this 463 | License. However, in accepting such obligations, You may act only 464 | on Your own behalf and on Your sole responsibility, not on behalf 465 | of any other Contributor, and only if You agree to indemnify, 466 | defend, and hold each Contributor harmless for any liability 467 | incurred by, or claims asserted against, such Contributor by reason 468 | of your accepting any such warranty or additional liability. 469 | 470 | END OF TERMS AND CONDITIONS 471 | 472 | APPENDIX: How to apply the Apache License to your work. 473 | 474 | To apply the Apache License to your work, attach the following 475 | boilerplate notice, with the fields enclosed by brackets "{}" 476 | replaced with your own identifying information. (Don't include 477 | the brackets!) The text should be enclosed in the appropriate 478 | comment syntax for the file format. We also recommend that a 479 | file or class name and description of purpose be included on the 480 | same "printed page" as the copyright notice for easier 481 | identification within third-party archives. 482 | 483 | Copyright 2018 Gregor Martynus and other contributors. 484 | 485 | Licensed under the Apache License, Version 2.0 (the "License"); 486 | you may not use this file except in compliance with the License. 487 | You may obtain a copy of the License at 488 | 489 | http://www.apache.org/licenses/LICENSE-2.0 490 | 491 | Unless required by applicable law or agreed to in writing, software 492 | distributed under the License is distributed on an "AS IS" BASIS, 493 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 494 | See the License for the specific language governing permissions and 495 | limitations under the License. 496 | 497 | 498 | bottleneck 499 | MIT 500 | The MIT License (MIT) 501 | 502 | Copyright (c) 2014 Simon Grondin 503 | 504 | Permission is hereby granted, free of charge, to any person obtaining a copy of 505 | this software and associated documentation files (the "Software"), to deal in 506 | the Software without restriction, including without limitation the rights to 507 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 508 | the Software, and to permit persons to whom the Software is furnished to do so, 509 | subject to the following conditions: 510 | 511 | The above copyright notice and this permission notice shall be included in all 512 | copies or substantial portions of the Software. 513 | 514 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 515 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 516 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 517 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 518 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 519 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 520 | 521 | 522 | deprecation 523 | ISC 524 | The ISC License 525 | 526 | Copyright (c) Gregor Martynus and contributors 527 | 528 | Permission to use, copy, modify, and/or distribute this software for any 529 | purpose with or without fee is hereby granted, provided that the above 530 | copyright notice and this permission notice appear in all copies. 531 | 532 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 533 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 534 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 535 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 536 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 537 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 538 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 539 | 540 | 541 | github-username-regex-js 542 | CC0-1.0 543 | Creative Commons Legal Code 544 | 545 | CC0 1.0 Universal 546 | 547 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 548 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 549 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 550 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 551 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 552 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 553 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 554 | HEREUNDER. 555 | 556 | Statement of Purpose 557 | 558 | The laws of most jurisdictions throughout the world automatically confer 559 | exclusive Copyright and Related Rights (defined below) upon the creator 560 | and subsequent owner(s) (each and all, an "owner") of an original work of 561 | authorship and/or a database (each, a "Work"). 562 | 563 | Certain owners wish to permanently relinquish those rights to a Work for 564 | the purpose of contributing to a commons of creative, cultural and 565 | scientific works ("Commons") that the public can reliably and without fear 566 | of later claims of infringement build upon, modify, incorporate in other 567 | works, reuse and redistribute as freely as possible in any form whatsoever 568 | and for any purposes, including without limitation commercial purposes. 569 | These owners may contribute to the Commons to promote the ideal of a free 570 | culture and the further production of creative, cultural and scientific 571 | works, or to gain reputation or greater distribution for their Work in 572 | part through the use and efforts of others. 573 | 574 | For these and/or other purposes and motivations, and without any 575 | expectation of additional consideration or compensation, the person 576 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 577 | is an owner of Copyright and Related Rights in the Work, voluntarily 578 | elects to apply CC0 to the Work and publicly distribute the Work under its 579 | terms, with knowledge of his or her Copyright and Related Rights in the 580 | Work and the meaning and intended legal effect of CC0 on those rights. 581 | 582 | 1. Copyright and Related Rights. A Work made available under CC0 may be 583 | protected by copyright and related or neighboring rights ("Copyright and 584 | Related Rights"). Copyright and Related Rights include, but are not 585 | limited to, the following: 586 | 587 | i. the right to reproduce, adapt, distribute, perform, display, 588 | communicate, and translate a Work; 589 | ii. moral rights retained by the original author(s) and/or performer(s); 590 | iii. publicity and privacy rights pertaining to a person's image or 591 | likeness depicted in a Work; 592 | iv. rights protecting against unfair competition in regards to a Work, 593 | subject to the limitations in paragraph 4(a), below; 594 | v. rights protecting the extraction, dissemination, use and reuse of data 595 | in a Work; 596 | vi. database rights (such as those arising under Directive 96/9/EC of the 597 | European Parliament and of the Council of 11 March 1996 on the legal 598 | protection of databases, and under any national implementation 599 | thereof, including any amended or successor version of such 600 | directive); and 601 | vii. other similar, equivalent or corresponding rights throughout the 602 | world based on applicable law or treaty, and any national 603 | implementations thereof. 604 | 605 | 2. Waiver. To the greatest extent permitted by, but not in contravention 606 | of, applicable law, Affirmer hereby overtly, fully, permanently, 607 | irrevocably and unconditionally waives, abandons, and surrenders all of 608 | Affirmer's Copyright and Related Rights and associated claims and causes 609 | of action, whether now known or unknown (including existing as well as 610 | future claims and causes of action), in the Work (i) in all territories 611 | worldwide, (ii) for the maximum duration provided by applicable law or 612 | treaty (including future time extensions), (iii) in any current or future 613 | medium and for any number of copies, and (iv) for any purpose whatsoever, 614 | including without limitation commercial, advertising or promotional 615 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 616 | member of the public at large and to the detriment of Affirmer's heirs and 617 | successors, fully intending that such Waiver shall not be subject to 618 | revocation, rescission, cancellation, termination, or any other legal or 619 | equitable action to disrupt the quiet enjoyment of the Work by the public 620 | as contemplated by Affirmer's express Statement of Purpose. 621 | 622 | 3. Public License Fallback. Should any part of the Waiver for any reason 623 | be judged legally invalid or ineffective under applicable law, then the 624 | Waiver shall be preserved to the maximum extent permitted taking into 625 | account Affirmer's express Statement of Purpose. In addition, to the 626 | extent the Waiver is so judged Affirmer hereby grants to each affected 627 | person a royalty-free, non transferable, non sublicensable, non exclusive, 628 | irrevocable and unconditional license to exercise Affirmer's Copyright and 629 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 630 | maximum duration provided by applicable law or treaty (including future 631 | time extensions), (iii) in any current or future medium and for any number 632 | of copies, and (iv) for any purpose whatsoever, including without 633 | limitation commercial, advertising or promotional purposes (the 634 | "License"). The License shall be deemed effective as of the date CC0 was 635 | applied by Affirmer to the Work. Should any part of the License for any 636 | reason be judged legally invalid or ineffective under applicable law, such 637 | partial invalidity or ineffectiveness shall not invalidate the remainder 638 | of the License, and in such case Affirmer hereby affirms that he or she 639 | will not (i) exercise any of his or her remaining Copyright and Related 640 | Rights in the Work or (ii) assert any associated claims and causes of 641 | action with respect to the Work, in either case contrary to Affirmer's 642 | express Statement of Purpose. 643 | 644 | 4. Limitations and Disclaimers. 645 | 646 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 647 | surrendered, licensed or otherwise affected by this document. 648 | b. Affirmer offers the Work as-is and makes no representations or 649 | warranties of any kind concerning the Work, express, implied, 650 | statutory or otherwise, including without limitation warranties of 651 | title, merchantability, fitness for a particular purpose, non 652 | infringement, or the absence of latent or other defects, accuracy, or 653 | the present or absence of errors, whether or not discoverable, all to 654 | the greatest extent permissible under applicable law. 655 | c. Affirmer disclaims responsibility for clearing rights of other persons 656 | that may apply to the Work or any use thereof, including without 657 | limitation any person's Copyright and Related Rights in the Work. 658 | Further, Affirmer disclaims responsibility for obtaining any necessary 659 | consents, permissions or other rights required for any use of the 660 | Work. 661 | d. Affirmer understands and acknowledges that Creative Commons is not a 662 | party to this document and has no duty or obligation with respect to 663 | this CC0 or use of the Work. 664 | 665 | 666 | once 667 | ISC 668 | The ISC License 669 | 670 | Copyright (c) Isaac Z. Schlueter and Contributors 671 | 672 | Permission to use, copy, modify, and/or distribute this software for any 673 | purpose with or without fee is hereby granted, provided that the above 674 | copyright notice and this permission notice appear in all copies. 675 | 676 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 677 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 678 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 679 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 680 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 681 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 682 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 683 | 684 | 685 | tunnel 686 | MIT 687 | The MIT License (MIT) 688 | 689 | Copyright (c) 2012 Koichi Kobayashi 690 | 691 | Permission is hereby granted, free of charge, to any person obtaining a copy 692 | of this software and associated documentation files (the "Software"), to deal 693 | in the Software without restriction, including without limitation the rights 694 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 695 | copies of the Software, and to permit persons to whom the Software is 696 | furnished to do so, subject to the following conditions: 697 | 698 | The above copyright notice and this permission notice shall be included in 699 | all copies or substantial portions of the Software. 700 | 701 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 702 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 703 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 704 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 705 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 706 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 707 | THE SOFTWARE. 708 | 709 | 710 | undici 711 | MIT 712 | MIT License 713 | 714 | Copyright (c) Matteo Collina and Undici contributors 715 | 716 | Permission is hereby granted, free of charge, to any person obtaining a copy 717 | of this software and associated documentation files (the "Software"), to deal 718 | in the Software without restriction, including without limitation the rights 719 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 720 | copies of the Software, and to permit persons to whom the Software is 721 | furnished to do so, subject to the following conditions: 722 | 723 | The above copyright notice and this permission notice shall be included in all 724 | copies or substantial portions of the Software. 725 | 726 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 727 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 728 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 729 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 730 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 731 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 732 | SOFTWARE. 733 | 734 | 735 | universal-user-agent 736 | ISC 737 | # [ISC License](https://spdx.org/licenses/ISC) 738 | 739 | Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m) 740 | 741 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 742 | 743 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 744 | 745 | 746 | wrappy 747 | ISC 748 | The ISC License 749 | 750 | Copyright (c) Isaac Z. Schlueter and Contributors 751 | 752 | Permission to use, copy, modify, and/or distribute this software for any 753 | purpose with or without fee is hereby granted, provided that the above 754 | copyright notice and this permission notice appear in all copies. 755 | 756 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 757 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 758 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 759 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 760 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 761 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 762 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 763 | -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={296:e=>{var r=Object.prototype.toString;var n=typeof Buffer!=="undefined"&&typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},599:(e,r,n)=>{e=n.nmd(e);var t=n(927).SourceMapConsumer;var o=n(928);var i;try{i=n(896);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(296);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var d=[];var h=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=d.slice(0);var _=h.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){d.length=0}d.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){h.length=0}h.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){d.length=0;h.length=0;d=S.slice(0);h=_.slice(0);v=handlerExec(h);m=handlerExec(d)}},517:(e,r,n)=>{var t=n(297);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(158);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},24:(e,r,n)=>{var t=n(297);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.P=MappingList},299:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(297);var i=n(197);var a=n(517).C;var u=n(818);var s=n(299).g;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){h.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(h,o.compareByOriginalPositions);this.__originalMappings=h};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(818);var o=n(297);var i=n(517).C;var a=n(24).P;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var d=0,h=g.length;d0){if(!o.compareByGeneratedPositionsInflated(c,g[d-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.x=SourceMapGenerator},565:(e,r,n)=>{var t;var o=n(163).x;var i=n(297);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},927:(e,r,n)=>{n(163).x;r.SourceMapConsumer=n(684).SourceMapConsumer;n(565)},896:e=>{"use strict";e.exports=require("fs")},928:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};__webpack_require__(599).install();module.exports=n})(); -------------------------------------------------------------------------------- /docs/assets/parameters.md: -------------------------------------------------------------------------------- 1 | # Parameters 2 | 3 | Given the highly customizable nature of the `command` Action, users may often find that they need to pass in a number of parameters into subsequent steps during their workflows. This Action provides a way to pass in parameters to your command without any required structure or format. 4 | 5 | > All examples will use `.restart` as the example command 6 | 7 | ## Example 8 | 9 | Here are a few examples of how to pass in parameters to the `.restart` command and why they might be used. 10 | 11 | ### Example 1 12 | 13 | **Command**: 14 | 15 | ```text 16 | .restart | LOG_LEVEL=debug CPU_CORES=4 17 | ``` 18 | 19 | **Outputs**: `params` = `LOG_LEVEL=debug CPU_CORES=4` 20 | 21 | **Why**: A user might need to restart a VM and tell subsequent workflow steps to use a `LOG_LEVEL` of `debug` and during the restart we should use `CPU_CORES` of `4` 22 | 23 | ### Example 2 24 | 25 | **Command**: 26 | 27 | ```text 28 | .restart | server1 server2 server3 29 | ``` 30 | 31 | **Outputs**: `params` = `server1 server2 server3` 32 | 33 | **Why**: This example shows that the `params` output is just a string that can be literally anything your heart desires. It is up to the user to parse the string and use it in subsequent steps. 34 | 35 | ## Parameter Separator 36 | 37 | The `param_separator` input defaults to `|` and will collect any text that is provided after this character and save it as a GitHub Actions output called `params`. This output can then be used in subsequent steps. 38 | 39 | This value can be configured to be any character (or string) that you want. 40 | 41 | ## Parameter Output 42 | 43 | The `params` output can be accessed just like any other output from the `command` Action. Here is a quick example: 44 | 45 | ```yaml 46 | - name: command 47 | id: command 48 | uses: github/command@vX.X.X 49 | with: 50 | command: .restart 51 | param_separator: "|" 52 | 53 | - name: example 54 | if: steps.command.outputs.continue == 'true' 55 | run: | 56 | echo "params: ${{ steps.command.outputs.params }}" 57 | ``` 58 | 59 | If a user were to comment `.restart | superServer1` on a pull request, the result of this Action workflow would be the `command` step succeeding, setting a `continue = "true"` output, and then the `example` step running which would echo `params: superServer1` to the console's stdout. 60 | -------------------------------------------------------------------------------- /docs/assets/ship-it.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/command/edb02d330f2b853f47f73fd2e9e5f94d4085fcbb/docs/assets/ship-it.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "command", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "IssueOps commands in GitHub Actions", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "format": "prettier --write '**/*.js'", 9 | "format-check": "prettier --check '**/*.js'", 10 | "lint": "eslint src/**/*.js", 11 | "package": "ncc build src/main.js -o dist --source-map --license licenses.txt", 12 | "test": "(COMMAND_ACTION_JEST_TEST=true jest && make-coverage-badge --output-path ./badges/coverage.svg) || make-coverage-badge --output-path ./badges/coverage.svg", 13 | "ci-test": "COMMAND_ACTION_JEST_TEST=true jest", 14 | "all": "npm run format && npm run lint && npm run package", 15 | "bundle": "npm run format && npm run package" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/github/command.git" 20 | }, 21 | "keywords": [ 22 | "actions", 23 | "issueops", 24 | "github" 25 | ], 26 | "author": "Grant Birkinbine", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@actions/core": "^1.11.1", 30 | "@actions/github": "^6.0.0", 31 | "@octokit/plugin-retry": "^6.0.1", 32 | "@octokit/request": "^9.2.3", 33 | "dedent-js": "^1.0.1", 34 | "github-username-regex-js": "^1.0.0", 35 | "@octokit/rest": "^21.1.0" 36 | }, 37 | "jest": { 38 | "coverageReporters": [ 39 | "json-summary", 40 | "text", 41 | "lcov" 42 | ], 43 | "collectCoverage": true, 44 | "collectCoverageFrom": [ 45 | "./src/**" 46 | ], 47 | "coverageThreshold": { 48 | "global": { 49 | "lines": 100 50 | } 51 | } 52 | }, 53 | "devDependencies": { 54 | "@babel/plugin-transform-modules-commonjs": "^7.27.1", 55 | "@babel/preset-env": "^7.27.1", 56 | "@types/node": "^22.15.3", 57 | "@vercel/ncc": "^0.38.3", 58 | "@babel/core": "^7.27.1", 59 | "babel-jest": "^29.7.0", 60 | "eslint": "^8.57.0", 61 | "eslint-plugin-jest": "^28.11.0", 62 | "jest": "^29.7.0", 63 | "js-yaml": "^4.1.0", 64 | "make-coverage-badge": "^1.2.0", 65 | "prettier": "^3.5.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: 4 | # script/release 5 | 6 | # COLORS 7 | OFF='\033[0m' 8 | RED='\033[0;31m' 9 | GREEN='\033[0;32m' 10 | BLUE='\033[0;34m' 11 | 12 | # Read the version from src/version.js 13 | version_file="src/version.js" 14 | if [[ ! -f $version_file ]]; then 15 | echo -e "${RED}ERROR${OFF} - Version file not found: $version_file" 16 | exit 1 17 | fi 18 | 19 | version_line=$(grep 'export const VERSION' $version_file) 20 | if [[ -z $version_line ]]; then 21 | echo -e "${RED}ERROR${OFF} - Version line not found in: $version_file" 22 | exit 1 23 | fi 24 | 25 | # Extract the version value 26 | new_tag=$(echo $version_line | sed -E "s/export const VERSION = '([^']+)'/\1/") 27 | if [[ -z $new_tag ]]; then 28 | echo -e "${RED}ERROR${OFF} - Failed to extract version from: $version_file" 29 | exit 1 30 | fi 31 | 32 | # Validate the version tag format 33 | tag_regex='^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$' 34 | echo "$new_tag" | grep -E "$tag_regex" > /dev/null 35 | 36 | if [[ $? -ne 0 ]]; then 37 | echo -e "${RED}ERROR${OFF} - Tag: $new_tag is not valid. Please use vX.X.X or vX.X.X-rc.X format." 38 | exit 1 39 | fi 40 | 41 | # Get the latest tag 42 | latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1)) 43 | echo -e "The latest release tag is: ${BLUE}${latest_tag}${OFF}" 44 | 45 | # Confirm the new tag 46 | read -p "New Release Tag (press ENTER for default: ${new_tag}): " input_tag 47 | new_tag=${input_tag:-$new_tag} 48 | 49 | # Tag the new release 50 | git tag -a $new_tag -m "$new_tag Release" 51 | if [[ $? -ne 0 ]]; then 52 | echo -e "${RED}ERROR${OFF} - Failed to create tag: $new_tag" 53 | exit 1 54 | fi 55 | 56 | echo -e "${GREEN}OK${OFF} - Tagged: $new_tag" 57 | 58 | # Push the tags to remote 59 | git push --tags 60 | if [[ $? -ne 0 ]]; then 61 | echo -e "${RED}ERROR${OFF} - Failed to push tags to remote" 62 | exit 1 63 | fi 64 | 65 | echo -e "${GREEN}OK${OFF} - Tags pushed to remote!" 66 | echo -e "${GREEN}DONE${OFF}" 67 | -------------------------------------------------------------------------------- /src/functions/action-status.js: -------------------------------------------------------------------------------- 1 | // Default failure reaction 2 | const thumbsDown = '-1' 3 | // Default success reaction 4 | const rocket = 'rocket' 5 | // Alt success reaction 6 | const thumbsUp = '+1' 7 | 8 | // Helper function to add a status update for the action that is running an IssueOps command 9 | // It also updates the original comment with a reaction depending on the status of the operation 10 | // :param context: The context of the action 11 | // :param octokit: The octokit object 12 | // :param reactionId: The id of the original reaction added to our trigger comment (Integer) 13 | // :param message: The message to be added to the action status (String) 14 | // :param success: Boolean indicating whether the operation was successful (Boolean) 15 | // :param altSuccessReaction: Boolean indicating whether to use the alternate success reaction (Boolean) 16 | // :returns: Nothing 17 | export async function actionStatus( 18 | context, 19 | octokit, 20 | reactionId, 21 | message, 22 | success, 23 | altSuccessReaction 24 | ) { 25 | // check if message is null or empty 26 | if (!message || message.length === 0) { 27 | const log_url = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}` 28 | message = 'Unknown error, [check logs](' + log_url + ') for more details.' 29 | } 30 | 31 | // add a comment to the issue with the message 32 | await octokit.rest.issues.createComment({ 33 | ...context.repo, 34 | issue_number: context.issue.number, 35 | body: message 36 | }) 37 | 38 | // Select the reaction to add to the issue_comment 39 | var reaction 40 | if (success) { 41 | if (altSuccessReaction) { 42 | reaction = thumbsUp 43 | } else { 44 | reaction = rocket 45 | } 46 | } else { 47 | reaction = thumbsDown 48 | } 49 | 50 | // add a reaction to the issue_comment to indicate success or failure 51 | await octokit.rest.reactions.createForIssueComment({ 52 | ...context.repo, 53 | comment_id: context.payload.comment.id, 54 | content: reaction 55 | }) 56 | 57 | // remove the initial reaction on the IssueOp comment that triggered this action 58 | await octokit.rest.reactions.deleteForIssueComment({ 59 | ...context.repo, 60 | comment_id: context.payload.comment.id, 61 | reaction_id: reactionId 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/functions/allowlist.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import githubUsernameRegex from 'github-username-regex-js' 4 | 5 | // Helper function to check if a user exists in an org team 6 | // :param actor: The user to check 7 | // :param orgTeams: An array of org/team names 8 | // :returns: True if the user is in the org team, false otherwise 9 | async function orgTeamCheck(actor, orgTeams) { 10 | // This pat needs org read permissions if you are using org/teams to define allowlist'd users 11 | const allowlistPat = core.getInput('allowlist_pat') 12 | 13 | // If no allowlist_pat is provided, then we cannot check for org team memberships 14 | if (!allowlistPat || allowlistPat.length === 0 || allowlistPat === 'false') { 15 | core.warning( 16 | 'no allowlist_pat provided, skipping allowlist check for org team membership' 17 | ) 18 | return false 19 | } 20 | 21 | // Create a new octokit client with the allowlist_pat 22 | const octokit = github.getOctokit(allowlistPat) 23 | 24 | // Loop through all org/team names 25 | for (const orgTeam of orgTeams) { 26 | // Split the org/team name into org and team 27 | var [org, team] = orgTeam.split('/') 28 | 29 | try { 30 | // Make an API call to get the org id 31 | const orgData = await octokit.rest.orgs.get({ 32 | org: org 33 | }) 34 | const orgId = orgData.data.id 35 | 36 | // Make an API call to get the team id 37 | const teamData = await octokit.rest.teams.getByName({ 38 | org: org, 39 | team_slug: team 40 | }) 41 | const teamId = teamData.data.id 42 | 43 | // This API call checks if the user exists in the team for the given org 44 | const result = await octokit.request( 45 | `GET /organizations/${orgId}/team/${teamId}/members/${actor}` 46 | ) 47 | 48 | // If the status code is a 204, the user is in the team 49 | if (result.status === 204) { 50 | core.debug(`${actor} is in ${orgTeam}`) 51 | return true 52 | // If some other status code occurred, return false and output a warning 53 | } else { 54 | core.warning(`non 204 response from org team check: ${result.status}`) 55 | } 56 | } catch (error) { 57 | // If any of the API calls returns a 404, the user is not in the team 58 | if (error.status === 404) { 59 | core.debug(`${actor} is not a member of the ${orgTeam} team`) 60 | // If some other error occurred, output a warning 61 | } else { 62 | core.warning(`Error checking org team membership: ${error}`) 63 | } 64 | } 65 | } 66 | 67 | // If we get here, the user is not in any of the org teams 68 | return false 69 | } 70 | 71 | // Helper function to check if a user is allowed to run the IssueOps command 72 | // :param context: The GitHub Actions event context 73 | // :returns: true if the user is allowed, false otherwise (Boolean) 74 | export async function isAllowed(context) { 75 | // Get the allowlist string from the action inputs 76 | const allowlist = core.getInput('allowlist') 77 | 78 | core.debug(`raw allowlist value: ${allowlist}`) 79 | 80 | // if the allowlist is set to "false" or it is empty, then all users are allowed 81 | if (!allowlist || allowlist.length === 0 || allowlist === 'false') { 82 | core.debug('no allowlist provided, all users are allowed') 83 | return true 84 | } 85 | 86 | // Sanitized the input to remove any whitespace and split into an array 87 | const allowlistSanitized = allowlist 88 | .split(',') 89 | .map(operator => operator.trim().toLowerCase()) 90 | 91 | // loop through the allowlist 92 | var handles = [] 93 | var orgTeams = [] 94 | allowlistSanitized.forEach(operator => { 95 | // If the item contains a '/', then it is a org/team 96 | if (operator.includes('/')) { 97 | orgTeams.push(operator) 98 | } 99 | // Otherwise, it is a github handle 100 | else { 101 | // Check if the github handle is valid 102 | if (githubUsernameRegex.test(operator)) { 103 | // Add the handle to the list of handles and remove @ from the start of the handle 104 | handles.push(operator.replace('@', '')) 105 | } else { 106 | core.debug( 107 | `${operator} is not a valid GitHub username... skipping allowlist check` 108 | ) 109 | } 110 | } 111 | }) 112 | 113 | // Check if the user is in the operator handle list 114 | if (handles.includes(context.actor.toLowerCase())) { 115 | core.debug( 116 | `${context.actor} is an allowlisted operator via handle reference` 117 | ) 118 | return true 119 | } 120 | 121 | // Check if the user is in the org/team list 122 | if (orgTeams.length > 0) { 123 | const result = await orgTeamCheck(context.actor, orgTeams) 124 | if (result) { 125 | core.debug( 126 | `${context.actor} is an allowlisted operator via org team reference` 127 | ) 128 | return true 129 | } 130 | } 131 | 132 | // If we get here, the user is not an operator 133 | core.debug(`${context.actor} is not an allowed operator for this command`) 134 | return false 135 | } 136 | -------------------------------------------------------------------------------- /src/functions/colors.js: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | highlight: '\u001b[35m', // magenta 3 | info: '\u001b[34m', // blue 4 | success: '\u001b[32m', // green 5 | warning: '\u001b[33m', // yellow 6 | error: '\u001b[31m', // red 7 | reset: '\u001b[0m' // reset 8 | } 9 | -------------------------------------------------------------------------------- /src/functions/context-check.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {stringToArray} from './string-to-array' 3 | 4 | const contextDefaults = ['pull_request', 'issue'] 5 | 6 | // A simple function that checks the event context to make sure it is valid 7 | // :param context: The GitHub Actions event context 8 | // :returns: Map - {valid: true/false, context: 'issue'/'pull_request'} 9 | export async function contextCheck(context) { 10 | core.debug(`checking if the context of '${context.eventName}' is valid`) 11 | 12 | // exit right away if the event isn't a comment of some kind 13 | // IssueOps commands by their very nature are comments 14 | if (context.eventName !== 'issue_comment') { 15 | core.saveState('bypass', 'true') 16 | core.warning( 17 | 'this Action can only be run in the context of an issue_comment' 18 | ) 19 | return {valid: false, context: context.eventName} 20 | } 21 | 22 | // fetch the defined contexts from the Action input 23 | const allowedContexts = await stringToArray( 24 | core.getInput('allowed_contexts', {required: true}) 25 | ) 26 | 27 | // check to see if the allowedContexts variable contains at least one item from the contextDefaults array 28 | // if it does not, log a warning and exit 29 | if (!allowedContexts.some(r => contextDefaults.includes(r))) { 30 | core.warning( 31 | `the 'allowed_contexts' input must contain at least one of the following: ${contextDefaults.join( 32 | ', ' 33 | )}` 34 | ) 35 | return {valid: false, context: context.eventName} 36 | } 37 | 38 | // check if the event is a PR 39 | const isPullRequest = context?.payload?.issue?.pull_request !== undefined 40 | 41 | // if the only allowed context is 'pull_request' check if the context is valid 42 | if (allowedContexts.length === 1 && allowedContexts[0] === 'pull_request') { 43 | // if the context is not from a PR and it is an issue comment, that means it... 44 | // ... came from an issue, so return false 45 | if (!isPullRequest && context.eventName === 'issue_comment') { 46 | core.saveState('bypass', 'true') 47 | core.warning( 48 | 'this Action can only be run in the context of a pull request comment' 49 | ) 50 | return {valid: false, context: context.eventName} 51 | } 52 | 53 | // if the only allowed context is 'issue_comment' check if the context is valid 54 | } else if (allowedContexts.length === 1 && allowedContexts[0] === 'issue') { 55 | // if the context is an issue comment, but that issue comment was on a PR, return false 56 | // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment 57 | if (context.eventName === 'issue_comment' && isPullRequest) { 58 | core.saveState('bypass', 'true') 59 | core.warning( 60 | 'this Action can only be run in the context of an issue comment' 61 | ) 62 | return {valid: false, context: context.eventName} 63 | } 64 | } 65 | 66 | // if we make it here, the context is valid, we just need to figure out if it is a... 67 | // ... PR or an issue comment 68 | var contextType 69 | if (isPullRequest) { 70 | contextType = 'pull_request' 71 | } else { 72 | contextType = 'issue' 73 | } 74 | 75 | return {valid: true, context: contextType} 76 | } 77 | -------------------------------------------------------------------------------- /src/functions/parameters.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {COLORS} from './colors' 3 | 4 | // Helper function that checks and returns params used in a command 5 | // :param body: The body of the comment 6 | // :param param_separator: The separator used to seperate the command from the parameters 7 | // :returns: the parameters used in the command (String) or null if none are used 8 | export async function parameters(body, param_separator = '|') { 9 | // Seperate the issueops command on the 'param_separator' 10 | var paramCheck = body.split(param_separator) 11 | paramCheck.shift() // remove everything before the 'param_separator' 12 | const params = paramCheck.join(param_separator) // join it all back together (in case there is another separator) 13 | // if there is anything after the 'param_separator'; output it, log it, and remove it from the body for env checks 14 | var paramsTrim = null 15 | if (params !== '') { 16 | paramsTrim = params.trim() 17 | core.info( 18 | `🧮 detected parameters in command: ${COLORS.highlight}${paramsTrim}` 19 | ) 20 | core.setOutput('params', paramsTrim) 21 | } else { 22 | core.debug('no parameters detected in command') 23 | core.setOutput('params', '') 24 | } 25 | 26 | return paramsTrim 27 | } 28 | -------------------------------------------------------------------------------- /src/functions/post-reactions.js: -------------------------------------------------------------------------------- 1 | // Helper function for adding reactions to the issue_comment on the 'post' event 2 | // :param octokit: An authenticated octokit client 3 | // :param context: The github context 4 | // :param reaction: The reaction to add to the issue_comment 5 | // :param reaction_id: The reaction_id of the initial reaction on the issue_comment 6 | export async function postReactions(octokit, context, reaction, reaction_id) { 7 | // remove the initial reaction on the IssueOp comment that triggered this action 8 | await octokit.rest.reactions.deleteForIssueComment({ 9 | ...context.repo, 10 | comment_id: context.payload.comment.id, 11 | reaction_id: parseInt(reaction_id) 12 | }) 13 | 14 | // Update the action status to indicate the result of the action as a reaction 15 | // add a reaction to the issue_comment to indicate success or failure 16 | await octokit.rest.reactions.createForIssueComment({ 17 | ...context.repo, 18 | comment_id: context.payload.comment.id, 19 | content: reaction 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/functions/post.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {contextCheck} from './context-check' 3 | import * as github from '@actions/github' 4 | import {context} from '@actions/github' 5 | import {postReactions} from './post-reactions' 6 | import {octokitRetry} from '@octokit/plugin-retry' 7 | import {VERSION} from '../version' 8 | 9 | // Default failure reaction 10 | const thumbsDown = '-1' 11 | // Default success reaction 12 | const thumbsUp = '+1' 13 | 14 | export async function post() { 15 | try { 16 | const reaction_id = core.getState('reaction_id') 17 | const token = core.getState('actionsToken') 18 | const bypass = core.getState('bypass') 19 | const status = core.getInput('status') 20 | const skip_completing = core.getBooleanInput('skip_completing') 21 | 22 | // If bypass is set, exit the workflow 23 | if (bypass === 'true') { 24 | core.warning('bypass set, exiting') 25 | return 26 | } 27 | 28 | // Check the context of the event to ensure it is valid, return if it is not 29 | const contextCheckResults = await contextCheck(context) 30 | if (!contextCheckResults.valid) { 31 | return 32 | } 33 | 34 | // if skip_completing is set, return 35 | if (skip_completing) { 36 | core.info('⏩ skip_completing set, exiting') 37 | return 38 | } 39 | 40 | // Create an octokit client with the retry plugin 41 | const octokit = github.getOctokit(token, { 42 | userAgent: `github/command@${VERSION}`, 43 | additionalPlugins: [octokitRetry] 44 | }) 45 | 46 | // Check the Action status 47 | var success 48 | if (status === 'success') { 49 | success = true 50 | } else { 51 | success = false 52 | } 53 | 54 | // Select the reaction to add to the issue_comment 55 | // If it is a success, use the user defined reaction 56 | // Otherwise, add a thumbs down reaction 57 | var reaction 58 | if (success) { 59 | reaction = core.getInput('success_reaction') || thumbsUp 60 | } else { 61 | reaction = core.getInput('failure_reaction') || thumbsDown 62 | } 63 | 64 | // Update the reactions on the command comment 65 | await postReactions(octokit, context, reaction, reaction_id) 66 | 67 | return 68 | } catch (error) { 69 | core.error(error.stack) 70 | core.setFailed(error.message) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/functions/prechecks.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {validPermissions} from './valid-permissions' 3 | import {isAllowed} from './allowlist' 4 | import {COLORS} from './colors' 5 | 6 | // Runs precheck logic before the IssueOps command can proceed 7 | // :param issue_number: The issue number of the event (String) 8 | // :param allowForks: Boolean which defines whether the Action can run from forks or not 9 | // :param skipCi: Boolean which defines whether CI checks should be skipped or not 10 | // :param skipReviews: Boolean which defines whether PR reviews should be skipped or not 11 | // :param allowDraftPRs: Boolean which defines whether draft PRs should be allowed or not 12 | // :param contextType: The type of context (issue or pull_request) 13 | // :param forkReviewBypass: Boolean which defines whether the Action should bypass the lack of reviews on a fork or not (dangerous) 14 | // :param context: The context of the event 15 | // :param octokit: The octokit client 16 | // :returns: An object that contains the results of the prechecks, message, ref, and status 17 | export async function prechecks( 18 | issue_number, 19 | allowForks, 20 | skipCi, 21 | skipReviews, 22 | allowDraftPRs, 23 | forkReviewBypass, 24 | contextType, 25 | context, 26 | octokit 27 | ) { 28 | // Setup the message variable 29 | var message 30 | 31 | // Check if the user has valid permissions 32 | const validPermissionsRes = await validPermissions(octokit, context) 33 | if (validPermissionsRes !== true) { 34 | return {message: validPermissionsRes, status: false} 35 | } 36 | 37 | // Get allowed operator data 38 | if (!(await isAllowed(context))) { 39 | message = `### ⚠️ Cannot proceed with operation\n\n> User ${context.actor} is not an allowed operator` 40 | return {message: message, status: false} 41 | } 42 | 43 | // if this is an issue comment, we can skip all the logic below here as it... 44 | // ... only applies to pull requests 45 | if (contextType === 'issue') { 46 | message = `✅ operation requested on an ${COLORS.highlight}issue` 47 | core.info(message) 48 | return {message: message, status: true, ref: null, sha: null} 49 | } 50 | 51 | // Get the PR data 52 | const pr = await octokit.rest.pulls.get({ 53 | ...context.repo, 54 | pull_number: context.issue.number 55 | }) 56 | if (pr.status !== 200) { 57 | message = `Could not retrieve PR info: ${pr.status}` 58 | return {message: message, status: false} 59 | } 60 | 61 | // save sha and ref 62 | var sha = pr.data.head.sha 63 | var ref = pr.data.head.ref 64 | 65 | // set an output which is the branch name this PR is targeting to merge into 66 | const baseRef = pr?.data?.base?.ref 67 | core.setOutput('base_ref', baseRef) 68 | core.debug(`base_ref: ${baseRef}`) 69 | 70 | const isFork = pr?.data?.head?.repo?.fork == true 71 | 72 | // Determine whether to use the ref or sha depending on if the PR is from a fork or not 73 | // Note: We should not export fork values if the stable_branch is being used here 74 | if (isFork === true) { 75 | core.info(`🍴 the pull request is a ${COLORS.highlight}fork`) 76 | core.setOutput('fork', 'true') 77 | 78 | // If this Action's inputs have been configured to explicitly prevent forks, exit 79 | if (allowForks === false) { 80 | message = `### ⚠️ Cannot proceed with operation\n\nThis Action has been explicity configured to prevent operations from forks. You can change this via this Action's inputs if needed` 81 | return {message: message, status: false} 82 | } 83 | 84 | // Set some outputs specific to forks 85 | const label = pr.data.head.label 86 | const forkRef = pr.data.head.ref 87 | const forkCheckout = `${label.replace(':', '-')} ${forkRef}` 88 | const forkFullName = pr.data.head.repo.full_name 89 | core.setOutput('fork_ref', forkRef) 90 | core.setOutput('fork_label', label) 91 | core.setOutput('fork_checkout', forkCheckout) 92 | core.setOutput('fork_full_name', forkFullName) 93 | core.debug(`fork_ref: ${forkRef}`) 94 | core.debug(`fork_label: ${label}`) 95 | core.debug(`fork_checkout: ${forkCheckout}`) 96 | core.debug(`fork_full_name: ${forkFullName}`) 97 | 98 | // If this pull request is a fork, use the exact SHA rather than the branch name 99 | ref = pr.data.head.sha 100 | } else { 101 | // If this PR is NOT a fork, we can safely use the branch name 102 | core.setOutput('fork', 'false') 103 | } 104 | 105 | // Check to ensure PR CI checks are passing and the PR has been reviewed 106 | // mergeStateStatus is in the query below but not used at this time 107 | const query = `query($owner:String!, $name:String!, $number:Int!) { 108 | repository(owner:$owner, name:$name) { 109 | pullRequest(number:$number) { 110 | reviewDecision 111 | commits(last: 1) { 112 | nodes { 113 | commit { 114 | checkSuites { 115 | totalCount 116 | } 117 | statusCheckRollup { 118 | state 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | }` 126 | // Note: https://docs.github.com/en/graphql/overview/schema-previews#merge-info-preview (mergeStateStatus) 127 | const variables = { 128 | owner: context.repo.owner, 129 | name: context.repo.repo, 130 | number: parseInt(issue_number), 131 | headers: { 132 | Accept: 'application/vnd.github.merge-info-preview+json' 133 | } 134 | } 135 | // Make the GraphQL query 136 | const result = await octokit.graphql(query, variables) 137 | 138 | // Check the reviewDecision 139 | var reviewDecision 140 | if (skipReviews && isFork === false) { 141 | // If skipReviews is true, we bypass the results the graphql 142 | // This logic is not applied on forks as all PRs from forks must have the required reviews (if requested) 143 | reviewDecision = 'skip_reviews' 144 | } else if (skipReviews && isFork === true && forkReviewBypass === true) { 145 | // If skipReviews is true, we bypass the results the graphql 146 | // This logic is only applied on forks if forkReviewBypass is true 147 | core.warning( 148 | '🚨 bypassing required reviews on a fork - this is potentially dangerous if operating on a PR fork where code is being checked out - this is a good read about the risk https://github.com/github/branch-deploy/pull/331' 149 | ) 150 | reviewDecision = 'skip_reviews' 151 | } else { 152 | // Otherwise, grab the reviewDecision from the GraphQL result 153 | reviewDecision = result.repository.pullRequest.reviewDecision 154 | } 155 | 156 | // If pull request reviews are not required and the PR is from a fork we need to alert the user that this is potentially dangerous 157 | if (reviewDecision === null && isFork === true) { 158 | core.warning( 159 | '🚨 pull request reviews are not enforced by this repository and this operation is being performed on a fork - this operation is dangerous! You should require reviews via branch protection settings (or rulesets) to ensure that the changes being operated on are the changes that you reviewed.' 160 | ) 161 | } 162 | 163 | // Grab the draft status 164 | const isDraft = pr.data.draft 165 | 166 | // log some extra details if the state of the PR is in a 'draft' 167 | if (isDraft && !allowDraftPRs) { 168 | core.warning( 169 | `operation requested on a draft PR when draft PRs are not allowed` 170 | ) 171 | } else if (isDraft && allowDraftPRs) { 172 | core.info( 173 | `📓 operation requested on a ${COLORS.highlight}draft${COLORS.reset} pull request` 174 | ) 175 | } 176 | 177 | // Grab the statusCheckRollup state from the GraphQL result 178 | var commitStatus 179 | try { 180 | // Check to see if skipCi is set 181 | if (skipCi) { 182 | core.info( 183 | `✅ CI checks have been ${COLORS.highlight}disabled${COLORS.reset} for this operation` 184 | ) 185 | commitStatus = 'skip_ci' 186 | } 187 | 188 | // If there are no CI checks defined at all, we can set the commitStatus to null 189 | else if ( 190 | result.repository.pullRequest.commits.nodes[0].commit.checkSuites 191 | .totalCount === 0 192 | ) { 193 | core.info('💡 no CI checks have been defined for this pull request') 194 | commitStatus = null 195 | 196 | // If there are CI checked defined, we need to check for the 'state' of the latest commit 197 | } else { 198 | commitStatus = 199 | result.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup 200 | .state 201 | } 202 | } catch (e) { 203 | core.debug(`could not retrieve PR commit status: ${e} - Handled: OK`) 204 | core.debug('this repo may not have any CI checks defined') 205 | core.debug('skipping commit status check and proceeding...') 206 | commitStatus = null 207 | 208 | // Try to display the raw GraphQL result for debugging purposes 209 | try { 210 | core.debug('raw graphql result for debugging:') 211 | core.debug(result) 212 | } catch { 213 | // istanbul ignore next 214 | core.debug( 215 | 'Could not output raw graphql result for debugging - This is bad' 216 | ) 217 | } 218 | } 219 | 220 | // log values for debugging 221 | core.debug('precheck values for debugging:') 222 | core.debug(`reviewDecision: ${reviewDecision}`) 223 | core.debug(`commitStatus: ${commitStatus}`) 224 | core.debug(`skipCi: ${skipCi}`) 225 | core.debug(`skipReviews: ${skipReviews}`) 226 | core.debug(`allowForks: ${allowForks}`) 227 | 228 | if (isDraft && !allowDraftPRs) { 229 | message = `### ⚠️ Cannot proceed with operation\n\n> Your pull request is in a draft state` 230 | return {message: message, status: false} 231 | 232 | // If the requested operation is taking place on a fork, the context is a PR, and the PR is not approved/reviewed -> do not allow bypassing the... 233 | // lack of reviews. Enforce that ALL PRs originating from forks must have the required reviews. 234 | // Operating on forks without reviews is a security risk and will not be allowed. 235 | // This logic will even apply when the value of skip_reviews is set out of an abundance of caution. 236 | // This logic will also apply even if the requested operator is an admin 237 | } else if ( 238 | isFork === true && 239 | forkReviewBypass === false && 240 | (reviewDecision === 'REVIEW_REQUIRED' || 241 | reviewDecision === 'CHANGES_REQUESTED') 242 | ) { 243 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n\n> All operations from forks **must** have the required reviews before they can proceed. Please ensure this PR has been reviewed and approved before trying again.` 244 | return {message: message, status: false} 245 | } else if (reviewDecision === 'APPROVED' && commitStatus === 'SUCCESS') { 246 | message = '✅ PR is approved and all CI checks passed' 247 | core.info(message) 248 | 249 | // CI checks have not been defined AND required reviewers have not been defined 250 | } else if (reviewDecision === null && commitStatus === null) { 251 | message = 252 | '🎛️ CI checks have not been defined and required reviewers have not been defined' 253 | core.info(message) 254 | 255 | // CI checks are passing and the reviewers is undefined 256 | } else if (reviewDecision === null && commitStatus === 'SUCCESS') { 257 | message = '✅ CI checks are passing and reviews are not defined' 258 | core.info(message) 259 | 260 | // CI checks are passing and reviews are set to be bypassed 261 | } else if (commitStatus === 'SUCCESS' && reviewDecision == 'skip_reviews') { 262 | message = 263 | '✅ CI checks are passing and reviews have been disabled for this operation' 264 | core.info(message) 265 | 266 | // CI checks are set to be bypassed and the pull request is approved 267 | } else if (commitStatus === 'skip_ci' && reviewDecision === 'APPROVED') { 268 | message = 269 | '✅ CI requirements have been disabled for this operation and the PR has been approved' 270 | core.info(message) 271 | 272 | // CI checks are set to be bypassed and reviews are undefined 273 | } else if (commitStatus === 'skip_ci' && reviewDecision === null) { 274 | message = 275 | '✅ CI requirements have been disabled for this operation and reviews are not required' 276 | core.info(message) 277 | 278 | // CI checks are set to be bypassed and the PR has not been reviewed 279 | } else if ( 280 | commitStatus === 'skip_ci' && 281 | (reviewDecision === 'REVIEW_REQUIRED' || 282 | reviewDecision === 'CHANGES_REQUESTED') 283 | ) { 284 | message = `### ⚠️ Cannot proceed with operation\n\n> CI checks are not required for this operation but the PR has not been reviewed` 285 | return {message: message, status: false} 286 | 287 | // If CI checks are set to be bypassed and PR reviews are also set to by bypassed 288 | } else if (commitStatus === 'skip_ci' && reviewDecision === 'skip_reviews') { 289 | message = '✅ CI and PR reviewers are not required for this operation' 290 | core.info(message) 291 | 292 | // If CI is passing but the PR has not been reviewed 293 | } else if ( 294 | (reviewDecision === 'REVIEW_REQUIRED' || 295 | reviewDecision === 'CHANGES_REQUESTED') && 296 | commitStatus === 'SUCCESS' 297 | ) { 298 | message = `### ⚠️ Cannot proceed with operation\n\n> CI checks are passing but the PR has not been reviewed` 299 | return {message: message, status: false} 300 | 301 | // If CI has not been defined but the PR has been approved 302 | } else if (commitStatus === null && reviewDecision === 'APPROVED') { 303 | message = '✅ CI checks have not been defined but the PR has been approved' 304 | core.info(message) 305 | 306 | // If CI is pending and the PR has not been reviewed 307 | } else if ( 308 | (reviewDecision === 'REVIEW_REQUIRED' || 309 | reviewDecision === 'CHANGES_REQUESTED') && 310 | commitStatus === 'PENDING' 311 | ) { 312 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> CI is still in a pending state and reviews are also required for this operation` 313 | return {message: message, status: false} 314 | 315 | // If CI is pending and reviewers have not been defined 316 | } else if (reviewDecision === null && commitStatus === 'PENDING') { 317 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> CI checks must be passing in order to continue` 318 | return {message: message, status: false} 319 | 320 | // If CI is undefined and the PR has not been reviewed 321 | } else if ( 322 | (reviewDecision === 'REVIEW_REQUIRED' || 323 | reviewDecision === 'CHANGES_REQUESTED') && 324 | commitStatus === null 325 | ) { 326 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> CI checks have not been defined but reviews are required for this operation` 327 | return {message: message, status: false} 328 | 329 | // If CI checks are pending and the PR has not been reviewed 330 | } else if ( 331 | (reviewDecision === 'APPROVED' || 332 | reviewDecision === null || 333 | reviewDecision === 'skip_reviews') && 334 | commitStatus === 'PENDING' 335 | ) { 336 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> Reviews are not required for this operation but CI checks must be passing in order to continue` 337 | return {message: message, status: false} 338 | 339 | // If CI is passing but the PR is missing an approval, let the user know 340 | } else if (reviewDecision === 'APPROVED' && commitStatus === 'FAILURE') { 341 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> Your pull request is approved but CI checks are failing` 342 | return {message: message, status: false} 343 | 344 | // If the PR does not require approval but CI is failing 345 | } else if ( 346 | (reviewDecision === null || reviewDecision === 'skip_reviews') && 347 | commitStatus === 'FAILURE' 348 | ) { 349 | message = `### ⚠️ Cannot proceed with operation\n\n- reviewDecision: \`${reviewDecision}\`\n- commitStatus: \`${commitStatus}\`\n\n> Reviews are not required for this operation but CI checks must be passing in order to continue` 350 | return {message: message, status: false} 351 | } 352 | 353 | // Return a success message 354 | return { 355 | message: message, 356 | status: true, 357 | ref: ref, 358 | sha: sha 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/functions/react-emote.js: -------------------------------------------------------------------------------- 1 | // Fixed presets of allowed emote types as defined by GitHub 2 | const presets = [ 3 | '+1', 4 | '-1', 5 | 'laugh', 6 | 'confused', 7 | 'heart', 8 | 'hooray', 9 | 'rocket', 10 | 'eyes' 11 | ] 12 | 13 | // Helper function to add a reaction to an issue_comment 14 | // :param reaction: A string which determines the reaction to use (String) 15 | // :param context: The GitHub Actions event context 16 | // :param octokit: The octokit client 17 | // :returns: The reactRes object which contains the reaction ID among other things. Returns nil if no reaction was specified, or throws an error if it fails 18 | export async function reactEmote(reaction, context, octokit) { 19 | // Get the owner and repo from the context 20 | const {owner, repo} = context.repo 21 | 22 | // If the reaction is not specified, return 23 | if (!reaction || reaction.trim() === '') { 24 | return 25 | } 26 | 27 | // Find the reaction in the list of presets, otherwise throw an error 28 | const preset = presets.find(preset => preset === reaction.trim()) 29 | if (!preset) { 30 | throw new Error(`Reaction "${reaction}" is not a valid preset`) 31 | } 32 | 33 | // Add the reaction to the issue_comment 34 | const reactRes = await octokit.rest.reactions.createForIssueComment({ 35 | owner, 36 | repo, 37 | comment_id: context.payload.comment.id, 38 | content: preset 39 | }) 40 | 41 | // Return the reactRes which contains the id for reference later 42 | return reactRes 43 | } 44 | -------------------------------------------------------------------------------- /src/functions/string-to-array.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | // Helper function to convert a String to an Array specifically in Actions 4 | // :param string: A comma seperated string to convert to an array 5 | // :return Array: The function returns an Array - can be empty 6 | export async function stringToArray(string) { 7 | try { 8 | // If the String is empty, return an empty Array 9 | if (string.trim() === '') { 10 | core.debug( 11 | 'in stringToArray(), an empty String was found so an empty Array was returned' 12 | ) 13 | return [] 14 | } 15 | 16 | // Split up the String on commas, trim each element, and return the Array 17 | const stringArray = string.split(',').map(target => target.trim()) 18 | var results = [] 19 | 20 | // filter out empty items 21 | for (const item of stringArray) { 22 | if (item === '') { 23 | continue 24 | } 25 | results.push(item) 26 | } 27 | 28 | return results 29 | } catch (error) { 30 | /* istanbul ignore next */ 31 | core.error(`failed string for debugging purposes: ${string}`) 32 | /* istanbul ignore next */ 33 | throw new Error(`could not convert String to Array - error: ${error}`) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/functions/trigger-check.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {COLORS} from './colors' 3 | 4 | // A simple function that checks the body of the message against the trigger 5 | // :param body: The content body of the message being checked (String) 6 | // :param trigger: The "trigger" phrase which is searched for in the body of the message 7 | // :returns: true if a message activates the trigger, false otherwise 8 | export async function triggerCheck(body, trigger) { 9 | // Set the output of the comment body for later use with other actions 10 | core.setOutput('comment_body', body) 11 | 12 | // If the trigger is not activated, set the output to false and return with false 13 | if (!body.startsWith(trigger)) { 14 | core.debug( 15 | `trigger ${COLORS.highlight}${trigger}${COLORS.reset} not found in the comment body` 16 | ) 17 | return false 18 | } 19 | 20 | return true 21 | } 22 | -------------------------------------------------------------------------------- /src/functions/valid-permissions.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {stringToArray} from './string-to-array' 3 | 4 | // Helper function to check if an actor has permissions to use this Action in a given repository 5 | // :param octokit: The octokit client 6 | // :param context: The GitHub Actions event context 7 | // :returns: An error string if the actor doesn't have permissions, otherwise true 8 | export async function validPermissions(octokit, context) { 9 | // fetch the defined permissions from the Action input 10 | const validPermissionsArray = await stringToArray( 11 | core.getInput('permissions', {required: true}) 12 | ) 13 | 14 | core.setOutput('actor', context.actor) 15 | 16 | // Get the permissions of the user who made the comment 17 | const permissionRes = await octokit.rest.repos.getCollaboratorPermissionLevel( 18 | { 19 | ...context.repo, 20 | username: context.actor 21 | } 22 | ) 23 | 24 | // Check permission API call status code 25 | if (permissionRes.status !== 200) { 26 | return `Permission check returns non-200 status: ${permissionRes.status}` 27 | } 28 | 29 | // Check to ensure the user has at least write permission on the repo 30 | const actorPermission = permissionRes.data.permission 31 | if (!validPermissionsArray.includes(actorPermission)) { 32 | return `👋 __${ 33 | context.actor 34 | }__, seems as if you have not ${validPermissionsArray.join( 35 | '/' 36 | )} permissions in this repo, permissions: ${actorPermission}` 37 | } 38 | 39 | // Return true if the user has permissions 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import {context} from '@actions/github' 4 | import {octokitRetry} from '@octokit/plugin-retry' 5 | import {triggerCheck} from './functions/trigger-check' 6 | import {contextCheck} from './functions/context-check' 7 | import {reactEmote} from './functions/react-emote' 8 | import {parameters} from './functions/parameters' 9 | import {actionStatus} from './functions/action-status' 10 | import {prechecks} from './functions/prechecks' 11 | import {post} from './functions/post' 12 | import {COLORS} from './functions/colors' 13 | import {VERSION} from './version' 14 | 15 | // :returns: 'success', 'failure', 'safe-exit' or raises an error 16 | export async function run() { 17 | try { 18 | core.info(`🛸 github/command ${COLORS.info}${VERSION}${COLORS.reset}`) 19 | // Get the inputs for the 'command' Action 20 | const command = core.getInput('command', {required: true}) 21 | const token = core.getInput('github_token', {required: true}) 22 | const param_separator = core.getInput('param_separator') 23 | const reaction = core.getInput('reaction') 24 | const allowForks = core.getBooleanInput('allow_forks') 25 | const skipCi = core.getBooleanInput('skip_ci') 26 | const allow_drafts = core.getBooleanInput('allow_drafts') 27 | const skipReviews = core.getBooleanInput('skip_reviews') 28 | const forkReviewBypass = core.getBooleanInput('fork_review_bypass') 29 | 30 | // create an octokit client with the retry plugin 31 | const octokit = github.getOctokit(token, { 32 | userAgent: `github/command@${VERSION}`, 33 | additionalPlugins: [octokitRetry] 34 | }) 35 | 36 | // set the state so that the post run logic will trigger 37 | core.saveState('isPost', 'true') 38 | core.saveState('actionsToken', token) 39 | 40 | // get the body of the IssueOps command 41 | const body = context.payload.comment.body.trim() 42 | 43 | // check the context of the event to ensure it is valid, return if it is not 44 | const contextCheckResults = await contextCheck(context) 45 | if (!contextCheckResults.valid) { 46 | return 'safe-exit' 47 | } 48 | 49 | // get variables from the event context 50 | const issue_number = context.payload.issue.number 51 | core.setOutput('issue_number', issue_number) 52 | 53 | // check if the comment contains the command 54 | if (!(await triggerCheck(body, command))) { 55 | // if the comment does not contain the command, exit 56 | core.saveState('bypass', 'true') 57 | core.setOutput('triggered', 'false') 58 | core.info('⛔ no command detected in comment') 59 | return 'safe-exit' 60 | } 61 | 62 | // if we made it this far, the action has been triggered in one manner or another 63 | core.setOutput('triggered', 'true') 64 | 65 | // add the reaction to the issue_comment which triggered the Action 66 | const reactRes = await reactEmote(reaction, context, octokit) 67 | core.setOutput('comment_id', context.payload.comment.id) 68 | core.saveState('comment_id', context.payload.comment.id) 69 | core.setOutput('initial_reaction_id', reactRes.data.id) 70 | core.saveState('reaction_id', reactRes.data.id) 71 | core.setOutput('actor', context.payload.comment.user.login) 72 | 73 | // check if any parameters were used in the command 74 | // note: this function does have a return, but we don't care about it... 75 | // ... we just care that it sets the output variables 76 | await parameters( 77 | body, // comment body 78 | param_separator // param_separator action input 79 | ) 80 | 81 | // execute prechecks to ensure the Action can proceed 82 | const precheckResults = await prechecks( 83 | issue_number, 84 | allowForks, 85 | skipCi, 86 | skipReviews, 87 | allow_drafts, 88 | forkReviewBypass, 89 | contextCheckResults.context, 90 | context, 91 | octokit 92 | ) 93 | core.setOutput('ref', precheckResults.ref) 94 | core.setOutput('sha', precheckResults.sha) 95 | 96 | // if the prechecks failed, run the actionStatus function and return 97 | // note: if we don't pass in the 'success' bool, actionStatus will default to failure mode 98 | if (!precheckResults.status) { 99 | await actionStatus( 100 | context, 101 | octokit, 102 | reactRes.data.id, // original reaction id 103 | precheckResults.message // message 104 | ) 105 | // set the bypass state to true so that the post run logic will not run 106 | core.saveState('bypass', 'true') 107 | core.setFailed(precheckResults.message) 108 | return 'failure' 109 | } 110 | 111 | core.setOutput('continue', 'true') 112 | core.info(`🚀 ${COLORS.success}success!`) 113 | return 'success' 114 | } catch (error) { 115 | /* istanbul ignore next */ 116 | core.saveState('bypass', 'true') 117 | /* istanbul ignore next */ 118 | core.error(error.stack) 119 | /* istanbul ignore next */ 120 | core.setFailed(error.message) 121 | } 122 | } 123 | 124 | /* istanbul ignore next */ 125 | if (core.getState('isPost') === 'true') { 126 | post() 127 | } else { 128 | if ( 129 | process.env.CI === 'true' && 130 | process.env.COMMAND_ACTION_JEST_TEST !== 'true' 131 | ) { 132 | run() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | // The version of the command Action 2 | // Acceptable version formats: 3 | // - v1.0.0 4 | // - v4.5.1 5 | // - v10.123.44 6 | // - v1.1.1-rc.1 7 | // - etc 8 | 9 | export const VERSION = 'v2.0.1' 10 | --------------------------------------------------------------------------------