Compare commits

...

16 Commits

Author SHA1 Message Date
create-app-token-action-releaser[bot] f19919961c chore(main): release 3.2.0 2026-05-08 18:56:39 +00:00
Stefan 952a2a7073 feat: add support for enterprise-level GitHub Apps (#263)
This pull request adds support for generating GitHub App installation
tokens for enterprise-level installations.

### What changed

- Added a new `enterprise` input to `action.yml`.
- Wired `enterprise` through `main.js` and `lib/main.js`.
- Added validation so `enterprise` cannot be combined with `owner` or
`repositories`.
- Implemented enterprise installation lookup using the direct GitHub API
route `GET /enterprises/{enterprise}/installation`, then used the
returned installation ID to mint an installation token through
`@octokit/auth-app`.
- Updated `README.md` with enterprise installation usage and input
documentation.
- Updated `dist/main.cjs` for the bundled action.
- Shared token creation retry behavior across repository, owner, and
enterprise paths so server errors and transient network errors are
retried, while client errors fail immediately.

### Tests

Added focused test coverage for:

- enterprise token creation
- enterprise token creation with explicit permissions
- enterprise installation not found
- mutual exclusivity with `owner`
- mutual exclusivity with `repositories`
- owner installation client errors are not retried
- transient network errors are retried during token creation

### Notes

- This keeps the existing repository-scoped token behavior unchanged.
- Owner, repository, and enterprise token creation now share the same
retry policy: server errors and recognized transient network errors are
retried, while client errors fail immediately. This intentionally fixes
the previous owner-path behavior that retried client errors.

Refs:
-
https://github.blog/changelog/2025-07-01-enterprise-level-access-for-github-apps-and-installation-automation-apis/
-
https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-enterprise-installation-for-the-authenticated-app

---------

Co-authored-by: Parker Brown <17183625+parkerbxyz@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-08 11:56:11 -07:00
dependabot[bot] 43e5c345bf fix(deps): bump @actions/core from 3.0.0 to 3.0.1 in the production-dependencies group (#364)
Bumps the production-dependencies group with 1 update:
[@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core).

Updates `@actions/core` from 3.0.0 to 3.0.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md"><code>@​actions/core</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>3.0.1</h2>
<ul>
<li>Bump <code>undici</code> from <code>6.23.0</code> to
<code>6.24.1</code> <a
href="https://redirect.github.com/actions/toolkit/pull/2348">#2348</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/actions/toolkit/commits/HEAD/packages/core">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@actions/core&package-manager=npm_and_yarn&previous-version=3.0.0&new-version=3.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 19:09:31 -07:00
Parker Brown 6dbbbb1419 ci: use existing release tag format (#369)
## Summary

This updates the release-please manifest configuration to use the
repository's existing `vX.Y.Z` tag format. Without this setting,
manifest mode looks for component-prefixed tags like
`create-github-app-token-v3.1.1`, which do not exist in this repository
and can cause release-please to include already-released commits in a
new release PR.

## Changes

- Sets `include-component-in-tag` to `false` for stable releases.
- Sets `include-component-in-tag` to `false` for beta prereleases.

This should make release-please use tags like `v3.1.1` as the previous
release boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 18:58:55 -07:00
Parker Brown 6be28c6ca7 ci: migrate releases to release-please (#367)
## Summary

This updates release automation so the repository can enforce signed
commits. Release version and changelog updates now happen through
release-please PRs instead of direct workflow commits, which allows them
to use the repository's normal signed-commit path.

## Changes

- Replaces semantic-release configuration with release-please manifest
configuration for stable releases and beta prereleases.
- Builds committed `dist/**` assets from the trusted release workflow
after release-please creates or updates a release PR, then commits those
assets back to the release PR branch.
- Keeps floating major version tags, such as `v3`, updated with the
GitHub API after a release is created.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 18:13:10 -07:00
Josh Johanning 7989d206df docs: use APP_ prefix instead of reserved GITHUB_ prefix (#363)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-21 09:56:11 -07:00
semantic-release-bot 1b10c78c78 build(release): 3.1.1 [skip ci]
## [3.1.1](https://github.com/actions/create-github-app-token/compare/v3.1.0...v3.1.1) (2026-04-11)

### Bug Fixes

* improve error message when app identifier is empty ([#362](https://github.com/actions/create-github-app-token/issues/362)) ([07e2b76](https://github.com/actions/create-github-app-token/commit/07e2b760664f080c40eec4eacf7477256582db36)), closes [#249](https://github.com/actions/create-github-app-token/issues/249)
2026-04-11 06:43:26 +00:00
Parker Brown 07e2b76066 fix: improve error message when app identifier is empty (#362)
When `client-id` (or the deprecated `app-id`) resolves to an empty
string, for example because a secret or variable is not available in the
workflow context, the error message from `@octokit/auth-app` is not very
helpful:

```
[@octokit/auth-app] appId option is required
```

A validation check was added recently to catch this earlier, but its
message could be more informative:

```
Either 'client-id' or 'app-id' input must be set
```

This updates the message to clarify that the value resolved to empty and
nudges users toward checking their secret or variable availability:

```
The 'client-id' input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.
```

Closes #249

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 23:42:53 -07:00
Parker Brown ea0121618b ci: remove publish-immutable-action workflow (#361)
The `publish-immutable-action` workflow uses `actions/publish-immutable-action@v0.0.4`. The Immutable Actions Publishing feature (OCI-based) has been paused with no GA timeline. This removes the workflow; it can be re-added when the feature becomes generally available.

To fully address #352, the repository-level **immutable releases** setting has also be enabled. This [feature](https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/) locks release tags and assets after publication, directly preventing the class of attack described in [GHSA-mrrh-fwg8-r2c3](https://github.com/advisories/GHSA-mrrh-fwg8-r2c3).

The existing release workflow is compatible with immutable releases. Build artifacts are committed via `@semantic-release/git` before the tag is created, and major version tags (`v3`, etc.) are plain git tags (not releases) so they remain updatable.
2026-04-10 23:01:59 -07:00
semantic-release-bot 7bd0371149 build(release): 3.1.0 [skip ci]
# [3.1.0](https://github.com/actions/create-github-app-token/compare/v3.0.0...v3.1.0) (2026-04-11)

### Bug Fixes

* **deps:** bump p-retry from 7.1.1 to 8.0.0 ([#357](https://github.com/actions/create-github-app-token/issues/357)) ([3bbe07d](https://github.com/actions/create-github-app-token/commit/3bbe07d928e2d6c30bf3e37c6b89edbc4045facf))

### Features

* add `client-id` input and deprecate `app-id` ([#353](https://github.com/actions/create-github-app-token/issues/353)) ([e6bd4e6](https://github.com/actions/create-github-app-token/commit/e6bd4e6970172bed9fe138b2eaf4cbffa4cca8f9))
* update permission inputs ([#358](https://github.com/actions/create-github-app-token/issues/358)) ([076e948](https://github.com/actions/create-github-app-token/commit/076e9480ca6e9633bff412d05eff0fc2f1e7d2be))
2026-04-11 00:39:23 +00:00
Copilot e6bd4e6970 feat: add client-id input and deprecate app-id (#353)
GitHub now recommends using a GitHub App's Client ID for authentication.
This PR adds a first-class `client-id` input, keeps `app-id` available
for compatibility, and makes the migration path explicit in both runtime
behavior and documentation.

### Action inputs

- Adds a new `client-id` input
- Removes `required` from `app-id`
- Marks `app-id` as deprecated in `action.yml`

### Runtime behavior

- Updates input parsing to prefer `client-id`
- Falls back to `app-id` for existing workflows
- Adds a clear error when neither `client-id` nor `app-id` is provided

### Docs

- Updates the README to recommend `client-id`
- Switches usage examples to `client-id`
- Documents that `app-id` is deprecated and that `client-id` takes
precedence if both are set

### Regression coverage

- Adds a focused test proving a client-ID-shaped value works through the
new `client-id` input
- Adds coverage for the missing-ID validation path
- Updates snapshots to lock in the new metadata and runtime behavior

### Resulting usage

Users can migrate to the new input name directly:

```yaml
- uses: actions/create-github-app-token@v3
  with:
    client-id: ${{ vars.GITHUB_APP_CLIENT_ID }}
    private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: parkerbxyz <17183625+parkerbxyz@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 17:36:03 -07:00
dependabot[bot] 076e9480ca feat: update permission inputs (#358)
Bumps [@octokit/openapi](https://github.com/octokit/openapi) from 21.0.0
to 22.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/octokit/openapi/releases"><code>@​octokit/openapi</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v22.0.0</h2>
<h1><a
href="https://github.com/octokit/openapi/compare/v21.0.0...v22.0.0">22.0.0</a>
(2025-12-09)</h1>
<h3>Features</h3>
<ul>
<li>drop projects-classic endpoints, add GitHub API endpoints: cache
limits (retention &amp; storage) for repos/orgs/enterprises, billing
budgets &amp; usage, artifacts deployment metadata, and projectsV2
drafts &amp; fields (<a
href="https://redirect.github.com/octokit/openapi/issues/518">#518</a>)
(<a
href="https://github.com/octokit/openapi/commit/b0c44a4ab1b07a5524890cef1e8321cfc430bebb">b0c44a4</a>)</li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>Removed <code>/orgs/{org}/projects</code></li>
<li>Removed <code>/orgs/{org}/settings/billing/actions</code></li>
<li>Removed <code>/orgs/{org}/settings/billing/packages</code></li>
<li>Removed
<code>/orgs/{org}/settings/billing/shared-storage</code></li>
<li>Removed <code>/orgs/{org}/teams/{team_slug}/projects</code></li>
<li>Removed
<code>/orgs/{org}/teams/{team_slug}/projects/{project_id}</code></li>
<li>Removed <code>/projects/columns/{column_id}</code></li>
<li>Removed <code>/projects/columns/{column_id}/moves</code></li>
<li>Removed <code>/projects/{project_id}</code></li>
<li>Removed <code>/projects/{project_id}/collaborators</code></li>
<li>Removed
<code>/projects/{project_id}/collaborators/{username}</code></li>
<li>Removed
<code>/projects/{project_id}/collaborators/{username}/permission</code></li>
<li>Removed <code>/repos/{owner}/{repo}/projects</code></li>
<li>Removed <code>/teams/{team_id}/projects</code></li>
<li>Removed <code>/teams/{team_id}/projects/{project_id}</code></li>
<li>Removed <code>/user/projects</code></li>
<li>Removed <code>/users/{username}/projects</code></li>
<li>Removed <code>/users/{username}/settings/billing/actions</code></li>
<li>Removed
<code>/users/{username}/settings/billing/packages</code></li>
<li>Removed
<code>/users/{username}/settings/billing/shared-storage</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/octokit/openapi/commit/6f63b86ab7d2057cb62574681918a34b3d43f66b"><code>6f63b86</code></a>
build(deps): lock file maintenance (<a
href="https://redirect.github.com/octokit/openapi/issues/520">#520</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/b0c44a4ab1b07a5524890cef1e8321cfc430bebb"><code>b0c44a4</code></a>
feat: drop projects-classic endpoints, add GitHub API endpoints: cache
limits...</li>
<li><a
href="https://github.com/octokit/openapi/commit/a8043eb055618a1a9a779b6807bba796d9664604"><code>a8043eb</code></a>
ci(action): update actions/checkout action to v6 (<a
href="https://redirect.github.com/octokit/openapi/issues/519">#519</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/af315cd293aac70c81874623769bdb091da614be"><code>af315cd</code></a>
build(deps): lock file maintenance (<a
href="https://redirect.github.com/octokit/openapi/issues/514">#514</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/170f3965b9432f4171117aacb6b88339d5c2a937"><code>170f396</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/octokit/openapi/issues/516">#516</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/077a1b94a2e77bf56fa07ed8dc112055958b97ab"><code>077a1b9</code></a>
build(deps): lock file maintenance (<a
href="https://redirect.github.com/octokit/openapi/issues/508">#508</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/cfca956d308018be25c1405b52c6a4b8c924bdd6"><code>cfca956</code></a>
ci(action): update github/codeql-action action to v4 (<a
href="https://redirect.github.com/octokit/openapi/issues/510">#510</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/f15da93d54d4de07c1025b0984c5613a8ddd8acd"><code>f15da93</code></a>
ci(action): update peter-evans/create-or-update-comment action to v5 (<a
href="https://redirect.github.com/octokit/openapi/issues/509">#509</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/64bef332f5e1b11ead74082d8aaf0376409de9d0"><code>64bef33</code></a>
chore(deps): update dependency map-obj to v6 (<a
href="https://redirect.github.com/octokit/openapi/issues/507">#507</a>)</li>
<li><a
href="https://github.com/octokit/openapi/commit/4e8e223e564f467a455d7f39de15a0fb233f189e"><code>4e8e223</code></a>
chore(deps): update dependency github-enterprise-server-versions to v3
(<a
href="https://redirect.github.com/octokit/openapi/issues/511">#511</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/octokit/openapi/compare/v21.0.0...v22.0.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​octokit/openapi</code> since your
current version.</p>
</details>
<br />

Resolves https://github.com/github/gh-aw/issues/18921.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:35:53 -07:00
dependabot[bot] 3bbe07d928 fix(deps): bump p-retry from 7.1.1 to 8.0.0 (#357)
Bumps [p-retry](https://github.com/sindresorhus/p-retry) from 7.1.1 to
8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sindresorhus/p-retry/releases">p-retry's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h3>Breaking</h3>
<ul>
<li>Require Node.js 22  85cdece</li>
<li>Change retry callback order  616306e
<ul>
<li>Callback order changed from <code>onFailedAttempt</code> →
<code>shouldConsumeRetry</code> → <code>shouldRetry</code> to
<code>shouldConsumeRetry</code> → <code>onFailedAttempt</code> →
<code>shouldRetry</code>.</li>
<li>Consumption decisions are now made before failure notifications and
retry decisions.</li>
</ul>
</li>
</ul>
<h3>Improvements</h3>
<ul>
<li>Add <a
href="https://github.com/sindresorhus/p-retry#retrydelay"><code>retryDelay</code></a>
to <code>onFailedAttempt</code> context (<a
href="https://redirect.github.com/sindresorhus/p-retry/issues/66">#66</a>)
96cce98</li>
<li>Fix <code>TypeError</code> retry handling  f011d2e</li>
<li>Harden retry timing and callback validation  9d47b60</li>
</ul>
<hr />
<p><a
href="https://github.com/sindresorhus/p-retry/compare/v7.1.1...v8.0.0">https://github.com/sindresorhus/p-retry/compare/v7.1.1...v8.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/35681f6c70f8ca2bdcb9542281147679184269fa"><code>35681f6</code></a>
8.0.0</li>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/9d47b60e2c8fb324d35cce1987b8591464de24fe"><code>9d47b60</code></a>
Harden retry timing and callback validation</li>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/f011d2e837166f1bea3e739e59754caed4a2dde6"><code>f011d2e</code></a>
Fix <code>TypeError</code> retry handling</li>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/85cdece1c48f3c3fe09d995d86bf59c0d0e4b44f"><code>85cdece</code></a>
Require Node.js 22</li>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/616306ee84f828ffa17f0f02ae4e589815d4f767"><code>616306e</code></a>
Change retry callback order</li>
<li><a
href="https://github.com/sindresorhus/p-retry/commit/96cce98ea2f95c78a4abd780498b2d6af32ac7a4"><code>96cce98</code></a>
Add <code>retryDelay</code> to <code>onFailedAttempt</code> context</li>
<li>See full diff in <a
href="https://github.com/sindresorhus/p-retry/compare/v7.1.1...v8.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=p-retry&package-manager=npm_and_yarn&previous-version=7.1.1&new-version=8.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:35:43 -07:00
dependabot[bot] 28a99e369c build(deps-dev): bump c8 from 10.1.3 to 11.0.0
Bumps [c8](https://github.com/bcoe/c8) from 10.1.3 to 11.0.0.
- [Release notes](https://github.com/bcoe/c8/releases)
- [Changelog](https://github.com/bcoe/c8/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bcoe/c8/compare/v10.1.3...v11.0.0)

---
updated-dependencies:
- dependency-name: c8
  dependency-version: 11.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 15:51:15 -07:00
dependabot[bot] 4df50600ef build(deps-dev): bump open-cli from 8.0.0 to 9.0.0
Bumps [open-cli](https://github.com/sindresorhus/open-cli) from 8.0.0 to 9.0.0.
- [Release notes](https://github.com/sindresorhus/open-cli/releases)
- [Commits](https://github.com/sindresorhus/open-cli/compare/v8.0.0...v9.0.0)

---
updated-dependencies:
- dependency-name: open-cli
  dependency-version: 9.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 15:44:18 -07:00
dependabot[bot] 4843c538d9 build(deps-dev): bump the development-dependencies group with 3 updates
Bumps the development-dependencies group with 3 updates: [esbuild](https://github.com/evanw/esbuild), [undici](https://github.com/nodejs/undici) and [yaml](https://github.com/eemeli/yaml).


Updates `esbuild` from 0.27.3 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.3...v0.27.4)

Updates `undici` from 7.24.1 to 7.24.6
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.24.1...v7.24.6)

Updates `yaml` from 2.8.2 to 2.8.3
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.8.2...v2.8.3)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: undici
  dependency-version: 7.24.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: yaml
  dependency-version: 2.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 15:42:14 -07:00
30 changed files with 1482 additions and 981 deletions
@@ -1,17 +0,0 @@
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
+53 -12
View File
@@ -1,6 +1,7 @@
name: release
on:
workflow_dispatch:
push:
branches:
- "*.x"
@@ -17,24 +18,64 @@ jobs:
name: release
runs-on: ubuntu-latest
steps:
# build local version to create token
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm run build
- uses: ./
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
# install release dependencies and release
- run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag
- run: npx semantic-release --debug
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release-please
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:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+1
View File
@@ -1,3 +1,4 @@
.env
coverage
node_modules/
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
{
".": "3.2.0"
}
+13
View File
@@ -0,0 +1,13 @@
# 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))
+68 -35
View File
@@ -9,10 +9,10 @@ GitHub Action for creating a GitHub App installation access token.
In order to use this action, you need to:
1. [Register new GitHub App](https://docs.github.com/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app).
2. [Store the App's ID or Client ID in your repository environment variables](https://docs.github.com/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) (example: `APP_ID`).
3. [Store the App's private key in your repository secrets](https://docs.github.com/actions/security-guides/encrypted-secrets?tool=webui#creating-encrypted-secrets-for-a-repository) (example: `PRIVATE_KEY`).
2. [Store the App's Client ID in your repository variables](https://docs.github.com/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables#defining-configuration-variables-for-multiple-workflows) (example: `APP_CLIENT_ID`).
3. [Store the App's private key in your repository secrets](https://docs.github.com/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets?tool=webui#creating-secrets-for-a-repository) (example: `APP_PRIVATE_KEY`).
> [!IMPORTANT]
> [!IMPORTANT]
> An installation access token expires after 1 hour. Please [see this comment](https://github.com/actions/create-github-app-token/issues/121#issuecomment-2043214796) for alternative approaches if you have long-running processes.
### Create a token for the current repository
@@ -31,8 +31,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: ./actions/staging-tests
with:
token: ${{ steps.app-token.outputs.token }}
@@ -51,8 +51,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
@@ -77,8 +77,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -102,8 +102,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -138,8 +138,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v4
with:
@@ -160,8 +160,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
repo1
@@ -185,8 +185,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: another-owner
- uses: peter-evans/create-or-update-comment@v4
with:
@@ -195,10 +195,32 @@ jobs:
body: "Hello, World!"
```
### Create a token for an enterprise installation
```yaml
on: [workflow_dispatch]
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
enterprise: my-enterprise-slug
- name: Call enterprise management REST API with gh
run: |
gh api /enterprises/my-enterprise-slug/apps/installable_organizations
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
```
### Create a token with specific permissions
> [!NOTE]
> Selected permissions must be granted to the installation of the specified app and repository owner. Setting a permission that the installation does not have will result in an error.
> Selected permissions must be granted to the specified app installation. Setting a permission that the installation does not have will result in an error.
```yaml
on: [issues]
@@ -210,8 +232,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-issues: write
- uses: peter-evans/create-or-update-comment@v4
@@ -252,8 +274,8 @@ jobs:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ matrix.owners-and-repos.owner }}
repositories: ${{ join(matrix.owners-and-repos.repos) }}
- uses: octokit/request-action@v2.x
@@ -281,7 +303,7 @@ jobs:
id: create_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.GHES_APP_ID }}
client-id: ${{ vars.GHES_APP_CLIENT_ID }}
private-key: ${{ secrets.GHES_APP_PRIVATE_KEY }}
owner: ${{ vars.GHES_INSTALLATION_ORG }}
github-api-url: ${{ vars.GITHUB_API_URL }}
@@ -310,15 +332,18 @@ If you set `HTTP_PROXY` or `HTTPS_PROXY`, also set `NODE_USE_ENV_PROXY: "1"` on
NO_PROXY: github.example.com
NODE_USE_ENV_PROXY: "1"
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
```
## Inputs
### `app-id`
### `client-id` or `app-id`
**Required:** GitHub App ID.
**Required:** GitHub App Client ID.
> [!NOTE]
> The legacy `app-id` input is also accepted, but `client-id` is recommended.
### `private-key`
@@ -331,14 +356,14 @@ steps:
- name: Decode the GitHub App Private Key
id: decode
run: |
private_key=$(echo "${{ secrets.PRIVATE_KEY }}" | base64 -d | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null
private_key=$(echo "${{ secrets.APP_PRIVATE_KEY }}" | base64 -d | awk 'BEGIN {ORS="\\n"} {print}' | head -c -2) &> /dev/null
echo "::add-mask::$private_key"
echo "private-key=$private_key" >> "$GITHUB_OUTPUT"
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.APP_ID }}
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ steps.decode.outputs.private-key }}
```
@@ -353,6 +378,13 @@ steps:
> [!NOTE]
> If `owner` is set and `repositories` is empty, access will be scoped to all repositories in the provided repository owner's installation. If `owner` and `repositories` are empty, access will be scoped to only the current repository.
### `enterprise`
**Optional:** The slug of the enterprise account to generate a token for an enterprise installation.
> [!NOTE]
> The `enterprise` input is mutually exclusive with `owner` and `repositories`. Use it when the GitHub App is installed on an enterprise account. Enterprise installation tokens can call enterprise APIs, but do not grant organization or repository access.
### `permission-<permission name>`
**Optional:** The permissions to grant to the token. By default, the token inherits all of the installation's permissions. We recommend to explicitly list the permissions that are required for a use case. This follows GitHub's own recommendation to [control permissions of `GITHUB_TOKEN` in workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token). The documentation also lists all available permissions, just prefix the permission key with `permission-` (e.g., `pull-requests``permission-pull-requests`).
@@ -383,13 +415,14 @@ GitHub App slug.
## How it works
The action creates an installation access token using [the `POST /app/installations/{installation_id}/access_tokens` endpoint](https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app). By default,
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).
1. The token is scoped to the current repository or `repositories` if set.
2. The token inherits all the installation's permissions.
3. The token is set as output `token` which can be used in subsequent steps.
4. Unless the `skip-token-revoke` input is set to true, the token is revoked in the `post` step of the action, which means it cannot be passed to another job.
5. The token is masked, it cannot be logged accidentally.
The token target depends on the inputs: `enterprise` creates a token for an enterprise installation, `owner` without `repositories` creates a token for all repositories in the owner's installation, `repositories` scopes the token to those repositories, and no target inputs scopes the token to the current repository.
1. The token inherits all the installation's permissions.
2. The token is set as output `token` which can be used in subsequent steps.
3. Unless the `skip-token-revoke` input is set to true, the token is revoked in the `post` step of the action, which means it cannot be passed to another job.
4. The token is masked, it cannot be logged accidentally.
> [!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.
+16 -1
View File
@@ -5,9 +5,13 @@ branding:
icon: "lock"
color: "gray-dark"
inputs:
client-id:
description: "GitHub App Client ID"
required: false
app-id:
description: "GitHub App ID"
required: true
required: false
deprecationMessage: "Use 'client-id' instead."
private-key:
description: "GitHub App private key"
required: true
@@ -17,6 +21,9 @@ inputs:
repositories:
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
required: false
enterprise:
description: "The slug of the enterprise account where the GitHub App is installed (cannot be used with 'owner' or 'repositories')"
required: false
skip-token-revoke:
description: "If true, the token will not be revoked when the current job is complete"
required: false
@@ -31,6 +38,10 @@ inputs:
description: "The level of permission to grant the access token for GitHub Actions workflows, workflow runs, and artifacts. Can be set to 'read' or 'write'."
permission-administration:
description: "The level of permission to grant the access token for repository creation, deletion, settings, teams, and collaborators creation. Can be set to 'read' or 'write'."
permission-artifact-metadata:
description: "The level of permission to grant the access token to create and retrieve build artifact metadata records. Can be set to 'read' or 'write'."
permission-attestations:
description: "The level of permission to create and retrieve the access token for repository attestations. Can be set to 'read' or 'write'."
permission-checks:
description: "The level of permission to grant the access token for checks on code. Can be set to 'read' or 'write'."
permission-codespaces:
@@ -43,6 +54,8 @@ inputs:
description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'."
permission-deployments:
description: "The level of permission to grant the access token for deployments and deployment statuses. Can be set to 'read' or 'write'."
permission-discussions:
description: "The level of permission to grant the access token for discussions and related comments and labels. Can be set to 'read' or 'write'."
permission-email-addresses:
description: "The level of permission to grant the access token to manage the email addresses belonging to a user. Can be set to 'read' or 'write'."
permission-enterprise-custom-properties-for-organizations:
@@ -61,6 +74,8 @@ inputs:
description: "The level of permission to grant the access token for issues and related comments, assignees, labels, and milestones. Can be set to 'read' or 'write'."
permission-members:
description: "The level of permission to grant the access token for organization teams and members. Can be set to 'read' or 'write'."
permission-merge-queues:
description: "The level of permission to grant the access token to manage the merge queues for a repository. Can be set to 'read' or 'write'."
permission-metadata:
description: "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata. Can be set to 'read' or 'write'."
permission-organization-administration:
+211 -121
View File
@@ -22964,30 +22964,37 @@ var isError = (value) => objectToString.call(value) === "[object Error]";
var errorMessages = /* @__PURE__ */ new Set([
"network error",
// Chrome
"Failed to fetch",
// Chrome
"NetworkError when attempting to fetch resource.",
// Firefox
"The Internet connection appears to be offline.",
// Safari 16
"Load failed",
// Safari 17+
"Network request failed",
// `cross-fetch`
"fetch failed",
// Undici (Node.js)
"terminated"
"terminated",
// Undici (Node.js)
" A network error occurred.",
// Bun (WebKit)
"Network connection lost"
// Cloudflare Workers (fetch)
]);
function isNetworkError(error2) {
const isValid = error2 && isError(error2) && error2.name === "TypeError" && typeof error2.message === "string";
if (!isValid) {
return false;
}
if (error2.message === "Load failed") {
return error2.stack === void 0;
const { message, stack } = error2;
if (message === "Load failed" || message.startsWith("Load failed (") && message.endsWith(")")) {
return stack === void 0 || "__sentry_captured__" in error2;
}
return errorMessages.has(error2.message);
if (message.startsWith("error sending request for url")) {
return true;
}
if (message === "Failed to fetch" || message.startsWith("Failed to fetch (") && message.endsWith(")")) {
return true;
}
return errorMessages.has(message);
}
// node_modules/p-retry/index.js
@@ -23017,6 +23024,14 @@ function validateNumberOption(name, value, { min = 0, allowInfinity = false } =
throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
}
}
function validateFunctionOption(name, value) {
if (value === void 0) {
return;
}
if (typeof value !== "function") {
throw new TypeError(`Expected \`${name}\` to be a function.`);
}
}
var AbortError = class extends Error {
constructor(message) {
super();
@@ -23044,6 +23059,26 @@ function calculateRemainingTime(start, max) {
}
return max - (performance.now() - start);
}
async function delayForRetry(delay, options) {
if (delay <= 0) {
return;
}
await new Promise((resolve2, reject) => {
const onAbort = () => {
clearTimeout(timeoutToken);
options.signal?.removeEventListener("abort", onAbort);
reject(options.signal.reason);
};
const timeoutToken = setTimeout(() => {
options.signal?.removeEventListener("abort", onAbort);
resolve2();
}, delay);
if (options.unref) {
timeoutToken.unref?.();
}
options.signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function onAttemptFailure({ error: error2, attemptNumber, retriesConsumed, startTime, options }) {
const normalizedError = error2 instanceof Error ? error2 : new TypeError(`Non-error was thrown: "${error2}". You should only throw errors.`);
if (normalizedError instanceof AbortError) {
@@ -23051,55 +23086,60 @@ async function onAttemptFailure({ error: error2, attemptNumber, retriesConsumed,
}
const retriesLeft = Number.isFinite(options.retries) ? Math.max(0, options.retries - retriesConsumed) : options.retries;
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
const delayTime = calculateDelay(retriesConsumed, options);
const remainingTimeBeforeCallbacks = calculateRemainingTime(startTime, maxRetryTime);
if (remainingTimeBeforeCallbacks <= 0) {
const context2 = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed,
retryDelay: 0
});
await options.onFailedAttempt(context2);
throw normalizedError;
}
const consumeRetryContext = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed,
retryDelay: retriesLeft > 0 ? delayTime : 0
});
const consumeRetry = await options.shouldConsumeRetry(consumeRetryContext);
const effectiveDelay = consumeRetry && retriesLeft > 0 ? delayTime : 0;
const context = Object.freeze({
error: normalizedError,
attemptNumber,
retriesLeft,
retriesConsumed
retriesConsumed,
retryDelay: effectiveDelay
});
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;
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 delayTime = calculateDelay(retriesConsumed, options);
const finalDelay = Math.min(delayTime, remainingTime);
const finalDelay = Math.min(effectiveDelay, remainingTimeAfterShouldRetry);
options.signal?.throwIfAborted();
if (finalDelay > 0) {
await new Promise((resolve2, reject) => {
const onAbort = () => {
clearTimeout(timeoutToken);
options.signal?.removeEventListener("abort", onAbort);
reject(options.signal.reason);
};
const timeoutToken = setTimeout(() => {
options.signal?.removeEventListener("abort", onAbort);
resolve2();
}, finalDelay);
if (options.unref) {
timeoutToken.unref?.();
}
options.signal?.addEventListener("abort", onAbort, { once: true });
});
}
await delayForRetry(finalDelay, options);
options.signal?.throwIfAborted();
return true;
}
@@ -23119,6 +23159,9 @@ async function pRetry(input, options = {}) {
};
options.shouldRetry ??= () => true;
options.shouldConsumeRetry ??= () => true;
validateFunctionOption("onFailedAttempt", options.onFailedAttempt);
validateFunctionOption("shouldRetry", options.shouldRetry);
validateFunctionOption("shouldConsumeRetry", options.shouldConsumeRetry);
validateNumberOption("factor", options.factor, { min: 0, allowInfinity: false });
validateNumberOption("minTimeout", options.minTimeout, { min: 0, allowInfinity: false });
validateNumberOption("maxTimeout", options.maxTimeout, { min: 0, allowInfinity: true });
@@ -23153,80 +23196,20 @@ async function pRetry(input, options = {}) {
}
// lib/main.js
async function main(appId, privateKey, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
let parsedOwner = "";
let parsedRepositoryNames = [];
if (!owner && repositories.length === 0) {
const [owner2, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
parsedOwner = owner2;
parsedRepositoryNames = [repo];
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner2}/${repo}).`
);
}
if (owner && repositories.length === 0) {
parsedOwner = owner;
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
}
if (!owner && repositories.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories;
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
}
if (owner && repositories.length > 0) {
parsedOwner = owner;
parsedRepositoryNames = repositories;
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
async function main(clientId, privateKey, enterprise, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
if (enterprise && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
}
const target = resolveInstallationTarget(enterprise, owner, repositories, core);
const auth5 = createAppAuth2({
appId,
appId: clientId,
privateKey,
request: request2
});
let authentication, installationId, appSlug;
if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromRepository(
request2,
auth5,
parsedOwner,
parsedRepositoryNames,
permissions
),
{
shouldRetry: ({ error: error2 }) => error2.status >= 500,
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedRepositoryNames.join(
","
)}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
}
));
} else {
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request2, auth5, parsedOwner, permissions),
{
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
}
));
}
const { authentication, installationId, appSlug } = await pRetry(
() => getTokenFromTarget(request2, auth5, target, permissions),
createTokenRetryOptions(core, getTokenRetryDescription(target))
);
core.setSecret(authentication.token);
core.setOutput("token", authentication.token);
core.setOutput("installation-id", installationId);
@@ -23236,6 +23219,102 @@ async function main(appId, privateKey, owner, repositories, permissions, core, c
core.saveState("expiresAt", authentication.expiresAt);
}
}
function resolveInstallationTarget(enterprise, owner, repositories, core) {
if (enterprise) {
core.info(`Creating enterprise installation token for enterprise "${enterprise}".`);
return { type: "enterprise", enterprise };
}
if (!owner && repositories.length === 0) {
const [defaultOwner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${defaultOwner}/${repo}).`
);
return {
type: "repository",
owner: defaultOwner,
repositories: [repo]
};
}
if (owner && repositories.length === 0) {
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
return { type: "owner", owner };
}
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
if (!owner) {
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
} else {
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories.map((repo) => `
- ${parsedOwner}/${repo}`).join("")}`
);
}
return {
type: "repository",
owner: parsedOwner,
repositories
};
}
function getTokenRetryDescription(target) {
switch (target.type) {
case "enterprise":
return `enterprise "${target.enterprise}"`;
case "repository":
return `"${target.repositories.map((repository) => `${target.owner}/${repository}`).join(",")}"`;
case "owner":
return `"${target.owner}"`;
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function getTokenFromTarget(request2, auth5, target, permissions) {
switch (target.type) {
case "enterprise":
return getTokenFromEnterprise(request2, auth5, target.enterprise, permissions);
case "repository":
return getTokenFromRepository(
request2,
auth5,
target.owner,
target.repositories,
permissions
);
case "owner":
return getTokenFromOwner(request2, auth5, target.owner, permissions);
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function createTokenRetryOptions(core, targetDescription) {
return {
shouldRetry: ({ error: error2 }) => error2.status >= 500 || isNetworkError(error2),
onFailedAttempt: (context) => {
core.info(
`Failed to create token for ${targetDescription} (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3
};
}
async function createInstallationAuthResult(auth5, installation, permissions, options = {}) {
const authentication = await auth5({
type: "installation",
installationId: installation.id,
permissions,
...options
});
return {
authentication,
installationId: installation.id,
appSlug: installation["app_slug"]
};
}
async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
const response = await request2("GET /users/{username}/installation", {
username: parsedOwner,
@@ -23243,14 +23322,7 @@ async function getTokenFromOwner(request2, auth5, parsedOwner, permissions) {
hook: auth5.hook
}
});
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 };
return createInstallationAuthResult(auth5, response.data, permissions);
}
async function getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames, permissions) {
const response = await request2("GET /repos/{owner}/{repo}/installation", {
@@ -23260,15 +23332,28 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi
hook: auth5.hook
}
});
const authentication = await auth5({
type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames,
permissions
return createInstallationAuthResult(auth5, response.data, permissions, {
repositoryNames: parsedRepositoryNames
});
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
async function getTokenFromEnterprise(request2, auth5, enterprise, permissions) {
let response;
try {
response = await request2("GET /enterprises/{enterprise}/installation", {
enterprise,
request: {
hook: auth5.hook
}
});
} catch (error2) {
if (error2.status === 404) {
throw new Error(
`No enterprise installation found matching the enterprise slug "${enterprise}".`
);
}
throw error2;
}
return createInstallationAuthResult(auth5, response.data, permissions);
}
// lib/request.js
@@ -23307,15 +23392,20 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
}
async function run() {
ensureNativeProxySupport();
const appId = getInput("app-id");
const clientId = getInput("client-id") || getInput("app-id");
if (!clientId) {
throw new Error("The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.");
}
const privateKey = getInput("private-key");
const enterprise = getInput("enterprise");
const owner = getInput("owner");
const repositories = getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== "");
const skipTokenRevoke = getBooleanInput("skip-token-revoke");
const permissions = getPermissionsFromInputs(process.env);
return main(
appId,
clientId,
privateKey,
enterprise,
owner,
repositories,
permissions,
+160 -106
View File
@@ -1,9 +1,11 @@
import pRetry from "p-retry";
import isNetworkError from "is-network-error";
// @ts-check
/**
* @param {string} appId
* @param {string} clientId
* @param {string} privateKey
* @param {string} enterprise
* @param {string} owner
* @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions
@@ -13,107 +15,34 @@ import pRetry from "p-retry";
* @param {boolean} skipTokenRevoke
*/
export async function main(
appId,
clientId,
privateKey,
enterprise,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke
skipTokenRevoke,
) {
let parsedOwner = "";
let parsedRepositoryNames = [];
// If neither owner nor repositories are set, default to current repository
if (!owner && repositories.length === 0) {
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
parsedOwner = owner;
parsedRepositoryNames = [repo];
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
);
// Validate mutual exclusivity of enterprise with owner/repositories
if (enterprise && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
}
// 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 target = resolveInstallationTarget(enterprise, owner, repositories, core);
const auth = createAppAuth({
appId,
appId: clientId,
privateKey,
request,
});
let authentication, installationId, appSlug;
// If at least one repository is set, get installation ID from that repository
if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(
() =>
getTokenFromRepository(
request,
auth,
parsedOwner,
parsedRepositoryNames,
permissions
),
{
shouldRetry: ({ error }) => error.status >= 500,
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedRepositoryNames.join(
","
)}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
}
));
} else {
// Otherwise get the installation for the owner, which can either be an organization or a user account
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request, auth, parsedOwner, permissions),
{
onFailedAttempt: (context) => {
core.info(
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
}
));
}
const { authentication, installationId, appSlug } = await pRetry(
() => getTokenFromTarget(request, auth, target, permissions),
createTokenRetryOptions(core, getTokenRetryDescription(target))
);
// Register the token with the runner as a secret to ensure it is masked in logs
core.setSecret(authentication.token);
@@ -129,6 +58,125 @@ export async function main(
}
}
function resolveInstallationTarget(enterprise, owner, repositories, core) {
if (enterprise) {
core.info(`Creating enterprise installation token for enterprise "${enterprise}".`);
return { type: "enterprise", enterprise };
}
if (!owner && repositories.length === 0) {
const [defaultOwner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
core.info(
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${defaultOwner}/${repo}).`
);
return {
type: "repository",
owner: defaultOwner,
repositories: [repo],
};
}
if (owner && repositories.length === 0) {
core.info(
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
);
return { type: "owner", owner };
}
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
if (!owner) {
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}`
);
} else {
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
.join("")}`
);
}
return {
type: "repository",
owner: parsedOwner,
repositories,
};
}
function getTokenRetryDescription(target) {
switch (target.type) {
case "enterprise":
return `enterprise "${target.enterprise}"`;
case "repository":
return `"${target.repositories
.map((repository) => `${target.owner}/${repository}`)
.join(",")}"`;
case "owner":
return `"${target.owner}"`;
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function getTokenFromTarget(request, auth, target, permissions) {
switch (target.type) {
case "enterprise":
return getTokenFromEnterprise(request, auth, target.enterprise, permissions);
case "repository":
return getTokenFromRepository(
request,
auth,
target.owner,
target.repositories,
permissions
);
case "owner":
return getTokenFromOwner(request, auth, target.owner, permissions);
/* c8 ignore next 2 */
default:
throw new Error(`Unsupported installation target type: ${target.type}`);
}
}
function createTokenRetryOptions(core, targetDescription) {
return {
shouldRetry: ({ error }) => error.status >= 500 || isNetworkError(error),
onFailedAttempt: (context) => {
core.info(
`Failed to create token for ${targetDescription} (attempt ${context.attemptNumber}): ${context.error.message}`
);
},
retries: 3,
};
}
async function createInstallationAuthResult(
auth,
installation,
permissions,
options = {},
) {
const authentication = await auth({
type: "installation",
installationId: installation.id,
permissions,
...options,
});
return {
authentication,
installationId: installation.id,
appSlug: installation["app_slug"],
};
}
async function getTokenFromOwner(request, auth, parsedOwner, permissions) {
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
// This endpoint works for both users and organizations
@@ -139,17 +187,8 @@ async function getTokenFromOwner(request, auth, parsedOwner, permissions) {
},
});
// Get token for for all repositories of the given installation
const authentication = await auth({
type: "installation",
installationId: response.data.id,
permissions,
});
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
// Get token for all repositories of the given installation
return createInstallationAuthResult(auth, response.data, permissions);
}
async function getTokenFromRepository(
@@ -169,15 +208,30 @@ async function getTokenFromRepository(
});
// Get token for given repositories
const authentication = await auth({
type: "installation",
installationId: response.data.id,
return createInstallationAuthResult(auth, response.data, permissions, {
repositoryNames: parsedRepositoryNames,
permissions,
});
const installationId = response.data.id;
const appSlug = response.data["app_slug"];
return { authentication, installationId, appSlug };
}
async function getTokenFromEnterprise(request, auth, enterprise, permissions) {
let response;
try {
response = await request("GET /enterprises/{enterprise}/installation", {
enterprise,
request: {
hook: auth.hook,
},
});
} catch (error) {
if (error.status === 404) {
throw new Error(
`No enterprise installation found matching the enterprise slug "${enterprise}".`
);
}
throw error;
}
// Get token for the enterprise installation
return createInstallationAuthResult(auth, response.data, permissions);
}
+7 -2
View File
@@ -18,8 +18,12 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
async function run() {
ensureNativeProxySupport();
const appId = core.getInput("app-id");
const clientId = core.getInput("client-id") || core.getInput("app-id");
if (!clientId) {
throw new Error("The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.");
}
const privateKey = core.getInput("private-key");
const enterprise = core.getInput("enterprise");
const owner = core.getInput("owner");
const repositories = core
.getInput("repositories")
@@ -32,8 +36,9 @@ async function run() {
const permissions = getPermissionsFromInputs(process.env);
return main(
appId,
clientId,
privateKey,
enterprise,
owner,
repositories,
permissions,
+401 -636
View File
File diff suppressed because it is too large Load Diff
+10 -37
View File
@@ -2,7 +2,7 @@
"name": "create-github-app-token",
"private": true,
"type": "module",
"version": "3.0.0",
"version": "3.2.0",
"description": "GitHub Action for creating a GitHub App Installation Access Token",
"engines": {
"node": ">=24.4.0"
@@ -16,45 +16,18 @@
},
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/core": "^3.0.1",
"@octokit/auth-app": "^8.2.0",
"@octokit/request": "^10.0.8",
"p-retry": "^7.1.1"
"is-network-error": "^1.3.2",
"p-retry": "^8.0.0"
},
"devDependencies": {
"@octokit/openapi": "^21.0.0",
"c8": "^10.1.3",
"esbuild": "^0.27.3",
"open-cli": "^8.0.0",
"undici": "^7.24.1",
"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}"
}
]
]
"@octokit/openapi": "^22.0.0",
"c8": "^11.0.0",
"esbuild": "^0.27.4",
"open-cli": "^9.0.0",
"undici": "^7.24.6",
"yaml": "^2.8.3"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$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
@@ -0,0 +1,9 @@
{
"$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,6 +19,22 @@
"write"
]
},
"artifact_metadata": {
"type": "string",
"description": "The level of permission to grant the access token to create and retrieve build artifact metadata records.",
"enum": [
"read",
"write"
]
},
"attestations": {
"type": "string",
"description": "The level of permission to create and retrieve the access token for repository attestations.",
"enum": [
"read",
"write"
]
},
"checks": {
"type": "string",
"description": "The level of permission to grant the access token for checks on code.",
@@ -59,6 +75,14 @@
"write"
]
},
"discussions": {
"type": "string",
"description": "The level of permission to grant the access token for discussions and related comments and labels.",
"enum": [
"read",
"write"
]
},
"environments": {
"type": "string",
"description": "The level of permission to grant the access token for managing repository environments.",
@@ -75,6 +99,14 @@
"write"
]
},
"merge_queues": {
"type": "string",
"description": "The level of permission to grant the access token to manage the merge queues for a repository.",
"enum": [
"read",
"write"
]
},
"metadata": {
"type": "string",
"description": "The level of permission to grant the access token to search repositories, list collaborators, and access repository metadata.",
+2 -2
View File
@@ -32,5 +32,5 @@ node --test --test-update-snapshots tests/index.js
We have tests both for the `main.js` and `post.js` scripts.
- If you do not expect an error, take [main-token-permissions-set.test.js](tests/main-token-permissions-set.test.js) as a starting point.
- If your test has an expected error, take [main-missing-app-id.test.js](tests/main-missing-app-id.test.js) as a starting point.
- If you do not expect an error, take [main-token-permissions-set.test.js](main-token-permissions-set.test.js) as a starting point.
- If your test has an expected error, take [main-missing-client-and-app-id.test.js](main-missing-client-and-app-id.test.js) as a starting point.
+20 -4
View File
@@ -11,6 +11,13 @@ snapshot.setDefaultSnapshotSerializers([
(value) => (typeof value === "string" ? value : undefined),
]);
function normalizeStderr(stderr) {
return stderr
.replaceAll(/\u001B\[[0-9;]*m/g, "")
.replaceAll(process.cwd(), "<cwd>")
.replaceAll(/:\d+:\d+/g, ":<line>:<column>");
}
// Get all files in tests directory
const files = readdirSync("tests");
@@ -39,10 +46,19 @@ for (const file of testFiles) {
NODE_USE_ENV_PROXY,
...env
} = process.env;
const { stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
env,
});
const trimmedStderr = stderr.replace(/\r?\n$/, "");
let stderr, stdout;
try {
({ stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
env,
}));
} catch (error) {
if (!(error instanceof Error) || !("stderr" in error) || !("stdout" in error)) {
throw error;
}
({ stderr, stdout } = error);
}
const trimmedStderr = normalizeStderr(stderr).replace(/\r?\n$/, "");
const trimmedStdout = stdout.replace(/\r?\n$/, "");
await t.test("stderr", (t) => {
if (trimmedStderr) t.assert.snapshot(trimmedStderr);
+205 -7
View File
@@ -1,6 +1,43 @@
exports[`action-deprecated-inputs.test.js > stdout 1`] = `
app-id — Use 'client-id' instead.
`;
exports[`main-app-id-fallback.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-client-id-precedence.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-custom-github-api-url.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -17,6 +54,113 @@ POST /api/v3/app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;
exports[`main-enterprise-fail-response.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
Failed to create token for enterprise "test-enterprise" (attempt 1): GitHub API not available
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
null
`;
exports[`main-enterprise-installation-not-found.test.js > stderr 1`] = `
Error: No enterprise installation found matching the enterprise slug "test-enterprise".
at getTokenFromEnterprise (file://<cwd>/lib/main.js:<line>:<column>)
at process.processTicksAndRejections (node:internal/process/task_queues:<line>:<column>)
at async pRetry (file://<cwd>/node_modules/p-retry/index.js:<line>:<column>)
at async main (file://<cwd>/lib/main.js:<line>:<column>)
at async test (file://<cwd>/tests/main.js:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-installation-not-found.test.js:<line>:<column>
`;
exports[`main-enterprise-installation-not-found.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
Failed to create token for enterprise "test-enterprise" (attempt 1): No enterprise installation found matching the enterprise slug "test-enterprise".
::error::No enterprise installation found matching the enterprise slug "test-enterprise".
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
`;
exports[`main-enterprise-mutual-exclusivity-owner.test.js > stderr 1`] = `
Error: Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-mutual-exclusivity-owner.test.js:<line>:<column>
`;
exports[`main-enterprise-mutual-exclusivity-owner.test.js > stdout 1`] = `
::error::Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
`;
exports[`main-enterprise-mutual-exclusivity-repositories.test.js > stderr 1`] = `
Error: Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-enterprise-mutual-exclusivity-repositories.test.js:<line>:<column>
`;
exports[`main-enterprise-mutual-exclusivity-repositories.test.js > stdout 1`] = `
::error::Cannot use 'enterprise' input with 'owner' or 'repositories' inputs
`;
exports[`main-enterprise-only-success.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
null
`;
exports[`main-enterprise-token-permissions-set.test.js > stdout 1`] = `
Creating enterprise installation token for enterprise "test-enterprise".
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /enterprises/test-enterprise/installation
POST /app/installations/123456/access_tokens
{"permissions":{"enterprise_custom_properties_for_organizations":"read"}}
`;
exports[`main-missing-client-and-app-id.test.js > stderr 1`] = `
The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.
`;
exports[`main-missing-client-and-app-id.test.js > stdout 1`] = `
::error::The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string. If using a secret or variable, ensure it is available in this workflow context.
`;
exports[`main-missing-owner.test.js > stderr 1`] = `
GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'
`;
@@ -57,7 +201,6 @@ exports[`main-repo-skew.test.js > stderr 1`] = `
exports[`main-repo-skew.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -75,6 +218,45 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]}
`;
exports[`main-token-get-owner-set-client-error.test.js > stderr 1`] = `
RequestError [HttpError]: Forbidden
at fetchWrapper (file://<cwd>/node_modules/@octokit/request/dist-bundle/index.js:<line>:<column>)
at process.processTicksAndRejections (node:internal/process/task_queues:<line>:<column>)
at async hook (file://<cwd>/node_modules/@octokit/auth-app/dist-node/index.js:<line>:<column>)
at async getTokenFromOwner (file://<cwd>/lib/main.js:<line>:<column>)
at async pRetry (file://<cwd>/node_modules/p-retry/index.js:<line>:<column>)
at async main (file://<cwd>/lib/main.js:<line>:<column>)
at async test (file://<cwd>/tests/main.js:<line>:<column>)
at async file://<cwd>/tests/main-token-get-owner-set-client-error.test.js:<line>:<column> {
status: 403,
request: {
method: 'GET',
url: 'https://api.github.com/users/smockle/installation',
headers: {
accept: 'application/vnd.github.v3+json',
'user-agent': 'actions/create-github-app-token',
authorization: 'bearer [REDACTED]'
},
request: { hook: [Function: bound hook] AsyncFunction }
},
response: {
url: 'https://api.github.com/users/smockle/installation',
status: 403,
headers: { 'content-type': 'application/json' },
data: { message: 'Forbidden' }
},
[cause]: undefined
}
`;
exports[`main-token-get-owner-set-client-error.test.js > stdout 1`] = `
Input 'repositories' is not set. Creating token for all repositories owned by smockle.
Failed to create token for "smockle" (attempt 1): Forbidden
::error::Forbidden
--- REQUESTS ---
GET /users/smockle/installation
`;
exports[`main-token-get-owner-set-fail-response.test.js > stdout 1`] = `
Input 'repositories' is not set. Creating token for all repositories owned by smockle.
Failed to create token for "smockle" (attempt 1): GitHub API not available
@@ -96,9 +278,8 @@ null
exports[`main-token-get-owner-set-repo-fail-response.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/failed-repo
Failed to create token for "failed-repo" (attempt 1): GitHub API not available
Failed to create token for "actions/failed-repo" (attempt 1): GitHub API not available
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
@@ -115,9 +296,28 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]}
`;
exports[`main-token-get-owner-set-repo-network-error.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/network-repo
Failed to create token for "actions/network-repo" (attempt 1): fetch failed
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::set-output name=installation-id::123456
::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/network-repo/installation
GET /repos/actions/network-repo/installation
POST /app/installations/123456/access_tokens
{"repositories":["network-repo"]}
`;
exports[`main-token-get-owner-set-repo-set-to-many-newline.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
- actions/toolkit
- actions/checkout
@@ -138,7 +338,6 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-many.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
- actions/toolkit
- actions/checkout
@@ -159,7 +358,6 @@ POST /app/installations/123456/access_tokens
exports[`main-token-get-owner-set-repo-set-to-one.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
+11
View File
@@ -0,0 +1,11 @@
import { DEFAULT_ENV, test } from "./main.js";
// Verify `main` falls back to `app-id` when `client-id` is not set
await test(
() => {},
{
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "",
"INPUT_APP-ID": "123456",
}
);
+11
View File
@@ -0,0 +1,11 @@
import { DEFAULT_ENV, test } from "./main.js";
// Verify `client-id` takes precedence when both `client-id` and `app-id` are set
await test(
() => {},
{
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
"INPUT_APP-ID": "123456",
}
);
@@ -0,0 +1,39 @@
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" } },
);
});
@@ -0,0 +1,25 @@
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" } }
);
});
@@ -0,0 +1,13 @@
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;
@@ -0,0 +1,13 @@
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;
@@ -0,0 +1,30 @@
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" } }
);
});
@@ -0,0 +1,34 @@
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" } }
);
});
@@ -0,0 +1,20 @@
import { DEFAULT_ENV } from "./main.js";
for (const [key, value] of Object.entries({
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "",
"INPUT_APP-ID": "",
})) {
process.env[key] = value;
}
// Log only the error message, not the full stack trace, because the stack
// trace contains environment-specific paths and ANSI codes that differ
// between local and CI environments.
const _error = console.error;
console.error = (err) => _error(err?.message ?? err);
// Verify `main` exits with an error when neither `client-id` nor `app-id` is set.
const { default: promise } = await import("../main.js");
await promise;
process.exitCode = 0;
@@ -0,0 +1,23 @@
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" } },
);
});
@@ -0,0 +1,39 @@
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
"INPUT_GITHUB-API-URL": "https://api.github.com",
"INPUT_SKIP-TOKEN-REVOKE": "false",
"INPUT_APP-ID": "123456",
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
// This key is invalidated. Its from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
"INPUT_PRIVATE-KEY": `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL