Compare commits

..

4 Commits

Author SHA1 Message Date
Parker Brown ea0d3155e4 refactor: rename internal appId variable to clientId
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 23:05:19 -07:00
copilot-swe-agent[bot] 37f42c53d0 feat: add client-id input and deprecate app-id
Co-authored-by: parkerbxyz <17183625+parkerbxyz@users.noreply.github.com>
2026-03-18 20:32:46 +00:00
copilot-swe-agent[bot] 8204e76db8 docs: document client ID support for app-id
Co-authored-by: parkerbxyz <17183625+parkerbxyz@users.noreply.github.com>
2026-03-18 20:13:20 +00:00
copilot-swe-agent[bot] e9da44231a Initial plan 2026-03-18 20:10:08 +00:00
37 changed files with 1190 additions and 1368 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@v6
- uses: actions/setup-node@v6
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 .env
coverage coverage
node_modules/ node_modules/
.DS_Store
-3
View File
@@ -1,3 +0,0 @@
{
".": "3.1.1"
}
+32 -54
View File
@@ -9,8 +9,10 @@ GitHub Action for creating a GitHub App installation access token.
In order to use this action, you need to: 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). 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`). 2. [Store the App's Client ID in your repository environment variables](https://docs.github.com/actions/learn-github-actions/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`). 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`).
Pass the App's Client ID using the `client-id` input. The legacy `app-id` input remains available for compatibility, but is deprecated.
> [!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. > 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.
@@ -32,7 +34,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- uses: ./actions/staging-tests - uses: ./actions/staging-tests
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
@@ -52,7 +54,7 @@ jobs:
with: with:
# required # required
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
@@ -78,7 +80,7 @@ jobs:
with: with:
# required # required
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- name: Get GitHub App User ID - name: Get GitHub App User ID
id: get-user-id id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -103,7 +105,7 @@ jobs:
with: with:
# required # required
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- name: Get GitHub App User ID - name: Get GitHub App User ID
id: get-user-id id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -139,7 +141,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v4 - uses: peter-evans/create-or-update-comment@v4
with: with:
@@ -161,7 +163,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
repositories: | repositories: |
repo1 repo1
@@ -186,7 +188,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: another-owner owner: another-owner
- uses: peter-evans/create-or-update-comment@v4 - uses: peter-evans/create-or-update-comment@v4
with: with:
@@ -195,32 +197,10 @@ jobs:
body: "Hello, World!" 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 ### Create a token with specific permissions
> [!NOTE] > [!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 ```yaml
on: [issues] on: [issues]
@@ -233,7 +213,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
permission-issues: write permission-issues: write
- uses: peter-evans/create-or-update-comment@v4 - uses: peter-evans/create-or-update-comment@v4
@@ -275,7 +255,7 @@ jobs:
id: app-token id: app-token
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ matrix.owners-and-repos.owner }} owner: ${{ matrix.owners-and-repos.owner }}
repositories: ${{ join(matrix.owners-and-repos.repos) }} repositories: ${{ join(matrix.owners-and-repos.repos) }}
- uses: octokit/request-action@v2.x - uses: octokit/request-action@v2.x
@@ -333,17 +313,23 @@ If you set `HTTP_PROXY` or `HTTPS_PROXY`, also set `NODE_USE_ENV_PROXY: "1"` on
NODE_USE_ENV_PROXY: "1" NODE_USE_ENV_PROXY: "1"
with: with:
client-id: ${{ vars.APP_CLIENT_ID }} client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
``` ```
## Inputs ## Inputs
### `client-id` or `app-id` ### `client-id`
**Required:** GitHub App Client ID. **Optional:** GitHub App Client ID. This is the recommended input.
> [!NOTE] ### `app-id`
> The legacy `app-id` input is also accepted, but `client-id` is recommended.
**Optional:** GitHub App ID.
> [!WARNING]
> `app-id` is deprecated. Use `client-id` instead.
You must set either `client-id` or `app-id`. If both are set, `client-id` takes precedence.
### `private-key` ### `private-key`
@@ -356,7 +342,7 @@ steps:
- name: Decode the GitHub App Private Key - name: Decode the GitHub App Private Key
id: decode id: decode
run: | 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 "::add-mask::$private_key"
echo "private-key=$private_key" >> "$GITHUB_OUTPUT" echo "private-key=$private_key" >> "$GITHUB_OUTPUT"
- name: Generate GitHub App Token - name: Generate GitHub App Token
@@ -378,13 +364,6 @@ steps:
> [!NOTE] > [!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. > 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>` ### `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`). **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 +394,13 @@ GitHub App slug.
## How it works ## 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 is scoped to the current repository or `repositories` if set.
2. The token inherits all the installation's permissions.
1. The token inherits all the installation's permissions. 3. The token is set as output `token` which can be used in subsequent steps.
2. 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.
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. 5. The token is masked, it cannot be logged accidentally.
4. The token is masked, it cannot be logged accidentally.
> [!NOTE] > [!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. > 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.
-11
View File
@@ -21,9 +21,6 @@ inputs:
repositories: repositories:
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)" description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
required: false 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: skip-token-revoke:
description: "If true, the token will not be revoked when the current job is complete" description: "If true, the token will not be revoked when the current job is complete"
required: false required: false
@@ -38,10 +35,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'." 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: 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'." 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: permission-checks:
description: "The level of permission to grant the access token for checks on code. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token for checks on code. Can be set to 'read' or 'write'."
permission-codespaces: permission-codespaces:
@@ -54,8 +47,6 @@ inputs:
description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'."
permission-deployments: permission-deployments:
description: "The level of permission to grant the access token for deployments and deployment statuses. Can be set to 'read' or 'write'." 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: 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'." 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: permission-enterprise-custom-properties-for-organizations:
@@ -74,8 +65,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'." 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: permission-members:
description: "The level of permission to grant the access token for organization teams and members. Can be set to 'read' or 'write'." 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: 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'." 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: permission-organization-administration:
+119 -206
View File
@@ -22964,37 +22964,30 @@ var isError = (value) => objectToString.call(value) === "[object Error]";
var errorMessages = /* @__PURE__ */ new Set([ var errorMessages = /* @__PURE__ */ new Set([
"network error", "network error",
// Chrome // Chrome
"Failed to fetch",
// Chrome
"NetworkError when attempting to fetch resource.", "NetworkError when attempting to fetch resource.",
// Firefox // Firefox
"The Internet connection appears to be offline.", "The Internet connection appears to be offline.",
// Safari 16 // Safari 16
"Load failed",
// Safari 17+
"Network request failed", "Network request failed",
// `cross-fetch` // `cross-fetch`
"fetch failed", "fetch failed",
// Undici (Node.js) // Undici (Node.js)
"terminated", "terminated"
// Undici (Node.js) // Undici (Node.js)
" A network error occurred.",
// Bun (WebKit)
"Network connection lost"
// Cloudflare Workers (fetch)
]); ]);
function isNetworkError(error2) { function isNetworkError(error2) {
const isValid = error2 && isError(error2) && error2.name === "TypeError" && typeof error2.message === "string"; const isValid = error2 && isError(error2) && error2.name === "TypeError" && typeof error2.message === "string";
if (!isValid) { if (!isValid) {
return false; return false;
} }
const { message, stack } = error2; if (error2.message === "Load failed") {
if (message === "Load failed" || message.startsWith("Load failed (") && message.endsWith(")")) { return error2.stack === void 0;
return stack === void 0 || "__sentry_captured__" in error2;
} }
if (message.startsWith("error sending request for url")) { return errorMessages.has(error2.message);
return true;
}
if (message === "Failed to fetch" || message.startsWith("Failed to fetch (") && message.endsWith(")")) {
return true;
}
return errorMessages.has(message);
} }
// node_modules/p-retry/index.js // 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}.`); 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 { var AbortError = class extends Error {
constructor(message) { constructor(message) {
super(); super();
@@ -23059,26 +23044,6 @@ function calculateRemainingTime(start, max) {
} }
return max - (performance.now() - start); 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 }) { 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.`); const normalizedError = error2 instanceof Error ? error2 : new TypeError(`Non-error was thrown: "${error2}". You should only throw errors.`);
if (normalizedError instanceof AbortError) { 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 retriesLeft = Number.isFinite(options.retries) ? Math.max(0, options.retries - retriesConsumed) : options.retries;
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY; 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({ const context = Object.freeze({
error: normalizedError, error: normalizedError,
attemptNumber, attemptNumber,
retriesLeft, retriesLeft,
retriesConsumed, retriesConsumed
retryDelay: effectiveDelay
}); });
await options.onFailedAttempt(context); await options.onFailedAttempt(context);
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) { if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
throw normalizedError; throw normalizedError;
} }
const consumeRetry = await options.shouldConsumeRetry(context);
const remainingTime = calculateRemainingTime(startTime, maxRetryTime); const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTime <= 0 || retriesLeft <= 0) { if (remainingTime <= 0 || retriesLeft <= 0) {
throw normalizedError; throw normalizedError;
} }
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) { if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
throw normalizedError; if (consumeRetry) {
throw normalizedError;
}
options.signal?.throwIfAborted();
return false;
} }
if (!await options.shouldRetry(context)) { if (!await options.shouldRetry(context)) {
throw normalizedError; throw normalizedError;
} }
const remainingTimeAfterShouldRetry = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTimeAfterShouldRetry <= 0) {
throw normalizedError;
}
if (!consumeRetry) { if (!consumeRetry) {
options.signal?.throwIfAborted(); options.signal?.throwIfAborted();
return false; return false;
} }
const finalDelay = Math.min(effectiveDelay, remainingTimeAfterShouldRetry); const delayTime = calculateDelay(retriesConsumed, options);
const finalDelay = Math.min(delayTime, remainingTime);
options.signal?.throwIfAborted(); 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(); options.signal?.throwIfAborted();
return true; return true;
} }
@@ -23159,9 +23119,6 @@ async function pRetry(input, options = {}) {
}; };
options.shouldRetry ??= () => true; options.shouldRetry ??= () => true;
options.shouldConsumeRetry ??= () => 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("factor", options.factor, { min: 0, allowInfinity: false });
validateNumberOption("minTimeout", options.minTimeout, { min: 0, allowInfinity: false }); validateNumberOption("minTimeout", options.minTimeout, { min: 0, allowInfinity: false });
validateNumberOption("maxTimeout", options.maxTimeout, { min: 0, allowInfinity: true }); validateNumberOption("maxTimeout", options.maxTimeout, { min: 0, allowInfinity: true });
@@ -23196,20 +23153,80 @@ async function pRetry(input, options = {}) {
} }
// lib/main.js // lib/main.js
async function main(clientId, privateKey, enterprise, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) { async function main(clientId, privateKey, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
if (enterprise && (owner || repositories.length > 0)) { let parsedOwner = "";
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs"); 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({ const auth5 = createAppAuth2({
appId: clientId, appId: clientId,
privateKey, privateKey,
request: request2 request: request2
}); });
const { authentication, installationId, appSlug } = await pRetry( let authentication, installationId, appSlug;
() => getTokenFromTarget(request2, auth5, target, permissions), if (parsedRepositoryNames.length > 0) {
createTokenRetryOptions(core, getTokenRetryDescription(target)) ({ 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.setSecret(authentication.token);
core.setOutput("token", authentication.token); core.setOutput("token", authentication.token);
core.setOutput("installation-id", installationId); core.setOutput("installation-id", installationId);
@@ -23219,102 +23236,6 @@ async function main(clientId, privateKey, enterprise, owner, repositories, permi
core.saveState("expiresAt", authentication.expiresAt); 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) { async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
const response = await request2("GET /users/{username}/installation", { const response = await request2("GET /users/{username}/installation", {
username: parsedOwner, username: parsedOwner,
@@ -23322,7 +23243,14 @@ async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
hook: auth5.hook 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) { async function getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames, permissions) {
const response = await request2("GET /repos/{owner}/{repo}/installation", { const response = await request2("GET /repos/{owner}/{repo}/installation", {
@@ -23332,28 +23260,15 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi
hook: auth5.hook hook: auth5.hook
} }
}); });
return createInstallationAuthResult(auth5, response.data, permissions, { const authentication = await auth5({
repositoryNames: parsedRepositoryNames type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames,
permissions
}); });
} const installationId = response.data.id;
async function getTokenFromEnterprise(request2, auth5, enterprise, permissions) { const appSlug = response.data["app_slug"];
let response; return { authentication, installationId, appSlug };
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);
} }
// lib/request.js // lib/request.js
@@ -23394,10 +23309,9 @@ async function run() {
ensureNativeProxySupport(); ensureNativeProxySupport();
const clientId = getInput("client-id") || getInput("app-id"); const clientId = getInput("client-id") || getInput("app-id");
if (!clientId) { 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."); throw new Error("Either 'client-id' or 'app-id' input must be set");
} }
const privateKey = getInput("private-key"); const privateKey = getInput("private-key");
const enterprise = getInput("enterprise");
const owner = getInput("owner"); const owner = getInput("owner");
const repositories = getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== ""); const repositories = getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== "");
const skipTokenRevoke = getBooleanInput("skip-token-revoke"); const skipTokenRevoke = getBooleanInput("skip-token-revoke");
@@ -23405,7 +23319,6 @@ async function run() {
return main( return main(
clientId, clientId,
privateKey, privateKey,
enterprise,
owner, owner,
repositories, repositories,
permissions, permissions,
+103 -157
View File
@@ -1,11 +1,9 @@
import pRetry from "p-retry"; import pRetry from "p-retry";
import isNetworkError from "is-network-error";
// @ts-check // @ts-check
/** /**
* @param {string} clientId * @param {string} clientId
* @param {string} privateKey * @param {string} privateKey
* @param {string} enterprise
* @param {string} owner * @param {string} owner
* @param {string[]} repositories * @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions * @param {undefined | Record<string, string>} permissions
@@ -17,21 +15,59 @@ import isNetworkError from "is-network-error";
export async function main( export async function main(
clientId, clientId,
privateKey, privateKey,
enterprise,
owner, owner,
repositories, repositories,
permissions, permissions,
core, core,
createAppAuth, createAppAuth,
request, request,
skipTokenRevoke, skipTokenRevoke
) { ) {
// Validate mutual exclusivity of enterprise with owner/repositories let parsedOwner = "";
if (enterprise && (owner || repositories.length > 0)) { let parsedRepositoryNames = [];
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
// 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({ const auth = createAppAuth({
appId: clientId, appId: clientId,
@@ -39,10 +75,45 @@ export async function main(
request, request,
}); });
const { authentication, installationId, appSlug } = await pRetry( let authentication, installationId, appSlug;
() => getTokenFromTarget(request, auth, target, permissions), // If at least one repository is set, get installation ID from that repository
createTokenRetryOptions(core, getTokenRetryDescription(target))
); 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 // Register the token with the runner as a secret to ensure it is masked in logs
core.setSecret(authentication.token); 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) { 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 // 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 // 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 // Get token for for all repositories of the given installation
return createInstallationAuthResult(auth, response.data, permissions); 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( async function getTokenFromRepository(
@@ -208,30 +169,15 @@ async function getTokenFromRepository(
}); });
// Get token for given repositories // Get token for given repositories
return createInstallationAuthResult(auth, response.data, permissions, { const authentication = await auth({
type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames, repositoryNames: parsedRepositoryNames,
permissions,
}); });
}
const installationId = response.data.id;
async function getTokenFromEnterprise(request, auth, enterprise, permissions) { const appSlug = response.data["app_slug"];
let response;
try { return { authentication, installationId, appSlug };
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);
} }
+1 -3
View File
@@ -20,10 +20,9 @@ async function run() {
const clientId = core.getInput("client-id") || core.getInput("app-id"); const clientId = core.getInput("client-id") || core.getInput("app-id");
if (!clientId) { 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."); throw new Error("Either 'client-id' or 'app-id' input must be set");
} }
const privateKey = core.getInput("private-key"); const privateKey = core.getInput("private-key");
const enterprise = core.getInput("enterprise");
const owner = core.getInput("owner"); const owner = core.getInput("owner");
const repositories = core const repositories = core
.getInput("repositories") .getInput("repositories")
@@ -38,7 +37,6 @@ async function run() {
return main( return main(
clientId, clientId,
privateKey, privateKey,
enterprise,
owner, owner,
repositories, repositories,
permissions, permissions,
+638 -403
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", "name": "create-github-app-token",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "3.1.1", "version": "3.0.0",
"description": "GitHub Action for creating a GitHub App Installation Access Token", "description": "GitHub Action for creating a GitHub App Installation Access Token",
"engines": { "engines": {
"node": ">=24.4.0" "node": ">=24.4.0"
@@ -16,18 +16,45 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^3.0.1", "@actions/core": "^3.0.0",
"@octokit/auth-app": "^8.2.0", "@octokit/auth-app": "^8.2.0",
"@octokit/request": "^10.0.8", "@octokit/request": "^10.0.8",
"is-network-error": "^1.3.2", "p-retry": "^7.1.1"
"p-retry": "^8.0.0"
}, },
"devDependencies": { "devDependencies": {
"@octokit/openapi": "^22.0.0", "@octokit/openapi": "^21.0.0",
"c8": "^11.0.0", "c8": "^10.1.3",
"esbuild": "^0.28.0", "esbuild": "^0.27.3",
"open-cli": "^9.0.0", "open-cli": "^8.0.0",
"undici": "^8.2.0", "undici": "^7.24.1",
"yaml": "^2.8.4" "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-plugin-github-breaking-version-tag",
[
"@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" "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": { "checks": {
"type": "string", "type": "string",
"description": "The level of permission to grant the access token for checks on code.", "description": "The level of permission to grant the access token for checks on code.",
@@ -75,14 +59,6 @@
"write" "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": { "environments": {
"type": "string", "type": "string",
"description": "The level of permission to grant the access token for managing repository environments.", "description": "The level of permission to grant the access token for managing repository environments.",
@@ -99,14 +75,6 @@
"write" "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": { "metadata": {
"type": "string", "type": "string",
"description": "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata.", "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. 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 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-client-and-app-id.test.js](main-missing-client-and-app-id.test.js) as a starting point. - If your test has an expected error, take [main-missing-client-and-app-id.test.js](tests/main-missing-client-and-app-id.test.js) as a starting point.
+5 -27
View File
@@ -11,24 +11,11 @@ snapshot.setDefaultSnapshotSerializers([
(value) => (typeof value === "string" ? value : undefined), (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 // Get all files in tests directory
const files = readdirSync("tests"); const files = readdirSync("tests");
// Files to ignore // Files to ignore
const ignore = [ const ignore = ["index.js", "index.js.snapshot", "main.js", "README.md"];
"index.js",
"index.js.snapshot",
"main.js",
"mock-agent.js",
"README.md",
];
const testFiles = files.filter((file) => !ignore.includes(file)).sort(); const testFiles = files.filter((file) => !ignore.includes(file)).sort();
@@ -52,19 +39,10 @@ for (const file of testFiles) {
NODE_USE_ENV_PROXY, NODE_USE_ENV_PROXY,
...env ...env
} = process.env; } = process.env;
let stderr, stdout; const { stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
try { env,
({ stderr, stdout } = await execFileAsync("node", [`tests/${file}`], { });
env, const trimmedStderr = stderr.replace(/\r?\n$/, "");
}));
} 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 trimmedStdout = stdout.replace(/\r?\n$/, ""); const trimmedStdout = stdout.replace(/\r?\n$/, "");
await t.test("stderr", (t) => { await t.test("stderr", (t) => {
if (trimmedStderr) t.assert.snapshot(trimmedStderr); if (trimmedStderr) t.assert.snapshot(trimmedStderr);
+15 -179
View File
@@ -2,24 +2,7 @@ exports[`action-deprecated-inputs.test.js > stdout 1`] = `
app-id — Use 'client-id' instead. app-id — Use 'client-id' instead.
`; `;
exports[`main-app-id-fallback.test.js > stdout 1`] = ` exports[`main-client-id.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). Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -38,6 +21,7 @@ POST /app/installations/123456/access_tokens
exports[`main-custom-github-api-url.test.js > stdout 1`] = ` exports[`main-custom-github-api-url.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token - actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -54,111 +38,17 @@ POST /api/v3/app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]} {"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`] = ` 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. Error: Either 'client-id' or 'app-id' input must be set
at run (file:///home/runner/work/create-github-app-token/create-github-app-token/main.js:23:11)
at file:///home/runner/work/create-github-app-token/create-github-app-token/main.js:51:16
 at ModuleJob.run (node:internal/modules/esm/module_job:430:25)
 at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:661:26)
at async file:///home/runner/work/create-github-app-token/create-github-app-token/tests/main-missing-client-and-app-id.test.js:12:30
`; `;
exports[`main-missing-client-and-app-id.test.js > stdout 1`] = ` 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. ::error::Either 'client-id' or 'app-id' input must be set
`; `;
exports[`main-missing-owner.test.js > stderr 1`] = ` exports[`main-missing-owner.test.js > stderr 1`] = `
@@ -201,6 +91,7 @@ exports[`main-repo-skew.test.js > stderr 1`] = `
exports[`main-repo-skew.test.js > stdout 1`] = ` exports[`main-repo-skew.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo - actions/failed-repo
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -218,45 +109,6 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]} {"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`] = ` 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. 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 Failed to create token for "smockle" (attempt 1): GitHub API not available
@@ -278,8 +130,9 @@ null
exports[`main-token-get-owner-set-repo-fail-response.test.js > stdout 1`] = ` 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: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo - 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 ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -296,28 +149,9 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]} {"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`] = ` 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: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token - actions/create-github-app-token
- actions/toolkit - actions/toolkit
- actions/checkout - actions/checkout
@@ -338,6 +172,7 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-many.test.js > stdout 1`] = ` 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: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token - actions/create-github-app-token
- actions/toolkit - actions/toolkit
- actions/checkout - actions/checkout
@@ -358,6 +193,7 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-one.test.js > stdout 1`] = ` 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: Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token - actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::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",
}
);
@@ -1,11 +1,11 @@
import { DEFAULT_ENV, test } from "./main.js"; import { DEFAULT_ENV, test } from "./main.js";
// Verify `client-id` takes precedence when both `client-id` and `app-id` are set // Verify `main` accepts a GitHub App client ID via the `client-id` input
await test( await test(
() => {}, () => {},
{ {
...DEFAULT_ENV, ...DEFAULT_ENV,
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef", "INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
"INPUT_APP-ID": "123456", "INPUT_APP-ID": "",
} }
); );
@@ -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" } }
);
});
@@ -8,12 +8,6 @@ for (const [key, value] of Object.entries({
process.env[key] = value; 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. // Verify `main` exits with an error when neither `client-id` nor `app-id` is set.
const { default: promise } = await import("../main.js"); const { default: promise } = await import("../main.js");
await promise; await promise;
@@ -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. // Base for all `main` tests.
// @ts-check // @ts-check
import { createMockAgent } from "./mock-agent.js"; import { MockAgent, setGlobalDispatcher } from "undici";
export const DEFAULT_ENV = { export const DEFAULT_ENV = {
GITHUB_REPOSITORY_OWNER: "actions", 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 // https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
"INPUT_GITHUB-API-URL": "https://api.github.com", "INPUT_GITHUB-API-URL": "https://api.github.com",
"INPUT_SKIP-TOKEN-REVOKE": "false", "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. // This key is invalidated. Its from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
"INPUT_PRIVATE-KEY": `-----BEGIN RSA PRIVATE KEY----- "INPUT_PRIVATE-KEY": `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL
@@ -50,7 +50,9 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
// Set up mocking // Set up mocking
const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]); const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]);
const basePath = baseUrl.pathname === "/" ? "" : baseUrl.pathname; 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); const mockPool = mockAgent.get(baseUrl.origin);
// Calling `auth({ type: "app" })` to obtain a JWT doesnt make network requests, so no need to intercept. // 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_ // 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 // 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 Date.now() + 1000 * 60 * 60
).toISOString(); ).toISOString();
const mockAgent = createMockAgent(); const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request // Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com"); 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_ // 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 // 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 // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false"; process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false";
const mockAgent = createMockAgent(); const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request // Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com"); 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_ // 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 // 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 // 1 hour in the future, not expired
process.env.STATE_expiresAt = new Date(Date.now() + 1000 * 60 * 60).toISOString(); 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 // Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com"); 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_ // 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 // 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 // https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
process.env["INPUT_SKIP-TOKEN-REVOKE"] = "true"; process.env["INPUT_SKIP-TOKEN-REVOKE"] = "true";
const mockAgent = createMockAgent(); const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Provide the base url to the request // Provide the base url to the request
const mockPool = mockAgent.get("https://api.github.com"); const mockPool = mockAgent.get("https://api.github.com");