feat: Add a skip_token_revoke input for configuring token revocation (#54)
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"`.
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+7
-3
@@ -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);
|
||||
|
||||
Vendored
+5
@@ -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");
|
||||
|
||||
+6
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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");
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user