From 9ec88c41eef7052418d233d147c59fbdce19c56f Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 6 Oct 2023 12:10:49 -0400 Subject: [PATCH] feat: Add a `skip_token_revoke` input for configuring token revocation (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/actions/create-github-app-token/issues/55 Currently, `actions/create-github-app-token` always/unconditionally revokes the installation access token in a `post` step, at the completion of the current job. This prevents tokens from being used in other jobs. This PR makes this behavior configurable: - When the `skip-token-revoke` input is not specified (i.e. by default), the token is revoked in a `post` step (i.e. the current behavior). - When the `skip-token-revoke` input is set to a truthy value (e.g. `"true"`[^1]), the token is not revoked in a `post` step. This PR adds a test for the `skip-token-revoke: "true"` case. This is configurable in other app token actions, e.g. [tibdex/github-app-token](https://github.com/tibdex/github-app-token/blob/3eb77c7243b85c65e84acfa93fdbac02fb6bd532/README.md?plain=1#L46-L47) and [wow-actions/use-app-token](https://github.com/wow-actions/use-app-token/blob/cd772994fc762f99cf291f308797341327a49b0c/README.md?plain=1#L132). [^1]: Note that `"false"` is also truthy: `Boolean("false")` is `true`. If we think that’ll potentially confuse folks, I can require `skip-token-revoke` to be set explicitly to `"true"`. --- README.md | 6 +++++- action.yml | 3 +++ dist/main.cjs | 10 +++++++--- dist/post.cjs | 5 +++++ lib/main.js | 8 ++++++-- lib/post.js | 7 +++++++ main.js | 5 ++++- tests/README.md | 2 +- tests/post-token-skipped.test.js | 29 +++++++++++++++++++++++++++++ tests/snapshots/index.js.md | 10 ++++++++++ tests/snapshots/index.js.snap | Bin 201 -> 232 bytes 11 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 tests/post-token-skipped.test.js diff --git a/README.md b/README.md index e641316..af0fcc0 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,10 @@ jobs: > [!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. +### `skip_token_revoke` + +**Optional:** If truthy, the token will not be revoked when the current job is complete. + ## Outputs ### `token` @@ -158,7 +162,7 @@ The action creates an installation access token using [the `POST /app/installati 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. The token is revoked in the `post` step of the action, which means it cannot be passed to another job. +4. Unless the `skip_token_revoke` input is set to a truthy value, 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. > [!NOTE] diff --git a/action.yml b/action.yml index 7aedf40..ce09345 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: repositories: description: "Repositories to install the GitHub App on (defaults to current repository if owner is unset)" required: false + skip_token_revoke: + description: "If truthy, the token will not be revoked when the current job is complete" + required: false outputs: token: description: "GitHub installation access token" diff --git a/dist/main.cjs b/dist/main.cjs index dca6e04..b81d6d6 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -10006,7 +10006,7 @@ var import_core = __toESM(require_core(), 1); var import_auth_app = __toESM(require_dist_node12(), 1); // lib/main.js -async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2) { +async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2, skipTokenRevoke2) { let parsedOwner = ""; let parsedRepositoryNames = ""; if (!owner2 && !repositories2) { @@ -10082,7 +10082,9 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp } core2.setSecret(authentication.token); core2.setOutput("token", authentication.token); - core2.saveState("token", authentication.token); + if (!skipTokenRevoke2) { + core2.saveState("token", authentication.token); + } } // lib/request.js @@ -10105,6 +10107,7 @@ var appId = import_core.default.getInput("app_id"); var privateKey = import_core.default.getInput("private_key"); var owner = import_core.default.getInput("owner"); var repositories = import_core.default.getInput("repositories"); +var skipTokenRevoke = Boolean(import_core.default.getInput("skip_token_revoke")); main( appId, privateKey, @@ -10114,7 +10117,8 @@ main( import_auth_app.createAppAuth, request_default.defaults({ baseUrl: process.env["GITHUB_API_URL"] - }) + }), + skipTokenRevoke ).catch((error) => { console.error(error); import_core.default.setFailed(error.message); diff --git a/dist/post.cjs b/dist/post.cjs index 9be8245..c78241d 100644 --- a/dist/post.cjs +++ b/dist/post.cjs @@ -2973,6 +2973,11 @@ var import_core = __toESM(require_core(), 1); // lib/post.js async function post(core2, request2) { + const skipTokenRevoke = Boolean(core2.getInput("skip_token_revoke")); + if (skipTokenRevoke) { + core2.info("Token revocation was skipped"); + return; + } const token = core2.getState("token"); if (!token) { core2.info("Token is not set"); diff --git a/lib/main.js b/lib/main.js index e40d271..9efe021 100644 --- a/lib/main.js +++ b/lib/main.js @@ -8,6 +8,7 @@ * @param {import("@actions/core")} core * @param {import("@octokit/auth-app").createAppAuth} createAppAuth * @param {import("@octokit/request").request} request + * @param {boolean} skipTokenRevoke */ export async function main( appId, @@ -16,7 +17,8 @@ export async function main( repositories, core, createAppAuth, - request + request, + skipTokenRevoke ) { let parsedOwner = ""; let parsedRepositoryNames = ""; @@ -122,5 +124,7 @@ export async function main( core.setOutput("token", authentication.token); // Make token accessible to post function (so we can invalidate it) - core.saveState("token", authentication.token); + if (!skipTokenRevoke) { + core.saveState("token", authentication.token); + } } diff --git a/lib/post.js b/lib/post.js index 2658bc2..ef7f8d2 100644 --- a/lib/post.js +++ b/lib/post.js @@ -5,6 +5,13 @@ * @param {import("@octokit/request").request} request */ export async function post(core, request) { + const skipTokenRevoke = Boolean(core.getInput("skip_token_revoke")); + + if (skipTokenRevoke) { + core.info("Token revocation was skipped"); + return; + } + const token = core.getState("token"); if (!token) { diff --git a/main.js b/main.js index 123dca6..75ae569 100644 --- a/main.js +++ b/main.js @@ -19,6 +19,8 @@ const privateKey = core.getInput("private_key"); const owner = core.getInput("owner"); const repositories = core.getInput("repositories"); +const skipTokenRevoke = Boolean(core.getInput("skip_token_revoke")); + main( appId, privateKey, @@ -28,7 +30,8 @@ main( createAppAuth, request.defaults({ baseUrl: process.env["GITHUB_API_URL"], - }) + }), + skipTokenRevoke ).catch((error) => { console.error(error); core.setFailed(error.message); diff --git a/tests/README.md b/tests/README.md index bf4e863..b50533b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,7 @@ Add one test file per scenario. You can run them in isolation with: node tests/post-token-set.test.js ``` -All tests are run together in [tests/index.js](index.js), which can be execauted with ava +All tests are run together in [tests/index.js](index.js), which can be executed with ava ``` npx ava tests/index.js diff --git a/tests/post-token-skipped.test.js b/tests/post-token-skipped.test.js new file mode 100644 index 0000000..4185d1e --- /dev/null +++ b/tests/post-token-skipped.test.js @@ -0,0 +1,29 @@ +import { MockAgent, setGlobalDispatcher } from "undici"; + +// state variables are set as environment variables with the prefix STATE_ +// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions +process.env.STATE_token = "secret123"; + +// inputs are set as environment variables with the prefix INPUT_ +// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs +process.env.INPUT_SKIP_TOKEN_REVOKE = "true"; + +const mockAgent = new MockAgent(); + +setGlobalDispatcher(mockAgent); + +// Provide the base url to the request +const mockPool = mockAgent.get("https://api.github.com"); + +// intercept the request +mockPool + .intercept({ + path: "/installation/token", + method: "DELETE", + headers: { + authorization: "token secret123", + }, + }) + .reply(204); + +await import("../post.js"); diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index 652824d..1f0debc 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -14,6 +14,16 @@ Generated by [AVA](https://avajs.dev). 'Token revoked' +## post-token-skipped.test.js + +> stderr + + '' + +> stdout + + 'Token revocation was skipped' + ## post-token-unset.test.js > stderr diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index e33d42dada783ef2f6e0d561b40e9cda3be16609..954c16d1b0341937d755503bd229f084acfb6198 100644 GIT binary patch literal 232 zcmVrgyEiVs%-Fm1r+o zimd>vQm{F(h7h{K?J#ac>uWnH15{1wp^qi^?rgZaOahkYnIu}FIAgweE&$+HBO7&J-(;)7 zyS?gQvmc8H00000000A9n3j~2pPXIXv?#SCvm__AtRTO*M7JbAJ2g+YIJHEtB(=Ci zFRM7SI4`lFI3vFVs4yiZu_V!#nSq@F1Q=Nv1Q}9u5|dJM(uzw`Qj3Zp3L>PDRWR~~ zfQ(cqN-YD@DG2rXr6o|CD