Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit 8fbc2cd

Browse files
committed
Add support for organization and user installation retrieval and repository scoping
1 parent 3a5af6b commit 8fbc2cd

File tree

6 files changed

+119
-45
lines changed

6 files changed

+119
-45
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,29 @@ jobs:
2020
steps:
2121
- name: Generate token
2222
id: generate_token
23-
uses: tibdex/github-app-token@v1
23+
uses: tibdex/github-app-token@v2
2424
with:
2525
app_id: ${{ secrets.APP_ID }}
2626

2727
# Optional.
2828
# github_api_url: https://api.example.com
2929

3030
# Optional.
31-
# installation_id: 1337
31+
# installation_retrieval_mode: id
32+
33+
# Optional.
34+
# installation_retrieval_payload: 1337
3235

3336
# Optional.
3437
# Using a YAML multiline string to avoid escaping the JSON quotes.
3538
# permissions: >-
36-
# {"members": "read"}
39+
# {"pull_requests": "read"}
3740

3841
private_key: ${{ secrets.PRIVATE_KEY }}
3942

4043
# Optional.
41-
# repository: owner/repo
44+
# repositories: >-
45+
# ["actions/toolkit", "github/docs"]
4246

4347
- name: Use token
4448
env:

action.yml

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,45 @@ inputs:
55
app_id:
66
description: ID of the GitHub App.
77
required: true
8-
installation_id:
9-
description: The ID of the installation for which the token will be requested (defaults to the ID of the repository's installation).
108
github_api_url:
119
description: The API URL of the GitHub server.
1210
default: ${{ github.api_url }}
11+
installation_retrieval_mode:
12+
description: >-
13+
The mode used to retrieve the installation for which the token will be requested.
14+
15+
One of:
16+
- id: use the installation with the specified ID.
17+
- organization: fetch an organization installation (https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app).
18+
- repository: fetch a repository installation (https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app).
19+
- user: fetch a user installation (https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app).
20+
default: repository
21+
installation_retrieval_payload:
22+
description: >-
23+
The payload used to retrieve the installation.
24+
25+
Examples for each retrieval mode:
26+
- id: 1337
27+
- organization: github
28+
- repository: tibdex/github-app-token
29+
- user: tibdex
30+
default: ${{ github.repository }}
1331
permissions:
14-
description: The JSON-stringified permissions granted to the token (defaults to all the GitHub app permissions, see https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).
32+
description: >-
33+
The JSON-stringified permissions granted to the token.
34+
Default to all the GitHub app permissions.
35+
See https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app's `permissions`.
1536
private_key:
1637
description: Private key of the GitHub App (can be Base64 encoded).
1738
required: true
18-
repository:
19-
description: The full name of the repository for which the token will be requested.
20-
default: ${{ github.repository }}
39+
repositories:
40+
description: >-
41+
The JSON-stringified array of the full names of the repositories the token should have access to.
42+
Default to all repositories that the installation can access.
43+
See See https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app's `repositories`.
2144
outputs:
2245
token:
23-
description: An installation token for the GitHub App on the requested repository.
46+
description: An installation token for the GitHub App on the requested repositories.
2447
runs:
2548
using: node20
2649
main: dist/index.js

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "github-app-token",
3-
"version": "1.9.0",
3+
"version": "2.0.0",
44
"license": "MIT",
55
"type": "module",
66
"files": [

src/fetch-installation-token.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ import { getOctokit } from "@actions/github";
22
import { createAppAuth } from "@octokit/auth-app";
33
import { request } from "@octokit/request";
44

5+
import {InstallationRetrievalDetails} from "./installation-retrieval-details.js";
6+
57
export const fetchInstallationToken = async ({
68
appId,
79
githubApiUrl,
8-
installationId,
9-
owner,
10+
installationRetrievalDetails,
1011
permissions,
1112
privateKey,
12-
repo,
13+
repositories,
1314
}: Readonly<{
1415
appId: string;
1516
githubApiUrl: URL;
16-
installationId?: number;
17-
owner: string;
17+
installationRetrievalDetails: InstallationRetrievalDetails;
1818
permissions?: Record<string, string>;
1919
privateKey: string;
20-
repo: string;
20+
repositories?: string[];
2121
}>): Promise<string> => {
2222
const app = createAppAuth({
2323
appId,
@@ -33,26 +33,50 @@ export const fetchInstallationToken = async ({
3333
const authApp = await app({ type: "app" });
3434
const octokit = getOctokit(authApp.token);
3535

36-
if (installationId === undefined) {
37-
try {
38-
({
39-
data: { id: installationId },
40-
} = await octokit.rest.apps.getRepoInstallation({ owner, repo }));
41-
} catch (error: unknown) {
42-
throw new Error(
43-
"Could not get repo installation. Is the app installed on this repo?",
44-
{ cause: error },
45-
);
36+
let installationId: number;
37+
38+
try {
39+
switch (installationRetrievalDetails.mode) {
40+
case "id":
41+
({id: installationId} = installationRetrievalDetails);
42+
break;
43+
case "organization":
44+
({
45+
data: { id: installationId },
46+
} = await octokit.request("GET /orgs/{org}/installation", {
47+
org: installationRetrievalDetails.org
48+
}));
49+
break;
50+
case "repository":
51+
({
52+
data: { id: installationId },
53+
} = await octokit.request("GET /repos/{owner}/{repo}/installation", {
54+
owner: installationRetrievalDetails.owner,
55+
repo: installationRetrievalDetails.repo
56+
}));
57+
break;
58+
case "user":
59+
({
60+
data: { id: installationId },
61+
} = await octokit.request("GET /users/{username}/installation", {
62+
username: installationRetrievalDetails.username,
63+
}));
64+
break;
4665
}
66+
} catch (error: unknown) {
67+
throw new Error(
68+
"Could not get retrieve installation.",
69+
{ cause: error },
70+
);
4771
}
4872

4973
try {
50-
const { data: installation } =
74+
const { data: {token} } =
5175
await octokit.rest.apps.createInstallationAccessToken({
5276
installation_id: installationId,
5377
permissions,
5478
});
55-
return installation.token;
79+
return token;
5680
} catch (error: unknown) {
5781
throw new Error("Could not create installation access token.", {
5882
cause: error,

src/index.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import ensureError from "ensure-error";
55
import isBase64 from "is-base64";
66

77
import { fetchInstallationToken } from "./fetch-installation-token.js";
8+
import {getInstallationRetrievalDetails} from "./installation-retrieval-details.js"
89

910
try {
1011
const appId = getInput("app_id", { required: true });
1112

12-
const installationIdInput = getInput("installation_id");
13-
const installationId = installationIdInput
14-
? Number(installationIdInput)
15-
: undefined;
13+
const githubApiUrlInput = getInput("github_api_url", { required: true });
14+
const githubApiUrl = new URL(githubApiUrlInput);
15+
16+
const installationRetrievalMode = getInput("installation_retrieval_mode", { required: true });
17+
const installationRetrievalPayload = getInput("installation_retrieval_payload", { required: true });
18+
const installationRetrievalDetails = getInstallationRetrievalDetails({mode: installationRetrievalMode, payload: installationRetrievalPayload});
1619

1720
const permissionsInput = getInput("permissions");
1821
const permissions = permissionsInput
@@ -24,24 +27,22 @@ try {
2427
? Buffer.from(privateKeyInput, "base64").toString("utf8")
2528
: privateKeyInput;
2629

27-
const repositoryInput = getInput("repository", { required: true });
28-
const [owner, repo] = repositoryInput.split("/");
29-
30-
const githubApiUrlInput = getInput("github_api_url", { required: true });
31-
const githubApiUrl = new URL(githubApiUrlInput);
30+
const repositoriesInput = getInput("repositories");
31+
const repositories = repositoriesInput
32+
? (JSON.parse(repositoriesInput) as string[])
33+
: undefined;
3234

33-
const installationToken = await fetchInstallationToken({
35+
const token = await fetchInstallationToken({
3436
appId,
3537
githubApiUrl,
36-
installationId,
37-
owner,
38+
installationRetrievalDetails,
3839
permissions,
3940
privateKey,
40-
repo,
41+
repositories,
4142
});
4243

43-
setSecret(installationToken);
44-
setOutput("token", installationToken);
44+
setSecret(token);
45+
setOutput("token", token);
4546
info("Token generated successfully!");
4647
} catch (_error: unknown) {
4748
const error = ensureError(_error);

src/installation-retrieval-details.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export type InstallationRetrievalDetails = Readonly<
2+
| { mode: "id"; id: number }
3+
| { mode: "organization"; org: string }
4+
| { mode: "repository"; owner: string; repo: string }
5+
| { mode: "user"; username: string }
6+
>;
7+
8+
export const getInstallationRetrievalDetails = ({mode, payload}: Readonly<{mode: string, payload: string}>): InstallationRetrievalDetails => {
9+
switch (mode) {
10+
case "id":
11+
return {mode, id: Number(payload)};
12+
case "organization":
13+
return {mode, org: payload};
14+
case "repository":
15+
const [owner, repo] = payload.split("/");
16+
return {mode, owner, repo};
17+
case "user":
18+
return {mode, username: payload};
19+
default:
20+
throw new Error(`Unsupported retrieval mode: "${mode}".`)
21+
}
22+
};

0 commit comments

Comments
 (0)