Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 37 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +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 environment variables](https://docs.github.com/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) (example: `GITHUB_APP_CLIENT_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: `GITHUB_APP_PRIVATE_KEY`).

Pass the App's Client ID using the `client-id` input. The legacy `app-id` input remains available for compatibility, but is deprecated.

> [!IMPORTANT]
> 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.
Expand All @@ -31,8 +33,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
- uses: ./actions/staging-tests
with:
token: ${{ steps.app-token.outputs.token }}
Expand All @@ -51,8 +53,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
Expand All @@ -77,8 +79,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_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"
Expand All @@ -102,8 +104,8 @@ jobs:
id: app-token
with:
# required
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
client-id: ${{ vars.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_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"
Expand Down Expand Up @@ -138,8 +140,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v4
with:
Expand All @@ -160,8 +162,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
repo1
Expand All @@ -185,8 +187,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
owner: another-owner
- uses: peter-evans/create-or-update-comment@v4
with:
Expand All @@ -210,8 +212,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-issues: write
- uses: peter-evans/create-or-update-comment@v4
Expand Down Expand Up @@ -252,8 +254,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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
owner: ${{ matrix.owners-and-repos.owner }}
repositories: ${{ join(matrix.owners-and-repos.repos) }}
- uses: octokit/request-action@v2.x
Expand Down Expand Up @@ -281,7 +283,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 }}
Expand Down Expand Up @@ -310,15 +312,24 @@ 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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
```

## Inputs

### `client-id`

**Optional:** GitHub App Client ID. This is the recommended input.

### `app-id`

**Required:** GitHub App ID.
**Optional:** GitHub App ID.

> [!WARNING]
> `app-id` is deprecated. Use `client-id` instead.

You must set either `client-id` or `app-id`. If both are set, `client-id` takes precedence.

### `private-key`

Expand All @@ -331,14 +342,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.GITHUB_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.GITHUB_APP_CLIENT_ID }}
private-key: ${{ steps.decode.outputs.private-key }}
```

Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions dist/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23153,7 +23153,7 @@ async function pRetry(input, options = {}) {
}

// lib/main.js
async function main(appId, privateKey, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
async function main(clientId, privateKey, owner, repositories, permissions, core, createAppAuth2, request2, skipTokenRevoke) {
let parsedOwner = "";
let parsedRepositoryNames = [];
if (!owner && repositories.length === 0) {
Expand Down Expand Up @@ -23188,7 +23188,7 @@ async function main(appId, privateKey, owner, repositories, permissions, core, c
);
}
const auth5 = createAppAuth2({
appId,
appId: clientId,
privateKey,
request: request2
});
Expand Down Expand Up @@ -23307,14 +23307,17 @@ 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("Either 'client-id' or 'app-id' input must be set");
}
const privateKey = getInput("private-key");
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,
owner,
repositories,
Expand Down
6 changes: 3 additions & 3 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import pRetry from "p-retry";
// @ts-check

/**
* @param {string} appId
* @param {string} clientId
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc and parameter name (clientId) suggest this argument is always a GitHub App Client ID, but callers pass either client-id or legacy app-id here. Tweaking the docs/parameter name to reflect that it can be either (e.g., appIdOrClientId) would make intent clearer, especially since it’s mapped into createAppAuth({ appId: ... }).

Suggested change
* @param {string} clientId
* @param {string} clientId GitHub App client ID (`client-id`) or legacy app ID (`app-id`)

Copilot uses AI. Check for mistakes.
* @param {string} privateKey
* @param {string} owner
* @param {string[]} repositories
Expand All @@ -13,7 +13,7 @@ import pRetry from "p-retry";
* @param {boolean} skipTokenRevoke
*/
export async function main(
appId,
clientId,
privateKey,
owner,
repositories,
Expand Down Expand Up @@ -70,7 +70,7 @@ export async function main(
}

const auth = createAppAuth({
appId,
appId: clientId,
privateKey,
request,
});
Expand Down
7 changes: 5 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ 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("Either 'client-id' or 'app-id' input must be set");
}
Comment on lines +21 to +24
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clientId is populated from either the client-id input or the legacy app-id input, so the name is a bit misleading (it may hold an App ID). Consider renaming this variable (and the corresponding argument passed into main(...)) to something neutral like appIdentifier / appIdOrClientId to reduce confusion during future maintenance.

Copilot uses AI. Check for mistakes.
const privateKey = core.getInput("private-key");
const owner = core.getInput("owner");
const repositories = core
Expand All @@ -32,7 +35,7 @@ async function run() {
const permissions = getPermissionsFromInputs(process.env);

return main(
appId,
clientId,
privateKey,
owner,
repositories,
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ 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 your test has an expected error, take [main-missing-client-and-app-id.test.js](tests/main-missing-client-and-app-id.test.js) as a starting point.
29 changes: 29 additions & 0 deletions tests/index.js.snapshot
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
exports[`action-deprecated-inputs.test.js > stdout 1`] = `
app-id — Use 'client-id' instead.
`;

exports[`main-client-id.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are not set. Creating token for this repository (actions/create-github-app-token).
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a

::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a

::set-output name=installation-id::123456

::set-output name=app-slug::github-actions
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---
GET /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;

exports[`main-custom-github-api-url.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:

Expand All @@ -17,6 +38,14 @@ POST /api/v3/app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;

exports[`main-missing-client-and-app-id.test.js > stderr 1`] = `
Either 'client-id' or 'app-id' input must be set
`;

exports[`main-missing-client-and-app-id.test.js > stdout 1`] = `
::error::Either 'client-id' or 'app-id' input must be set
`;

exports[`main-missing-owner.test.js > stderr 1`] = `
GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'
`;
Expand Down
11 changes: 11 additions & 0 deletions tests/main-client-id.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DEFAULT_ENV, test } from "./main.js";

// Verify `main` accepts a GitHub App client ID via the `client-id` input
await test(
() => {},
{
...DEFAULT_ENV,
"INPUT_CLIENT-ID": "Iv1.0123456789abcdef",
"INPUT_APP-ID": "",
}
);
20 changes: 20 additions & 0 deletions tests/main-missing-client-and-app-id.test.js
Original file line number Diff line number Diff line change
@@ -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;
Loading