Skip to content

Commit 9ec88c4

Browse files
authoredOct 6, 2023
feat: Add a skip_token_revoke input for configuring token revocation (#54)
Fixes #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"`.
1 parent d400084 commit 9ec88c4

11 files changed

+77
-8
lines changed
 

‎README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ jobs:
145145
> [!NOTE]
146146
> 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.
147147

148+
### `skip_token_revoke`
149+
150+
**Optional:** If truthy, the token will not be revoked when the current job is complete.
151+
148152
## Outputs
149153

150154
### `token`
@@ -158,7 +162,7 @@ The action creates an installation access token using [the `POST /app/installati
158162
1. The token is scoped to the current repository or `repositories` if set.
159163
2. The token inherits all the installation's permissions.
160164
3. The token is set as output `token` which can be used in subsequent steps.
161-
4. The token is revoked in the `post` step of the action, which means it cannot be passed to another job.
165+
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.
162166
5. The token is masked, it cannot be logged accidentally.
163167

164168
> [!NOTE]

‎action.yml

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ inputs:
1717
repositories:
1818
description: "Repositories to install the GitHub App on (defaults to current repository if owner is unset)"
1919
required: false
20+
skip_token_revoke:
21+
description: "If truthy, the token will not be revoked when the current job is complete"
22+
required: false
2023
outputs:
2124
token:
2225
description: "GitHub installation access token"

‎dist/main.cjs

+7-3
Original file line numberDiff line numberDiff line change
@@ -10006,7 +10006,7 @@ var import_core = __toESM(require_core(), 1);
1000610006
var import_auth_app = __toESM(require_dist_node12(), 1);
1000710007

1000810008
// lib/main.js
10009-
async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2) {
10009+
async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2, skipTokenRevoke2) {
1001010010
let parsedOwner = "";
1001110011
let parsedRepositoryNames = "";
1001210012
if (!owner2 && !repositories2) {
@@ -10082,7 +10082,9 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp
1008210082
}
1008310083
core2.setSecret(authentication.token);
1008410084
core2.setOutput("token", authentication.token);
10085-
core2.saveState("token", authentication.token);
10085+
if (!skipTokenRevoke2) {
10086+
core2.saveState("token", authentication.token);
10087+
}
1008610088
}
1008710089

1008810090
// lib/request.js
@@ -10105,6 +10107,7 @@ var appId = import_core.default.getInput("app_id");
1010510107
var privateKey = import_core.default.getInput("private_key");
1010610108
var owner = import_core.default.getInput("owner");
1010710109
var repositories = import_core.default.getInput("repositories");
10110+
var skipTokenRevoke = Boolean(import_core.default.getInput("skip_token_revoke"));
1010810111
main(
1010910112
appId,
1011010113
privateKey,
@@ -10114,7 +10117,8 @@ main(
1011410117
import_auth_app.createAppAuth,
1011510118
request_default.defaults({
1011610119
baseUrl: process.env["GITHUB_API_URL"]
10117-
})
10120+
}),
10121+
skipTokenRevoke
1011810122
).catch((error) => {
1011910123
console.error(error);
1012010124
import_core.default.setFailed(error.message);

‎dist/post.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -2973,6 +2973,11 @@ var import_core = __toESM(require_core(), 1);
29732973

29742974
// lib/post.js
29752975
async function post(core2, request2) {
2976+
const skipTokenRevoke = Boolean(core2.getInput("skip_token_revoke"));
2977+
if (skipTokenRevoke) {
2978+
core2.info("Token revocation was skipped");
2979+
return;
2980+
}
29762981
const token = core2.getState("token");
29772982
if (!token) {
29782983
core2.info("Token is not set");

‎lib/main.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @param {import("@actions/core")} core
99
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth
1010
* @param {import("@octokit/request").request} request
11+
* @param {boolean} skipTokenRevoke
1112
*/
1213
export async function main(
1314
appId,
@@ -16,7 +17,8 @@ export async function main(
1617
repositories,
1718
core,
1819
createAppAuth,
19-
request
20+
request,
21+
skipTokenRevoke
2022
) {
2123
let parsedOwner = "";
2224
let parsedRepositoryNames = "";
@@ -122,5 +124,7 @@ export async function main(
122124
core.setOutput("token", authentication.token);
123125

124126
// Make token accessible to post function (so we can invalidate it)
125-
core.saveState("token", authentication.token);
127+
if (!skipTokenRevoke) {
128+
core.saveState("token", authentication.token);
129+
}
126130
}

‎lib/post.js

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
* @param {import("@octokit/request").request} request
66
*/
77
export async function post(core, request) {
8+
const skipTokenRevoke = Boolean(core.getInput("skip_token_revoke"));
9+
10+
if (skipTokenRevoke) {
11+
core.info("Token revocation was skipped");
12+
return;
13+
}
14+
815
const token = core.getState("token");
916

1017
if (!token) {

‎main.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const privateKey = core.getInput("private_key");
1919
const owner = core.getInput("owner");
2020
const repositories = core.getInput("repositories");
2121

22+
const skipTokenRevoke = Boolean(core.getInput("skip_token_revoke"));
23+
2224
main(
2325
appId,
2426
privateKey,
@@ -28,7 +30,8 @@ main(
2830
createAppAuth,
2931
request.defaults({
3032
baseUrl: process.env["GITHUB_API_URL"],
31-
})
33+
}),
34+
skipTokenRevoke
3235
).catch((error) => {
3336
console.error(error);
3437
core.setFailed(error.message);

‎tests/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Add one test file per scenario. You can run them in isolation with:
66
node tests/post-token-set.test.js
77
```
88

9-
All tests are run together in [tests/index.js](index.js), which can be execauted with ava
9+
All tests are run together in [tests/index.js](index.js), which can be executed with ava
1010

1111
```
1212
npx ava tests/index.js

‎tests/post-token-skipped.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MockAgent, setGlobalDispatcher } from "undici";
2+
3+
// state variables are set as environment variables with the prefix STATE_
4+
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions
5+
process.env.STATE_token = "secret123";
6+
7+
// inputs are set as environment variables with the prefix INPUT_
8+
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
9+
process.env.INPUT_SKIP_TOKEN_REVOKE = "true";
10+
11+
const mockAgent = new MockAgent();
12+
13+
setGlobalDispatcher(mockAgent);
14+
15+
// Provide the base url to the request
16+
const mockPool = mockAgent.get("https://api.github.com");
17+
18+
// intercept the request
19+
mockPool
20+
.intercept({
21+
path: "/installation/token",
22+
method: "DELETE",
23+
headers: {
24+
authorization: "token secret123",
25+
},
26+
})
27+
.reply(204);
28+
29+
await import("../post.js");

‎tests/snapshots/index.js.md

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ Generated by [AVA](https://avajs.dev).
1414
1515
'Token revoked'
1616

17+
## post-token-skipped.test.js
18+
19+
> stderr
20+
21+
''
22+
23+
> stdout
24+
25+
'Token revocation was skipped'
26+
1727
## post-token-unset.test.js
1828

1929
> stderr

‎tests/snapshots/index.js.snap

31 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.