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
30 changed files with 983 additions and 1404 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
+12 -53
View File
@@ -1,7 +1,6 @@
name: release name: release
on: on:
workflow_dispatch:
push: push:
branches: branches:
- "*.x" - "*.x"
@@ -18,64 +17,24 @@ jobs:
name: release name: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# build local version to create token
- uses: actions/checkout@v6 - 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: ./ - uses: ./
id: app-token id: app-token
with: with:
app-id: ${{ vars.RELEASER_APP_ID }} app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
# install release dependencies and release
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 - run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag
id: release-please - run: npx semantic-release --debug
with:
token: ${{ steps.app-token.outputs.token }}
config-file: ${{ github.ref_name == 'beta' && 'release-please-config.beta.json' || 'release-please-config.json' }}
manifest-file: .release-please-manifest.json
target-branch: ${{ github.ref_name }}
- uses: actions/checkout@v6
if: steps.release-please.outputs.prs_created == 'true'
with:
ref: ${{ fromJSON(steps.release-please.outputs.pr).headBranchName }}
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-node@v6
if: steps.release-please.outputs.prs_created == 'true'
with:
node-version-file: package.json
- run: npm ci
if: steps.release-please.outputs.prs_created == 'true'
- run: npm run build
if: steps.release-please.outputs.prs_created == 'true'
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
if: steps.release-please.outputs.prs_created == 'true'
with:
commit_author: "${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>"
commit_message: "chore: update dist files"
file_pattern: dist/**
- name: Update major version tag
id: update-major-tag
if: steps.release-please.outputs.release_created == 'true' && github.ref_name != 'beta'
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
continue-on-error: true
with:
route: PATCH /repos/${{ github.repository }}/git/refs/tags/v${{ steps.release-please.outputs.major }}
sha: ${{ steps.release-please.outputs.sha }}
force: true
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Create major version tag
if: steps.release-please.outputs.release_created == 'true' && github.ref_name != 'beta' && steps.update-major-tag.outcome == 'failure'
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
with:
route: POST /repos/${{ github.repository }}/git/refs
ref: refs/tags/v${{ steps.release-please.outputs.major }}
sha: ${{ steps.release-please.outputs.sha }}
env: env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
-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.2.0"
}
-13
View File
@@ -1,13 +0,0 @@
# Changelog
## [3.2.0](https://github.com/actions/create-github-app-token/compare/v3.1.1...v3.2.0) (2026-05-08)
### Features
* add support for enterprise-level GitHub Apps ([#263](https://github.com/actions/create-github-app-token/issues/263)) ([952a2a7](https://github.com/actions/create-github-app-token/commit/952a2a7073df6bfa5f49bc469ec895b6ec1acea4))
### Bug Fixes
* **deps:** bump @actions/core from 3.0.0 to 3.0.1 in the production-dependencies group ([#364](https://github.com/actions/create-github-app-token/issues/364)) ([43e5c34](https://github.com/actions/create-github-app-token/commit/43e5c345bfd4d4f3ecea019ad0042001a09dd857))
+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:
+132 -219
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,10 +23044,46 @@ function calculateRemainingTime(start, max) {
} }
return max - (performance.now() - start); return max - (performance.now() - start);
} }
async function delayForRetry(delay, options) { async function onAttemptFailure({ error: error2, attemptNumber, retriesConsumed, startTime, options }) {
if (delay <= 0) { const normalizedError = error2 instanceof Error ? error2 : new TypeError(`Non-error was thrown: "${error2}". You should only throw errors.`);
return; if (normalizedError instanceof AbortError) {
throw normalizedError.originalError;
} }
const retriesLeft = Number.isFinite(options.retries) ? Math.max(0, options.retries - retriesConsumed) : options.retries;
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
const context = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
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)) {
if (consumeRetry) {
throw normalizedError;
}
options.signal?.throwIfAborted();
return false;
}
if (!await options.shouldRetry(context)) {
throw normalizedError;
}
if (!consumeRetry) {
options.signal?.throwIfAborted();
return false;
}
const delayTime = calculateDelay(retriesConsumed, options);
const finalDelay = Math.min(delayTime, remainingTime);
options.signal?.throwIfAborted();
if (finalDelay > 0) {
await new Promise((resolve2, reject) => { await new Promise((resolve2, reject) => {
const onAbort = () => { const onAbort = () => {
clearTimeout(timeoutToken); clearTimeout(timeoutToken);
@@ -23072,74 +23093,13 @@ async function delayForRetry(delay, options) {
const timeoutToken = setTimeout(() => { const timeoutToken = setTimeout(() => {
options.signal?.removeEventListener("abort", onAbort); options.signal?.removeEventListener("abort", onAbort);
resolve2(); resolve2();
}, delay); }, finalDelay);
if (options.unref) { if (options.unref) {
timeoutToken.unref?.(); timeoutToken.unref?.();
} }
options.signal?.addEventListener("abort", onAbort, { once: true }); 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) {
throw normalizedError.originalError;
} }
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
});
await options.onFailedAttempt(context);
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
throw normalizedError;
}
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTime <= 0 || retriesLeft <= 0) {
throw normalizedError;
}
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
throw normalizedError;
}
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);
options.signal?.throwIfAborted();
await delayForRetry(finalDelay, options);
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,
+102 -156
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,
+637 -402
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.2.0", "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.27.4", "esbuild": "^0.27.3",
"open-cli": "^9.0.0", "open-cli": "^8.0.0",
"undici": "^7.24.6", "undici": "^7.24.1",
"yaml": "^2.8.3" "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.
+3 -19
View File
@@ -11,13 +11,6 @@ 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");
@@ -46,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 {
({ stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
env, env,
})); });
} catch (error) { const trimmedStderr = stderr.replace(/\r?\n$/, "");
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" } },
);
});
+1 -1
View File
@@ -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