Compare commits

..
Author SHA1 Message Date
Parker Brown 0fde60e111 Simplify proxy environment setup in request.js
Removed undici proxy agent configuration and related error handling. Now only sets NODE_USE_ENV_PROXY to encourage Node to honor standard proxy environment variables.
2025-08-22 13:50:04 -07:00
Parker Brown 3ffe05f85c Support lowercase proxy environment variables
Adds detection for lowercase proxy environment variables (https_proxy, http_proxy) in addition to their uppercase counterparts when configuring the global dispatcher.
2025-08-22 13:46:06 -07:00
Parker Brown f57ea8787c Move proxy setup to request.js and update entrypoint
Proxy environment setup previously in bootstrap.js is now handled in lib/request.js for better encapsulation. The action entrypoint is updated from dist/bootstrap.cjs to dist/main.cjs, and bootstrap.js is removed. Build script is updated to exclude bootstrap.js.
2025-08-22 13:43:20 -07:00
Parker Brown 00ba6edc66 Switch build output to CommonJS (.cjs) format
Updated build script to output .cjs files instead of .js, and updated action.yml to reference the new .cjs files. Also clarified proxy environment variable handling in bootstrap.js for consistency.
2025-08-22 13:22:02 -07:00
Parker Brown 21c1159e4d Switch build output to ESM format and update entrypoints
Changed the build script to output ES modules instead of CommonJS. Updated action.yml to reference .js files instead of .cjs, and clarified proxy support logic in bootstrap.js.
2025-08-22 13:16:17 -07:00
Parker Brown 6423fe3683 Add bootstrap entry for env-based proxy support
Introduces bootstrap.js to set NODE_USE_ENV_PROXY before loading main.js, ensuring proxy support is enabled unless explicitly opted out. Updates action.yml to use bootstrap as the main entry and modifies build script to bundle bootstrap.js.
2025-08-22 13:10:57 -07:00
Parker Brown 39808af6a2 Move NODE_USE_ENV_PROXY assignment to top of file
Relocated the setting of process.env.NODE_USE_ENV_PROXY to the beginning of main.js for improved clarity and to ensure the environment variable is set before any imports or logic are executed.
2025-08-22 13:05:03 -07:00
Parker Brown fe4ba3360a Set NODE_USE_ENV_PROXY in main.js and update workflow
Moved NODE_USE_ENV_PROXY environment variable assignment from the GitHub Actions workflow to main.js for consistent runtime configuration. This change ensures the variable is always set when the application runs, regardless of workflow environment settings.
2025-08-22 12:31:42 -07:00
Parker Brown 926b8abad2 Set NODE_USE_ENV_PROXY in test workflow
Adds NODE_USE_ENV_PROXY=1 to the test job environment in GitHub Actions to enable proxy usage via environment variable.
2025-08-22 12:29:23 -07:00
Parker Brown d988c9aaad ci(test): set https_proxy env for test workflow
Adds the https_proxy environment variable to the test job in the GitHub Actions workflow to route requests through the specified proxy.
2025-08-22 12:24:32 -07:00
semantic-release-bot bf559f8544 build(release): 3.0.0-beta.2 [skip ci]
# [3.0.0-beta.2](https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2025-08-22)

### Bug Fixes

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

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

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

### BREAKING CHANGES

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

---------

