├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── publish-immutable-action.yml │ ├── release.yml │ ├── test.yml │ └── update-permission-inputs.yml ├── .gitignore ├── .node-version ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── dist ├── main.cjs └── post.cjs ├── lib ├── get-permissions-from-inputs.js ├── main.js ├── post.js └── request.js ├── main.js ├── package-lock.json ├── package.json ├── post.js ├── scripts ├── generated │ └── app-permissions.json └── update-permission-inputs.js └── tests ├── README.md ├── action-deprecated-inputs.test.js ├── index.js ├── main-custom-github-api-url.test.js ├── main-missing-owner.test.js ├── main-missing-repository.test.js ├── main-private-key-with-escaped-newlines.test.js ├── main-repo-skew.test.js ├── main-token-get-owner-set-fail-response.test.js ├── main-token-get-owner-set-repo-fail-response.test.js ├── main-token-get-owner-set-repo-set-to-many-newline.test.js ├── main-token-get-owner-set-repo-set-to-many.test.js ├── main-token-get-owner-set-repo-set-to-one.test.js ├── main-token-get-owner-set-repo-unset.test.js ├── main-token-get-owner-unset-repo-set.test.js ├── main-token-get-owner-unset-repo-unset.test.js ├── main-token-permissions-set.test.js ├── main.js ├── post-revoke-token-fail-response.test.js ├── post-token-expired.test.js ├── post-token-set.test.js ├── post-token-skipped.test.js ├── post-token-unset.test.js └── snapshots ├── index.js.md └── index.js.snap /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @actions/create-github-app-token-maintainers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | groups: 8 | production-dependencies: 9 | dependency-type: 'production' 10 | update-types: 11 | - minor 12 | - patch 13 | development-dependencies: 14 | dependency-type: 'development' 15 | update-types: 16 | - minor 17 | - patch 18 | commit-message: 19 | prefix: 'fix' 20 | prefix-development: 'build' 21 | include: 'scope' 22 | - package-ecosystem: 'github-actions' 23 | directory: '/' 24 | schedule: 25 | interval: 'monthly' 26 | groups: 27 | github-actions: 28 | update-types: 29 | - minor 30 | - patch 31 | -------------------------------------------------------------------------------- /.github/workflows/publish-immutable-action.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Immutable Action' 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | packages: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Publish Immutable Action 17 | uses: actions/publish-immutable-action@v0.0.4 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | issues: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release: 15 | name: release 16 | runs-on: ubuntu-latest 17 | steps: 18 | # build local version to create token 19 | - uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version-file: .node-version 26 | cache: 'npm' 27 | 28 | - run: npm ci 29 | - run: npm run build 30 | - uses: ./ 31 | id: app-token 32 | with: 33 | app-id: ${{ vars.RELEASER_APP_ID }} 34 | private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} 35 | # install release dependencies and release 36 | - run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag 37 | - run: npx semantic-release --debug 38 | env: 39 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | integration: 19 | name: Integration 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version-file: .node-version 27 | cache: 'npm' 28 | 29 | - run: npm ci 30 | - run: npm test 31 | 32 | end-to-end: 33 | name: End-to-End 34 | runs-on: ubuntu-latest 35 | # do not run from forks, as forks don’t have access to repository secrets 36 | if: github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 20 42 | cache: "npm" 43 | - run: npm ci 44 | - run: npm run build 45 | - uses: ./ # Uses the action in the root directory 46 | id: test 47 | with: 48 | app-id: ${{ vars.TEST_APP_ID }} 49 | private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }} 50 | - uses: octokit/request-action@v2.x 51 | id: get-repository 52 | env: 53 | GITHUB_TOKEN: ${{ steps.test.outputs.token }} 54 | with: 55 | route: GET /installation/repositories 56 | - run: echo '${{ steps.get-repository.outputs.data }}' 57 | -------------------------------------------------------------------------------- /.github/workflows/update-permission-inputs.yml: -------------------------------------------------------------------------------- 1 | name: Update Permission Inputs 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'package.json' 7 | - 'package-lock.json' 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | update-permission-inputs: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version-file: .node-version 25 | cache: 'npm' 26 | - name: Install dependencies 27 | run: npm ci 28 | - name: Run permission inputs update script 29 | run: node scripts/update-permission-inputs.js 30 | - name: Commit changes 31 | uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 32 | with: 33 | commit_message: 'feat: update permission inputs' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | coverage 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Initial setup 4 | 5 | ```console 6 | npm install 7 | ``` 8 | 9 | Run tests locally 10 | 11 | ```console 12 | npm test 13 | ``` 14 | 15 | Learn more about how the tests work in [tests/README.md](tests/README.md). 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Gregor Martynus 4 | Copyright (c) 2023 Parker Brown 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create GitHub App Token 2 | 3 | [![test](https://github.com/actions/create-github-app-token/actions/workflows/test.yml/badge.svg)](https://github.com/actions/create-github-app-token/actions/workflows/test.yml) 4 | 5 | GitHub Action for creating a GitHub App installation access token. 6 | 7 | ## Usage 8 | 9 | In order to use this action, you need to: 10 | 11 | 1. [Register new GitHub App](https://docs.github.com/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app). 12 | 2. [Store the App's ID or Client ID in your repository environment variables](https://docs.github.com/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) (example: `APP_ID`). 13 | 3. [Store the App's private key in your repository secrets](https://docs.github.com/actions/security-guides/encrypted-secrets?tool=webui#creating-encrypted-secrets-for-a-repository) (example: `PRIVATE_KEY`). 14 | 15 | > [!IMPORTANT] 16 | > An installation access token expires after 1 hour. Please [see this comment](https://github.com/actions/create-github-app-token/issues/121#issuecomment-2043214796) for alternative approaches if you have long-running processes. 17 | 18 | ### Create a token for the current repository 19 | 20 | ```yaml 21 | name: Run tests on staging 22 | on: 23 | push: 24 | branches: 25 | - main 26 | 27 | jobs: 28 | hello-world: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/create-github-app-token@v2 32 | id: app-token 33 | with: 34 | app-id: ${{ vars.APP_ID }} 35 | private-key: ${{ secrets.PRIVATE_KEY }} 36 | - uses: ./actions/staging-tests 37 | with: 38 | token: ${{ steps.app-token.outputs.token }} 39 | ``` 40 | 41 | ### Use app token with `actions/checkout` 42 | 43 | ```yaml 44 | on: [pull_request] 45 | 46 | jobs: 47 | auto-format: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/create-github-app-token@v2 51 | id: app-token 52 | with: 53 | # required 54 | app-id: ${{ vars.APP_ID }} 55 | private-key: ${{ secrets.PRIVATE_KEY }} 56 | - uses: actions/checkout@v4 57 | with: 58 | token: ${{ steps.app-token.outputs.token }} 59 | ref: ${{ github.head_ref }} 60 | # Make sure the value of GITHUB_TOKEN will not be persisted in repo's config 61 | persist-credentials: false 62 | - uses: creyD/prettier_action@v4.3 63 | with: 64 | github_token: ${{ steps.app-token.outputs.token }} 65 | ``` 66 | 67 | ### Create a git committer string for an app installation 68 | 69 | ```yaml 70 | on: [pull_request] 71 | 72 | jobs: 73 | auto-format: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/create-github-app-token@v2 77 | id: app-token 78 | with: 79 | # required 80 | app-id: ${{ vars.APP_ID }} 81 | private-key: ${{ secrets.PRIVATE_KEY }} 82 | - name: Get GitHub App User ID 83 | id: get-user-id 84 | run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" 85 | env: 86 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 87 | - id: committer 88 | run: echo "string=${{ steps.app-token.outputs.app-slug }}[bot] <${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>" >> "$GITHUB_OUTPUT" 89 | - run: echo "committer string is ${{ steps.committer.outputs.string }}" 90 | ``` 91 | 92 | ### Configure git CLI for an app's bot user 93 | 94 | ```yaml 95 | on: [pull_request] 96 | 97 | jobs: 98 | auto-format: 99 | runs-on: ubuntu-latest 100 | steps: 101 | - uses: actions/create-github-app-token@v2 102 | id: app-token 103 | with: 104 | # required 105 | app-id: ${{ vars.APP_ID }} 106 | private-key: ${{ secrets.PRIVATE_KEY }} 107 | - name: Get GitHub App User ID 108 | id: get-user-id 109 | run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" 110 | env: 111 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 112 | - run: | 113 | git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' 114 | git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com' 115 | # git commands like commit work using the bot user 116 | - run: | 117 | git add . 118 | git commit -m "Auto-generated changes" 119 | git push 120 | ``` 121 | 122 | > [!TIP] 123 | > The `` is the numeric user ID of the app's bot user, which can be found under `https://api.github.com/users/%5Bbot%5D`. 124 | > 125 | > For example, we can check at `https://api.github.com/users/dependabot[bot]` to see the user ID of Dependabot is 49699333. 126 | > 127 | > Alternatively, you can use the [octokit/request-action](https://github.com/octokit/request-action) to get the ID. 128 | 129 | ### Create a token for all repositories in the current owner's installation 130 | 131 | ```yaml 132 | on: [workflow_dispatch] 133 | 134 | jobs: 135 | hello-world: 136 | runs-on: ubuntu-latest 137 | steps: 138 | - uses: actions/create-github-app-token@v2 139 | id: app-token 140 | with: 141 | app-id: ${{ vars.APP_ID }} 142 | private-key: ${{ secrets.PRIVATE_KEY }} 143 | owner: ${{ github.repository_owner }} 144 | - uses: peter-evans/create-or-update-comment@v3 145 | with: 146 | token: ${{ steps.app-token.outputs.token }} 147 | issue-number: ${{ github.event.issue.number }} 148 | body: "Hello, World!" 149 | ``` 150 | 151 | ### Create a token for multiple repositories in the current owner's installation 152 | 153 | ```yaml 154 | on: [issues] 155 | 156 | jobs: 157 | hello-world: 158 | runs-on: ubuntu-latest 159 | steps: 160 | - uses: actions/create-github-app-token@v2 161 | id: app-token 162 | with: 163 | app-id: ${{ vars.APP_ID }} 164 | private-key: ${{ secrets.PRIVATE_KEY }} 165 | owner: ${{ github.repository_owner }} 166 | repositories: | 167 | repo1 168 | repo2 169 | - uses: peter-evans/create-or-update-comment@v3 170 | with: 171 | token: ${{ steps.app-token.outputs.token }} 172 | issue-number: ${{ github.event.issue.number }} 173 | body: "Hello, World!" 174 | ``` 175 | 176 | ### Create a token for all repositories in another owner's installation 177 | 178 | ```yaml 179 | on: [issues] 180 | 181 | jobs: 182 | hello-world: 183 | runs-on: ubuntu-latest 184 | steps: 185 | - uses: actions/create-github-app-token@v2 186 | id: app-token 187 | with: 188 | app-id: ${{ vars.APP_ID }} 189 | private-key: ${{ secrets.PRIVATE_KEY }} 190 | owner: another-owner 191 | - uses: peter-evans/create-or-update-comment@v3 192 | with: 193 | token: ${{ steps.app-token.outputs.token }} 194 | issue-number: ${{ github.event.issue.number }} 195 | body: "Hello, World!" 196 | ``` 197 | 198 | ### Create a token with specific permissions 199 | 200 | > [!NOTE] 201 | > Selected permissions must be granted to the installation of the specified app and repository owner. Setting a permission that the installation does not have will result in an error. 202 | 203 | ```yaml 204 | on: [issues] 205 | 206 | jobs: 207 | hello-world: 208 | runs-on: ubuntu-latest 209 | steps: 210 | - uses: actions/create-github-app-token@v2 211 | id: app-token 212 | with: 213 | app-id: ${{ vars.APP_ID }} 214 | private-key: ${{ secrets.PRIVATE_KEY }} 215 | owner: ${{ github.repository_owner }} 216 | permission-issues: write 217 | - uses: peter-evans/create-or-update-comment@v3 218 | with: 219 | token: ${{ steps.app-token.outputs.token }} 220 | issue-number: ${{ github.event.issue.number }} 221 | body: "Hello, World!" 222 | ``` 223 | 224 | ### Create tokens for multiple user or organization accounts 225 | 226 | You can use a matrix strategy to create tokens for multiple user or organization accounts. 227 | 228 | > [!NOTE] 229 | > See [this documentation](https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings) for information on using multiline strings in workflows. 230 | 231 | ```yaml 232 | on: [workflow_dispatch] 233 | 234 | jobs: 235 | set-matrix: 236 | runs-on: ubuntu-latest 237 | outputs: 238 | matrix: ${{ steps.set.outputs.matrix }} 239 | steps: 240 | - id: set 241 | run: echo 'matrix=[{"owner":"owner1"},{"owner":"owner2","repos":["repo1"]}]' >>"$GITHUB_OUTPUT" 242 | 243 | use-matrix: 244 | name: "@${{ matrix.owners-and-repos.owner }} installation" 245 | needs: [set-matrix] 246 | runs-on: ubuntu-latest 247 | strategy: 248 | matrix: 249 | owners-and-repos: ${{ fromJson(needs.set-matrix.outputs.matrix) }} 250 | 251 | steps: 252 | - uses: actions/create-github-app-token@v2 253 | id: app-token 254 | with: 255 | app-id: ${{ vars.APP_ID }} 256 | private-key: ${{ secrets.PRIVATE_KEY }} 257 | owner: ${{ matrix.owners-and-repos.owner }} 258 | repositories: ${{ join(matrix.owners-and-repos.repos) }} 259 | - uses: octokit/request-action@v2.x 260 | id: get-installation-repositories 261 | with: 262 | route: GET /installation/repositories 263 | env: 264 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 265 | - run: echo "$MULTILINE_JSON_STRING" 266 | env: 267 | MULTILINE_JSON_STRING: ${{ steps.get-installation-repositories.outputs.data }} 268 | ``` 269 | 270 | ### Run the workflow in a github.com repository against an organization in GitHub Enterprise Server 271 | 272 | ```yaml 273 | on: [push] 274 | 275 | jobs: 276 | create_issue: 277 | runs-on: self-hosted 278 | 279 | steps: 280 | - name: Create GitHub App token 281 | id: create_token 282 | uses: actions/create-github-app-token@v2 283 | with: 284 | app-id: ${{ vars.GHES_APP_ID }} 285 | private-key: ${{ secrets.GHES_APP_PRIVATE_KEY }} 286 | owner: ${{ vars.GHES_INSTALLATION_ORG }} 287 | github-api-url: ${{ vars.GITHUB_API_URL }} 288 | 289 | - name: Create issue 290 | uses: octokit/request-action@v2.x 291 | with: 292 | route: POST /repos/${{ github.repository }}/issues 293 | title: "New issue from workflow" 294 | body: "This is a new issue created from a GitHub Action workflow." 295 | env: 296 | GITHUB_TOKEN: ${{ steps.create_token.outputs.token }} 297 | ``` 298 | 299 | ## Inputs 300 | 301 | ### `app-id` 302 | 303 | **Required:** GitHub App ID. 304 | 305 | ### `private-key` 306 | 307 | **Required:** GitHub App private key. Escaped newlines (`\\n`) will be automatically replaced with actual newlines. 308 | 309 | Some other actions may require the private key to be Base64 encoded. To avoid recreating a new secret, it can be decoded on the fly, but it needs to be managed securely. Here is an example of how this can be achieved: 310 | 311 | ```yaml 312 | steps: 313 | - name: Decode the GitHub App Private Key 314 | id: decode 315 | run: | 316 | private_key=$(echo "${{ secrets.PRIVATE_KEY }}" | base64 -d | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null 317 | echo "::add-mask::$private_key" 318 | echo "private-key=$private_key" >> "$GITHUB_OUTPUT" 319 | - name: Generate GitHub App Token 320 | id: app-token 321 | uses: actions/create-github-app-token@v2 322 | with: 323 | app-id: ${{ vars.APP_ID }} 324 | private-key: ${{ steps.decode.outputs.private-key }} 325 | ``` 326 | 327 | ### `owner` 328 | 329 | **Optional:** The owner of the GitHub App installation. If empty, defaults to the current repository owner. 330 | 331 | ### `repositories` 332 | 333 | **Optional:** Comma or newline-separated list of repositories to grant access to. 334 | 335 | > [!NOTE] 336 | > If `owner` is set and `repositories` is empty, access will be scoped to all repositories in the provided repository owner's installation. If `owner` and `repositories` are empty, access will be scoped to only the current repository. 337 | 338 | ### `permission-` 339 | 340 | **Optional:** The permissions to grant to the token. By default, the token inherits all of the installation's permissions. We recommend to explicitly list the permissions that are required for a use case. This follows GitHub's own recommendation to [control permissions of `GITHUB_TOKEN` in workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token). The documentation also lists all available permissions, just prefix the permission key with `permission-` (e.g., `pull-requests` → `permission-pull-requests`). 341 | 342 | The reason we define one `permision-` input per permission is to benefit from type intelligence and input validation built into GitHub's action runner. 343 | 344 | ### `skip-token-revoke` 345 | 346 | **Optional:** If true, the token will not be revoked when the current job is complete. 347 | 348 | ### `github-api-url` 349 | 350 | **Optional:** The URL of the GitHub REST API. Defaults to the URL of the GitHub Rest API where the workflow is run from. 351 | 352 | ## Outputs 353 | 354 | ### `token` 355 | 356 | GitHub App installation access token. 357 | 358 | ### `installation-id` 359 | 360 | GitHub App installation ID. 361 | 362 | ### `app-slug` 363 | 364 | GitHub App slug. 365 | 366 | ## How it works 367 | 368 | The action creates an installation access token using [the `POST /app/installations/{installation_id}/access_tokens` endpoint](https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app). By default, 369 | 370 | 1. The token is scoped to the current repository or `repositories` if set. 371 | 2. The token inherits all the installation's permissions. 372 | 3. The token is set as output `token` which can be used in subsequent steps. 373 | 4. Unless the `skip-token-revoke` input is set to true, the token is revoked in the `post` step of the action, which means it cannot be passed to another job. 374 | 5. The token is masked, it cannot be logged accidentally. 375 | 376 | > [!NOTE] 377 | > Installation permissions can differ from the app's permissions they belong to. Installation permissions are set when an app is installed on an account. When the app adds more permissions after the installation, an account administrator will have to approve the new permissions before they are set on the installation. 378 | 379 | ## Contributing 380 | 381 | [CONTRIBUTING.md](CONTRIBUTING.md) 382 | 383 | ## License 384 | 385 | [MIT](LICENSE) 386 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Create GitHub App Token" 2 | description: "GitHub Action for creating a GitHub App installation access token" 3 | author: "Gregor Martynus and Parker Brown" 4 | branding: 5 | icon: "lock" 6 | color: "gray-dark" 7 | inputs: 8 | app-id: 9 | description: "GitHub App ID" 10 | required: true 11 | private-key: 12 | description: "GitHub App private key" 13 | required: true 14 | owner: 15 | description: "The owner of the GitHub App installation (defaults to current repository owner)" 16 | required: false 17 | repositories: 18 | description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)" 19 | required: false 20 | skip-token-revoke: 21 | description: "If true, the token will not be revoked when the current job is complete" 22 | required: false 23 | default: "false" 24 | # Make GitHub API configurable to support non-GitHub Cloud use cases 25 | # see https://github.com/actions/create-github-app-token/issues/77 26 | github-api-url: 27 | description: The URL of the GitHub REST API. 28 | default: ${{ github.api_url }} 29 | # 30 | permission-actions: 31 | description: "The level of permission to grant the access token for GitHub Actions workflows, workflow runs, and artifacts. Can be set to 'read' or 'write'." 32 | permission-administration: 33 | description: "The level of permission to grant the access token for repository creation, deletion, settings, teams, and collaborators creation. Can be set to 'read' or 'write'." 34 | permission-checks: 35 | description: "The level of permission to grant the access token for checks on code. Can be set to 'read' or 'write'." 36 | permission-codespaces: 37 | description: "The level of permission to grant the access token to create, edit, delete, and list Codespaces. Can be set to 'read' or 'write'." 38 | permission-contents: 39 | description: "The level of permission to grant the access token for repository contents, commits, branches, downloads, releases, and merges. Can be set to 'read' or 'write'." 40 | permission-dependabot-secrets: 41 | description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'." 42 | permission-deployments: 43 | description: "The level of permission to grant the access token for deployments and deployment statuses. Can be set to 'read' or 'write'." 44 | permission-email-addresses: 45 | description: "The level of permission to grant the access token to manage the email addresses belonging to a user. Can be set to 'read' or 'write'." 46 | permission-environments: 47 | description: "The level of permission to grant the access token for managing repository environments. Can be set to 'read' or 'write'." 48 | permission-followers: 49 | description: "The level of permission to grant the access token to manage the followers belonging to a user. Can be set to 'read' or 'write'." 50 | permission-git-ssh-keys: 51 | description: "The level of permission to grant the access token to manage git SSH keys. Can be set to 'read' or 'write'." 52 | permission-gpg-keys: 53 | description: "The level of permission to grant the access token to view and manage GPG keys belonging to a user. Can be set to 'read' or 'write'." 54 | permission-interaction-limits: 55 | description: "The level of permission to grant the access token to view and manage interaction limits on a repository. Can be set to 'read' or 'write'." 56 | permission-issues: 57 | description: "The level of permission to grant the access token for issues and related comments, assignees, labels, and milestones. Can be set to 'read' or 'write'." 58 | permission-members: 59 | description: "The level of permission to grant the access token for organization teams and members. Can be set to 'read' or 'write'." 60 | permission-metadata: 61 | description: "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata. Can be set to 'read' or 'write'." 62 | permission-organization-administration: 63 | description: "The level of permission to grant the access token to manage access to an organization. Can be set to 'read' or 'write'." 64 | permission-organization-announcement-banners: 65 | description: "The level of permission to grant the access token to view and manage announcement banners for an organization. Can be set to 'read' or 'write'." 66 | permission-organization-copilot-seat-management: 67 | description: "The level of permission to grant the access token for managing access to GitHub Copilot for members of an organization with a Copilot Business subscription. This property is in public preview and is subject to change. Can be set to 'write'." 68 | permission-organization-custom-org-roles: 69 | description: "The level of permission to grant the access token for custom organization roles management. Can be set to 'read' or 'write'." 70 | permission-organization-custom-properties: 71 | description: "The level of permission to grant the access token for custom property management. Can be set to 'read', 'write', or 'admin'." 72 | permission-organization-custom-roles: 73 | description: "The level of permission to grant the access token for custom repository roles management. Can be set to 'read' or 'write'." 74 | permission-organization-events: 75 | description: "The level of permission to grant the access token to view events triggered by an activity in an organization. Can be set to 'read'." 76 | permission-organization-hooks: 77 | description: "The level of permission to grant the access token to manage the post-receive hooks for an organization. Can be set to 'read' or 'write'." 78 | permission-organization-packages: 79 | description: "The level of permission to grant the access token for organization packages published to GitHub Packages. Can be set to 'read' or 'write'." 80 | permission-organization-personal-access-token-requests: 81 | description: "The level of permission to grant the access token for viewing and managing fine-grained personal access tokens that have been approved by an organization. Can be set to 'read' or 'write'." 82 | permission-organization-personal-access-tokens: 83 | description: "The level of permission to grant the access token for viewing and managing fine-grained personal access token requests to an organization. Can be set to 'read' or 'write'." 84 | permission-organization-plan: 85 | description: "The level of permission to grant the access token for viewing an organization's plan. Can be set to 'read'." 86 | permission-organization-projects: 87 | description: "The level of permission to grant the access token to manage organization projects and projects public preview (where available). Can be set to 'read', 'write', or 'admin'." 88 | permission-organization-secrets: 89 | description: "The level of permission to grant the access token to manage organization secrets. Can be set to 'read' or 'write'." 90 | permission-organization-self-hosted-runners: 91 | description: "The level of permission to grant the access token to view and manage GitHub Actions self-hosted runners available to an organization. Can be set to 'read' or 'write'." 92 | permission-organization-user-blocking: 93 | description: "The level of permission to grant the access token to view and manage users blocked by the organization. Can be set to 'read' or 'write'." 94 | permission-packages: 95 | description: "The level of permission to grant the access token for packages published to GitHub Packages. Can be set to 'read' or 'write'." 96 | permission-pages: 97 | description: "The level of permission to grant the access token to retrieve Pages statuses, configuration, and builds, as well as create new builds. Can be set to 'read' or 'write'." 98 | permission-profile: 99 | description: "The level of permission to grant the access token to manage the profile settings belonging to a user. Can be set to 'write'." 100 | permission-pull-requests: 101 | description: "The level of permission to grant the access token for pull requests and related comments, assignees, labels, milestones, and merges. Can be set to 'read' or 'write'." 102 | permission-repository-custom-properties: 103 | description: "The level of permission to grant the access token to view and edit custom properties for a repository, when allowed by the property. Can be set to 'read' or 'write'." 104 | permission-repository-hooks: 105 | description: "The level of permission to grant the access token to manage the post-receive hooks for a repository. Can be set to 'read' or 'write'." 106 | permission-repository-projects: 107 | description: "The level of permission to grant the access token to manage repository projects, columns, and cards. Can be set to 'read', 'write', or 'admin'." 108 | permission-secret-scanning-alerts: 109 | description: "The level of permission to grant the access token to view and manage secret scanning alerts. Can be set to 'read' or 'write'." 110 | permission-secrets: 111 | description: "The level of permission to grant the access token to manage repository secrets. Can be set to 'read' or 'write'." 112 | permission-security-events: 113 | description: "The level of permission to grant the access token to view and manage security events like code scanning alerts. Can be set to 'read' or 'write'." 114 | permission-single-file: 115 | description: "The level of permission to grant the access token to manage just a single file. Can be set to 'read' or 'write'." 116 | permission-starring: 117 | description: "The level of permission to grant the access token to list and manage repositories a user is starring. Can be set to 'read' or 'write'." 118 | permission-statuses: 119 | description: "The level of permission to grant the access token for commit statuses. Can be set to 'read' or 'write'." 120 | permission-team-discussions: 121 | description: "The level of permission to grant the access token to manage team discussions and related comments. Can be set to 'read' or 'write'." 122 | permission-vulnerability-alerts: 123 | description: "The level of permission to grant the access token to manage Dependabot alerts. Can be set to 'read' or 'write'." 124 | permission-workflows: 125 | description: "The level of permission to grant the access token to update GitHub Actions workflow files. Can be set to 'write'." 126 | # 127 | outputs: 128 | token: 129 | description: "GitHub installation access token" 130 | installation-id: 131 | description: "GitHub App installation ID" 132 | app-slug: 133 | description: "GitHub App slug" 134 | runs: 135 | using: "node20" 136 | main: "dist/main.cjs" 137 | post: "dist/post.cjs" 138 | -------------------------------------------------------------------------------- /lib/get-permissions-from-inputs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds all permissions passed via `permision-*` inputs and turns them into an object. 3 | * 4 | * @see https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#inputs 5 | * @param {NodeJS.ProcessEnv} env 6 | * @returns {undefined | Record} 7 | */ 8 | export function getPermissionsFromInputs(env) { 9 | return Object.entries(env).reduce((permissions, [key, value]) => { 10 | if (!key.startsWith("INPUT_PERMISSION-")) return permissions; 11 | if (!value) return permissions; 12 | 13 | const permission = key.slice("INPUT_PERMISSION-".length).toLowerCase() 14 | .replaceAll(/-/g, "_"); 15 | 16 | // Inherit app permissions if no permissions inputs are set 17 | if (permissions === undefined) { 18 | return { [permission]: value }; 19 | } 20 | 21 | return { 22 | // @ts-expect-error - needs to be typed correctly 23 | ...permissions, 24 | [permission]: value, 25 | }; 26 | }, undefined); 27 | } 28 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | import pRetry from "p-retry"; 2 | // @ts-check 3 | 4 | /** 5 | * @param {string} appId 6 | * @param {string} privateKey 7 | * @param {string} owner 8 | * @param {string[]} repositories 9 | * @param {undefined | Record} permissions 10 | * @param {import("@actions/core")} core 11 | * @param {import("@octokit/auth-app").createAppAuth} createAppAuth 12 | * @param {import("@octokit/request").request} request 13 | * @param {boolean} skipTokenRevoke 14 | */ 15 | export async function main( 16 | appId, 17 | privateKey, 18 | owner, 19 | repositories, 20 | permissions, 21 | core, 22 | createAppAuth, 23 | request, 24 | skipTokenRevoke 25 | ) { 26 | let parsedOwner = ""; 27 | let parsedRepositoryNames = []; 28 | 29 | // If neither owner nor repositories are set, default to current repository 30 | if (!owner && repositories.length === 0) { 31 | const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); 32 | parsedOwner = owner; 33 | parsedRepositoryNames = [repo]; 34 | 35 | core.info( 36 | `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).` 37 | ); 38 | } 39 | 40 | // If only an owner is set, default to all repositories from that owner 41 | if (owner && repositories.length === 0) { 42 | parsedOwner = owner; 43 | 44 | core.info( 45 | `Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.` 46 | ); 47 | } 48 | 49 | // If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER` 50 | if (!owner && repositories.length > 0) { 51 | parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); 52 | parsedRepositoryNames = repositories; 53 | 54 | core.info( 55 | `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories 56 | .map((repo) => `\n- ${parsedOwner}/${repo}`) 57 | .join("")}` 58 | ); 59 | } 60 | 61 | // If both owner and repositories are set, use those values 62 | if (owner && repositories.length > 0) { 63 | parsedOwner = owner; 64 | parsedRepositoryNames = repositories; 65 | 66 | core.info( 67 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: 68 | ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` 69 | ); 70 | } 71 | 72 | const auth = createAppAuth({ 73 | appId, 74 | privateKey, 75 | request, 76 | }); 77 | 78 | let authentication, installationId, appSlug; 79 | // If at least one repository is set, get installation ID from that repository 80 | 81 | if (parsedRepositoryNames.length > 0) { 82 | ({ authentication, installationId, appSlug } = await pRetry( 83 | () => 84 | getTokenFromRepository( 85 | request, 86 | auth, 87 | parsedOwner, 88 | parsedRepositoryNames, 89 | permissions 90 | ), 91 | { 92 | shouldRetry: (error) => error.status >= 500, 93 | onFailedAttempt: (error) => { 94 | core.info( 95 | `Failed to create token for "${parsedRepositoryNames.join( 96 | "," 97 | )}" (attempt ${error.attemptNumber}): ${error.message}` 98 | ); 99 | }, 100 | retries: 3, 101 | } 102 | )); 103 | } else { 104 | // Otherwise get the installation for the owner, which can either be an organization or a user account 105 | ({ authentication, installationId, appSlug } = await pRetry( 106 | () => getTokenFromOwner(request, auth, parsedOwner, permissions), 107 | { 108 | onFailedAttempt: (error) => { 109 | core.info( 110 | `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` 111 | ); 112 | }, 113 | retries: 3, 114 | } 115 | )); 116 | } 117 | 118 | // Register the token with the runner as a secret to ensure it is masked in logs 119 | core.setSecret(authentication.token); 120 | 121 | core.setOutput("token", authentication.token); 122 | core.setOutput("installation-id", installationId); 123 | core.setOutput("app-slug", appSlug); 124 | 125 | // Make token accessible to post function (so we can invalidate it) 126 | if (!skipTokenRevoke) { 127 | core.saveState("token", authentication.token); 128 | core.saveState("expiresAt", authentication.expiresAt); 129 | } 130 | } 131 | 132 | async function getTokenFromOwner(request, auth, parsedOwner, permissions) { 133 | // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app 134 | // This endpoint works for both users and organizations 135 | const response = await request("GET /users/{username}/installation", { 136 | username: parsedOwner, 137 | request: { 138 | hook: auth.hook, 139 | }, 140 | }); 141 | 142 | // Get token for for all repositories of the given installation 143 | const authentication = await auth({ 144 | type: "installation", 145 | installationId: response.data.id, 146 | permissions, 147 | }); 148 | 149 | const installationId = response.data.id; 150 | const appSlug = response.data["app_slug"]; 151 | 152 | return { authentication, installationId, appSlug }; 153 | } 154 | 155 | async function getTokenFromRepository( 156 | request, 157 | auth, 158 | parsedOwner, 159 | parsedRepositoryNames, 160 | permissions 161 | ) { 162 | // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app 163 | const response = await request("GET /repos/{owner}/{repo}/installation", { 164 | owner: parsedOwner, 165 | repo: parsedRepositoryNames[0], 166 | request: { 167 | hook: auth.hook, 168 | }, 169 | }); 170 | 171 | // Get token for given repositories 172 | const authentication = await auth({ 173 | type: "installation", 174 | installationId: response.data.id, 175 | repositoryNames: parsedRepositoryNames, 176 | permissions, 177 | }); 178 | 179 | const installationId = response.data.id; 180 | const appSlug = response.data["app_slug"]; 181 | 182 | return { authentication, installationId, appSlug }; 183 | } 184 | -------------------------------------------------------------------------------- /lib/post.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("@actions/core")} core 5 | * @param {import("@octokit/request").request} request 6 | */ 7 | export async function post(core, request) { 8 | const skipTokenRevoke = core.getBooleanInput("skip-token-revoke"); 9 | 10 | if (skipTokenRevoke) { 11 | core.info("Token revocation was skipped"); 12 | return; 13 | } 14 | 15 | const token = core.getState("token"); 16 | 17 | if (!token) { 18 | core.info("Token is not set"); 19 | return; 20 | } 21 | 22 | const expiresAt = core.getState("expiresAt"); 23 | if (expiresAt && tokenExpiresIn(expiresAt) < 0) { 24 | core.info("Token expired, skipping token revocation"); 25 | return; 26 | } 27 | 28 | try { 29 | await request("DELETE /installation/token", { 30 | headers: { 31 | authorization: `token ${token}`, 32 | }, 33 | }); 34 | core.info("Token revoked"); 35 | } catch (error) { 36 | core.warning(`Token revocation failed: ${error.message}`); 37 | } 38 | } 39 | 40 | /** 41 | * @param {string} expiresAt 42 | */ 43 | function tokenExpiresIn(expiresAt) { 44 | const now = new Date(); 45 | const expiresAtDate = new Date(expiresAt); 46 | 47 | return Math.round((expiresAtDate.getTime() - now.getTime()) / 1000); 48 | } 49 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | import core from "@actions/core"; 2 | import { request } from "@octokit/request"; 3 | import { ProxyAgent, fetch as undiciFetch } from "undici"; 4 | 5 | const baseUrl = core.getInput("github-api-url").replace(/\/$/, ""); 6 | 7 | // https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/using-a-proxy-server-with-self-hosted-runners 8 | const proxyUrl = 9 | process.env.https_proxy || 10 | process.env.HTTPS_PROXY || 11 | process.env.http_proxy || 12 | process.env.HTTP_PROXY; 13 | 14 | /* c8 ignore start */ 15 | // Native support for proxies in Undici is under consideration: https://github.com/nodejs/undici/issues/1650 16 | // Until then, we need to use a custom fetch function to add proxy support. 17 | const proxyFetch = (url, options) => { 18 | const urlHost = new URL(url).hostname; 19 | const noProxy = (process.env.no_proxy || process.env.NO_PROXY || "").split( 20 | ",", 21 | ); 22 | 23 | if (!noProxy.includes(urlHost)) { 24 | options = { 25 | ...options, 26 | dispatcher: new ProxyAgent(String(proxyUrl)), 27 | }; 28 | } 29 | 30 | return undiciFetch(url, options); 31 | }; 32 | /* c8 ignore stop */ 33 | 34 | export default request.defaults({ 35 | headers: { 36 | "user-agent": "actions/create-github-app-token", 37 | }, 38 | baseUrl, 39 | /* c8 ignore next */ 40 | request: proxyUrl ? { fetch: proxyFetch } : {}, 41 | }); 42 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import core from "@actions/core"; 4 | import { createAppAuth } from "@octokit/auth-app"; 5 | 6 | import { getPermissionsFromInputs } from "./lib/get-permissions-from-inputs.js"; 7 | import { main } from "./lib/main.js"; 8 | import request from "./lib/request.js"; 9 | 10 | if (!process.env.GITHUB_REPOSITORY) { 11 | throw new Error("GITHUB_REPOSITORY missing, must be set to '/'"); 12 | } 13 | 14 | if (!process.env.GITHUB_REPOSITORY_OWNER) { 15 | throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to ''"); 16 | } 17 | 18 | const appId = core.getInput("app-id"); 19 | const privateKey = core.getInput("private-key"); 20 | const owner = core.getInput("owner"); 21 | const repositories = core 22 | .getInput("repositories") 23 | .split(/[\n,]+/) 24 | .map((s) => s.trim()) 25 | .filter((x) => x !== ""); 26 | 27 | const skipTokenRevoke = core.getBooleanInput("skip-token-revoke"); 28 | 29 | const permissions = getPermissionsFromInputs(process.env); 30 | 31 | // Export promise for testing 32 | export default main( 33 | appId, 34 | privateKey, 35 | owner, 36 | repositories, 37 | permissions, 38 | core, 39 | createAppAuth, 40 | request, 41 | skipTokenRevoke, 42 | ).catch((error) => { 43 | /* c8 ignore next 3 */ 44 | console.error(error); 45 | core.setFailed(error.message); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-github-app-token", 3 | "private": true, 4 | "type": "module", 5 | "version": "2.0.6", 6 | "description": "GitHub Action for creating a GitHub App Installation Access Token", 7 | "scripts": { 8 | "build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node20.0.0 --packages=bundle", 9 | "test": "c8 --100 ava tests/index.js", 10 | "coverage": "c8 report --reporter html", 11 | "postcoverage": "open-cli coverage/index.html" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@actions/core": "^1.11.1", 16 | "@octokit/auth-app": "^7.2.1", 17 | "@octokit/request": "^9.2.2", 18 | "p-retry": "^6.2.1", 19 | "undici": "^7.8.0" 20 | }, 21 | "devDependencies": { 22 | "@octokit/openapi": "^19.0.0", 23 | "@sinonjs/fake-timers": "^14.0.0", 24 | "ava": "^6.3.0", 25 | "c8": "^10.1.3", 26 | "dotenv": "^16.5.0", 27 | "esbuild": "^0.25.3", 28 | "execa": "^9.5.2", 29 | "open-cli": "^8.0.0", 30 | "yaml": "^2.7.1" 31 | }, 32 | "release": { 33 | "branches": [ 34 | "+([0-9]).x", 35 | "main" 36 | ], 37 | "plugins": [ 38 | "@semantic-release/commit-analyzer", 39 | "@semantic-release/release-notes-generator", 40 | "@semantic-release/github", 41 | "@semantic-release/npm", 42 | "semantic-release-plugin-github-breaking-version-tag", 43 | [ 44 | "@semantic-release/git", 45 | { 46 | "assets": [ 47 | "package.json", 48 | "package-lock.json", 49 | "dist/*" 50 | ], 51 | "message": "build(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 52 | } 53 | ] 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /post.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import core from "@actions/core"; 4 | 5 | import { post } from "./lib/post.js"; 6 | import request from "./lib/request.js"; 7 | 8 | post(core, request).catch((error) => { 9 | /* c8 ignore next 3 */ 10 | console.error(error); 11 | core.setFailed(error.message); 12 | }); 13 | -------------------------------------------------------------------------------- /scripts/generated/app-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "App Permissions", 3 | "type": "object", 4 | "description": "The permissions granted to the user access token.", 5 | "properties": { 6 | "actions": { 7 | "type": "string", 8 | "description": "The level of permission to grant the access token for GitHub Actions workflows, workflow runs, and artifacts.", 9 | "enum": [ 10 | "read", 11 | "write" 12 | ] 13 | }, 14 | "administration": { 15 | "type": "string", 16 | "description": "The level of permission to grant the access token for repository creation, deletion, settings, teams, and collaborators creation.", 17 | "enum": [ 18 | "read", 19 | "write" 20 | ] 21 | }, 22 | "checks": { 23 | "type": "string", 24 | "description": "The level of permission to grant the access token for checks on code.", 25 | "enum": [ 26 | "read", 27 | "write" 28 | ] 29 | }, 30 | "codespaces": { 31 | "type": "string", 32 | "description": "The level of permission to grant the access token to create, edit, delete, and list Codespaces.", 33 | "enum": [ 34 | "read", 35 | "write" 36 | ] 37 | }, 38 | "contents": { 39 | "type": "string", 40 | "description": "The level of permission to grant the access token for repository contents, commits, branches, downloads, releases, and merges.", 41 | "enum": [ 42 | "read", 43 | "write" 44 | ] 45 | }, 46 | "dependabot_secrets": { 47 | "type": "string", 48 | "description": "The level of permission to grant the access token to manage Dependabot secrets.", 49 | "enum": [ 50 | "read", 51 | "write" 52 | ] 53 | }, 54 | "deployments": { 55 | "type": "string", 56 | "description": "The level of permission to grant the access token for deployments and deployment statuses.", 57 | "enum": [ 58 | "read", 59 | "write" 60 | ] 61 | }, 62 | "environments": { 63 | "type": "string", 64 | "description": "The level of permission to grant the access token for managing repository environments.", 65 | "enum": [ 66 | "read", 67 | "write" 68 | ] 69 | }, 70 | "issues": { 71 | "type": "string", 72 | "description": "The level of permission to grant the access token for issues and related comments, assignees, labels, and milestones.", 73 | "enum": [ 74 | "read", 75 | "write" 76 | ] 77 | }, 78 | "metadata": { 79 | "type": "string", 80 | "description": "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata.", 81 | "enum": [ 82 | "read", 83 | "write" 84 | ] 85 | }, 86 | "packages": { 87 | "type": "string", 88 | "description": "The level of permission to grant the access token for packages published to GitHub Packages.", 89 | "enum": [ 90 | "read", 91 | "write" 92 | ] 93 | }, 94 | "pages": { 95 | "type": "string", 96 | "description": "The level of permission to grant the access token to retrieve Pages statuses, configuration, and builds, as well as create new builds.", 97 | "enum": [ 98 | "read", 99 | "write" 100 | ] 101 | }, 102 | "pull_requests": { 103 | "type": "string", 104 | "description": "The level of permission to grant the access token for pull requests and related comments, assignees, labels, milestones, and merges.", 105 | "enum": [ 106 | "read", 107 | "write" 108 | ] 109 | }, 110 | "repository_custom_properties": { 111 | "type": "string", 112 | "description": "The level of permission to grant the access token to view and edit custom properties for a repository, when allowed by the property.", 113 | "enum": [ 114 | "read", 115 | "write" 116 | ] 117 | }, 118 | "repository_hooks": { 119 | "type": "string", 120 | "description": "The level of permission to grant the access token to manage the post-receive hooks for a repository.", 121 | "enum": [ 122 | "read", 123 | "write" 124 | ] 125 | }, 126 | "repository_projects": { 127 | "type": "string", 128 | "description": "The level of permission to grant the access token to manage repository projects, columns, and cards.", 129 | "enum": [ 130 | "read", 131 | "write", 132 | "admin" 133 | ] 134 | }, 135 | "secret_scanning_alerts": { 136 | "type": "string", 137 | "description": "The level of permission to grant the access token to view and manage secret scanning alerts.", 138 | "enum": [ 139 | "read", 140 | "write" 141 | ] 142 | }, 143 | "secrets": { 144 | "type": "string", 145 | "description": "The level of permission to grant the access token to manage repository secrets.", 146 | "enum": [ 147 | "read", 148 | "write" 149 | ] 150 | }, 151 | "security_events": { 152 | "type": "string", 153 | "description": "The level of permission to grant the access token to view and manage security events like code scanning alerts.", 154 | "enum": [ 155 | "read", 156 | "write" 157 | ] 158 | }, 159 | "single_file": { 160 | "type": "string", 161 | "description": "The level of permission to grant the access token to manage just a single file.", 162 | "enum": [ 163 | "read", 164 | "write" 165 | ] 166 | }, 167 | "statuses": { 168 | "type": "string", 169 | "description": "The level of permission to grant the access token for commit statuses.", 170 | "enum": [ 171 | "read", 172 | "write" 173 | ] 174 | }, 175 | "vulnerability_alerts": { 176 | "type": "string", 177 | "description": "The level of permission to grant the access token to manage Dependabot alerts.", 178 | "enum": [ 179 | "read", 180 | "write" 181 | ] 182 | }, 183 | "workflows": { 184 | "type": "string", 185 | "description": "The level of permission to grant the access token to update GitHub Actions workflow files.", 186 | "enum": [ 187 | "write" 188 | ] 189 | }, 190 | "members": { 191 | "type": "string", 192 | "description": "The level of permission to grant the access token for organization teams and members.", 193 | "enum": [ 194 | "read", 195 | "write" 196 | ] 197 | }, 198 | "organization_administration": { 199 | "type": "string", 200 | "description": "The level of permission to grant the access token to manage access to an organization.", 201 | "enum": [ 202 | "read", 203 | "write" 204 | ] 205 | }, 206 | "organization_custom_roles": { 207 | "type": "string", 208 | "description": "The level of permission to grant the access token for custom repository roles management.", 209 | "enum": [ 210 | "read", 211 | "write" 212 | ] 213 | }, 214 | "organization_custom_org_roles": { 215 | "type": "string", 216 | "description": "The level of permission to grant the access token for custom organization roles management.", 217 | "enum": [ 218 | "read", 219 | "write" 220 | ] 221 | }, 222 | "organization_custom_properties": { 223 | "type": "string", 224 | "description": "The level of permission to grant the access token for custom property management.", 225 | "enum": [ 226 | "read", 227 | "write", 228 | "admin" 229 | ] 230 | }, 231 | "organization_copilot_seat_management": { 232 | "type": "string", 233 | "description": "The level of permission to grant the access token for managing access to GitHub Copilot for members of an organization with a Copilot Business subscription. This property is in public preview and is subject to change.", 234 | "enum": [ 235 | "write" 236 | ] 237 | }, 238 | "organization_announcement_banners": { 239 | "type": "string", 240 | "description": "The level of permission to grant the access token to view and manage announcement banners for an organization.", 241 | "enum": [ 242 | "read", 243 | "write" 244 | ] 245 | }, 246 | "organization_events": { 247 | "type": "string", 248 | "description": "The level of permission to grant the access token to view events triggered by an activity in an organization.", 249 | "enum": [ 250 | "read" 251 | ] 252 | }, 253 | "organization_hooks": { 254 | "type": "string", 255 | "description": "The level of permission to grant the access token to manage the post-receive hooks for an organization.", 256 | "enum": [ 257 | "read", 258 | "write" 259 | ] 260 | }, 261 | "organization_personal_access_tokens": { 262 | "type": "string", 263 | "description": "The level of permission to grant the access token for viewing and managing fine-grained personal access token requests to an organization.", 264 | "enum": [ 265 | "read", 266 | "write" 267 | ] 268 | }, 269 | "organization_personal_access_token_requests": { 270 | "type": "string", 271 | "description": "The level of permission to grant the access token for viewing and managing fine-grained personal access tokens that have been approved by an organization.", 272 | "enum": [ 273 | "read", 274 | "write" 275 | ] 276 | }, 277 | "organization_plan": { 278 | "type": "string", 279 | "description": "The level of permission to grant the access token for viewing an organization's plan.", 280 | "enum": [ 281 | "read" 282 | ] 283 | }, 284 | "organization_projects": { 285 | "type": "string", 286 | "description": "The level of permission to grant the access token to manage organization projects and projects public preview (where available).", 287 | "enum": [ 288 | "read", 289 | "write", 290 | "admin" 291 | ] 292 | }, 293 | "organization_packages": { 294 | "type": "string", 295 | "description": "The level of permission to grant the access token for organization packages published to GitHub Packages.", 296 | "enum": [ 297 | "read", 298 | "write" 299 | ] 300 | }, 301 | "organization_secrets": { 302 | "type": "string", 303 | "description": "The level of permission to grant the access token to manage organization secrets.", 304 | "enum": [ 305 | "read", 306 | "write" 307 | ] 308 | }, 309 | "organization_self_hosted_runners": { 310 | "type": "string", 311 | "description": "The level of permission to grant the access token to view and manage GitHub Actions self-hosted runners available to an organization.", 312 | "enum": [ 313 | "read", 314 | "write" 315 | ] 316 | }, 317 | "organization_user_blocking": { 318 | "type": "string", 319 | "description": "The level of permission to grant the access token to view and manage users blocked by the organization.", 320 | "enum": [ 321 | "read", 322 | "write" 323 | ] 324 | }, 325 | "team_discussions": { 326 | "type": "string", 327 | "description": "The level of permission to grant the access token to manage team discussions and related comments.", 328 | "enum": [ 329 | "read", 330 | "write" 331 | ] 332 | }, 333 | "email_addresses": { 334 | "type": "string", 335 | "description": "The level of permission to grant the access token to manage the email addresses belonging to a user.", 336 | "enum": [ 337 | "read", 338 | "write" 339 | ] 340 | }, 341 | "followers": { 342 | "type": "string", 343 | "description": "The level of permission to grant the access token to manage the followers belonging to a user.", 344 | "enum": [ 345 | "read", 346 | "write" 347 | ] 348 | }, 349 | "git_ssh_keys": { 350 | "type": "string", 351 | "description": "The level of permission to grant the access token to manage git SSH keys.", 352 | "enum": [ 353 | "read", 354 | "write" 355 | ] 356 | }, 357 | "gpg_keys": { 358 | "type": "string", 359 | "description": "The level of permission to grant the access token to view and manage GPG keys belonging to a user.", 360 | "enum": [ 361 | "read", 362 | "write" 363 | ] 364 | }, 365 | "interaction_limits": { 366 | "type": "string", 367 | "description": "The level of permission to grant the access token to view and manage interaction limits on a repository.", 368 | "enum": [ 369 | "read", 370 | "write" 371 | ] 372 | }, 373 | "profile": { 374 | "type": "string", 375 | "description": "The level of permission to grant the access token to manage the profile settings belonging to a user.", 376 | "enum": [ 377 | "write" 378 | ] 379 | }, 380 | "starring": { 381 | "type": "string", 382 | "description": "The level of permission to grant the access token to list and manage repositories a user is starring.", 383 | "enum": [ 384 | "read", 385 | "write" 386 | ] 387 | } 388 | }, 389 | "example": { 390 | "contents": "read", 391 | "issues": "read", 392 | "deployments": "write", 393 | "single_file": "read" 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /scripts/update-permission-inputs.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | 3 | import OctokitOpenapi from "@octokit/openapi"; 4 | 5 | const appPermissionsSchema = 6 | OctokitOpenapi.schemas["api.github.com"].components.schemas[ 7 | "app-permissions" 8 | ]; 9 | 10 | await writeFile( 11 | `scripts/generated/app-permissions.json`, 12 | JSON.stringify(appPermissionsSchema, null, 2) + "\n", 13 | "utf8" 14 | ); 15 | 16 | const permissionsInputs = Object.entries(appPermissionsSchema.properties) 17 | .sort((a, b) => a[0].localeCompare(b[0])) 18 | .reduce((result, [key, value]) => { 19 | const formatter = new Intl.ListFormat("en", { 20 | style: "long", 21 | type: "disjunction", 22 | }); 23 | const permissionAccessValues = formatter.format( 24 | value.enum.map((p) => `'${p}'`) 25 | ); 26 | 27 | const description = `${value.description} Can be set to ${permissionAccessValues}.`; 28 | return `${result} 29 | permission-${key.replace(/_/g, "-")}: 30 | description: "${description}"`; 31 | }, ""); 32 | 33 | const actionsYamlContent = await readFile("action.yml", "utf8"); 34 | 35 | // In the action.yml file, replace the content between the `` and `` comments with the new content 36 | const updatedActionsYamlContent = actionsYamlContent.replace( 37 | /(?<=# )(.|\n)*(?=# )/, 38 | permissionsInputs + "\n " 39 | ); 40 | 41 | await writeFile("action.yml", updatedActionsYamlContent, "utf8"); 42 | console.log("Updated action.yml with new permissions inputs"); 43 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Add one test file per scenario. You can run them in isolation with: 4 | 5 | ```bash 6 | node tests/post-token-set.test.js 7 | ``` 8 | 9 | All tests are run together in [tests/index.js](index.js), which can be executed with ava 10 | 11 | ``` 12 | npx ava tests/index.js 13 | ``` 14 | 15 | or with npm 16 | 17 | ``` 18 | npm test 19 | ``` 20 | 21 | ## How the tests work 22 | 23 | The output from the tests is captured into a snapshot ([tests/snapshots/index.js.md](snapshots/index.js.md)). It includes all requests sent by our scripts to verify it's working correctly and to prevent regressions. 24 | 25 | ## How to add a new test 26 | 27 | We have tests both for the `main.js` and `post.js` scripts. 28 | 29 | - If you do not expect an error, take [main-token-permissions-set.test.js](tests/main-token-permissions-set.test.js) as a starting point. 30 | - If your test has an expected error, take [main-missing-app-id.test.js](tests/main-missing-app-id.test.js) as a starting point. 31 | -------------------------------------------------------------------------------- /tests/action-deprecated-inputs.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import * as url from "node:url"; 3 | import YAML from "yaml"; 4 | 5 | const action = YAML.parse( 6 | readFileSync( 7 | url.fileURLToPath(new URL("../action.yml", import.meta.url)), 8 | "utf8" 9 | ) 10 | ); 11 | 12 | for (const [key, value] of Object.entries(action.inputs)) { 13 | if ("deprecationMessage" in value) { 14 | console.log(`${key} — ${value.deprecationMessage}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "node:fs"; 2 | 3 | import test from "ava"; 4 | import { execa } from "execa"; 5 | 6 | // Get all files in tests directory 7 | const files = readdirSync("tests"); 8 | 9 | // Files to ignore 10 | const ignore = ["index.js", "main.js", "README.md", "snapshots"]; 11 | 12 | const testFiles = files.filter((file) => !ignore.includes(file)); 13 | 14 | // Throw an error if there is a file that does not end with test.js in the tests directory 15 | for (const file of testFiles) { 16 | if (!file.endsWith(".test.js")) { 17 | throw new Error(`File ${file} does not end with .test.js`); 18 | } 19 | test(file, async (t) => { 20 | // Override Actions environment variables that change `core`’s behavior 21 | const env = { 22 | GITHUB_OUTPUT: undefined, 23 | GITHUB_STATE: undefined, 24 | }; 25 | const { stderr, stdout } = await execa("node", [`tests/${file}`], { env }); 26 | t.snapshot(stderr, "stderr"); 27 | t.snapshot(stdout, "stdout"); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /tests/main-custom-github-api-url.test.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ENV, test } from "./main.js"; 2 | 3 | // Verify that main works with a custom GitHub API URL passed as `github-api-url` input 4 | await test( 5 | () => { 6 | process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; 7 | const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; 8 | process.env.INPUT_REPOSITORIES = currentRepoName; 9 | }, 10 | { 11 | ...DEFAULT_ENV, 12 | "INPUT_GITHUB-API-URL": "https://github.acme-inc.com/api/v3", 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /tests/main-missing-owner.test.js: -------------------------------------------------------------------------------- 1 | process.env.GITHUB_REPOSITORY = "actions/create-github-app-token"; 2 | delete process.env.GITHUB_REPOSITORY_OWNER; 3 | 4 | // Verify `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing. 5 | try { 6 | await import("../main.js"); 7 | } catch (error) { 8 | console.error(error.message); 9 | } 10 | -------------------------------------------------------------------------------- /tests/main-missing-repository.test.js: -------------------------------------------------------------------------------- 1 | delete process.env.GITHUB_REPOSITORY; 2 | 3 | // Verify `main` exits with an error when `GITHUB_REPOSITORY` is missing. 4 | try { 5 | await import("../main.js"); 6 | } catch (error) { 7 | console.error(error.message); 8 | } 9 | -------------------------------------------------------------------------------- /tests/main-private-key-with-escaped-newlines.test.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ENV, test } from "./main.js"; 2 | 3 | // Verify `main` works correctly when `private-key` input has escaped newlines 4 | await test(() => { 5 | process.env["INPUT_PRIVATE-KEY"] = DEFAULT_ENV["INPUT_PRIVATE-KEY"].replace( 6 | /\n/g, 7 | "\\n" 8 | ); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/main-repo-skew.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | import { install } from "@sinonjs/fake-timers"; 4 | 5 | // Verify `main` retry when the clock has drifted. 6 | await test((mockPool) => { 7 | process.env.INPUT_OWNER = "actions"; 8 | process.env.INPUT_REPOSITORIES = "failed-repo"; 9 | const owner = process.env.INPUT_OWNER; 10 | const repo = process.env.INPUT_REPOSITORIES; 11 | const mockInstallationId = "123456"; 12 | const mockAppSlug = "github-actions"; 13 | 14 | install({ now: 0, toFake: ["Date"] }); 15 | 16 | mockPool 17 | .intercept({ 18 | path: `/repos/${owner}/${repo}/installation`, 19 | method: "GET", 20 | headers: { 21 | accept: "application/vnd.github.v3+json", 22 | "user-agent": "actions/create-github-app-token", 23 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 24 | }, 25 | }) 26 | .reply(({ headers }) => { 27 | const [_, jwt] = (headers.authorization || "").split(" "); 28 | const payload = JSON.parse( 29 | Buffer.from(jwt.split(".")[1], "base64").toString(), 30 | ); 31 | 32 | if (payload.iat < 0) { 33 | return { 34 | statusCode: 401, 35 | data: { 36 | message: 37 | "'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued.", 38 | }, 39 | responseOptions: { 40 | headers: { 41 | "content-type": "application/json", 42 | date: new Date(Date.now() + 30000).toUTCString(), 43 | }, 44 | }, 45 | }; 46 | } 47 | 48 | return { 49 | statusCode: 200, 50 | data: { 51 | id: mockInstallationId, 52 | app_slug: mockAppSlug, 53 | }, 54 | responseOptions: { 55 | headers: { 56 | "content-type": "application/json", 57 | }, 58 | }, 59 | }; 60 | }) 61 | .times(2); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-fail-response.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify retries work when getting a token for a user or organization fails on the first attempt. 4 | await test((mockPool) => { 5 | process.env.INPUT_OWNER = "smockle"; 6 | delete process.env.INPUT_REPOSITORIES; 7 | 8 | // Mock installation ID and app slug request 9 | const mockInstallationId = "123456"; 10 | const mockAppSlug = "github-actions"; 11 | mockPool 12 | .intercept({ 13 | path: `/users/smockle/installation`, 14 | method: "GET", 15 | headers: { 16 | accept: "application/vnd.github.v3+json", 17 | "user-agent": "actions/create-github-app-token", 18 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 19 | }, 20 | }) 21 | .reply(500, "GitHub API not available"); 22 | mockPool 23 | .intercept({ 24 | path: `/users/smockle/installation`, 25 | method: "GET", 26 | headers: { 27 | accept: "application/vnd.github.v3+json", 28 | "user-agent": "actions/create-github-app-token", 29 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 30 | }, 31 | }) 32 | .reply( 33 | 200, 34 | { id: mockInstallationId, app_slug: mockAppSlug }, 35 | { headers: { "content-type": "application/json" } }, 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-repo-fail-response.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` retry when the GitHub API returns a 500 error. 4 | await test((mockPool) => { 5 | process.env.INPUT_OWNER = "actions"; 6 | process.env.INPUT_REPOSITORIES = "failed-repo"; 7 | const owner = process.env.INPUT_OWNER; 8 | const repo = process.env.INPUT_REPOSITORIES; 9 | const mockInstallationId = "123456"; 10 | const mockAppSlug = "github-actions"; 11 | 12 | mockPool 13 | .intercept({ 14 | path: `/repos/${owner}/${repo}/installation`, 15 | method: "GET", 16 | headers: { 17 | accept: "application/vnd.github.v3+json", 18 | "user-agent": "actions/create-github-app-token", 19 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 20 | }, 21 | }) 22 | .reply(500, "GitHub API not available"); 23 | 24 | mockPool 25 | .intercept({ 26 | path: `/repos/${owner}/${repo}/installation`, 27 | method: "GET", 28 | headers: { 29 | accept: "application/vnd.github.v3+json", 30 | "user-agent": "actions/create-github-app-token", 31 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 32 | }, 33 | }) 34 | .reply( 35 | 200, 36 | { id: mockInstallationId, app_slug: mockAppSlug }, 37 | { headers: { "content-type": "application/json" } }, 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-repo-set-to-many-newline.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos). 4 | await test(() => { 5 | process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; 6 | const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; 7 | // Intentional unnecessary whitespace to test parsing to array 8 | process.env.INPUT_REPOSITORIES = `\n ${currentRepoName}\ntoolkit \n\n checkout \n`; 9 | }); 10 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-repo-set-to-many.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos). 4 | await test(() => { 5 | process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; 6 | const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; 7 | // Intentional unnecessary whitespace to test parsing to array 8 | process.env.INPUT_REPOSITORIES = ` ${currentRepoName}, toolkit ,checkout`; 9 | }); 10 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-repo-set-to-one.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a single repo). 4 | await test(() => { 5 | process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; 6 | const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; 7 | process.env.INPUT_REPOSITORIES = currentRepoName; 8 | }); 9 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-set-repo-unset.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when the `owner` input is set, and the `repositories` input isn’t set. 4 | await test((mockPool) => { 5 | process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; 6 | delete process.env.INPUT_REPOSITORIES; 7 | 8 | // Mock installation ID and app slug request 9 | const mockInstallationId = "123456"; 10 | const mockAppSlug = "github-actions"; 11 | mockPool 12 | .intercept({ 13 | path: `/users/${process.env.INPUT_OWNER}/installation`, 14 | method: "GET", 15 | headers: { 16 | accept: "application/vnd.github.v3+json", 17 | "user-agent": "actions/create-github-app-token", 18 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 19 | }, 20 | }) 21 | .reply( 22 | 200, 23 | { id: mockInstallationId, app_slug: mockAppSlug }, 24 | { headers: { "content-type": "application/json" } }, 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-unset-repo-set.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when the `owner` input is not set, but the `repositories` input is set. 4 | await test(() => { 5 | delete process.env.INPUT_OWNER; 6 | const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; 7 | process.env.INPUT_REPOSITORIES = currentRepoName; 8 | }); 9 | -------------------------------------------------------------------------------- /tests/main-token-get-owner-unset-repo-unset.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully obtains a token when neither the `owner` nor `repositories` input is set. 4 | await test((mockPool) => { 5 | delete process.env.INPUT_OWNER; 6 | delete process.env.INPUT_REPOSITORIES; 7 | 8 | // Mock installation ID and app slug request 9 | const mockInstallationId = "123456"; 10 | const mockAppSlug = "github-actions"; 11 | mockPool 12 | .intercept({ 13 | path: `/repos/${process.env.GITHUB_REPOSITORY}/installation`, 14 | method: "GET", 15 | headers: { 16 | accept: "application/vnd.github.v3+json", 17 | "user-agent": "actions/create-github-app-token", 18 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 19 | }, 20 | }) 21 | .reply( 22 | 200, 23 | { id: mockInstallationId, app_slug: mockAppSlug }, 24 | { headers: { "content-type": "application/json" } }, 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/main-token-permissions-set.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "./main.js"; 2 | 3 | // Verify `main` successfully sets permissions 4 | await test(() => { 5 | process.env["INPUT_PERMISSION-ISSUES"] = `write`; 6 | process.env["INPUT_PERMISSION-PULL-REQUESTS"] = `read`; 7 | }); 8 | -------------------------------------------------------------------------------- /tests/main.js: -------------------------------------------------------------------------------- 1 | // Base for all `main` tests. 2 | // @ts-check 3 | import { MockAgent, setGlobalDispatcher } from "undici"; 4 | 5 | export const DEFAULT_ENV = { 6 | GITHUB_REPOSITORY_OWNER: "actions", 7 | GITHUB_REPOSITORY: "actions/create-github-app-token", 8 | // inputs are set as environment variables with the prefix INPUT_ 9 | // https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 10 | "INPUT_GITHUB-API-URL": "https://api.github.com", 11 | "INPUT_SKIP-TOKEN-REVOKE": "false", 12 | "INPUT_APP-ID": "123456", 13 | // This key is invalidated. It’s from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327. 14 | "INPUT_PRIVATE-KEY": `-----BEGIN RSA PRIVATE KEY----- 15 | MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL 16 | Z9puc5Q/nFBU15NKwHyQNb+OG2hTCkjd1Xi9XPzEOH1r42YQmTGq8YCkUSkk6KZA 17 | 5dnhLwN9pFquT9fQgrf4r1D5GJj3rqvj8JDr1sBmunArqY5u4gziSrIohcjLIZV0 18 | cIMz/RUIMe/EAsNeiwzEteHAtf/WpMs+OfF94SIUrDlkPr0H0H3DER8l1HZAvE0e 19 | eD3ZJ6njrF6UHQWDVrekSTB0clpVTTU9TMpe+gs2nnFww9G8As+WsW8xHVjVipJy 20 | AwqBhiR+s7wlcbh2i0NQqt8GL9/jIFTmleiwsQIDAQABAoIBAHNyP8pgl/yyzKzk 21 | /0871wUBMTQ7zji91dGCaFtJM0HrcDK4D/uOOPEv7nE1qDpKPLr5Me1pHUS7+NGw 22 | EAPtlNhgUtew2JfppdIwyi5qeOPADoi7ud6AH8xHsxg+IMwC+JuP8WhzyUHoJj9y 23 | PRi/pX94Mvy9qdE25HqKddjx1mLdaHhxqPkr16/em23uYZqm1lORsCPD3vTlthj7 24 | WiEbBSqmpYvjj8iFP4yFk2N+LvuWgSilRzq1Af3qE7PUp4xhjmcOPs78Sag1T7nl 25 | ww/pgqCegISABHik7j++/5T+UlI5cLsyp/XENU9zAd4kCIczwNKpun2bn+djJdft 26 | ravyX4ECgYEA+k2mHfi1zwKF3wT+cJbf30+uXrJczK2yZ33//4RKnhBaq7nSbQAI 27 | nhWz2JESBK0TEo0+L7yYYq3HnT9vcES5R1NxzruH9wXgxssSx3JUj6w1raXYPh3B 28 | +1XpYQsa6/bo2LmBELEx47F8FQbNgD5dmTJ4jBrf6MV4rRh9h6Bs7UkCgYEA4M3K 29 | eAw52c2MNMIxH/LxdOQtEBq5GMu3AQC8I64DSSRrAoiypfEgyTV6S4gWJ5TKgYfD 30 | zclnOVJF+tITe3neO9wHoZp8iCjCnoijcT6p2cJ4IaW68LEHPOokWBk0EpLjF4p2 31 | 7sFi9+lUpXYXfCN7aMJ77QpGzB7dNsBf0KUxMCkCgYEAjw/mjGbk82bLwUaHby6s 32 | 0mQmk7V6WPpGZ+Sadx7TzzglutVAslA8nK5m1rdEBywtJINaMcqnhm8xEm15cj+1 33 | blEBUVnaQpQ3fyf+mcR9FIknPRL3X7l+b/sQowjH4GqFd6m/XR0KGMwO0a3Lsyry 34 | MGeqgtmxdMe5S6YdyXEmERECgYAgQsgklDSVIh9Vzux31kh6auhgoEUh3tJDbZSS 35 | Vj2YeIZ21aE1mTYISglj34K2aW7qSc56sMWEf18VkKJFHQccdgYOVfo7HAZZ8+fo 36 | r4J2gqb0xTDfq7gLMNrIXc2QQM4gKbnJp60JQM3p9NmH8huavBZGvSvNzTwXyGG3 37 | so0tiQKBgGQXZaxaXhYUcxYHuCkQ3V4Vsj3ezlM92xXlP32SGFm3KgFhYy9kATxw 38 | Cax1ytZzvlrKLQyQFVK1COs2rHt7W4cJ7op7C8zXfsigXCiejnS664oAuX8sQZID 39 | x3WQZRiXlWejSMUAHuMwXrhGlltF3lw83+xAjnqsVp75kGS6OH61 40 | -----END RSA PRIVATE KEY-----`, 41 | // The Actions runner sets all inputs to empty strings if not set. 42 | "INPUT_PERMISSION-ADMINISTRATION": "", 43 | }; 44 | 45 | export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) { 46 | for (const [key, value] of Object.entries(env)) { 47 | process.env[key] = value; 48 | } 49 | 50 | // Set up mocking 51 | const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]); 52 | const basePath = baseUrl.pathname === "/" ? "" : baseUrl.pathname; 53 | const mockAgent = new MockAgent({ enableCallHistory: true }); 54 | mockAgent.disableNetConnect(); 55 | setGlobalDispatcher(mockAgent); 56 | const mockPool = mockAgent.get(baseUrl.origin); 57 | 58 | // Calling `auth({ type: "app" })` to obtain a JWT doesn’t make network requests, so no need to intercept. 59 | 60 | // Mock installation ID and app slug request 61 | const mockInstallationId = "123456"; 62 | const mockAppSlug = "github-actions"; 63 | const owner = env.INPUT_OWNER ?? env.GITHUB_REPOSITORY_OWNER; 64 | const currentRepoName = env.GITHUB_REPOSITORY.split("/")[1]; 65 | const repo = encodeURIComponent( 66 | (env.INPUT_REPOSITORIES ?? currentRepoName).split(",")[0] 67 | ); 68 | 69 | mockPool 70 | .intercept({ 71 | path: `${basePath}/repos/${owner}/${repo}/installation`, 72 | method: "GET", 73 | headers: { 74 | accept: "application/vnd.github.v3+json", 75 | "user-agent": "actions/create-github-app-token", 76 | // Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 77 | }, 78 | }) 79 | .reply( 80 | 200, 81 | { id: mockInstallationId, app_slug: mockAppSlug }, 82 | { headers: { "content-type": "application/json" } } 83 | ); 84 | 85 | // Mock installation access token request 86 | const mockInstallationAccessToken = 87 | "ghs_16C7e42F292c6912E7710c838347Ae178B4a"; // This token is invalidated. It’s from https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app. 88 | const mockExpiresAt = "2016-07-11T22:14:10Z"; 89 | 90 | mockPool 91 | .intercept({ 92 | path: `${basePath}/app/installations/${mockInstallationId}/access_tokens`, 93 | method: "POST", 94 | headers: { 95 | accept: "application/vnd.github.v3+json", 96 | "user-agent": "actions/create-github-app-token", 97 | // Note: Intentionally omitting the `authorization` header, since JWT creation is not idempotent. 98 | }, 99 | }) 100 | .reply( 101 | 201, 102 | { token: mockInstallationAccessToken, expires_at: mockExpiresAt }, 103 | { headers: { "content-type": "application/json" } } 104 | ); 105 | 106 | // Run the callback 107 | cb(mockPool); 108 | 109 | // Run the main script 110 | const { default: promise } = await import("../main.js"); 111 | await promise; 112 | 113 | console.log("--- REQUESTS ---"); 114 | const calls = mockAgent 115 | .getCallHistory() 116 | .calls() 117 | .map((call) => { 118 | const route = `${call.method} ${call.path}`; 119 | if (call.method === "GET") return route; 120 | 121 | return `${route}\n${call.body}`; 122 | }); 123 | 124 | console.log(calls.join("\n")); 125 | } 126 | -------------------------------------------------------------------------------- /tests/post-revoke-token-fail-response.test.js: -------------------------------------------------------------------------------- 1 | import { MockAgent, setGlobalDispatcher } from "undici"; 2 | 3 | // state variables are set as environment variables with the prefix STATE_ 4 | // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions 5 | process.env.STATE_token = "secret123"; 6 | 7 | // inputs are set as environment variables with the prefix INPUT_ 8 | // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 9 | process.env["INPUT_GITHUB-API-URL"] = "https://api.github.com"; 10 | process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false"; 11 | 12 | // 1 hour in the future, not expired 13 | process.env.STATE_expiresAt = new Date( 14 | Date.now() + 1000 * 60 * 60 15 | ).toISOString(); 16 | 17 | const mockAgent = new MockAgent(); 18 | 19 | setGlobalDispatcher(mockAgent); 20 | 21 | // Provide the base url to the request 22 | const mockPool = mockAgent.get("https://api.github.com"); 23 | 24 | // intercept the request 25 | mockPool 26 | .intercept({ 27 | path: "/installation/token", 28 | method: "DELETE", 29 | headers: { 30 | authorization: "token secret123", 31 | }, 32 | }) 33 | .reply(401); 34 | 35 | await import("../post.js"); 36 | -------------------------------------------------------------------------------- /tests/post-token-expired.test.js: -------------------------------------------------------------------------------- 1 | import { MockAgent, setGlobalDispatcher } from "undici"; 2 | 3 | // state variables are set as environment variables with the prefix STATE_ 4 | // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions 5 | process.env.STATE_token = "secret123"; 6 | 7 | // 1 hour in the past, expired 8 | process.env.STATE_expiresAt = new Date(Date.now() - 1000 * 60 * 60).toISOString(); 9 | 10 | // inputs are set as environment variables with the prefix INPUT_ 11 | // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 12 | process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false"; 13 | 14 | const mockAgent = new MockAgent(); 15 | 16 | setGlobalDispatcher(mockAgent); 17 | 18 | // Provide the base url to the request 19 | const mockPool = mockAgent.get("https://api.github.com"); 20 | 21 | // intercept the request 22 | mockPool 23 | .intercept({ 24 | path: "/installation/token", 25 | method: "DELETE", 26 | headers: { 27 | authorization: "token secret123", 28 | }, 29 | }) 30 | .reply(204); 31 | 32 | await import("../post.js"); 33 | -------------------------------------------------------------------------------- /tests/post-token-set.test.js: -------------------------------------------------------------------------------- 1 | import { MockAgent, setGlobalDispatcher } from "undici"; 2 | 3 | // state variables are set as environment variables with the prefix STATE_ 4 | // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions 5 | process.env.STATE_token = "secret123"; 6 | 7 | // inputs are set as environment variables with the prefix INPUT_ 8 | // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 9 | process.env["INPUT_GITHUB-API-URL"] = "https://api.github.com"; 10 | process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false"; 11 | 12 | // 1 hour in the future, not expired 13 | process.env.STATE_expiresAt = new Date(Date.now() + 1000 * 60 * 60).toISOString(); 14 | 15 | const mockAgent = new MockAgent(); 16 | 17 | setGlobalDispatcher(mockAgent); 18 | 19 | // Provide the base url to the request 20 | const mockPool = mockAgent.get("https://api.github.com"); 21 | 22 | // intercept the request 23 | mockPool 24 | .intercept({ 25 | path: "/installation/token", 26 | method: "DELETE", 27 | headers: { 28 | authorization: "token secret123", 29 | }, 30 | }) 31 | .reply(204); 32 | 33 | await import("../post.js"); 34 | -------------------------------------------------------------------------------- /tests/post-token-skipped.test.js: -------------------------------------------------------------------------------- 1 | import { MockAgent, setGlobalDispatcher } from "undici"; 2 | 3 | // state variables are set as environment variables with the prefix STATE_ 4 | // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions 5 | process.env.STATE_token = "secret123"; 6 | 7 | // inputs are set as environment variables with the prefix INPUT_ 8 | // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 9 | process.env["INPUT_SKIP-TOKEN-REVOKE"] = "true"; 10 | 11 | const mockAgent = new MockAgent(); 12 | 13 | setGlobalDispatcher(mockAgent); 14 | 15 | // Provide the base url to the request 16 | const mockPool = mockAgent.get("https://api.github.com"); 17 | 18 | // intercept the request 19 | mockPool 20 | .intercept({ 21 | path: "/installation/token", 22 | method: "DELETE", 23 | headers: { 24 | authorization: "token secret123", 25 | }, 26 | }) 27 | .reply(204); 28 | 29 | await import("../post.js"); 30 | -------------------------------------------------------------------------------- /tests/post-token-unset.test.js: -------------------------------------------------------------------------------- 1 | // state variables are set as environment variables with the prefix STATE_ 2 | // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions 3 | delete process.env.STATE_token; 4 | 5 | // inputs are set as environment variables with the prefix INPUT_ 6 | // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs 7 | process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false"; 8 | 9 | await import("../post.js"); 10 | -------------------------------------------------------------------------------- /tests/snapshots/index.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `tests/index.js` 2 | 3 | The actual snapshot is saved in `index.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## action-deprecated-inputs.test.js 8 | 9 | > stderr 10 | 11 | '' 12 | 13 | > stdout 14 | 15 | '' 16 | 17 | ## main-custom-github-api-url.test.js 18 | 19 | > stderr 20 | 21 | '' 22 | 23 | > stdout 24 | 25 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 26 | ␊ 27 | - actions/create-github-app-token␊ 28 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 29 | ␊ 30 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 31 | ␊ 32 | ::set-output name=installation-id::123456␊ 33 | ␊ 34 | ::set-output name=app-slug::github-actions␊ 35 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 36 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 37 | --- REQUESTS ---␊ 38 | GET /api/v3/repos/actions/create-github-app-token/installation␊ 39 | POST /api/v3/app/installations/123456/access_tokens␊ 40 | {"repositories":["create-github-app-token"]}` 41 | 42 | ## main-missing-owner.test.js 43 | 44 | > stderr 45 | 46 | 'GITHUB_REPOSITORY_OWNER missing, must be set to \'\'' 47 | 48 | > stdout 49 | 50 | '' 51 | 52 | ## main-missing-repository.test.js 53 | 54 | > stderr 55 | 56 | 'GITHUB_REPOSITORY missing, must be set to \'/\'' 57 | 58 | > stdout 59 | 60 | '' 61 | 62 | ## main-private-key-with-escaped-newlines.test.js 63 | 64 | > stderr 65 | 66 | '' 67 | 68 | > stdout 69 | 70 | `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).␊ 71 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 72 | ␊ 73 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 74 | ␊ 75 | ::set-output name=installation-id::123456␊ 76 | ␊ 77 | ::set-output name=app-slug::github-actions␊ 78 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 79 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 80 | --- REQUESTS ---␊ 81 | GET /repos/actions/create-github-app-token/installation␊ 82 | POST /app/installations/123456/access_tokens␊ 83 | {"repositories":["create-github-app-token"]}` 84 | 85 | ## main-repo-skew.test.js 86 | 87 | > stderr 88 | 89 | `'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued.␊ 90 | [@octokit/auth-app] GitHub API time and system time are different by 30 seconds. Retrying request with the difference accounted for.` 91 | 92 | > stdout 93 | 94 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 95 | ␊ 96 | - actions/failed-repo␊ 97 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 98 | ␊ 99 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 100 | ␊ 101 | ::set-output name=installation-id::123456␊ 102 | ␊ 103 | ::set-output name=app-slug::github-actions␊ 104 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 105 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 106 | --- REQUESTS ---␊ 107 | GET /repos/actions/failed-repo/installation␊ 108 | GET /repos/actions/failed-repo/installation␊ 109 | POST /app/installations/123456/access_tokens␊ 110 | {"repositories":["failed-repo"]}` 111 | 112 | ## main-token-get-owner-set-fail-response.test.js 113 | 114 | > stderr 115 | 116 | '' 117 | 118 | > stdout 119 | 120 | `Input 'repositories' is not set. Creating token for all repositories owned by smockle.␊ 121 | Failed to create token for "smockle" (attempt 1): GitHub API not available␊ 122 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 123 | ␊ 124 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 125 | ␊ 126 | ::set-output name=installation-id::123456␊ 127 | ␊ 128 | ::set-output name=app-slug::github-actions␊ 129 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 130 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 131 | --- REQUESTS ---␊ 132 | GET /users/smockle/installation␊ 133 | GET /users/smockle/installation␊ 134 | POST /app/installations/123456/access_tokens␊ 135 | null` 136 | 137 | ## main-token-get-owner-set-repo-fail-response.test.js 138 | 139 | > stderr 140 | 141 | '' 142 | 143 | > stdout 144 | 145 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 146 | ␊ 147 | - actions/failed-repo␊ 148 | Failed to create token for "failed-repo" (attempt 1): GitHub API not available␊ 149 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 150 | ␊ 151 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 152 | ␊ 153 | ::set-output name=installation-id::123456␊ 154 | ␊ 155 | ::set-output name=app-slug::github-actions␊ 156 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 157 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 158 | --- REQUESTS ---␊ 159 | GET /repos/actions/failed-repo/installation␊ 160 | GET /repos/actions/failed-repo/installation␊ 161 | POST /app/installations/123456/access_tokens␊ 162 | {"repositories":["failed-repo"]}` 163 | 164 | ## main-token-get-owner-set-repo-set-to-many-newline.test.js 165 | 166 | > stderr 167 | 168 | '' 169 | 170 | > stdout 171 | 172 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 173 | ␊ 174 | - actions/create-github-app-token␊ 175 | - actions/toolkit␊ 176 | - actions/checkout␊ 177 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 178 | ␊ 179 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 180 | ␊ 181 | ::set-output name=installation-id::123456␊ 182 | ␊ 183 | ::set-output name=app-slug::github-actions␊ 184 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 185 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 186 | --- REQUESTS ---␊ 187 | GET /repos/actions/create-github-app-token/installation␊ 188 | POST /app/installations/123456/access_tokens␊ 189 | {"repositories":["create-github-app-token","toolkit","checkout"]}` 190 | 191 | ## main-token-get-owner-set-repo-set-to-many.test.js 192 | 193 | > stderr 194 | 195 | '' 196 | 197 | > stdout 198 | 199 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 200 | ␊ 201 | - actions/create-github-app-token␊ 202 | - actions/toolkit␊ 203 | - actions/checkout␊ 204 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 205 | ␊ 206 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 207 | ␊ 208 | ::set-output name=installation-id::123456␊ 209 | ␊ 210 | ::set-output name=app-slug::github-actions␊ 211 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 212 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 213 | --- REQUESTS ---␊ 214 | GET /repos/actions/create-github-app-token/installation␊ 215 | POST /app/installations/123456/access_tokens␊ 216 | {"repositories":["create-github-app-token","toolkit","checkout"]}` 217 | 218 | ## main-token-get-owner-set-repo-set-to-one.test.js 219 | 220 | > stderr 221 | 222 | '' 223 | 224 | > stdout 225 | 226 | `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:␊ 227 | ␊ 228 | - actions/create-github-app-token␊ 229 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 230 | ␊ 231 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 232 | ␊ 233 | ::set-output name=installation-id::123456␊ 234 | ␊ 235 | ::set-output name=app-slug::github-actions␊ 236 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 237 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 238 | --- REQUESTS ---␊ 239 | GET /repos/actions/create-github-app-token/installation␊ 240 | POST /app/installations/123456/access_tokens␊ 241 | {"repositories":["create-github-app-token"]}` 242 | 243 | ## main-token-get-owner-set-repo-unset.test.js 244 | 245 | > stderr 246 | 247 | '' 248 | 249 | > stdout 250 | 251 | `Input 'repositories' is not set. Creating token for all repositories owned by actions.␊ 252 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 253 | ␊ 254 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 255 | ␊ 256 | ::set-output name=installation-id::123456␊ 257 | ␊ 258 | ::set-output name=app-slug::github-actions␊ 259 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 260 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 261 | --- REQUESTS ---␊ 262 | GET /users/actions/installation␊ 263 | POST /app/installations/123456/access_tokens␊ 264 | null` 265 | 266 | ## main-token-get-owner-unset-repo-set.test.js 267 | 268 | > stderr 269 | 270 | '' 271 | 272 | > stdout 273 | 274 | `No 'owner' input provided. Using default owner 'actions' to create token for the following repositories:␊ 275 | - actions/create-github-app-token␊ 276 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 277 | ␊ 278 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 279 | ␊ 280 | ::set-output name=installation-id::123456␊ 281 | ␊ 282 | ::set-output name=app-slug::github-actions␊ 283 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 284 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 285 | --- REQUESTS ---␊ 286 | GET /repos/actions/create-github-app-token/installation␊ 287 | POST /app/installations/123456/access_tokens␊ 288 | {"repositories":["create-github-app-token"]}` 289 | 290 | ## main-token-get-owner-unset-repo-unset.test.js 291 | 292 | > stderr 293 | 294 | '' 295 | 296 | > stdout 297 | 298 | `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).␊ 299 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 300 | ␊ 301 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 302 | ␊ 303 | ::set-output name=installation-id::123456␊ 304 | ␊ 305 | ::set-output name=app-slug::github-actions␊ 306 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 307 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 308 | --- REQUESTS ---␊ 309 | GET /repos/actions/create-github-app-token/installation␊ 310 | POST /app/installations/123456/access_tokens␊ 311 | {"repositories":["create-github-app-token"]}` 312 | 313 | ## main-token-permissions-set.test.js 314 | 315 | > stderr 316 | 317 | '' 318 | 319 | > stdout 320 | 321 | `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).␊ 322 | ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 323 | ␊ 324 | ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 325 | ␊ 326 | ::set-output name=installation-id::123456␊ 327 | ␊ 328 | ::set-output name=app-slug::github-actions␊ 329 | ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ 330 | ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ 331 | --- REQUESTS ---␊ 332 | GET /repos/actions/create-github-app-token/installation␊ 333 | POST /app/installations/123456/access_tokens␊ 334 | {"repositories":["create-github-app-token"],"permissions":{"issues":"write","pull_requests":"read"}}` 335 | 336 | ## post-revoke-token-fail-response.test.js 337 | 338 | > stderr 339 | 340 | '' 341 | 342 | > stdout 343 | 344 | '::warning::Token revocation failed: ' 345 | 346 | ## post-token-expired.test.js 347 | 348 | > stderr 349 | 350 | '' 351 | 352 | > stdout 353 | 354 | 'Token expired, skipping token revocation' 355 | 356 | ## post-token-set.test.js 357 | 358 | > stderr 359 | 360 | '' 361 | 362 | > stdout 363 | 364 | 'Token revoked' 365 | 366 | ## post-token-skipped.test.js 367 | 368 | > stderr 369 | 370 | '' 371 | 372 | > stdout 373 | 374 | 'Token revocation was skipped' 375 | 376 | ## post-token-unset.test.js 377 | 378 | > stderr 379 | 380 | '' 381 | 382 | > stdout 383 | 384 | 'Token is not set' 385 | -------------------------------------------------------------------------------- /tests/snapshots/index.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actions/create-github-app-token/dff4b11d10ecc84d937fdd0653d8343a88c5b9c4/tests/snapshots/index.js.snap --------------------------------------------------------------------------------