Compare commits

...

9 Commits

Author SHA1 Message Date
semantic-release-bot
9d97a4282b build(release): 1.5.0 [skip ci]
# [1.5.0](https://github.com/actions/create-github-app-token/compare/v1.4.0...v1.5.0) (2023-10-06)

### Features

* use dash notation for inputs (deprecates underscore notation) ([#59](https://github.com/actions/create-github-app-token/issues/59)) ([7b1d2ae](7b1d2aef87)), closes [#57](https://github.com/actions/create-github-app-token/issues/57) [/github.com/actions/create-github-app-token/issues/57#issuecomment-1751272252](https://github.com//github.com/actions/create-github-app-token/issues/57/issues/issuecomment-1751272252)
2023-10-06 20:42:48 +00:00
Gregor Martynus
d21ec768fd ci(release): do not persist credentials when checking out (#62) 2023-10-06 13:42:08 -07:00
Gregor Martynus
998b8757ce ci(release): use local version (#60)
follow up to
https://github.com/actions/create-github-app-token/pull/59#issuecomment-1751353971

---------

Co-authored-by: Parker Brown <17183625+parkerbxyz@users.noreply.github.com>
2023-10-06 13:29:38 -07:00
Parker Brown
b94a200224 docs: update skip-token-revoke input name (#61)
This is a quick follow-up to #59. I was mid-review when it merged and
noticed one instance of `skip_token_revoke` in the README that didn't
get changed to `skip-token-revoke`. The PR merged just before I pushed a
commit to fix it.
2023-10-06 13:19:45 -07:00
Clay Miller
7b1d2aef87 feat: use dash notation for inputs (deprecates underscore notation) (#59)
Fixes #57 

This PR implements the 3-step plan proposed by @gr2m in
https://github.com/actions/create-github-app-token/issues/57#issuecomment-1751272252:

> 1. Support both input types
> 2. Log a deprecation warning for the old notation
> 3. Add a test for deprecations

Although this PR supports both input formats simultaneously, I opted
_not_ to document the old format in the updated README. That’s a
decision I’m happy to revisit, if y’all would prefer to have
documentation for both the old and new formats.
2023-10-06 13:10:49 -07:00
Gregor Martynus
bdb2377ad0 test: add coverage (#58) 2023-10-06 12:54:48 -07:00
Clay Miller
9b283559f1 test: integration tests for main.js (#56)
Part of https://github.com/actions/create-github-app-token/issues/43

This PR adds tests for
[`main.js`](https://github.com/actions/create-github-app-token/blob/main/lib/main.js),
similar to [the tests that already exist for
`post.js`](https://github.com/actions/create-github-app-token/tree/main/tests).

Specifically, it tests that:
- `main` exits with an error when `GITHUB_REPOSITORY` is missing.
- `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing.
- `main` successfully obtains a token when…
- …the `owner` and `repositories` inputs are set (and the latter is a
single repo).
- …the `owner` and `repositories` inputs are set (and the latter is a
list of repos).
- …the `owner` input is set (to an org), but the `repositories` input
isn’t set.
- …the `owner` input is set (to a user), but the `repositories` input
isn’t set.
  - …the `owner` input is not set, but the `repositories` input is set.
  - …neither the `owner` nor `repositories` input is set.

❧

Architecturally, in order to keep individual tests concise, this PR adds
`tests/main.js`, which:
- sets commonly-used inputs, environment variables, and mocks, then
- calls a callback function that can edit the variables and add
additional mocks, then
- runs `main.js` itself.

The `tests/main-token-get-*.test.js` test files run `tests/main.js` with
various scenario-specific callback functions.
2023-10-06 12:39:35 -07:00
semantic-release-bot
8210939678 build(release): 1.4.0 [skip ci]
# [1.4.0](https://github.com/actions/create-github-app-token/compare/v1.3.0...v1.4.0) (2023-10-06)

### Features

* Add a `skip_token_revoke` input for configuring token revocation ([#54](https://github.com/actions/create-github-app-token/issues/54)) ([9ec88c4](9ec88c41ee)), closes [1#L46-L47](https://github.com/1/issues/L46-L47) [1#L132](https://github.com/1/issues/L132)
2023-10-06 16:11:28 +00:00
Clay Miller
9ec88c41ee 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](3eb77c7243/README.md (L46-L47))
and
[wow-actions/use-app-token](cd772994fc/README.md (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"`.
2023-10-06 09:10:49 -07:00
30 changed files with 2240 additions and 44 deletions

View File

@@ -14,16 +14,23 @@ jobs:
name: release name: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v1 # build local version to create token
id: app-token
with:
app_id: ${{ vars.RELEASER_APP_ID }}
private_key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
token: ${{ steps.app-token.outputs.token }} persist-credentials: false
- run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag - uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build - run: npm run build
- uses: ./
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
# install release dependencies and release
- run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag
- run: npx semantic-release --debug - run: npx semantic-release --debug
env: env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}

View File

@@ -38,8 +38,8 @@ jobs:
- uses: ./ # Uses the action in the root directory - uses: ./ # Uses the action in the root directory
id: test id: test
with: with:
app_id: ${{ vars.TEST_APP_ID }} app-id: ${{ vars.TEST_APP_ID }}
private_key: ${{ secrets.TEST_APP_PRIVATE_KEY }} private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
- uses: octokit/request-action@v2.x - uses: octokit/request-action@v2.x
id: get-repository id: get-repository
env: env:

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules/
.env .env
coverage
node_modules/

View File

@@ -22,8 +22,8 @@ jobs:
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app_id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- uses: peter-evans/create-or-update-comment@v3 - uses: peter-evans/create-or-update-comment@v3
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
@@ -44,8 +44,8 @@ jobs:
id: app-token id: app-token
with: with:
# required # required
app_id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
@@ -69,8 +69,8 @@ jobs:
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app_id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v3 - uses: peter-evans/create-or-update-comment@v3
with: with:
@@ -91,8 +91,8 @@ jobs:
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app_id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
repositories: "repo1,repo2" repositories: "repo1,repo2"
- uses: peter-evans/create-or-update-comment@v3 - uses: peter-evans/create-or-update-comment@v3
@@ -114,8 +114,8 @@ jobs:
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app_id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: another-owner owner: another-owner
- uses: peter-evans/create-or-update-comment@v3 - uses: peter-evans/create-or-update-comment@v3
with: with:
@@ -126,11 +126,11 @@ jobs:
## Inputs ## Inputs
### `app_id` ### `app-id`
**Required:** GitHub App ID. **Required:** GitHub App ID.
### `private_key` ### `private-key`
**Required:** GitHub App private key. **Required:** GitHub App private key.
@@ -145,6 +145,10 @@ jobs:
> [!NOTE] > [!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. > 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 ## Outputs
### `token` ### `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. 1. The token is scoped to the current repository or `repositories` if set.
2. The token inherits all the installation's permissions. 2. The token inherits all the installation's permissions.
3. The token is set as output `token` which can be used in subsequent steps. 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. 5. The token is masked, it cannot be logged accidentally.
> [!NOTE] > [!NOTE]

View File

@@ -5,18 +5,33 @@ branding:
icon: "lock" icon: "lock"
color: "gray-dark" color: "gray-dark"
inputs: inputs:
app-id:
description: "GitHub App ID"
required: false # TODO: When 'app_id' is removed, make 'app-id' required
app_id: app_id:
description: "GitHub App ID" description: "GitHub App ID"
required: true required: false
deprecationMessage: "'app_id' is deprecated and will be removed in a future version. Use 'app-id' instead."
private-key:
description: "GitHub App private key"
required: false # TODO: When 'private_key' is removed, make 'private-key' required
private_key: private_key:
description: "GitHub App private key" description: "GitHub App private key"
required: true required: false
deprecationMessage: "'private_key' is deprecated and will be removed in a future version. Use 'private-key' instead."
owner: owner:
description: "GitHub App owner (defaults to current repository owner)" description: "GitHub App owner (defaults to current repository owner)"
required: false required: false
repositories: repositories:
description: "Repositories to install the GitHub App on (defaults to current repository if owner is unset)" description: "Repositories to install the GitHub App on (defaults to current repository if owner is unset)"
required: false required: false
skip-token-revoke:
description: "If truthy, the token will not be revoked when the current job is complete"
required: false
skip_token_revoke:
description: "If truthy, the token will not be revoked when the current job is complete"
required: false
deprecationMessage: "'skip_token_revoke' is deprecated and will be removed in a future version. Use 'skip-token-revoke' instead."
outputs: outputs:
token: token:
description: "GitHub installation access token" description: "GitHub installation access token"

20
dist/main.cjs vendored
View File

@@ -10006,7 +10006,7 @@ var import_core = __toESM(require_core(), 1);
var import_auth_app = __toESM(require_dist_node12(), 1); var import_auth_app = __toESM(require_dist_node12(), 1);
// lib/main.js // 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 parsedOwner = "";
let parsedRepositoryNames = ""; let parsedRepositoryNames = "";
if (!owner2 && !repositories2) { if (!owner2 && !repositories2) {
@@ -10082,8 +10082,10 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp
} }
core2.setSecret(authentication.token); core2.setSecret(authentication.token);
core2.setOutput("token", authentication.token); core2.setOutput("token", authentication.token);
if (!skipTokenRevoke2) {
core2.saveState("token", authentication.token); core2.saveState("token", authentication.token);
} }
}
// lib/request.js // lib/request.js
var import_request = __toESM(require_dist_node5(), 1); var import_request = __toESM(require_dist_node5(), 1);
@@ -10101,10 +10103,19 @@ if (!process.env.GITHUB_REPOSITORY) {
if (!process.env.GITHUB_REPOSITORY_OWNER) { if (!process.env.GITHUB_REPOSITORY_OWNER) {
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'"); throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
} }
var appId = import_core.default.getInput("app_id"); var appId = import_core.default.getInput("app-id") || import_core.default.getInput("app_id");
var privateKey = import_core.default.getInput("private_key"); if (!appId) {
throw new Error("Input required and not supplied: app-id");
}
var privateKey = import_core.default.getInput("private-key") || import_core.default.getInput("private_key");
if (!privateKey) {
throw new Error("Input required and not supplied: private-key");
}
var owner = import_core.default.getInput("owner"); var owner = import_core.default.getInput("owner");
var repositories = import_core.default.getInput("repositories"); var repositories = import_core.default.getInput("repositories");
var skipTokenRevoke = Boolean(
import_core.default.getInput("skip-token-revoke") || import_core.default.getInput("skip_token_revoke")
);
main( main(
appId, appId,
privateKey, privateKey,
@@ -10114,7 +10125,8 @@ main(
import_auth_app.createAppAuth, import_auth_app.createAppAuth,
request_default.defaults({ request_default.defaults({
baseUrl: process.env["GITHUB_API_URL"] baseUrl: process.env["GITHUB_API_URL"]
}) }),
skipTokenRevoke
).catch((error) => { ).catch((error) => {
console.error(error); console.error(error);
import_core.default.setFailed(error.message); import_core.default.setFailed(error.message);

7
dist/post.cjs vendored
View File

@@ -2973,6 +2973,13 @@ var import_core = __toESM(require_core(), 1);
// lib/post.js // lib/post.js
async function post(core2, request2) { async function post(core2, request2) {
const skipTokenRevoke = Boolean(
core2.getInput("skip-token-revoke") || core2.getInput("skip_token_revoke")
);
if (skipTokenRevoke) {
core2.info("Token revocation was skipped");
return;
}
const token = core2.getState("token"); const token = core2.getState("token");
if (!token) { if (!token) {
core2.info("Token is not set"); core2.info("Token is not set");

View File

@@ -8,6 +8,7 @@
* @param {import("@actions/core")} core * @param {import("@actions/core")} core
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth * @param {import("@octokit/auth-app").createAppAuth} createAppAuth
* @param {import("@octokit/request").request} request * @param {import("@octokit/request").request} request
* @param {boolean} skipTokenRevoke
*/ */
export async function main( export async function main(
appId, appId,
@@ -16,7 +17,8 @@ export async function main(
repositories, repositories,
core, core,
createAppAuth, createAppAuth,
request request,
skipTokenRevoke
) { ) {
let parsedOwner = ""; let parsedOwner = "";
let parsedRepositoryNames = ""; let parsedRepositoryNames = "";
@@ -98,6 +100,7 @@ export async function main(
authorization: `bearer ${appAuthentication.token}`, authorization: `bearer ${appAuthentication.token}`,
}, },
}).catch((error) => { }).catch((error) => {
/* c8 ignore next */
if (error.status !== 404) throw error; if (error.status !== 404) throw error;
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
@@ -122,5 +125,7 @@ export async function main(
core.setOutput("token", authentication.token); core.setOutput("token", authentication.token);
// Make token accessible to post function (so we can invalidate it) // Make token accessible to post function (so we can invalidate it)
if (!skipTokenRevoke) {
core.saveState("token", authentication.token); core.saveState("token", authentication.token);
} }
}

View File

@@ -5,6 +5,15 @@
* @param {import("@octokit/request").request} request * @param {import("@octokit/request").request} request
*/ */
export async function post(core, request) { export async function post(core, request) {
const skipTokenRevoke = Boolean(
core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke")
);
if (skipTokenRevoke) {
core.info("Token revocation was skipped");
return;
}
const token = core.getState("token"); const token = core.getState("token");
if (!token) { if (!token) {

20
main.js
View File

@@ -14,11 +14,23 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'"); throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
} }
const appId = core.getInput("app_id"); const appId = core.getInput("app-id") || core.getInput("app_id");
const privateKey = core.getInput("private_key"); if (!appId) {
// The 'app_id' input was previously required, but it and 'app-id' are both optional now, until the former is removed. Still, we want to ensure that at least one of them is set.
throw new Error("Input required and not supplied: app-id");
}
const privateKey = core.getInput("private-key") || core.getInput("private_key");
if (!privateKey) {
// The 'private_key' input was previously required, but it and 'private-key' are both optional now, until the former is removed. Still, we want to ensure that at least one of them is set.
throw new Error("Input required and not supplied: private-key");
}
const owner = core.getInput("owner"); const owner = core.getInput("owner");
const repositories = core.getInput("repositories"); const repositories = core.getInput("repositories");
const skipTokenRevoke = Boolean(
core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke")
);
main( main(
appId, appId,
privateKey, privateKey,
@@ -28,8 +40,10 @@ main(
createAppAuth, createAppAuth,
request.defaults({ request.defaults({
baseUrl: process.env["GITHUB_API_URL"], baseUrl: process.env["GITHUB_API_URL"],
}) }),
skipTokenRevoke
).catch((error) => { ).catch((error) => {
/* c8 ignore next 3 */
console.error(error); console.error(error);
core.setFailed(error.message); core.setFailed(error.message);
}); });

1687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,13 @@
"name": "create-github-app-token", "name": "create-github-app-token",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "1.3.0", "version": "1.5.0",
"description": "GitHub Action for creating a GitHub App Installation Access Token", "description": "GitHub Action for creating a GitHub App Installation Access Token",
"scripts": { "scripts": {
"build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node16.16", "build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node16.16",
"test": "ava tests/index.js" "test": "c8 --100 ava tests/index.js",
"coverage": "c8 report --reporter html",
"postcoverage": "open-cli coverage/index.html"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -16,10 +18,13 @@
}, },
"devDependencies": { "devDependencies": {
"ava": "^5.3.1", "ava": "^5.3.1",
"c8": "^8.0.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"esbuild": "^0.19.4", "esbuild": "^0.19.4",
"execa": "^8.0.1", "execa": "^8.0.1",
"undici": "^5.25.2" "open-cli": "^7.2.0",
"undici": "^5.25.2",
"yaml": "^2.3.2"
}, },
"release": { "release": {
"branches": [ "branches": [

View File

@@ -11,6 +11,7 @@ post(
baseUrl: process.env["GITHUB_API_URL"], baseUrl: process.env["GITHUB_API_URL"],
}) })
).catch((error) => { ).catch((error) => {
/* c8 ignore next 3 */
console.error(error); console.error(error);
core.setFailed(error.message); core.setFailed(error.message);
}); });

View File

@@ -6,7 +6,7 @@ Add one test file per scenario. You can run them in isolation with:
node tests/post-token-set.test.js 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 npx ava tests/index.js

View File

@@ -0,0 +1,16 @@
import { readFileSync } from "node:fs";
import * as url from "node:url";
import YAML from "yaml";
const action = YAML.parse(
readFileSync(
url.fileURLToPath(new URL("../action.yml", import.meta.url)),
"utf8"
)
);
for (const [key, value] of Object.entries(action.inputs)) {
if ("deprecationMessage" in value) {
console.log(`${key}${value.deprecationMessage}`);
}
}

View File

@@ -7,7 +7,12 @@ const tests = readdirSync("tests").filter((file) => file.endsWith(".test.js"));
for (const file of tests) { for (const file of tests) {
test(file, async (t) => { test(file, async (t) => {
const { stderr, stdout } = await execa("node", [`tests/${file}`]); // Override Actions environment variables that change `core`s behavior
const env = {
GITHUB_OUTPUT: undefined,
GITHUB_STATE: undefined,
};
const { stderr, stdout } = await execa("node", [`tests/${file}`], { env });
t.snapshot(stderr, "stderr"); t.snapshot(stderr, "stderr");
t.snapshot(stdout, "stdout"); t.snapshot(stdout, "stdout");
}); });

View File

@@ -0,0 +1,9 @@
process.env.GITHUB_REPOSITORY_OWNER = "actions";
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
// Verify `main` exits with an error when neither the `app-id` nor `app_id` input is set.
try {
await import("../main.js");
} catch (error) {
console.error(error.message);
}

View File

@@ -0,0 +1,9 @@
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
delete process.env.GITHUB_REPOSITORY_OWNER;
// Verify `main` exits with an error when `GITHUB_REPOSITORY_OWNER` is missing.
try {
await import("../main.js");
} catch (error) {
console.error(error.message);
}

View File

@@ -0,0 +1,10 @@
process.env.GITHUB_REPOSITORY_OWNER = "actions";
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
process.env["INPUT_APP-ID"] = "123456";
// Verify `main` exits with an error when neither the `private-key` nor `private_key` input is set.
try {
await import("../main.js");
} catch (error) {
console.error(error.message);
}

View File

@@ -0,0 +1,8 @@
delete process.env.GITHUB_REPOSITORY;
// Verify `main` exits with an error when `GITHUB_REPOSITORY` is missing.
try {
await import("../main.js");
} catch (error) {
console.error(error.message);
}

View File

@@ -0,0 +1,7 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos).
await test(() => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
process.env.INPUT_REPOSITORIES = `${process.env.GITHUB_REPOSITORY},actions/toolkit`;
});

View File

@@ -0,0 +1,7 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a single repo).
await test(() => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
});

View File

@@ -0,0 +1,25 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` input is set (to an org), but the `repositories` input isnt set.
await test((mockPool) => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
delete process.env.INPUT_REPOSITORIES;
// Mock installation id request
const mockInstallationId = "123456";
mockPool
.intercept({
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId },
{ headers: { "content-type": "application/json" } }
);
});

View File

@@ -0,0 +1,36 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` input is set (to a user), but the `repositories` input isnt set.
await test((mockPool) => {
process.env.INPUT_OWNER = "smockle";
delete process.env.INPUT_REPOSITORIES;
// Mock installation id request
const mockInstallationId = "123456";
mockPool
.intercept({
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(404);
mockPool
.intercept({
path: `/users/${process.env.INPUT_OWNER}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId },
{ headers: { "content-type": "application/json" } }
);
});

View File

@@ -0,0 +1,7 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` input is not set, but the `repositories` input is set.
await test(() => {
delete process.env.INPUT_OWNER;
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
});

View File

@@ -0,0 +1,25 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when neither the `owner` nor `repositories` input is set.
await test((mockPool) => {
delete process.env.INPUT_OWNER;
delete process.env.INPUT_REPOSITORIES;
// Mock installation id request
const mockInstallationId = "123456";
mockPool
.intercept({
path: `/repos/${process.env.GITHUB_REPOSITORY}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId },
{ headers: { "content-type": "application/json" } }
);
});

96
tests/main.js Normal file
View File

@@ -0,0 +1,96 @@
// Base for all `main` tests.
// @ts-check
import { MockAgent, setGlobalDispatcher } from "undici";
export async function test(cb = (_mockPool) => {}) {
// Set required environment variables and inputs
process.env.GITHUB_REPOSITORY_OWNER = "actions";
process.env.GITHUB_REPOSITORY = "actions/create-github-app-token";
// 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_APP-ID"] = "123456";
process.env["INPUT_PRIVATE-KEY"] = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA280nfuUM9w00Ib9E2rvZJ6Qu3Ua3IqR34ZlK53vn/Iobn2EL
Z9puc5Q/nFBU15NKwHyQNb+OG2hTCkjd1Xi9XPzEOH1r42YQmTGq8YCkUSkk6KZA
5dnhLwN9pFquT9fQgrf4r1D5GJj3rqvj8JDr1sBmunArqY5u4gziSrIohcjLIZV0
cIMz/RUIMe/EAsNeiwzEteHAtf/WpMs+OfF94SIUrDlkPr0H0H3DER8l1HZAvE0e
eD3ZJ6njrF6UHQWDVrekSTB0clpVTTU9TMpe+gs2nnFww9G8As+WsW8xHVjVipJy
AwqBhiR+s7wlcbh2i0NQqt8GL9/jIFTmleiwsQIDAQABAoIBAHNyP8pgl/yyzKzk
/0871wUBMTQ7zji91dGCaFtJM0HrcDK4D/uOOPEv7nE1qDpKPLr5Me1pHUS7+NGw
EAPtlNhgUtew2JfppdIwyi5qeOPADoi7ud6AH8xHsxg+IMwC+JuP8WhzyUHoJj9y
PRi/pX94Mvy9qdE25HqKddjx1mLdaHhxqPkr16/em23uYZqm1lORsCPD3vTlthj7
WiEbBSqmpYvjj8iFP4yFk2N+LvuWgSilRzq1Af3qE7PUp4xhjmcOPs78Sag1T7nl
ww/pgqCegISABHik7j++/5T+UlI5cLsyp/XENU9zAd4kCIczwNKpun2bn+djJdft
ravyX4ECgYEA+k2mHfi1zwKF3wT+cJbf30+uXrJczK2yZ33//4RKnhBaq7nSbQAI
nhWz2JESBK0TEo0+L7yYYq3HnT9vcES5R1NxzruH9wXgxssSx3JUj6w1raXYPh3B
+1XpYQsa6/bo2LmBELEx47F8FQbNgD5dmTJ4jBrf6MV4rRh9h6Bs7UkCgYEA4M3K
eAw52c2MNMIxH/LxdOQtEBq5GMu3AQC8I64DSSRrAoiypfEgyTV6S4gWJ5TKgYfD
zclnOVJF+tITe3neO9wHoZp8iCjCnoijcT6p2cJ4IaW68LEHPOokWBk0EpLjF4p2
7sFi9+lUpXYXfCN7aMJ77QpGzB7dNsBf0KUxMCkCgYEAjw/mjGbk82bLwUaHby6s
0mQmk7V6WPpGZ+Sadx7TzzglutVAslA8nK5m1rdEBywtJINaMcqnhm8xEm15cj+1
blEBUVnaQpQ3fyf+mcR9FIknPRL3X7l+b/sQowjH4GqFd6m/XR0KGMwO0a3Lsyry
MGeqgtmxdMe5S6YdyXEmERECgYAgQsgklDSVIh9Vzux31kh6auhgoEUh3tJDbZSS
Vj2YeIZ21aE1mTYISglj34K2aW7qSc56sMWEf18VkKJFHQccdgYOVfo7HAZZ8+fo
r4J2gqb0xTDfq7gLMNrIXc2QQM4gKbnJp60JQM3p9NmH8huavBZGvSvNzTwXyGG3
so0tiQKBgGQXZaxaXhYUcxYHuCkQ3V4Vsj3ezlM92xXlP32SGFm3KgFhYy9kATxw
Cax1ytZzvlrKLQyQFVK1COs2rHt7W4cJ7op7C8zXfsigXCiejnS664oAuX8sQZID
x3WQZRiXlWejSMUAHuMwXrhGlltF3lw83+xAjnqsVp75kGS6OH61
-----END RSA PRIVATE KEY-----`; // This key is invalidated. Its from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
// Set up mocking
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const mockPool = mockAgent.get("https://api.github.com");
// Calling `auth({ type: "app" })` to obtain a JWT doesnt make network requests, so no need to intercept.
// Mock installation id request
const mockInstallationId = "123456";
const owner = process.env.INPUT_OWNER ?? process.env.GITHUB_REPOSITORY_OWNER;
const repo = encodeURIComponent(
(process.env.INPUT_REPOSITORIES ?? process.env.GITHUB_REPOSITORY).split(
","
)[0]
);
mockPool
.intercept({
path: `/repos/${owner}/${repo}/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
200,
{ id: mockInstallationId },
{ headers: { "content-type": "application/json" } }
);
// Mock installation access token request
const mockInstallationAccessToken =
"ghs_16C7e42F292c6912E7710c838347Ae178B4a"; // This token is invalidated. Its from https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app.
mockPool
.intercept({
path: `/app/installations/${mockInstallationId}/access_tokens`,
method: "POST",
headers: {
accept: "application/vnd.github.v3+json",
"user-agent": "actions/create-github-app-token",
// Note: Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
},
})
.reply(
201,
{ token: mockInstallationAccessToken },
{ headers: { "content-type": "application/json" } }
);
// Run the callback
cb(mockPool);
// Run the main script
await import("../main.js");
}

View File

@@ -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");

View File

@@ -4,6 +4,142 @@ The actual snapshot is saved in `index.js.snap`.
Generated by [AVA](https://avajs.dev). Generated by [AVA](https://avajs.dev).
## action-deprecated-inputs.test.js
> stderr
''
> stdout
`app_id — 'app_id' is deprecated and will be removed in a future version. Use 'app-id' instead.␊
private_key — 'private_key' is deprecated and will be removed in a future version. Use 'private-key' instead.␊
skip_token_revoke — 'skip_token_revoke' is deprecated and will be removed in a future version. Use 'skip-token-revoke' instead.`
## main-missing-app-id.test.js
> stderr
'Input required and not supplied: app-id'
> stdout
''
## main-missing-owner.test.js
> stderr
'GITHUB_REPOSITORY_OWNER missing, must be set to \'<owner>\''
> stdout
''
## main-missing-private-key.test.js
> stderr
'Input required and not supplied: private-key'
> stdout
''
## main-missing-repository.test.js
> stderr
'GITHUB_REPOSITORY missing, must be set to \'<owner>/<repo>\''
> stdout
''
## main-token-get-owner-set-repo-set-to-many.test.js
> stderr
''
> stdout
`owner and repositories set, creating token for repositories "actions/create-github-app-token,actions/toolkit" owned by "actions"␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## main-token-get-owner-set-repo-set-to-one.test.js
> stderr
''
> stdout
`owner and repositories set, creating token for repositories "actions/create-github-app-token" owned by "actions"␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## main-token-get-owner-set-to-org-repo-unset.test.js
> stderr
''
> stdout
`repositories not set, creating token for all repositories for given owner "actions"␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## main-token-get-owner-set-to-user-repo-unset.test.js
> stderr
''
> stdout
`repositories not set, creating token for all repositories for given owner "smockle"␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## main-token-get-owner-unset-repo-set.test.js
> stderr
''
> stdout
`owner not set, creating owner for given repositories "actions/create-github-app-token" in current owner ("actions")␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## main-token-get-owner-unset-repo-unset.test.js
> stderr
''
> stdout
`owner and repositories not set, creating token for the current repository ("create-github-app-token")␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
## post-token-set.test.js ## post-token-set.test.js
> stderr > stderr
@@ -14,6 +150,16 @@ Generated by [AVA](https://avajs.dev).
'Token revoked' 'Token revoked'
## post-token-skipped.test.js
> stderr
''
> stdout
'Token revocation was skipped'
## post-token-unset.test.js ## post-token-unset.test.js
> stderr > stderr

Binary file not shown.