Co-authored-by: Parker Brown <[email protected]>
2025-08-15 12:55:04 -07:00
26 changed files with 23408 additions and 75463 deletions
@@ -12,6 +12,6 @@ jobs:
id-token: write id-token: write
packages: write packages: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- name: Publish Immutable Action - name: Publish Immutable Action
uses: actions/[email protected] uses: actions/[email protected]
+3 -3
View File
@@ -18,14 +18,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# build local version to create token # build local version to create token
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version-file: package.json node-version-file: package.json
cache: 'npm'
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
-34
View File
@@ -1,34 +0,0 @@
# 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.'
+11 -8
View File
@@ -5,7 +5,6 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
merge_group:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -17,32 +16,36 @@ permissions:
jobs: jobs:
integration: integration:
name: integration name: Integration
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version-file: package.json node-version-file: package.json
cache: 'npm'
- run: npm ci - run: npm ci
- run: npm test - run: npm test
end-to-end: end-to-end:
name: end-to-end name: End-to-End
runs-on: ubuntu-latest runs-on: ubuntu-latest
# do not run from forks, as forks dont have access to repository secrets # do not run from forks, as forks dont have access to repository secrets
if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login if: github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version-file: package.json node-version-file: package.json
cache: 'npm'
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- uses: ./ # Uses the action in the root directory - uses: ./ # Uses the action in the root directory
id: test id: test
env:
https_proxy: https://example.com
with: with:
app-id: ${{ vars.TEST_APP_ID }} app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }} private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
+5 -14
View File
@@ -13,30 +13,21 @@ concurrency:
permissions: permissions:
contents: write contents: write
pull-requests: write
jobs: jobs:
update-permission-inputs: update-permission-inputs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
COMMIT_MESSAGE: 'feat: update permission inputs'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version-file: package.json node-version-file: package.json
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run permission inputs update script - name: Run permission inputs update script
run: node scripts/update-permission-inputs.js run: node scripts/update-permission-inputs.js
- name: Commit changes - name: Commit changes
id: auto-commit uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with: with:
commit_message: ${{ env.COMMIT_MESSAGE }} commit_message: 'feat: update permission inputs'
- 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
+12 -41
View File
@@ -28,7 +28,7 @@ jobs:
hello-world: hello-world:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -47,13 +47,13 @@ jobs:
auto-format: auto-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
# required # required
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
@@ -73,7 +73,7 @@ jobs:
auto-format: auto-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
# required # required
@@ -98,7 +98,7 @@ jobs:
auto-format: auto-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
# required # required
@@ -135,7 +135,7 @@ jobs:
hello-world: hello-world:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -157,7 +157,7 @@ jobs:
hello-world: hello-world:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -182,7 +182,7 @@ jobs:
hello-world: hello-world:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -195,28 +195,6 @@ 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@v2
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
enterprise-slug: 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]
@@ -229,7 +207,7 @@ jobs:
hello-world: hello-world:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -271,7 +249,7 @@ jobs:
owners-and-repos: ${{ fromJson(needs.set-matrix.outputs.matrix) }} owners-and-repos: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
steps: steps:
- uses: actions/create-github-app-token@v2 - uses: actions/create-github-app-token@v3
id: app-token id: app-token
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
@@ -301,7 +279,7 @@ jobs:
steps: steps:
- name: Create GitHub App token - name: Create GitHub App token
id: create_token id: create_token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@v3
with: with:
app-id: ${{ vars.GHES_APP_ID }} app-id: ${{ vars.GHES_APP_ID }}
private-key: ${{ secrets.GHES_APP_PRIVATE_KEY }} private-key: ${{ secrets.GHES_APP_PRIVATE_KEY }}
@@ -340,7 +318,7 @@ steps:
echo "private-key=$private_key" >> "$GITHUB_OUTPUT" echo "private-key=$private_key" >> "$GITHUB_OUTPUT"
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: app-token id: app-token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@v3
with: with:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private-key: ${{ steps.decode.outputs.private-key }} private-key: ${{ steps.decode.outputs.private-key }}
@@ -357,13 +335,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-slug`
**Optional:** The slug of the enterprise to generate a token for enterprise-level app installations.
> [!NOTE]
> The `enterprise-slug` input is mutually exclusive with `owner` and `repositories`. GitHub Apps can be installed on enterprise accounts with permissions that let them call enterprise management APIs. Enterprise installations do not grant access to organization or repository resources.
### `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`).
+2 -9
View File
@@ -17,9 +17,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-slug:
description: "Enterprise slug for enterprise-level app installations (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
@@ -40,16 +37,12 @@ inputs:
description: "The level of permission to grant the access token to create, edit, delete, and list Codespaces. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token to create, edit, delete, and list Codespaces. Can be set to 'read' or 'write'."
permission-contents: permission-contents:
description: "The level of permission to grant the access token for repository contents, commits, branches, downloads, releases, and merges. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token for repository contents, commits, branches, downloads, releases, and merges. Can be set to 'read' or 'write'."
permission-custom-properties-for-organizations:
description: "The level of permission to grant the access token to view and edit custom properties for an organization, when allowed by the property. Can be set to 'read' or 'write'."
permission-dependabot-secrets: permission-dependabot-secrets:
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-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:
description: "The level of permission to grant the access token for organization custom properties management at the enterprise level. Can be set to 'read', 'write', or 'admin'."
permission-environments: permission-environments:
description: "The level of permission to grant the access token for managing repository environments. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token for managing repository environments. Can be set to 'read' or 'write'."
permission-followers: permission-followers:
@@ -75,7 +68,7 @@ inputs:
permission-organization-custom-org-roles: permission-organization-custom-org-roles:
description: "The level of permission to grant the access token for custom organization roles management. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token for custom organization roles management. Can be set to 'read' or 'write'."
permission-organization-custom-properties: permission-organization-custom-properties:
description: "The level of permission to grant the access token for repository custom properties management at the organization level. Can be set to 'read', 'write', or 'admin'." description: "The level of permission to grant the access token for custom property management. Can be set to 'read', 'write', or 'admin'."
permission-organization-custom-roles: permission-organization-custom-roles:
description: "The level of permission to grant the access token for custom repository roles management. Can be set to 'read' or 'write'." description: "The level of permission to grant the access token for custom repository roles management. Can be set to 'read' or 'write'."
permission-organization-events: permission-organization-events:
@@ -139,6 +132,6 @@ outputs:
app-slug: app-slug:
description: "GitHub App slug" description: "GitHub App slug"
runs: runs:
using: "node20" using: "node24"
main: "dist/main.cjs" main: "dist/main.cjs"
post: "dist/post.cjs" post: "dist/post.cjs"
+11405 -37239
View File
File diff suppressed because one or more lines are too long
+11092 -37048
View File
File diff suppressed because one or more lines are too long
+42 -103
View File
@@ -4,7 +4,6 @@ import pRetry from "p-retry";
/** /**
* @param {string} appId * @param {string} appId
* @param {string} privateKey * @param {string} privateKey
* @param {string} enterpriseSlug
* @param {string} owner * @param {string} owner
* @param {string[]} repositories * @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions * @param {undefined | Record<string, string>} permissions
@@ -16,70 +15,58 @@ import pRetry from "p-retry";
export async function main( export async function main(
appId, appId,
privateKey, privateKey,
enterpriseSlug,
owner, owner,
repositories, repositories,
permissions, permissions,
core, core,
createAppAuth, createAppAuth,
request, request,
skipTokenRevoke, skipTokenRevoke
) { ) {
// Validate mutual exclusivity of enterprise-slug with owner/repositories
if (enterpriseSlug && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs");
}
let parsedOwner = ""; let parsedOwner = "";
let parsedRepositoryNames = []; let parsedRepositoryNames = [];
// Skip owner/repository parsing if enterprise-slug is set // If neither owner nor repositories are set, default to current repository
if (!enterpriseSlug) { if (!owner && repositories.length === 0) {
// If neither owner nor repositories are set, default to current repository const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
if (!owner && repositories.length === 0) { parsedOwner = owner;
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); parsedRepositoryNames = [repo];
parsedOwner = owner;
parsedRepositoryNames = [repo];
core.info( core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).` `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
); );
} }
// If only an owner is set, default to all repositories from that owner // If only an owner is set, default to all repositories from that owner
if (owner && repositories.length === 0) { if (owner && repositories.length === 0) {
parsedOwner = owner; parsedOwner = owner;
core.info( core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.` `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 repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
if (!owner && repositories.length > 0) { if (!owner && repositories.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories; parsedRepositoryNames = repositories;
core.info( core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`) .map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}` .join("")}`
); );
} }
// If both owner and repositories are set, use those values // If both owner and repositories are set, use those values
if (owner && repositories.length > 0) { if (owner && repositories.length > 0) {
parsedOwner = owner; parsedOwner = owner;
parsedRepositoryNames = repositories; parsedRepositoryNames = repositories;
core.info( core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}`
); );
}
} else {
core.info(`Creating enterprise installation token for enterprise "${enterpriseSlug}".`);
} }
const auth = createAppAuth({ const auth = createAppAuth({
@@ -89,22 +76,9 @@ export async function main(
}); });
let authentication, installationId, appSlug; let authentication, installationId, appSlug;
// If at least one repository is set, get installation ID from that repository
// If enterprise-slug is set, get installation ID from the enterprise
if (enterpriseSlug) { if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromEnterprise(request, auth, enterpriseSlug, permissions),
{
shouldRetry: (error) => error.status >= 500,
onFailedAttempt: (error) => {
core.info(
`Failed to create token for enterprise "${enterpriseSlug}" (attempt ${error.attemptNumber}): ${error.message}`
);
},
retries: 3,
}
));
} else if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry( ({ authentication, installationId, appSlug } = await pRetry(
() => () =>
getTokenFromRepository( getTokenFromRepository(
@@ -115,12 +89,12 @@ export async function main(
permissions permissions
), ),
{ {
shouldRetry: ({ error }) => error.status >= 500, shouldRetry: (error) => error.status >= 500,
onFailedAttempt: (context) => { onFailedAttempt: (error) => {
core.info( core.info(
`Failed to create token for "${parsedRepositoryNames.join( `Failed to create token for "${parsedRepositoryNames.join(
"," ","
)}" (attempt ${context.attemptNumber}): ${context.error.message}` )}" (attempt ${error.attemptNumber}): ${error.message}`
); );
}, },
retries: 3, retries: 3,
@@ -131,9 +105,9 @@ export async function main(
({ authentication, installationId, appSlug } = await pRetry( ({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request, auth, parsedOwner, permissions), () => getTokenFromOwner(request, auth, parsedOwner, permissions),
{ {
onFailedAttempt: (context) => { onFailedAttempt: (error) => {
core.info( core.info(
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}` `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
); );
}, },
retries: 3, retries: 3,
@@ -207,38 +181,3 @@ async function getTokenFromRepository(
return { authentication, installationId, appSlug }; return { authentication, installationId, appSlug };
} }
async function getTokenFromEnterprise(request, auth, enterpriseSlug, permissions) {
// Get all installations and find the enterprise one
// https://docs.github.com/rest/apps/apps#list-installations-for-the-authenticated-app
// Note: Currently we do not have a way to get the installation for an enterprise directly,
// so as a workaround we need to list all installations and filter for the enterprise one.
const response = await request("GET /app/installations", {
request: {
hook: auth.hook,
},
});
// Find the enterprise installation
const enterpriseInstallation = response.data.find(
installation => installation.target_type === "Enterprise" &&
installation.account?.slug === enterpriseSlug
);
/* c8 ignore next 3 */
if (!enterpriseInstallation) {
throw new Error(`No enterprise installation found matching the name ${enterpriseSlug}. Available installations: ${response.data.map(i => `${i.target_type}:${i.account?.login || 'N/A'}`).join(', ')}`);
}
// Get token for the enterprise installation
const authentication = await auth({
type: "installation",
installationId: enterpriseInstallation.id,
permissions,
});
const installationId = enterpriseInstallation.id;
const appSlug = enterpriseInstallation["app_slug"];
return { authentication, installationId, appSlug };
}
+11 -34
View File
@@ -1,41 +1,18 @@
import * as core from "@actions/core"; import core from "@actions/core";
import { request } from "@octokit/request"; import { request } from "@octokit/request";
import { ProxyAgent, fetch as undiciFetch } from "undici";
const baseUrl = core.getInput("github-api-url").replace(/\/$/, ""); /* c8 ignore start -- env knob setup */
// Encourage Node to honor standard *_PROXY vars for core HTTP(S) agents.
// https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/using-a-proxy-server-with-self-hosted-runners if (process.env.NODE_USE_ENV_PROXY == null) {
const proxyUrl = process.env.NODE_USE_ENV_PROXY = "1";
process.env.https_proxy || }
process.env.HTTPS_PROXY ||
process.env.http_proxy ||
process.env.HTTP_PROXY;
/* c8 ignore start */
// Native support for proxies in Undici is under consideration: https://github.com/nodejs/undici/issues/1650
// Until then, we need to use a custom fetch function to add proxy support.
const proxyFetch = (url, options) => {
const urlHost = new URL(url).hostname;
const noProxy = (process.env.no_proxy || process.env.NO_PROXY || "").split(
",",
);
if (!noProxy.includes(urlHost)) {
options = {
...options,
dispatcher: new ProxyAgent(String(proxyUrl)),
};
}
return undiciFetch(url, options);
};
/* c8 ignore stop */ /* c8 ignore stop */
// Get the GitHub API URL from the action input and remove any trailing slash
const baseUrl = core.getInput("github-api-url").replace(/\/$/, "");
// Configure the default settings for GitHub API requests
export default request.defaults({ export default request.defaults({
headers: { headers: { "user-agent": "actions/create-github-app-token" },
"user-agent": "actions/create-github-app-token",
},
baseUrl, baseUrl,
/* c8 ignore next */
request: proxyUrl ? { fetch: proxyFetch } : {},
}); });
+4 -10
View File
@@ -1,6 +1,5 @@
// @ts-check // @ts-check
import core from "@actions/core";
import * as core from "@actions/core";
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { getPermissionsFromInputs } from "./lib/get-permissions-from-inputs.js"; import { getPermissionsFromInputs } from "./lib/get-permissions-from-inputs.js";
@@ -17,7 +16,6 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
const appId = core.getInput("app-id"); const appId = core.getInput("app-id");
const privateKey = core.getInput("private-key"); const privateKey = core.getInput("private-key");
const enterpriseSlug = core.getInput("enterprise-slug");
const owner = core.getInput("owner"); const owner = core.getInput("owner");
const repositories = core const repositories = core
.getInput("repositories") .getInput("repositories")
@@ -33,19 +31,15 @@ const permissions = getPermissionsFromInputs(process.env);
export default main( export default main(
appId, appId,
privateKey, privateKey,
enterpriseSlug,
owner, owner,
repositories, repositories,
permissions, permissions,
core, core,
createAppAuth, createAppAuth,
request, request,
skipTokenRevoke, skipTokenRevoke
).catch((error) => { ).catch((error) => {
/* c8 ignore next 5 */ /* c8 ignore next 3 */
console.error(error); console.error(error);
// Don't set failed in test mode (when GITHUB_OUTPUT is undefined) core.setFailed(error.message);
if (process.env.GITHUB_OUTPUT !== undefined) {
core.setFailed(error.message);
}
}); });
+804 -560
View File
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -2,36 +2,35 @@
"name": "create-github-app-token", "name": "create-github-app-token",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "2.2.2", "version": "3.0.0-beta.2",
"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": ">=20" "node": ">=24.4.0"
}, },
"packageManager": "[email protected]",
"scripts": { "scripts": {
"build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node20.0.0 --packages=bundle", "build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --packages=bundle",
"test": "c8 --100 ava tests/index.js", "test": "c8 --100 ava tests/index.js",
"coverage": "c8 report --reporter html", "coverage": "c8 report --reporter html",
"postcoverage": "open-cli coverage/index.html" "postcoverage": "open-cli coverage/index.html"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^3.0.0", "@actions/core": "^1.11.1",
"@octokit/auth-app": "^8.2.0", "@octokit/auth-app": "^7.2.1",
"@octokit/request": "^10.0.8", "@octokit/request": "^9.2.2",
"p-retry": "^7.1.1", "p-retry": "^6.2.1"
"undici": "^7.24.1"
}, },
"devDependencies": { "devDependencies": {
"@octokit/openapi": "^21.0.0", "@octokit/openapi": "^19.1.0",
"@sinonjs/fake-timers": "^15.1.0", "@sinonjs/fake-timers": "^14.0.0",
"ava": "^6.4.1", "ava": "^6.4.1",
"c8": "^10.1.3", "c8": "^10.1.3",
"dotenv": "^17.3.1", "dotenv": "^17.2.1",
"esbuild": "^0.27.3", "esbuild": "^0.25.8",
"execa": "^9.6.1", "execa": "^9.6.0",
"open-cli": "^8.0.0", "open-cli": "^8.0.0",
"yaml": "^2.8.2" "undici": "^7.13.0",
"yaml": "^2.8.1"
}, },
"release": { "release": {
"branches": [ "branches": [
@@ -47,7 +46,6 @@
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@semantic-release/github", "@semantic-release/github",
"@semantic-release/npm", "@semantic-release/npm",
"semantic-release-plugin-github-breaking-version-tag",
[ [
"@semantic-release/git", "@semantic-release/git",
{ {
+1 -1
View File
@@ -1,6 +1,6 @@
// @ts-check // @ts-check
import * as core from "@actions/core"; import core from "@actions/core";
import { post } from "./lib/post.js"; import { post } from "./lib/post.js";
import request from "./lib/request.js"; import request from "./lib/request.js";
+1 -18
View File
@@ -187,14 +187,6 @@
"write" "write"
] ]
}, },
"custom_properties_for_organizations": {
"type": "string",
"description": "The level of permission to grant the access token to view and edit custom properties for an organization, when allowed by the property.",
"enum": [
"read",
"write"
]
},
"members": { "members": {
"type": "string", "type": "string",
"description": "The level of permission to grant the access token for organization teams and members.", "description": "The level of permission to grant the access token for organization teams and members.",
@@ -229,7 +221,7 @@
}, },
"organization_custom_properties": { "organization_custom_properties": {
"type": "string", "type": "string",
"description": "The level of permission to grant the access token for repository custom properties management at the organization level.", "description": "The level of permission to grant the access token for custom property management.",
"enum": [ "enum": [
"read", "read",
"write", "write",
@@ -392,15 +384,6 @@
"read", "read",
"write" "write"
] ]
},
"enterprise_custom_properties_for_organizations": {
"type": "string",
"description": "The level of permission to grant the access token for organization custom properties management at the enterprise level.",
"enum": [
"read",
"write",
"admin"
]
} }
}, },
"example": { "example": {
@@ -1,40 +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-SLUG"] = "test-enterprise";
// Mock the /app/installations endpoint to return only non-enterprise installations
mockPool
.intercept({
path: "/app/installations",
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: "111111",
app_slug: "github-actions",
target_type: "Organization",
account: { login: "some-org" }
},
{
id: "222222",
app_slug: "github-actions",
target_type: "User",
account: { login: "some-user" }
}
],
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,16 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
// Verify `main` exits with an error when `enterprise-slug` is used with both `owner` and `repositories` inputs.
try {
// Set up environment with enterprise-slug, owner, and repositories all set
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
process.env.INPUT_OWNER = "test-owner";
process.env.INPUT_REPOSITORIES = "repo1,repo2";
await import("../main.js");
} catch (error) {
console.error(error.message);
}
@@ -1,15 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
// Verify `main` exits with an error when `enterprise-slug` is used with `owner` input.
try {
// Set up environment with enterprise-slug and owner set
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
process.env.INPUT_OWNER = "test-owner";
await import("../main.js");
} catch (error) {
console.error(error.message);
}
@@ -1,15 +0,0 @@
import { DEFAULT_ENV } from "./main.js";
// Verify `main` exits with an error when `enterprise-slug` is used with `repositories` input.
try {
// Set up environment with enterprise-slug and repositories set
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
process.env.INPUT_REPOSITORIES = "repo1,repo2";
await import("../main.js");
} catch (error) {
console.error(error.message);
}
@@ -1,34 +0,0 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when only the `enterprise-slug` input is set.
await test((mockPool) => {
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
// Mock the /app/installations endpoint to return an enterprise installation
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/app/installations",
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,
target_type: "Enterprise",
account: { login: "test-enterprise", slug: "test-enterprise" }
}
],
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,34 +0,0 @@
import { test } from "./main.js";
// Verify `main` successfully generates enterprise token with basic functionality.
await test((mockPool) => {
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
// Mock the /app/installations endpoint to return an enterprise installation
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/app/installations",
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,
target_type: "Enterprise",
account: { login: "test-enterprise", slug: "test-enterprise" }
}
],
{ headers: { "content-type": "application/json" } }
);
});
@@ -1,36 +0,0 @@
import { test } from "./main.js";
// Verify `main` successfully generates enterprise token with specific permissions.
await test((mockPool) => {
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
process.env["INPUT_PERMISSION-ENTERPRISE-ORGANIZATIONS"] = "read";
process.env["INPUT_PERMISSION-ENTERPRISE-PEOPLE"] = "write";
// Mock the /app/installations endpoint to return an enterprise installation
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: "/app/installations",
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,
target_type: "Enterprise",
account: { login: "test-enterprise", slug: "test-enterprise" }
}
],
{ headers: { "content-type": "application/json" } }
);
});
-133
View File
@@ -39,139 +39,6 @@ Generated by [AVA](https://avajs.dev).
POST /api/v3/app/installations/123456/access_tokens␊ POST /api/v3/app/installations/123456/access_tokens␊
{"repositories":["create-github-app-token"]}` {"repositories":["create-github-app-token"]}`
## main-enterprise-installation-not-found.test.js
> stderr
`Error: No enterprise installation found matching the name test-enterprise. Available installations: Organization:some-org, User:some-user␊
at getTokenFromEnterprise (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:230:11)␊
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)␊
at async pRetry (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/node_modules/p-retry/index.js:197:19)␊
at async main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:95:52)␊
at async test (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main.js:111:3)␊
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-installation-not-found.test.js:5:1`
> stdout
`Creating enterprise installation token for enterprise "test-enterprise".␊
Failed to create token for enterprise "test-enterprise" (attempt 1): undefined␊
--- REQUESTS ---␊
GET /app/installations`
## main-enterprise-mutual-exclusivity-both.test.js
> stderr
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-both.test.js:13:3`
> stdout
''
## main-enterprise-mutual-exclusivity-owner.test.js
> stderr
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-owner.test.js:12:3`
> stdout
''
## main-enterprise-mutual-exclusivity-repositories.test.js
> stderr
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-repositories.test.js:12:3`
> stdout
''
## main-enterprise-only-success.test.js
> stderr
''
> stdout
`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 /app/installations␊
POST /app/installations/123456/access_tokens␊
null`
## main-enterprise-token-success.test.js
> stderr
''
> stdout
`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 /app/installations␊
POST /app/installations/123456/access_tokens␊
null`
## main-enterprise-token-with-permissions.test.js
> stderr
''
> stdout
`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 /app/installations␊
POST /app/installations/123456/access_tokens␊
{"permissions":{"enterprise_organizations":"read","enterprise_people":"write"}}`
## main-missing-owner.test.js ## main-missing-owner.test.js
> stderr > stderr
Binary file not shown.