Compare commits

..

15 Commits

Author SHA1 Message Date
semantic-release-bot a0d050558c build(release): 3.0.0-beta.6 [skip ci]
# [3.0.0-beta.6](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2026-03-13)

### Bug Fixes

* **deps:** bump @actions/core from 1.11.1 to 3.0.0 ([#337](https://github.com/actions/create-github-app-token/issues/337)) ([b044133](https://github.com/actions/create-github-app-token/commit/b04413352d4644ac2131b9a90c074f5e93ca18a1))
* **deps:** bump minimatch from 9.0.5 to 9.0.9 ([#335](https://github.com/actions/create-github-app-token/issues/335)) ([5cbc656](https://github.com/actions/create-github-app-token/commit/5cbc65624c9ddc4589492bda7c8b146223e8c3e4))
* **deps:** bump the production-dependencies group with 4 updates ([#336](https://github.com/actions/create-github-app-token/issues/336)) ([6bda5bc](https://github.com/actions/create-github-app-token/commit/6bda5bc1410576b9a0879ce6076d53345485bba9))
* **deps:** bump undici from 7.16.0 to 7.18.2 ([#323](https://github.com/actions/create-github-app-token/issues/323)) ([b4f638f](https://github.com/actions/create-github-app-token/commit/b4f638f48ee0dcdbb0bc646c48e4cb2a2de847fe))
2026-03-13 23:02:39 +00:00
Parker Brown 6d13b5ae0d Merge remote-tracking branch 'origin/main' into pr-345
# Conflicts:
#	dist/main.cjs
#	dist/post.cjs
#	package-lock.json
#	package.json
2026-03-13 15:57:54 -07:00
semantic-release-bot 28bdc1ab05 build(release): 3.0.0-beta.5 [skip ci]
# [3.0.0-beta.5](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2026-03-13)

* fix!: require `NODE_USE_ENV_PROXY` for proxy support ([#342](https://github.com/actions/create-github-app-token/issues/342)) ([d53a1cd](https://github.com/actions/create-github-app-token/commit/d53a1cdfde844c958786293adcaf739ecb8b5eb9))

### BREAKING CHANGES

* Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.
2026-03-13 21:50:56 +00:00
Parker Brown d53a1cdfde fix!: require NODE_USE_ENV_PROXY for proxy support (#342)
BREAKING CHANGE: Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.
2026-03-13 14:49:57 -07:00
Parker Brown f863ba5554 test: migrate from AVA to Node.js native test runner (#346)
AVA stores snapshots in a binary format (`.snap`), which produces no
meaningful diffs and bloats Git history. This replaces AVA with the
built-in `node:test` module, whose snapshot support generates
human-readable text files that are easy to diff and review in pull
requests.

The migration also replaces `@sinonjs/fake-timers` and `execa` with
Node.js built-ins (`node:test` mock timers and `node:child_process`),
removing three dev dependencies total.

### Changes

- **`tests/index.js`**: Rewritten to use `node:test` with a custom
snapshot serializer that renders strings with actual newlines. Uses
subtests for labeled `stderr`/`stdout` snapshots, and only snapshots
non-empty output.
- **`tests/main-repo-skew.test.js`**: Replace `@sinonjs/fake-timers`
with `mock.timers.enable()` from `node:test`.
- **`tests/README.md`**: Updated documentation to reflect `node --test`
and the new snapshot file.
- **`package.json`**: Remove `ava`, `@sinonjs/fake-timers`, and `execa`
from devDependencies. Update test script to `c8 --100 node --test
tests/index.js`.
- **`tests/index.js.snapshot`**: New text-based snapshot file replacing
binary `tests/snapshots/index.js.snap`.
- **`tests/snapshots/`**: Deleted.

All 22 test scenarios (66 subtests) pass with 100% code coverage.

Closes #344

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 09:05:29 -07:00
Parker Brown c2937b00bd Rename end-to-end proxy job in test workflow 2026-03-13 09:00:25 -07:00
semantic-release-bot a7f885bf45 build(release): 3.0.0-beta.4 [skip ci]
# [3.0.0-beta.4](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2026-03-13)

### Bug Fixes

* **deps:** bump @octokit/auth-app from 7.2.1 to 8.0.1 ([#257](https://github.com/actions/create-github-app-token/issues/257)) ([bef1eaf](https://github.com/actions/create-github-app-token/commit/bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1))
* **deps:** bump @octokit/request from 9.2.3 to 10.0.2 ([#256](https://github.com/actions/create-github-app-token/issues/256)) ([5d7307b](https://github.com/actions/create-github-app-token/commit/5d7307be63501c0070c634b0ae8fec74e8208130))
* **deps:** bump glob from 10.4.5 to 10.5.0 ([#305](https://github.com/actions/create-github-app-token/issues/305)) ([5480f43](https://github.com/actions/create-github-app-token/commit/5480f4325a18c025ee16d7e081413854624e9edc))
* **deps:** bump p-retry from 6.2.1 to 7.1.0 ([#294](https://github.com/actions/create-github-app-token/issues/294)) ([dce3be8](https://github.com/actions/create-github-app-token/commit/dce3be8b284f45e65caed11a610e2bef738d15b4))
* **deps:** bump the production-dependencies group with 2 updates ([#292](https://github.com/actions/create-github-app-token/issues/292)) ([55e2a4b](https://github.com/actions/create-github-app-token/commit/55e2a4b2ccaaa8364303e6ab9f77e31ad02298e5))
* **deps:** bump the production-dependencies group with 2 updates ([#311](https://github.com/actions/create-github-app-token/issues/311)) ([b212e6a](https://github.com/actions/create-github-app-token/commit/b212e6a739dec02d8488610fbaf8f049f82ee999))
* **deps:** bump undici from 7.8.0 to 7.10.0 in the production-dependencies group ([#254](https://github.com/actions/create-github-app-token/issues/254)) ([f3d5ec2](https://github.com/actions/create-github-app-token/commit/f3d5ec20739b0cf6f0d52e5a051b65484c378ec9))

### Features

* update permission inputs ([#296](https://github.com/actions/create-github-app-token/issues/296)) ([d90aa53](https://github.com/actions/create-github-app-token/commit/d90aa532332d33f6dc9656fd4491a98441595a37))
2026-03-13 06:27:07 +00:00
Parker Brown b60ed23e06 Merge branch 'main' into beta 2026-03-12 23:26:03 -07:00
semantic-release-bot d28ad69b67 build(release): 3.0.0-beta.3 [skip ci]
# [3.0.0-beta.3](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2026-03-13)

### Bug Fixes

* require `NODE_USE_ENV_PROXY` for proxy support ([#342](https://github.com/actions/create-github-app-token/issues/342)) ([54e58b6](https://github.com/actions/create-github-app-token/commit/54e58b612c0c4e52564c3c87486532017ad95b22))
2026-03-13 06:19:27 +00:00
Parker Brown 54e58b612c fix: require NODE_USE_ENV_PROXY for proxy support (#342)
This PR switches proxy support to Node's native env-proxy handling and
makes the required configuration explicit.

## What changed

- fail fast in both `main` and `post` when proxy configuration is
present without `NODE_USE_ENV_PROXY=1`
- document the supported proxy configuration in `README.md`
- add regression tests for the proxy guard in both entrypoints
- keep the existing successful end-to-end coverage and add a smaller
proxy-specific workflow check that enables native proxy support, points
`https_proxy` at an unreachable proxy, and asserts the action fails
- update the test workflow so the same checks also run on pushes to
`beta`

## Proxy configuration

When using `HTTP_PROXY` or `HTTPS_PROXY`, set `NODE_USE_ENV_PROXY=1` on
the action step. If you need bypass rules, set `NO_PROXY` alongside
them.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 23:18:56 -07:00
semantic-release-bot bf559f8544 build(release): 3.0.0-beta.2 [skip ci]
# [3.0.0-beta.2](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2025-08-22)

### Bug Fixes

* remove custom proxy handling ([#143](https://github.com/actions/create-github-app-token/issues/143)) ([cda91bf](https://github.com/actions/create-github-app-token/commit/cda91bf2b93cf1d3306b458b2a4f7fcd9de9175f)), closes [#134](https://github.com/actions/create-github-app-token/issues/134)
2025-08-22 19:16:51 +00:00
Parker Brown cda91bf2b9 fix: remove custom proxy handling (#143)
Undici has added native support for proxy handling, so it is no longer necessary for us to have our own custom proxy handling.

Reverts #102 and resolves #134.
2025-08-22 12:16:16 -07:00
Parker Brown 2ae58da528 Disable semantic-release-plugin-github-breaking-version-tag
https://github.com/gr2m/semantic-release-plugin-update-version-in-files/issues/52
2025-08-15 13:03:02 -07:00
semantic-release-bot fb1c7fda2b build(release): 3.0.0-beta.1 [skip ci]
# [3.0.0-beta.1](https://github.com/actions/create-github-app-token/compare/v2.1.1...v3.0.0-beta.1) (2025-08-15)

* feat!: node 24 support ([#275](https://github.com/actions/create-github-app-token/issues/275)) ([6178938](https://github.com/actions/create-github-app-token/commit/61789386cb26150ab580cab449a9ae053bb9fd24))

### BREAKING CHANGES

* Requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later if you are using a self-hosted runner.
2025-08-15 19:55:36 +00:00
Salman Chishti 61789386cb feat!: node 24 support (#275)
BREAKING CHANGE: Requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later if you are using a self-hosted runner.

---------

Co-authored-by: Parker Brown <17183625+parkerbxyz@users.noreply.github.com>
2025-08-15 12:55:04 -07:00
37 changed files with 1209 additions and 1454 deletions
@@ -0,0 +1,17 @@
name: 'Publish Immutable Action'
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: actions/checkout@v6
- name: Publish Immutable Action
uses: actions/publish-immutable-action@v0.0.4
+40
View File
@@ -0,0 +1,40 @@
name: release
on:
push:
branches:
- "*.x"
- main
- beta
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
# build local version to create token
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm run build
- uses: ./
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
# install release dependencies and release
- run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag
- run: npx semantic-release --debug
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+34
View File
@@ -0,0 +1,34 @@
# This workflow warns and then closes issues that have had no activity for a specified amount of time.
# https://github.com/actions/stale
name: Stale
on:
workflow_dispatch:
schedule:
# 00:00 UTC on Mondays
- cron: '0 0 * * 1'
permissions:
issues: write
pull-requests: write
env:
DAYS_BEFORE_STALE: 180
DAYS_BEFORE_CLOSE: 60
STALE_LABEL: 'stale'
STALE_LABEL_URL: ${{github.server_url}}/${{github.repository}}/labels/stale
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
operations-per-run: 100
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
stale-issue-label: ${{ env.STALE_LABEL }}
stale-pr-label: ${{ env.STALE_LABEL }}
stale-issue-message: 'This issue has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this issue if it is no longer needed. If this issue is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
stale-pr-message: 'This pull request has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this pull request if it is no longer needed. If this pull request is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
+81
View File
@@ -0,0 +1,81 @@
name: test
on:
push:
branches:
- main
- beta
pull_request:
merge_group:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
integration:
name: integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm test
end-to-end:
name: end-to-end
runs-on: ubuntu-latest
# do not run from forks, as forks dont have access to repository secrets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
id: test
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
- uses: octokit/request-action@v2.x
id: get-repository
env:
GITHUB_TOKEN: ${{ steps.test.outputs.token }}
with:
route: GET /installation/repositories
- run: echo '${{ steps.get-repository.outputs.data }}'
end-to-end-proxy:
name: end-to-end with unreachable proxy
runs-on: ubuntu-latest
# do not run from forks, as forks dont have access to repository secrets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
continue-on-error: true
id: test
env:
NODE_USE_ENV_PROXY: "1"
https_proxy: http://127.0.0.1:9
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
- name: Assert action failed through unreachable proxy
run: test "${{ steps.test.outcome }}" = "failure"
@@ -0,0 +1,42 @@
name: Update Permission Inputs
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
update-permission-inputs:
runs-on: ubuntu-latest
env:
COMMIT_MESSAGE: 'feat: update permission inputs'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Install dependencies
run: npm ci
- name: Run permission inputs update script
run: node scripts/update-permission-inputs.js
- name: Commit changes
id: auto-commit
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: ${{ env.COMMIT_MESSAGE }}
- name: Update PR title
if: github.event_name == 'pull_request' && steps.auto-commit.outputs.changes_detected == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --title "${{ env.COMMIT_MESSAGE }}"
-1
View File
@@ -1,4 +1,3 @@
.env
coverage
node_modules/
.DS_Store
-3
View File
@@ -1,3 +0,0 @@
{
".": "3.1.1"
}
+41 -74
View File
@@ -9,10 +9,10 @@ GitHub Action for creating a GitHub App installation access token.
In order to use this action, you need to:
1. [Register new GitHub App](https://docs.github.com/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app).
2. [Store the App's Client ID in your repository variables](https://docs.github.com/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables#defining-configuration-variables-for-multiple-workflows) (example: `APP_CLIENT_ID`).
3. [Store the App's private key in your repository secrets](https://docs.github.com/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets?tool=webui#creating-secrets-for-a-repository) (example: `APP_PRIVATE_KEY`).
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`).
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`).
> [!IMPORTANT]
> [!IMPORTANT]
> 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.
### Create a token for the current repository
@@ -31,8 +31,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- uses: ./actions/staging-tests
with:
token: ${{ steps.app-token.outputs.token }}
@@ -51,15 +51,15 @@ jobs:
id: app-token
with:
# required
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.head_ref }}
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
persist-credentials: false
- uses: creyD/prettier_action@v6
- uses: creyD/prettier_action@v4.3
with:
github_token: ${{ steps.app-token.outputs.token }}
```
@@ -77,8 +77,8 @@ jobs:
id: app-token
with:
# required
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -102,8 +102,8 @@ jobs:
id: app-token
with:
# required
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -138,10 +138,10 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
@@ -160,13 +160,13 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
repo1
repo2
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
@@ -185,42 +185,20 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: another-owner
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
body: "Hello, World!"
```
### Create a token for an enterprise installation
```yaml
on: [workflow_dispatch]
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
enterprise: my-enterprise-slug
- name: Call enterprise management REST API with gh
run: |
gh api /enterprises/my-enterprise-slug/apps/installable_organizations
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
```
### Create a token with specific permissions
> [!NOTE]
> Selected permissions must be granted to the specified app installation. Setting a permission that the installation does not have will result in an error.
> 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.
```yaml
on: [issues]
@@ -232,11 +210,11 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-issues: write
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
@@ -274,8 +252,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ matrix.owners-and-repos.owner }}
repositories: ${{ join(matrix.owners-and-repos.repos) }}
- uses: octokit/request-action@v2.x
@@ -303,7 +281,7 @@ jobs:
id: create_token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.GHES_APP_CLIENT_ID }}
app-id: ${{ vars.GHES_APP_ID }}
private-key: ${{ secrets.GHES_APP_PRIVATE_KEY }}
owner: ${{ vars.GHES_INSTALLATION_ORG }}
github-api-url: ${{ vars.GITHUB_API_URL }}
@@ -332,18 +310,15 @@ If you set `HTTP_PROXY` or `HTTPS_PROXY`, also set `NODE_USE_ENV_PROXY: "1"` on
NO_PROXY: github.example.com
NODE_USE_ENV_PROXY: "1"
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
```
## Inputs
### `client-id` or `app-id`
### `app-id`
**Required:** GitHub App Client ID.
> [!NOTE]
> The legacy `app-id` input is also accepted, but `client-id` is recommended.
**Required:** GitHub App ID.
### `private-key`
@@ -356,14 +331,14 @@ steps:
- name: Decode the GitHub App Private Key
id: decode
run: |
private_key=$(echo "${{ secrets.APP_PRIVATE_KEY }}" | base64 -d | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null
private_key=$(echo "${{ secrets.PRIVATE_KEY }}" | base64 -d | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null
echo "::add-mask::$private_key"
echo "private-key=$private_key" >> "$GITHUB_OUTPUT"
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.APP_CLIENT_ID }}
app-id: ${{ vars.APP_ID }}
private-key: ${{ steps.decode.outputs.private-key }}
```
@@ -378,13 +353,6 @@ steps:
> [!NOTE]
> 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.
### `enterprise`
**Optional:** The slug of the enterprise account to generate a token for an enterprise installation.
> [!NOTE]
> The `enterprise` input is mutually exclusive with `owner` and `repositories`. Use it when the GitHub App is installed on an enterprise account. Enterprise installation tokens can call enterprise APIs, but do not grant organization or repository access.
### `permission-<permission name>`
**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`).
@@ -415,14 +383,13 @@ GitHub App slug.
## How it works
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).
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,
The token target depends on the inputs: `enterprise` creates a token for an enterprise installation, `owner` without `repositories` creates a token for all repositories in the owner's installation, `repositories` scopes the token to those repositories, and no target inputs scopes the token to the current repository.
1. The token inherits all the installation's permissions.
2. The token is set as output `token` which can be used in subsequent steps.
3. 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.
4. The token is masked, it cannot be logged accidentally.
1. The token is scoped to the current repository or `repositories` if set.
2. The token inherits all the installation's permissions.
3. The token is set as output `token` which can be used in subsequent steps.
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.
5. The token is masked, it cannot be logged accidentally.
> [!NOTE]
> 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.
+1 -16
View File
@@ -5,13 +5,9 @@ branding:
icon: "lock"
color: "gray-dark"
inputs:
client-id:
description: "GitHub App Client ID"
required: false
app-id:
description: "GitHub App ID"
required: false
deprecationMessage: "Use 'client-id' instead."
required: true
private-key:
description: "GitHub App private key"
required: true
@@ -21,9 +17,6 @@ inputs:
repositories:
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
required: false
enterprise:
description: "The slug of the enterprise account where the GitHub App is installed (cannot be used with 'owner' or 'repositories')"
required: false
skip-token-revoke:
description: "If true, the token will not be revoked when the current job is complete"
required: false
@@ -38,10 +31,6 @@ inputs:
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'."
permission-administration:
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'."
permission-artifact-metadata:
description: "The level of permission to grant the access token to create and retrieve build artifact metadata records. Can be set to 'read' or 'write'."
permission-attestations:
description: "The level of permission to create and retrieve the access token for repository attestations. Can be set to 'read' or 'write'."
permission-checks:
description: "The level of permission to grant the access token for checks on code. Can be set to 'read' or 'write'."
permission-codespaces:
@@ -54,8 +43,6 @@ inputs:
description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'."
permission-deployments:
description: "The level of permission to grant the access token for deployments and deployment statuses. Can be set to 'read' or 'write'."
permission-discussions:
description: "The level of permission to grant the access token for discussions and related comments and labels. Can be set to 'read' or 'write'."
permission-email-addresses:
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'."
permission-enterprise-custom-properties-for-organizations:
@@ -74,8 +61,6 @@ inputs:
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'."
permission-members:
description: "The level of permission to grant the access token for organization teams and members. Can be set to 'read' or 'write'."
permission-merge-queues:
description: "The level of permission to grant the access token to manage the merge queues for a repository. Can be set to 'read' or 'write'."
permission-metadata:
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'."
permission-organization-administration:
+121 -211
View File
@@ -22964,37 +22964,30 @@ var isError = (value) => objectToString.call(value) === "[object Error]";
var errorMessages = /* @__PURE__ */ new Set([
"network error",
// Chrome
"Failed to fetch",
// Chrome
"NetworkError when attempting to fetch resource.",
// Firefox
"The Internet connection appears to be offline.",
// Safari 16
"Load failed",
// Safari 17+
"Network request failed",
// `cross-fetch`
"fetch failed",
// Undici (Node.js)
"terminated",
"terminated"
// Undici (Node.js)
" A network error occurred.",
// Bun (WebKit)
"Network connection lost"
// Cloudflare Workers (fetch)
]);
function isNetworkError(error2) {
const isValid = error2 && isError(error2) && error2.name === "TypeError" && typeof error2.message === "string";
if (!isValid) {
return false;
}
const { message, stack } = error2;
if (message === "Load failed" || message.startsWith("Load failed (") && message.endsWith(")")) {
return stack === void 0 || "__sentry_captured__" in error2;
if (error2.message === "Load failed") {
return error2.stack === void 0;
}
if (message.startsWith("error sending request for url")) {
return true;
}
if (message === "Failed to fetch" || message.startsWith("Failed to fetch (") && message.endsWith(")")) {
return true;
}
return errorMessages.has(message);
return errorMessages.has(error2.message);
}
// node_modules/p-retry/index.js
@@ -23024,14 +23017,6 @@ function validateNumberOption(name, value, { min = 0, allowInfinity = false } =
throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
}
}
function validateFunctionOption(name, value) {
if (value === void 0) {
return;
}
if (typeof value !== "function") {
throw new TypeError(`Expected \`${name}\` to be a function.`);
}
}
var AbortError = class extends Error {
constructor(message) {
super();
@@ -23059,26 +23044,6 @@ function calculateRemainingTime(start, max) {
}
return max - (performance.now() - start);
}
async function delayForRetry(delay, options) {
if (delay <= 0) {
return;
}
await new Promise((resolve2, reject) => {
const onAbort = () => {
clearTimeout(timeoutToken);
options.signal?.removeEventListener("abort", onAbort);
reject(options.signal.reason);
};
const timeoutToken = setTimeout(() => {
options.signal?.removeEventListener("abort", onAbort);
resolve2();
}, delay);
if (options.unref) {
timeoutToken.unref?.();
}
options.signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function onAttemptFailure({ error: error2, attemptNumber, retriesConsumed, startTime, options }) {
const normalizedError = error2 instanceof Error ? error2 : new TypeError(`Non-error was thrown: "${error2}". You should only throw errors.`);
if (normalizedError instanceof AbortError) {
@@ -23086,60 +23051,55 @@ async function onAttemptFailure({ error: error2, attemptNumber, retriesConsumed,
}
const retriesLeft = Number.isFinite(options.retries) ? Math.max(0, options.retries - retriesConsumed) : options.retries;
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
const delayTime = calculateDelay(retriesConsumed, options);
const remainingTimeBeforeCallbacks = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTimeBeforeCallbacks <= 0) {
const context2 = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed,
retryDelay: 0
});
await options.onFailedAttempt(context2);
throw normalizedError;
}
const consumeRetryContext = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed,
retryDelay: retriesLeft > 0 ? delayTime : 0
});
const consumeRetry = await options.shouldConsumeRetry(consumeRetryContext);
const effectiveDelay = consumeRetry && retriesLeft > 0 ? delayTime : 0;
const context = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed,
retryDelay: effectiveDelay
retriesConsumed
});
await options.onFailedAttempt(context);
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
throw normalizedError;
}
const consumeRetry = await options.shouldConsumeRetry(context);
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTime <= 0 || retriesLeft <= 0) {
throw normalizedError;
}
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
throw normalizedError;
if (consumeRetry) {
throw normalizedError;
}
options.signal?.throwIfAborted();
return false;
}
if (!await options.shouldRetry(context)) {
throw normalizedError;
}
const remainingTimeAfterShouldRetry = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTimeAfterShouldRetry <= 0) {
throw normalizedError;
}
if (!consumeRetry) {
options.signal?.throwIfAborted();
return false;
}
const finalDelay = Math.min(effectiveDelay, remainingTimeAfterShouldRetry);
const delayTime = calculateDelay(retriesConsumed, options);
const finalDelay = Math.min(delayTime, remainingTime);
options.signal?.throwIfAborted();
await delayForRetry(finalDelay, options);
if (finalDelay > 0) {
await new Promise((resolve2, reject) => {
const onAbort = () => {
clearTimeout(timeoutToken);
options.signal?.removeEventListener("abort", onAbort);
reject(options.signal.reason);
};
const timeoutToken = setTimeout(() => {
options.signal?.removeEventListener("abort", onAbort);
resolve2();
}, finalDelay);
if (options.unref) {
timeoutToken.unref?.();
}
options.signal?.addEventListener("abort", onAbort, { once: true });
});
}
options.signal?.throwIfAborted();
return true;
}
@@ -23159,9 +23119,6 @@ async function pRetry(input, options = {}) {
};
options.shouldRetry ??= () => true;
options.shouldConsumeRetry ??= () => true;
validateFunctionOption("onFailedAttempt", options.onFailedAttempt);
validateFunctionOption("shouldRetry", options.shouldRetry);
validateFunctionOption("shouldConsumeRetry", options.shouldConsumeRetry);
validateNumberOption("factor", options.factor, { min: 0, allowInfinity: false });
validateNumberOption("minTimeout", options.minTimeout, { min: 0, allowInfinity: false });
validateNumberOption("maxTimeout", options.maxTimeout, { min: 0, allowInfinity: true });
@@ -23196,20 +23153,80 @@ async function pRetry(input, options = {}) {
}
// lib/main.js
async function main(clientId, privateKey, enterprise, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
if (enterprise && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
async function main(appId, privateKey, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
let parsedOwner = "";
let parsedRepositoryNames = [];
if (!owner && repositories.length === 0) {
const [owner2, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
parsedOwner = owner2;
parsedRepositoryNames = [repo];
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner2}/${repo}).`
);
}
if (owner && repositories.length === 0) {
parsedOwner = owner;
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
}
if (!owner && repositories.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories;
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
}
if (owner && repositories.length > 0) {
parsedOwner = owner;
parsedRepositoryNames = repositories;
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
}
const target = resolveInstallationTarget(enterprise, owner, repositories, core);
const auth5 = createAppAuth2({
appId: clientId,
appId,
privateKey,
request: request2
});
const { authentication, installationId, appSlug } = await pRetry(
() => getTokenFromTarget(request2, auth5, target, permissions),
createTokenRetryOptions(core, getTokenRetryDescription(target))
);
let authentication, installationId, appSlug;
if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromRepository(
request2,
auth5,
parsedOwner,
parsedRepositoryNames,
permissions
),
{
shouldRetry: ({ error: error2 }) => error2.status >= 500,
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedRepositoryNames.join(
","
)}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
}
));
} else {
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request2, auth5, parsedOwner, permissions),
{
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
}
));
}
core.setSecret(authentication.token);
core.setOutput("token", authentication.token);
core.setOutput("installation-id", installationId);
@@ -23219,102 +23236,6 @@ async function main(clientId, privateKey, enterprise, owner, repositories, permi
core.saveState("expiresAt", authentication.expiresAt);
}
}
function resolveInstallationTarget(enterprise, owner, repositories, core) {
if (enterprise) {
core.info(`Creating enterprise installation token for enterprise "${enterprise}".`);
return { type: "enterprise", enterprise };
}
if (!owner && repositories.length === 0) {
const [defaultOwner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${defaultOwner}/${repo}).`
);
return {
type: "repository",
owner: defaultOwner,
repositories: [repo]
};
}
if (owner && repositories.length === 0) {
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
return { type: "owner", owner };
}
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
if (!owner) {
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
} else {
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
}
return {
type: "repository",
owner: parsedOwner,
repositories
};
}
function getTokenRetryDescription(target) {
switch (target.type) {
case "enterprise":
return `enterprise "${target.enterprise}"`;
case "repository":
return `"${target.repositories.map((repository) => `${target.owner}/${repository}`).join(",")}"`;
case "owner":
return `"${target.owner}"`;
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function getTokenFromTarget(request2, auth5, target, permissions) {
switch (target.type) {
case "enterprise":
return getTokenFromEnterprise(request2, auth5, target.enterprise, permissions);
case "repository":
return getTokenFromRepository(
request2,
auth5,
target.owner,
target.repositories,
permissions
);
case "owner":
return getTokenFromOwner(request2, auth5, target.owner, permissions);
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function createTokenRetryOptions(core, targetDescription) {
return {
shouldRetry: ({ error: error2 }) => error2.status >= 500 || isNetworkError(error2),
onFailedAttempt: (context) => {
core.info(
`Failed to create token for ${targetDescription} (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
};
}
async function createInstallationAuthResult(auth5, installation, permissions, options = {}) {
const authentication = await auth5({
type: "installation",
installationId: installation.id,
permissions,
...options
});
return {
authentication,
installationId: installation.id,
appSlug: installation["app_slug"]
};
}
async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
const response = await request2("GET /users/{username}/installation", {
username: parsedOwner,
@@ -23322,7 +23243,14 @@ async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
hook: auth5.hook
}
});
return createInstallationAuthResult(auth5, response.data, permissions);
const authentication = await auth5({
type: "installation",
installationId: response.data.id,
permissions
});
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
async function getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames, permissions) {
const response = await request2("GET /repos/{owner}/{repo}/installation", {
@@ -23332,28 +23260,15 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi
hook: auth5.hook
}
});
return createInstallationAuthResult(auth5, response.data, permissions, {
repositoryNames: parsedRepositoryNames
const authentication = await auth5({
type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames,
permissions
});
}
async function getTokenFromEnterprise(request2, auth5, enterprise, permissions) {
let response;
try {
response = await request2("GET /enterprises/{enterprise}/installation", {
enterprise,
request: {
hook: auth5.hook
}
});
} catch (error2) {
if (error2.status === 404) {
throw new Error(
`No enterprise installation found matching the enterprise slug "${enterprise}".`
);
}
throw error2;
}
return createInstallationAuthResult(auth5, response.data, permissions);
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
// lib/request.js
@@ -23392,20 +23307,15 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
}
async function run() {
ensureNativeProxySupport();
const clientId = getInput("client-id") || getInput("app-id");
if (!clientId) {
throw new Error("The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.");
}
const appId = getInput("app-id");
const privateKey = getInput("private-key");
const enterprise = getInput("enterprise");
const owner = getInput("owner");
const repositories = getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== "");
const skipTokenRevoke = getBooleanInput("skip-token-revoke");
const permissions = getPermissionsFromInputs(process.env);
return main(
clientId,
appId,
privateKey,
enterprise,
owner,
repositories,
permissions,
+106 -160
View File
@@ -1,11 +1,9 @@
import pRetry from "p-retry";
import isNetworkError from "is-network-error";
// @ts-check
/**
* @param {string} clientId
* @param {string} appId
* @param {string} privateKey
* @param {string} enterprise
* @param {string} owner
* @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions
@@ -15,34 +13,107 @@ import isNetworkError from "is-network-error";
* @param {boolean} skipTokenRevoke
*/
export async function main(
clientId,
appId,
privateKey,
enterprise,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke,
skipTokenRevoke
) {
// Validate mutual exclusivity of enterprise with owner/repositories
if (enterprise && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
let parsedOwner = "";
let parsedRepositoryNames = [];
// If neither owner nor repositories are set, default to current repository
if (!owner && repositories.length === 0) {
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
parsedOwner = owner;
parsedRepositoryNames = [repo];
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
);
}
const target = resolveInstallationTarget(enterprise, owner, repositories, core);
// If only an owner is set, default to all repositories from that owner
if (owner && repositories.length === 0) {
parsedOwner = owner;
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
}
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
if (!owner && repositories.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories;
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}`
);
}
// If both owner and repositories are set, use those values
if (owner && repositories.length > 0) {
parsedOwner = owner;
parsedRepositoryNames = repositories;
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}`
);
}
const auth = createAppAuth({
appId: clientId,
appId,
privateKey,
request,
});
const { authentication, installationId, appSlug } = await pRetry(
() => getTokenFromTarget(request, auth, target, permissions),
createTokenRetryOptions(core, getTokenRetryDescription(target))
);
let authentication, installationId, appSlug;
// If at least one repository is set, get installation ID from that repository
if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(
() =>
getTokenFromRepository(
request,
auth,
parsedOwner,
parsedRepositoryNames,
permissions
),
{
shouldRetry: ({ error }) => error.status >= 500,
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedRepositoryNames.join(
","
)}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
}
));
} else {
// Otherwise get the installation for the owner, which can either be an organization or a user account
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request, auth, parsedOwner, permissions),
{
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
}
));
}
// Register the token with the runner as a secret to ensure it is masked in logs
core.setSecret(authentication.token);
@@ -58,125 +129,6 @@ export async function main(
}
}
function resolveInstallationTarget(enterprise, owner, repositories, core) {
if (enterprise) {
core.info(`Creating enterprise installation token for enterprise "${enterprise}".`);
return { type: "enterprise", enterprise };
}
if (!owner && repositories.length === 0) {
const [defaultOwner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${defaultOwner}/${repo}).`
);
return {
type: "repository",
owner: defaultOwner,
repositories: [repo],
};
}
if (owner && repositories.length === 0) {
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
return { type: "owner", owner };
}
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
if (!owner) {
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}`
);
} else {
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}`
);
}
return {
type: "repository",
owner: parsedOwner,
repositories,
};
}
function getTokenRetryDescription(target) {
switch (target.type) {
case "enterprise":
return `enterprise "${target.enterprise}"`;
case "repository":
return `"${target.repositories
.map((repository) => `${target.owner}/${repository}`)
.join(",")}"`;
case "owner":
return `"${target.owner}"`;
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function getTokenFromTarget(request, auth, target, permissions) {
switch (target.type) {
case "enterprise":
return getTokenFromEnterprise(request, auth, target.enterprise, permissions);
case "repository":
return getTokenFromRepository(
request,
auth,
target.owner,
target.repositories,
permissions
);
case "owner":
return getTokenFromOwner(request, auth, target.owner, permissions);
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function createTokenRetryOptions(core, targetDescription) {
return {
shouldRetry: ({ error }) => error.status >= 500 || isNetworkError(error),
onFailedAttempt: (context) => {
core.info(
`Failed to create token for ${targetDescription} (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
};
}
async function createInstallationAuthResult(
auth,
installation,
permissions,
options = {},
) {
const authentication = await auth({
type: "installation",
installationId: installation.id,
permissions,
...options,
});
return {
authentication,
installationId: installation.id,
appSlug: installation["app_slug"],
};
}
async function getTokenFromOwner(request, auth, parsedOwner, permissions) {
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
// This endpoint works for both users and organizations
@@ -187,8 +139,17 @@ async function getTokenFromOwner(request, auth, parsedOwner, permissions) {
},
});
// Get token for all repositories of the given installation
return createInstallationAuthResult(auth, response.data, permissions);
// Get token for for all repositories of the given installation
const authentication = await auth({
type: "installation",
installationId: response.data.id,
permissions,
});
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
async function getTokenFromRepository(
@@ -208,30 +169,15 @@ async function getTokenFromRepository(
});
// Get token for given repositories
return createInstallationAuthResult(auth, response.data, permissions, {
const authentication = await auth({
type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames,
permissions,
});
}
async function getTokenFromEnterprise(request, auth, enterprise, permissions) {
let response;
try {
response = await request("GET /enterprises/{enterprise}/installation", {
enterprise,
request: {
hook: auth.hook,
},
});
} catch (error) {
if (error.status === 404) {
throw new Error(
`No enterprise installation found matching the enterprise slug "${enterprise}".`
);
}
throw error;
}
// Get token for the enterprise installation
return createInstallationAuthResult(auth, response.data, permissions);
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
+2 -7
View File
@@ -18,12 +18,8 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
async function run() {
ensureNativeProxySupport();
const clientId = core.getInput("client-id") || core.getInput("app-id");
if (!clientId) {
throw new Error("The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.");
}
const appId = core.getInput("app-id");
const privateKey = core.getInput("private-key");
const enterprise = core.getInput("enterprise");
const owner = core.getInput("owner");
const repositories = core
.getInput("repositories")
@@ -36,9 +32,8 @@ async function run() {
const permissions = getPermissionsFromInputs(process.env);
return main(
clientId,
appId,
privateKey,
enterprise,
owner,
repositories,
permissions,
+652 -404
View File
File diff suppressed because it is too large Load Diff
+37 -10
View File
@@ -2,7 +2,7 @@
"name": "create-github-app-token",
"private": true,
"type": "module",
"version": "3.1.1",
"version": "3.0.0-beta.6",
"description": "GitHub Action for creating a GitHub App Installation Access Token",
"engines": {
"node": ">=24.4.0"
@@ -16,18 +16,45 @@
},
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/core": "^3.0.0",
"@octokit/auth-app": "^8.2.0",
"@octokit/request": "^10.0.8",
"is-network-error": "^1.3.2",
"p-retry": "^8.0.0"
"p-retry": "^7.1.1",
"undici": "^7.24.1"
},
"devDependencies": {
"@octokit/openapi": "^22.0.0",
"c8": "^11.0.0",
"esbuild": "^0.28.0",
"open-cli": "^9.0.0",
"undici": "^8.2.0",
"yaml": "^2.8.4"
"@octokit/openapi": "^21.0.0",
"c8": "^10.1.3",
"dotenv": "^17.3.1",
"esbuild": "^0.27.3",
"open-cli": "^8.0.0",
"yaml": "^2.8.2"
},
"release": {
"branches": [
"+([0-9]).x",
"main",
{
"name": "beta",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github",
"@semantic-release/npm",
[
"@semantic-release/git",
{
"assets": [
"package.json",
"package-lock.json",
"dist/*"
],
"message": "build(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"prerelease": true,
"prerelease-type": "beta",
"include-component-in-tag": false,
"release-type": "node",
"versioning": "prerelease"
}
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"include-component-in-tag": false,
"release-type": "node"
}
}
}
-32
View File
@@ -19,22 +19,6 @@
"write"
]
},
"artifact_metadata": {
"type": "string",
"description": "The level of permission to grant the access token to create and retrieve build artifact metadata records.",
"enum": [
"read",
"write"
]
},
"attestations": {
"type": "string",
"description": "The level of permission to create and retrieve the access token for repository attestations.",
"enum": [
"read",
"write"
]
},
"checks": {
"type": "string",
"description": "The level of permission to grant the access token for checks on code.",
@@ -75,14 +59,6 @@
"write"
]
},
"discussions": {
"type": "string",
"description": "The level of permission to grant the access token for discussions and related comments and labels.",
"enum": [
"read",
"write"
]
},
"environments": {
"type": "string",
"description": "The level of permission to grant the access token for managing repository environments.",
@@ -99,14 +75,6 @@
"write"
]
},
"merge_queues": {
"type": "string",
"description": "The level of permission to grant the access token to manage the merge queues for a repository.",
"enum": [
"read",
"write"
]
},
"metadata": {
"type": "string",
"description": "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata.",
+2 -2
View File
@@ -32,5 +32,5 @@ node --test --test-update-snapshots tests/index.js
We have tests both for the `main.js` and `post.js` scripts.
- If you do not expect an error, take [main-token-permissions-set.test.js](main-token-permissions-set.test.js) as a starting point.
- If your test has an expected error, take [main-missing-client-and-app-id.test.js](main-missing-client-and-app-id.test.js) as a starting point.
- 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.
- 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.
+5 -27
View File
@@ -11,24 +11,11 @@ snapshot.setDefaultSnapshotSerializers([
(value) => (typeof value === "string" ? value : undefined),
]);
function normalizeStderr(stderr) {
return stderr
.replaceAll(/\u001B\[[0-9;]*m/g, "")
.replaceAll(process.cwd(), "<cwd>")
.replaceAll(/:\d+:\d+/g, ":<line>:<column>");
}
// Get all files in tests directory
const files = readdirSync("tests");
// Files to ignore
const ignore = [
"index.js",
"index.js.snapshot",
"main.js",
"mock-agent.js",
"README.md",
];
const ignore = ["index.js", "index.js.snapshot", "main.js", "README.md"];
const testFiles = files.filter((file) => !ignore.includes(file)).sort();
@@ -52,19 +39,10 @@ for (const file of testFiles) {
NODE_USE_ENV_PROXY,
...env
} = process.env;
let stderr, stdout;
try {
({ stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
env,
}));
} catch (error) {
if (!(error instanceof Error) || !("stderr" in error) || !("stdout" in error)) {
throw error;
}
({ stderr, stdout } = error);
}
const trimmedStderr = normalizeStderr(stderr).replace(/\r?\n$/, "");
const { stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
env,
});
const trimmedStderr = stderr.replace(/\r?\n$/, "");
const trimmedStdout = stdout.replace(/\r?\n$/, "");
await t.test("stderr", (t) => {
if (trimmedStderr) t.assert.snapshot(trimmedStderr);
+7 -205
View File
@@ -1,43 +1,6 @@
exports[`action-deprecated-inputs.test.js > stdout 1`] = `
app-id — Use 'client-id' instead.
`;
exports[`main-app-id-fallback.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-client-id-precedence.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-custom-github-api-url.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -54,113 +17,6 @@ POST /api/v3/app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-enterprise-fail-response.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
Failed to create token for enterprise "test-enterprise" (attempt 1): GitHub API not available
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
null
`;
exports[`main-enterprise-installation-not-found.test.js > stderr 1`] = `
Error: No enterprise installation found matching the enterprise slug "test-enterprise".
at getTokenFromEnterprise (file://<cwd>/lib/main.js:<line>:<column>)
at process.processTicksAndRejections (node:internal/process/task_queues:<line>:<column>)
at async pRetry (file://<cwd>/node_modules/p-retry/index.js:<line>:<column>)
at async main (file://<cwd>/lib/main.js:<line>:<column>)
at async test (file://<cwd>/tests/main.js:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-installation-not-found.test.js:<line>:<column>
`;
exports[`main-enterprise-installation-not-found.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
Failed to create token for enterprise "test-enterprise" (attempt 1): No enterprise installation found matching the enterprise slug "test-enterprise".
::error::No enterprise installation found matching the enterprise slug "test-enterprise".
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
`;
exports[`main-enterprise-mutual-exclusivity-owner.test.js > stderr 1`] = `
Error: Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-mutual-exclusivity-owner.test.js:<line>:<column>
`;
exports[`main-enterprise-mutual-exclusivity-owner.test.js > stdout 1`] = `
::error::Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
`;
exports[`main-enterprise-mutual-exclusivity-repositories.test.js > stderr 1`] = `
Error: Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-mutual-exclusivity-repositories.test.js:<line>:<column>
`;
exports[`main-enterprise-mutual-exclusivity-repositories.test.js > stdout 1`] = `
::error::Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
`;
exports[`main-enterprise-only-success.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
null
`;
exports[`main-enterprise-token-permissions-set.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
{"permissions":{"enterprise_custom_properties_for_organizations":"read"}}
`;
exports[`main-missing-client-and-app-id.test.js > stderr 1`] = `
The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.
`;
exports[`main-missing-client-and-app-id.test.js > stdout 1`] = `
::error::The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.
`;
exports[`main-missing-owner.test.js > stderr 1`] = `
GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'
`;
@@ -201,6 +57,7 @@ exports[`main-repo-skew.test.js > stderr 1`] = `
exports[`main-repo-skew.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -218,45 +75,6 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]}
`;
exports[`main-token-get-owner-set-client-error.test.js > stderr 1`] = `
RequestError [HttpError]: Forbidden
at fetchWrapper (file://<cwd>/node_modules/@octokit/request/dist-bundle/index.js:<line>:<column>)
at process.processTicksAndRejections (node:internal/process/task_queues:<line>:<column>)
at async hook (file://<cwd>/node_modules/@octokit/auth-app/dist-node/index.js:<line>:<column>)
at async getTokenFromOwner (file://<cwd>/lib/main.js:<line>:<column>)
at async pRetry (file://<cwd>/node_modules/p-retry/index.js:<line>:<column>)
at async main (file://<cwd>/lib/main.js:<line>:<column>)
at async test (file://<cwd>/tests/main.js:<line>:<column>)
at async file://<cwd>/tests/main-token-get-owner-set-client-error.test.js:<line>:<column> {
status: 403,
request: {
method: 'GET',
url: 'https://api.github.com/users/smockle/installation',
headers: {
accept: 'application/vnd.github.v3+json',
'user-agent': 'actions/create-github-app-token',
authorization: 'bearer [REDACTED]'
},
request: { hook: [Function: bound hook] AsyncFunction }
},
response: {
url: 'https://api.github.com/users/smockle/installation',
status: 403,
headers: { 'content-type': 'application/json' },
data: { message: 'Forbidden' }
},
[cause]: undefined
}
`;
exports[`main-token-get-owner-set-client-error.test.js > stdout 1`] = `
Input 'repositories' is not set. Creating token for all repositories owned by smockle.
Failed to create token for "smockle" (attempt 1): Forbidden
::error::Forbidden
--- REQUESTS ---
GET /users/smockle/installation
`;
exports[`main-token-get-owner-set-fail-response.test.js > stdout 1`] = `
Input 'repositories' is not set. Creating token for all repositories owned by smockle.
Failed to create token for "smockle" (attempt 1): GitHub API not available
@@ -278,8 +96,9 @@ null
exports[`main-token-get-owner-set-repo-fail-response.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo
Failed to create token for "actions/failed-repo" (attempt 1): GitHub API not available
Failed to create token for "failed-repo" (attempt 1): GitHub API not available
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -296,28 +115,9 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]}
`;
exports[`main-token-get-owner-set-repo-network-error.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/network-repo
Failed to create token for "actions/network-repo" (attempt 1): fetch failed
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/network-repo/installation
GET /repos/actions/network-repo/installation
POST /app/installations/123456/access_tokens
{"repositories":["network-repo"]}
`;
exports[`main-token-get-owner-set-repo-set-to-many-newline.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
- actions/toolkit
- actions/checkout
@@ -338,6 +138,7 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-many.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
- actions/toolkit
- actions/checkout
@@ -358,6 +159,7 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-one.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
-11
View File
@@ -1,11 +0,0 @@
import { DEFAULT_ENV, test } from "./main.js";
// Verify `main` falls back to `app-id` when `client-id` is not set
await test(
() => {},
{
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "",
"INPUT_APP-ID": "123456",
}
);
-11
View File
@@ -1,11 +0,0 @@
import { DEFAULT_ENV, test } from "./main.js";
// Verify `client-id` takes precedence when both `client-id` and `app-id` are set
await test(
() => {},
{
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
"INPUT_APP-ID": "123456",
}
);
@@ -1,39 +0,0 @@
import { test } from "./main.js";
// Verify enterprise installation lookup retries when the GitHub API returns a 500 error.
await test((mockPool) => {
process.env.INPUT_ENTERPRISE = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/enterprises/test-enterprise/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(500, "GitHub API not available");
mockPool
.intercept({
path: "/enterprises/test-enterprise/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } },
);
});
@@ -1,25 +0,0 @@
import { test } from "./main.js";
// Verify `main` handles when no enterprise installation is found.
await test((mockPool) => {
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
process.env.INPUT_ENTERPRISE = "test-enterprise";
// Mock the enterprise installation endpoint to return no matching installation
mockPool
.intercept({
path: "/enterprises/test-enterprise/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
404,
{ message: "Not Found" },
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,13 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
// Verify `main` exits with an error when `enterprise` is used with `owner` input.
// Set up environment with enterprise and owner set
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}
process.env.INPUT_ENTERPRISE = "test-enterprise";
process.env.INPUT_OWNER = "test-owner";
const { default: promise } = await import("../main.js");
await promise;
@@ -1,13 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
// Verify `main` exits with an error when `enterprise` is used with `repositories` input.
// Set up environment with enterprise and repositories set
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}
process.env.INPUT_ENTERPRISE = "test-enterprise";
process.env.INPUT_REPOSITORIES = "repo1,repo2";
const { default: promise } = await import("../main.js");
await promise;
@@ -1,30 +0,0 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when only the `enterprise` input is set.
await test((mockPool) => {
process.env.INPUT_ENTERPRISE = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
// Mock the enterprise installation endpoint
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/enterprises/test-enterprise/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{
id: mockInstallationId,
app_slug: mockAppSlug,
},
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,34 +0,0 @@
import { test } from "./main.js";
// Use a declared enterprise permission from the generated schema to verify
// enterprise token requests forward permission inputs to token creation.
await test((mockPool) => {
process.env.INPUT_ENTERPRISE = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
process.env[
"INPUT_PERMISSION-ENTERPRISE-CUSTOM-PROPERTIES-FOR-ORGANIZATIONS"
] = "read";
// Mock the enterprise installation endpoint
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/enterprises/test-enterprise/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{
id: mockInstallationId,
app_slug: mockAppSlug,
},
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,20 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
for (const [key, value] of Object.entries({
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "",
"INPUT_APP-ID": "",
})) {
process.env[key] = value;
}
// Log only the error message, not the full stack trace, because the stack
// trace contains environment-specific paths and ANSI codes that differ
// between local and CI environments.
const _error = console.error;
console.error = (err) => _error(err?.message ?? err);
// Verify `main` exits with an error when neither `client-id` nor `app-id` is set.
const { default: promise } = await import("../main.js");
await promise;
process.exitCode = 0;
@@ -1,23 +0,0 @@
import { test } from "./main.js";
// Verify client errors are not retried when getting a token for a user or organization.
await test((mockPool) => {
process.env.INPUT_OWNER = "smockle";
delete process.env.INPUT_REPOSITORIES;
mockPool
.intercept({
path: "/users/smockle/installation",
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
403,
{ message: "Forbidden" },
{ headers: { "content-type": "application/json" } },
);
});
@@ -1,39 +0,0 @@
import { test } from "./main.js";
// Verify transient network errors are retried when getting a repository token.
await test((mockPool) => {
process.env.INPUT_OWNER = "actions";
process.env.INPUT_REPOSITORIES = "network-repo";
const owner = process.env.INPUT_OWNER;
const repo = process.env.INPUT_REPOSITORIES;
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: `/repos/${owner}/${repo}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.replyWithError(new TypeError("fetch failed"));
mockPool
.intercept({
path: `/repos/${owner}/${repo}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } },
);
});
+5 -3
View File
@@ -1,6 +1,6 @@
// Base for all `main` tests.
// @ts-check
import { createMockAgent } from "./mock-agent.js";
import { MockAgent, setGlobalDispatcher } from "undici";
export const DEFAULT_ENV = {
GITHUB_REPOSITORY_OWNER: "actions",
@@ -9,7 +9,7 @@ export const DEFAULT_ENV = {
// https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
"INPUT_GITHUB-API-URL": "https://api.github.com",
"INPUT_SKIP-TOKEN-REVOKE": "false",
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
"INPUT_APP-ID": "123456",
// This key is invalidated. Its from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
"INPUT_PRIVATE-KEY": `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL
@@ -50,7 +50,9 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
// Set up mocking
const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]);
const basePath = baseUrl.pathname === "/" ? "" : baseUrl.pathname;
const mockAgent = createMockAgent({ enableCallHistory: true });
const mockAgent = new MockAgent({ enableCallHistory: true });
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const mockPool = mockAgent.get(baseUrl.origin);
// Calling `auth({ type: "app" })` to obtain a JWT doesnt make network requests, so no need to intercept.
-12
View File
@@ -1,12 +0,0 @@
import { install, MockAgent, setGlobalDispatcher } from "undici";
// Ensure MockAgent intercepts requests made through global fetch.
install();
export function createMockAgent(options) {
const mockAgent = new MockAgent(options);
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
return mockAgent;
}
@@ -1,4 +1,4 @@
import { createMockAgent } from "./mock-agent.js";
import { MockAgent, setGlobalDispatcher } from "undici";
// state variables are set as environment variables with the prefix STATE_
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions
@@ -14,7 +14,9 @@ process.env.STATE_expiresAt = new Date(
Date.now() + 1000 * 60 * 60
).toISOString();
const mockAgent = createMockAgent();
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com");
+4 -2
View File
@@ -1,4 +1,4 @@
import { createMockAgent } from "./mock-agent.js";
import { MockAgent, setGlobalDispatcher } from "undici";
// state variables are set as environment variables with the prefix STATE_
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions
@@ -11,7 +11,9 @@ process.env.STATE_expiresAt = new Date(Date.now() - 1000 * 60 * 60).toISOString(
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false";
const mockAgent = createMockAgent();
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com");
+4 -2
View File
@@ -1,4 +1,4 @@
import { createMockAgent } from "./mock-agent.js";
import { MockAgent, setGlobalDispatcher } from "undici";
// state variables are set as environment variables with the prefix STATE_
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions
@@ -12,7 +12,9 @@ process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false";
// 1 hour in the future, not expired
process.env.STATE_expiresAt = new Date(Date.now() + 1000 * 60 * 60).toISOString();
const mockAgent = createMockAgent();
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com");
+4 -2
View File
@@ -1,4 +1,4 @@
import { createMockAgent } from "./mock-agent.js";
import { MockAgent, setGlobalDispatcher } from "undici";
// state variables are set as environment variables with the prefix STATE_
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions
@@ -8,7 +8,9 @@ process.env.STATE_token = "secret123";
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
process.env["INPUT_SKIP-TOKEN-REVOKE"] = "true";
const mockAgent = createMockAgent();
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com");