Skip to content

Commit

Permalink
fixup! Try revoking token
Browse files Browse the repository at this point in the history
  • Loading branch information
tibdex committed Sep 9, 2023
1 parent b69f4b9 commit afe68de
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 124 deletions.
15 changes: 4 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
node-version: 20
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run build
- run: npm run prettier -- --check
# Optional integration test of the action using a dedicated GitHub App.
- id: create_token
if: ${{ vars.TEST_GITHUB_APP_ID != '' }}
Expand All @@ -25,15 +25,8 @@ jobs:
# The only required permission is `Repository permissions > Metadata: Read-only`.
app_id: ${{ vars.TEST_GITHUB_APP_ID }}
private_key: ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }}
- name: Revoke token
env:
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
run: >-
gh api --header "X-GitHub-Api-Version: 2022-11-28" --method DELETE /installation/token
- name: Revoke token (again)
env:
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
run: >-
gh api --header "X-GitHub-Api-Version: 2022-11-28" --method DELETE /installation/token
permissions: >-
{"pull_requests": "read"}
- run: node --eval "assert('${{ steps.create_token.outputs.token }}'.length > 0);"
if: ${{ steps.create_token.outcome != 'skipped' }}
- run: npm run prettier -- --check
3 changes: 2 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ outputs:
description: An installation access token for the GitHub App.
runs:
using: node20
main: dist/index.js
main: dist/main/index.js
post: dist/post/index.js
branding:
icon: unlock
color: gray-dark
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
"dist"
],
"scripts": {
"prebuild": "tsc --build",
"build": "ncc build src/index.ts --minify --target es2022 --v8-cache",
"prettier": "prettier --ignore-path .gitignore \"./**/*.{js,json,md,ts,yml}\""
"build": "npm run build:main && npm run build:post",
"build:main": "npm run compile -- --out ./dist/main src/main.ts ",
"build:post": "npm run compile -- --out ./dist/post src/post.ts",
"compile": "ncc build --minify --no-cache --target es2022 --v8-cache",
"prettier": "prettier --ignore-path .gitignore \"./**/*.{js,json,md,ts,yml}\"",
"typecheck": "tsc --build"
},
"dependencies": {
"@actions/core": "^1.10.0",
Expand Down
89 changes: 31 additions & 58 deletions src/create-installation-access-token.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,49 @@
import { debug } from "@actions/core";
import { getOctokit } from "@actions/github";
import { createAppAuth } from "@octokit/auth-app";
import { request } from "@octokit/request";

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

export const createInstallationAccessToken = async ({
appId,
githubApiUrl,
installationRetrievalDetails,
permissions,
privateKey,
repositories,
}: Readonly<{
export type InstallationAccessTokenCreationOptions = Readonly<{
appId: string;
githubApiUrl: URL;
installationRetrievalDetails: InstallationRetrievalDetails;
permissions?: Record<string, string>;
privateKey: string;
repositories?: string[];
}>): Promise<string> => {
const app = createAppAuth({
appId,
privateKey,
request: request.defaults({
baseUrl: githubApiUrl
.toString()
// Remove optional trailing `/`.
.replace(/\/$/, ""),
}),
});

const authApp = await app({ type: "app" });
const octokit = getOctokit(authApp.token);

let installationId: number;
}>;

export const createInstallationAccessToken = async ({
appId,
githubApiUrl,
installationRetrievalDetails,
permissions,
privateKey,
repositories,
}: InstallationAccessTokenCreationOptions): Promise<string> => {
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 retrieve installation.", { cause: error });
}
const app = createAppAuth({
appId,
privateKey,
request: request.defaults({
baseUrl: githubApiUrl
.toString()
// Remove optional trailing `/`.
.replace(/\/$/, ""),
}),
});

debug(`Retrieved installation ID: ${installationId}.`);
const authApp = await app({ type: "app" });
const octokit = getOctokit(authApp.token);

const installationId = await retrieveInstallationId(
installationRetrievalDetails,
{ octokit },
);

try {
const {
data: { token },
} = await octokit.request(
Expand Down
28 changes: 0 additions & 28 deletions src/installation-retrieval-details.ts

This file was deleted.

72 changes: 72 additions & 0 deletions src/installation-retrieval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { debug } from "@actions/core";
import { getOctokit } from "@actions/github";

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: parseInt(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}".`);
}
};

export const retrieveInstallationId = async (
details: InstallationRetrievalDetails,
{ octokit }: Readonly<{ octokit: ReturnType<typeof getOctokit> }>,
): Promise<number> => {
let id: number;
try {
switch (details.mode) {
case "id":
({ id } = details);
break;
case "organization":
({
data: { id },
} = await octokit.request("GET /orgs/{org}/installation", {
org: details.org,
}));
break;
case "repository":
({
data: { id },
} = await octokit.request("GET /repos/{owner}/{repo}/installation", {
owner: details.owner,
repo: details.repo,
}));
break;
case "user":
({
data: { id },
} = await octokit.request("GET /users/{username}/installation", {
username: details.username,
}));
break;
}
} catch (error: unknown) {
throw new Error("Could not retrieve installation.", { cause: error });
}

debug(`Retrieved installation ID: ${id}.`);
return id;
};
15 changes: 15 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { info, saveState, setOutput, setSecret } from "@actions/core";

import { createInstallationAccessToken } from "./create-installation-access-token.js";
import { parseOptions } from "./parse-options.js";
import { run } from "./run.js";
import { tokenKey } from "./state.js";

await run(async () => {
const options = parseOptions();
const token = await createInstallationAccessToken(options);
setSecret(token);
saveState(tokenKey, token);
setOutput("token", token);
info("Token created successfully");
});
30 changes: 7 additions & 23 deletions src/index.ts → src/parse-options.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { Buffer } from "node:buffer";

import {
debug,
getInput,
info,
setFailed,
setOutput,
setSecret,
} from "@actions/core";
import { debug, getInput } from "@actions/core";
import isBase64 from "is-base64";

import { createInstallationAccessToken } from "./create-installation-access-token.js";
import { getInstallationRetrievalDetails } from "./installation-retrieval-details.js";
import { InstallationAccessTokenCreationOptions } from "./create-installation-access-token.js";
import { getInstallationRetrievalDetails } from "./installation-retrieval.js";

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

const githubApiUrlInput = getInput("github_api_url", { required: true });
Expand Down Expand Up @@ -53,21 +46,12 @@ try {
: undefined;
debug(`Requested repositories: ${JSON.stringify(repositories)}.`);

const token = await createInstallationAccessToken({
return {
appId,
githubApiUrl,
installationRetrievalDetails,
permissions,
privateKey,
repositories,
});

setSecret(token);
setOutput("token", token);
info("Token created successfully!");
} catch (error) {
// Using `console.error()` instead of only passing `error` to `setFailed()` for better error reporting.
// See https://github.com/actions/toolkit/issues/1527.
console.error(error);
setFailed("");
}
};
};
12 changes: 12 additions & 0 deletions src/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getState, info } from "@actions/core";

import { revokeInstallationAccessToken } from "./revoke-installation-access-token.js";
import { run } from "./run.js";
import { tokenKey } from "./state.js";

await run(async () => {
const token = getState(tokenKey);
await revokeInstallationAccessToken(token);
info("Token revoked successfully");
await revokeInstallationAccessToken(token);
});
15 changes: 15 additions & 0 deletions src/revoke-installation-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getOctokit } from "@actions/github";

export const revokeInstallationAccessToken = async (
token: string,
): Promise<void> => {
try {
const octokit = getOctokit(token);

await octokit.request("DELETE /installation/token");
} catch (error: unknown) {
throw new Error("Could not revoke installation access token.", {
cause: error,
});
}
};
12 changes: 12 additions & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { setFailed } from "@actions/core";

export const run = async (callback: () => Promise<unknown>) => {
try {
await callback();
} catch (error) {
// Using `console.error()` instead of only passing `error` to `setFailed()` for better error reporting.
// See https://github.com/actions/toolkit/issues/1527.
console.error(error);
setFailed("");
}
};
1 change: 1 addition & 0 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const tokenKey = "token";

0 comments on commit afe68de

Please sign in to comment.