├── .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 | [](https://github.com/github/command/actions/workflows/codeql-analysis.yml) [](https://github.com/github/command/actions/workflows/test.yml) [](https://github.com/github/command/actions/workflows/package-check.yml) [](https://github.com/github/command/actions/workflows/lint.yml) [](https://github.com/github/command/actions/workflows/actions-config-validation.yml) [](./badges/coverage.svg)
4 |
5 | IssueOps commands in GitHub Actions!
6 |
7 | > _Like ChatOps but for GitHub Issues and Pull Requests_ 🤩
8 |
9 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------