Skip to content

Commit

Permalink
feat: Add a skip_token_revoke input for configuring token revocation (
Browse files Browse the repository at this point in the history
actions#54)

Fixes actions#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"`.
  • Loading branch information
smockle authored Oct 6, 2023
1 parent d400084 commit 9ec88c4
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 8 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions dist/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions dist/post.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
8 changes: 6 additions & 2 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,7 +17,8 @@ export async function main(
repositories,
core,
createAppAuth,
request
request,
skipTokenRevoke
) {
let parsedOwner = "";
let parsedRepositoryNames = "";
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions lib/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,7 +30,8 @@ main(
createAppAuth,
request.defaults({
baseUrl: process.env["GITHUB_API_URL"],
})
}),
skipTokenRevoke
).catch((error) => {
console.error(error);
core.setFailed(error.message);
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions tests/post-token-skipped.test.js
Original file line number Diff line number Diff line change
@@ -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");
10 changes: 10 additions & 0 deletions tests/snapshots/index.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified tests/snapshots/index.js.snap
Binary file not shown.

0 comments on commit 9ec88c4

Please sign in to comment.