Skip to content

Commit

Permalink
Add support for organization and user installation retrieval and repo…
Browse files Browse the repository at this point in the history
…sitory scoping
  • Loading branch information
tibdex committed Sep 9, 2023
1 parent 3a5af6b commit 8fbc2cd
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 45 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@ jobs:
steps:
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v1
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}

# Optional.
# github_api_url: https://api.example.com

# Optional.
# installation_id: 1337
# installation_retrieval_mode: id

# Optional.
# installation_retrieval_payload: 1337

# Optional.
# Using a YAML multiline string to avoid escaping the JSON quotes.
# permissions: >-
# {"members": "read"}
# {"pull_requests": "read"}

private_key: ${{ secrets.PRIVATE_KEY }}

# Optional.
# repository: owner/repo
# repositories: >-
# ["actions/toolkit", "github/docs"]

- name: Use token
env:
Expand Down
37 changes: 30 additions & 7 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,45 @@ inputs:
app_id:
description: ID of the GitHub App.
required: true
installation_id:
description: The ID of the installation for which the token will be requested (defaults to the ID of the repository's installation).
github_api_url:
description: The API URL of the GitHub server.
default: ${{ github.api_url }}
installation_retrieval_mode:
description: >-
The mode used to retrieve the installation for which the token will be requested.
One of:
- id: use the installation with the specified ID.
- 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).
- 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).
- 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).
default: repository
installation_retrieval_payload:
description: >-
The payload used to retrieve the installation.
Examples for each retrieval mode:
- id: 1337
- organization: github
- repository: tibdex/github-app-token
- user: tibdex
default: ${{ github.repository }}
permissions:
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).
description: >-
The JSON-stringified permissions granted to the token.
Default to all the GitHub app permissions.
See https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app's `permissions`.
private_key:
description: Private key of the GitHub App (can be Base64 encoded).
required: true
repository:
description: The full name of the repository for which the token will be requested.
default: ${{ github.repository }}
repositories:
description: >-
The JSON-stringified array of the full names of the repositories the token should have access to.
Default to all repositories that the installation can access.
See See https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app's `repositories`.
outputs:
token:
description: An installation token for the GitHub App on the requested repository.
description: An installation token for the GitHub App on the requested repositories.
runs:
using: node20
main: dist/index.js
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-app-token",
"version": "1.9.0",
"version": "2.0.0",
"license": "MIT",
"type": "module",
"files": [
Expand Down
60 changes: 42 additions & 18 deletions src/fetch-installation-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import { getOctokit } from "@actions/github";
import { createAppAuth } from "@octokit/auth-app";
import { request } from "@octokit/request";

import {InstallationRetrievalDetails} from "./installation-retrieval-details.js";

export const fetchInstallationToken = async ({
appId,
githubApiUrl,
installationId,
owner,
installationRetrievalDetails,
permissions,
privateKey,
repo,
repositories,
}: Readonly<{
appId: string;
githubApiUrl: URL;
installationId?: number;
owner: string;
installationRetrievalDetails: InstallationRetrievalDetails;
permissions?: Record<string, string>;
privateKey: string;
repo: string;
repositories?: string[];
}>): Promise<string> => {
const app = createAppAuth({
appId,
Expand All @@ -33,26 +33,50 @@ export const fetchInstallationToken = async ({
const authApp = await app({ type: "app" });
const octokit = getOctokit(authApp.token);

if (installationId === undefined) {
try {
({
data: { id: installationId },
} = await octokit.rest.apps.getRepoInstallation({ owner, repo }));
} catch (error: unknown) {
throw new Error(
"Could not get repo installation. Is the app installed on this repo?",
{ cause: error },
);
let installationId: number;

try {
switch (installationRetrievalDetails.mode) {
case "id":
({id: installationId} = installationRetrievalDetails);
break;
case "organization":
({
data: { id: installationId },
} = await octokit.request("GET /orgs/{org}/installation", {
org: installationRetrievalDetails.org
}));
break;
case "repository":
({
data: { id: installationId },
} = await octokit.request("GET /repos/{owner}/{repo}/installation", {
owner: installationRetrievalDetails.owner,
repo: installationRetrievalDetails.repo
}));
break;
case "user":
({
data: { id: installationId },
} = await octokit.request("GET /users/{username}/installation", {
username: installationRetrievalDetails.username,
}));
break;
}
} catch (error: unknown) {
throw new Error(
"Could not get retrieve installation.",
{ cause: error },
);
}

try {
const { data: installation } =
const { data: {token} } =
await octokit.rest.apps.createInstallationAccessToken({
installation_id: installationId,
permissions,
});
return installation.token;
return token;
} catch (error: unknown) {
throw new Error("Could not create installation access token.", {
cause: error,
Expand Down
31 changes: 16 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import ensureError from "ensure-error";
import isBase64 from "is-base64";

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

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

const installationIdInput = getInput("installation_id");
const installationId = installationIdInput
? Number(installationIdInput)
: undefined;
const githubApiUrlInput = getInput("github_api_url", { required: true });
const githubApiUrl = new URL(githubApiUrlInput);

const installationRetrievalMode = getInput("installation_retrieval_mode", { required: true });
const installationRetrievalPayload = getInput("installation_retrieval_payload", { required: true });
const installationRetrievalDetails = getInstallationRetrievalDetails({mode: installationRetrievalMode, payload: installationRetrievalPayload});

const permissionsInput = getInput("permissions");
const permissions = permissionsInput
Expand All @@ -24,24 +27,22 @@ try {
? Buffer.from(privateKeyInput, "base64").toString("utf8")
: privateKeyInput;

const repositoryInput = getInput("repository", { required: true });
const [owner, repo] = repositoryInput.split("/");

const githubApiUrlInput = getInput("github_api_url", { required: true });
const githubApiUrl = new URL(githubApiUrlInput);
const repositoriesInput = getInput("repositories");
const repositories = repositoriesInput
? (JSON.parse(repositoriesInput) as string[])
: undefined;

const installationToken = await fetchInstallationToken({
const token = await fetchInstallationToken({
appId,
githubApiUrl,
installationId,
owner,
installationRetrievalDetails,
permissions,
privateKey,
repo,
repositories,
});

setSecret(installationToken);
setOutput("token", installationToken);
setSecret(token);
setOutput("token", token);
info("Token generated successfully!");
} catch (_error: unknown) {
const error = ensureError(_error);
Expand Down
22 changes: 22 additions & 0 deletions src/installation-retrieval-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type InstallationRetrievalDetails = Readonly<
| { mode: "id"; id: number }
| { mode: "organization"; org: string }
| { mode: "repository"; owner: string; repo: string }
| { mode: "user"; username: string }
>;

export const getInstallationRetrievalDetails = ({mode, payload}: Readonly<{mode: string, payload: string}>): InstallationRetrievalDetails => {
switch (mode) {
case "id":
return {mode, id: Number(payload)};
case "organization":
return {mode, org: payload};
case "repository":
const [owner, repo] = payload.split("/");
return {mode, owner, repo};
case "user":
return {mode, username: payload};
default:
throw new Error(`Unsupported retrieval mode: "${mode}".`)
}
};

0 comments on commit 8fbc2cd

Please sign in to comment.