Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c6bf6752 | ||
|
|
1f82f7df93 | ||
|
|
1f18aabaea | ||
|
|
86576b355d | ||
|
|
837e2752e0 | ||
|
|
c4fa18d55c | ||
|
|
0c014070f9 | ||
|
|
f04aa94d10 | ||
|
|
323044ff31 | ||
|
|
936979e9ad | ||
|
|
2986852ad8 | ||
|
|
495056a515 | ||
|
|
8746053070 | ||
|
|
46e4a501e1 | ||
|
|
2e5cc74af2 | ||
|
|
474769db88 | ||
|
|
e995b4e40a | ||
|
|
0f3b4d7df9 | ||
|
|
9769eb4076 | ||
|
|
bb368d6a10 | ||
|
|
eaddb9eb7e | ||
|
|
6375dcedb2 | ||
|
|
9577d5145a | ||
|
|
7f24dca8d7 | ||
|
|
d2cdb754af | ||
|
|
3bb715c53d | ||
|
|
9d97a4282b | ||
|
|
d21ec768fd | ||
|
|
998b8757ce | ||
|
|
b94a200224 | ||
|
|
7b1d2aef87 | ||
|
|
bdb2377ad0 | ||
|
|
9b283559f1 | ||
|
|
8210939678 | ||
|
|
9ec88c41ee |
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: release
|
||||
"on":
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -14,16 +15,25 @@ jobs:
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app_id: ${{ vars.RELEASER_APP_ID }}
|
||||
private_key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
|
||||
# build local version to create token
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- run: npm install --no-save @semantic-release/git semantic-release-plugin-github-breaking-version-tag
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
- 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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -15,10 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
node-version-file: .node-version
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
@@ -29,7 +32,7 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
@@ -38,8 +41,8 @@ jobs:
|
||||
- uses: ./ # Uses the action in the root directory
|
||||
id: test
|
||||
with:
|
||||
app_id: ${{ vars.TEST_APP_ID }}
|
||||
private_key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
|
||||
app-id: ${{ vars.TEST_APP_ID }}
|
||||
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
|
||||
- uses: octokit/request-action@v2.x
|
||||
id: get-repository
|
||||
env:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
coverage
|
||||
node_modules/
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
20.9.0
|
||||
101
README.md
101
README.md
@@ -1,5 +1,7 @@
|
||||
# Create GitHub App Token
|
||||
|
||||
[](https://github.com/actions/create-github-app-token/actions/workflows/test.yml)
|
||||
|
||||
GitHub Action for creating a GitHub App installation access token.
|
||||
|
||||
## Usage
|
||||
@@ -13,7 +15,11 @@ In order to use this action, you need to:
|
||||
### Create a token for the current repository
|
||||
|
||||
```yaml
|
||||
on: [issues]
|
||||
name: Run tests on staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
hello-world:
|
||||
@@ -22,13 +28,12 @@ jobs:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app_id: ${{ vars.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
github-api-url: "https://github.acme-inc.com/api/v3"
|
||||
- uses: ./actions/staging-tests
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: "Hello, World!"
|
||||
```
|
||||
|
||||
### Use app token with `actions/checkout`
|
||||
@@ -44,9 +49,9 @@ jobs:
|
||||
id: app-token
|
||||
with:
|
||||
# required
|
||||
app_id: ${{ vars.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v3
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
ref: ${{ github.head_ref }}
|
||||
@@ -69,8 +74,8 @@ jobs:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app_id: ${{ vars.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
@@ -91,8 +96,8 @@ jobs:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app_id: ${{ vars.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "repo1,repo2"
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
@@ -114,8 +119,8 @@ jobs:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app_id: ${{ vars.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: another-owner
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
@@ -124,13 +129,65 @@ jobs:
|
||||
body: "Hello, World!"
|
||||
```
|
||||
|
||||
### Create tokens for multiple user or organization accounts
|
||||
|
||||
You can use a matrix strategy to create tokens for multiple user or organization accounts.
|
||||
|
||||
> [!NOTE]
|
||||
> See [this documentation](https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings) for information on using multiline strings in workflows.
|
||||
|
||||
```yaml
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
set-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{steps.set.outputs.matrix }}
|
||||
steps:
|
||||
- id: set
|
||||
run: echo 'matrix=[{"owner":"owner1"},{"owner":"owner2","repos":["repo1"]}]' >>"$GITHUB_OUTPUT"
|
||||
|
||||
use-matrix:
|
||||
name: "@${{ matrix.owners-and-repos.owner }} installation"
|
||||
needs: [set-matrix]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
owners-and-repos: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ matrix.owners-and-repos.owner }}
|
||||
repositories: ${{ join(matrix.owners-and-repos.repos) }}
|
||||
- uses: octokit/request-action@v2.x
|
||||
id: get-installation-repositories
|
||||
with:
|
||||
route: GET /installation/repositories
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
- run: echo "$MULTILINE_JSON_STRING"
|
||||
env:
|
||||
MULTILINE_JSON_STRING: ${{ steps.get-installation-repositories.outputs.data }}
|
||||
```
|
||||
|
||||
### Run the workflow in a github.com repository against an organization in GitHub Enterprise Server
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
### `app_id`
|
||||
### `app-id`
|
||||
|
||||
**Required:** GitHub App ID.
|
||||
|
||||
### `private_key`
|
||||
### `private-key`
|
||||
|
||||
**Required:** GitHub App private key.
|
||||
|
||||
@@ -145,6 +202,14 @@ 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.
|
||||
|
||||
### `github-api-url`
|
||||
|
||||
**Optional:** The URL of the GitHub REST API. Defaults to the URL of the GitHub Rest API where the workflow is run from.
|
||||
|
||||
## Outputs
|
||||
|
||||
### `token`
|
||||
@@ -158,7 +223,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]
|
||||
|
||||
24
action.yml
24
action.yml
@@ -5,18 +5,38 @@ branding:
|
||||
icon: "lock"
|
||||
color: "gray-dark"
|
||||
inputs:
|
||||
app-id:
|
||||
description: "GitHub App ID"
|
||||
required: false # TODO: When 'app_id' is removed, make 'app-id' required
|
||||
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:
|
||||
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:
|
||||
description: "GitHub App owner (defaults to current repository owner)"
|
||||
required: false
|
||||
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
|
||||
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."
|
||||
# Make GitHub API configurable to support non-GitHub Cloud use cases
|
||||
# see https://github.com/actions/create-github-app-token/issues/77
|
||||
github-api-url:
|
||||
description: The URL of the GitHub REST API.
|
||||
default: ${{ github.api_url }}
|
||||
outputs:
|
||||
token:
|
||||
description: "GitHub installation access token"
|
||||
|
||||
25
badges/coverage.svg
Normal file
25
badges/coverage.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="106"
|
||||
height="20" role="img" aria-label="Coverage: 100%">
|
||||
<title>Coverage: 100%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="106" height="20" rx="3" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="63" height="20" fill="#555" />
|
||||
<rect x="63" width="43" height="20" fill="#4c1" />
|
||||
<rect width="106" height="20" fill="url(#s)" />
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision" font-size="110">
|
||||
<text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3"
|
||||
transform="scale(.1)" textLength="530">Coverage</text>
|
||||
<text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text>
|
||||
<text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3"
|
||||
transform="scale(.1)" textLength="330">100%</text>
|
||||
<text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="330">100%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
18352
dist/main.cjs
vendored
18352
dist/main.cjs
vendored
File diff suppressed because one or more lines are too long
17698
dist/post.cjs
vendored
17698
dist/post.cjs
vendored
File diff suppressed because one or more lines are too long
110
lib/main.js
110
lib/main.js
@@ -1,3 +1,4 @@
|
||||
import pRetry from "p-retry";
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
@@ -8,6 +9,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 +18,8 @@ export async function main(
|
||||
repositories,
|
||||
core,
|
||||
createAppAuth,
|
||||
request
|
||||
request,
|
||||
skipTokenRevoke
|
||||
) {
|
||||
let parsedOwner = "";
|
||||
let parsedRepositoryNames = "";
|
||||
@@ -67,52 +70,28 @@ export async function main(
|
||||
request,
|
||||
});
|
||||
|
||||
const appAuthentication = await auth({
|
||||
type: "app",
|
||||
});
|
||||
|
||||
let authentication;
|
||||
// If at least one repository is set, get installation ID from that repository
|
||||
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
|
||||
|
||||
if (parsedRepositoryNames) {
|
||||
const response = await request("GET /repos/{owner}/{repo}/installation", {
|
||||
owner: parsedOwner,
|
||||
repo: parsedRepositoryNames.split(",")[0],
|
||||
headers: {
|
||||
authorization: `bearer ${appAuthentication.token}`,
|
||||
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), {
|
||||
onFailedAttempt: (error) => {
|
||||
core.info(
|
||||
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||
);
|
||||
},
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
// Get token for given repositories
|
||||
authentication = await auth({
|
||||
type: "installation",
|
||||
installationId: response.data.id,
|
||||
repositoryNames: parsedRepositoryNames.split(","),
|
||||
});
|
||||
} else {
|
||||
// Otherwise get the installation for the owner, which can either be an organization or a user account
|
||||
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
|
||||
const response = await request("GET /orgs/{org}/installation", {
|
||||
org: parsedOwner,
|
||||
headers: {
|
||||
authorization: `bearer ${appAuthentication.token}`,
|
||||
authentication = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), {
|
||||
onFailedAttempt: (error) => {
|
||||
core.info(
|
||||
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||
);
|
||||
},
|
||||
}).catch((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
|
||||
return request("GET /users/{username}/installation", {
|
||||
username: parsedOwner,
|
||||
headers: {
|
||||
authorization: `bearer ${appAuthentication.token}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Get token for for all repositories of the given installation
|
||||
authentication = await auth({
|
||||
type: "installation",
|
||||
installationId: response.data.id,
|
||||
retries: 3,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +99,58 @@ export async function main(
|
||||
core.setSecret(authentication.token);
|
||||
|
||||
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);
|
||||
core.setOutput("expiresAt", authentication.expiresAt);
|
||||
}
|
||||
}
|
||||
|
||||
async function getTokenFromOwner(request, auth, parsedOwner) {
|
||||
// https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app
|
||||
const response = await request("GET /orgs/{org}/installation", {
|
||||
org: parsedOwner,
|
||||
request: {
|
||||
hook: auth.hook,
|
||||
},
|
||||
}).catch((error) => {
|
||||
/* c8 ignore next */
|
||||
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
|
||||
return request("GET /users/{username}/installation", {
|
||||
username: parsedOwner,
|
||||
request: {
|
||||
hook: auth.hook,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Get token for for all repositories of the given installation
|
||||
const authentication = await auth({
|
||||
type: "installation",
|
||||
installationId: response.data.id,
|
||||
});
|
||||
return authentication;
|
||||
}
|
||||
|
||||
async function getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames) {
|
||||
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
|
||||
const response = await request("GET /repos/{owner}/{repo}/installation", {
|
||||
owner: parsedOwner,
|
||||
repo: parsedRepositoryNames.split(",")[0],
|
||||
request: {
|
||||
hook: auth.hook,
|
||||
},
|
||||
});
|
||||
|
||||
// Get token for given repositories
|
||||
const authentication = await auth({
|
||||
type: "installation",
|
||||
installationId: response.data.id,
|
||||
repositoryNames: parsedRepositoryNames.split(","),
|
||||
});
|
||||
|
||||
return authentication;
|
||||
}
|
||||
41
lib/post.js
41
lib/post.js
@@ -5,6 +5,15 @@
|
||||
* @param {import("@octokit/request").request} 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");
|
||||
|
||||
if (!token) {
|
||||
@@ -12,11 +21,31 @@ export async function post(core, request) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request("DELETE /installation/token", {
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const expiresAt = core.getState("expiresAt");
|
||||
if (expiresAt && tokenExpiresIn(expiresAt) < 0) {
|
||||
core.info("Token expired, skipping token revocation");
|
||||
return;
|
||||
}
|
||||
|
||||
core.info("Token revoked");
|
||||
try {
|
||||
await request("DELETE /installation/token", {
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
core.info("Token revoked");
|
||||
} catch (error) {
|
||||
core.warning(
|
||||
`Token revocation failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expiresAt
|
||||
*/
|
||||
function tokenExpiresIn(expiresAt) {
|
||||
const now = new Date();
|
||||
const expiresAtDate = new Date(expiresAt);
|
||||
|
||||
return Math.round((expiresAtDate.getTime() - now.getTime()) / 1000);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
import core from "@actions/core";
|
||||
import { request } from "@octokit/request";
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
|
||||
const baseUrl = core.getInput("github-api-url").replace(/\/$/, "");
|
||||
|
||||
// https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/using-a-proxy-server-with-self-hosted-runners
|
||||
const proxyUrl =
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTP_PROXY;
|
||||
|
||||
/* c8 ignore start */
|
||||
// Native support for proxies in Undici is under consideration: https://github.com/nodejs/undici/issues/1650
|
||||
// Until then, we need to use a custom fetch function to add proxy support.
|
||||
const proxyFetch = (url, options) => {
|
||||
const urlHost = new URL(url).hostname;
|
||||
const noProxy = (process.env.no_proxy || process.env.NO_PROXY || "").split(
|
||||
","
|
||||
);
|
||||
|
||||
if (!noProxy.includes(urlHost)) {
|
||||
options = {
|
||||
...options,
|
||||
dispatcher: new ProxyAgent(String(proxyUrl)),
|
||||
};
|
||||
}
|
||||
|
||||
return undiciFetch(url, options);
|
||||
};
|
||||
/* c8 ignore stop */
|
||||
|
||||
export default request.defaults({
|
||||
baseUrl: process.env["GITHUB_API_URL"],
|
||||
headers: {
|
||||
"user-agent": "actions/create-github-app-token",
|
||||
},
|
||||
baseUrl,
|
||||
/* c8 ignore next */
|
||||
request: proxyUrl ? { fetch: proxyFetch } : {},
|
||||
});
|
||||
|
||||
22
main.js
22
main.js
@@ -14,11 +14,23 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
|
||||
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
|
||||
}
|
||||
|
||||
const appId = core.getInput("app_id");
|
||||
const privateKey = core.getInput("private_key");
|
||||
const appId = core.getInput("app-id") || core.getInput("app_id");
|
||||
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 repositories = core.getInput("repositories");
|
||||
|
||||
const skipTokenRevoke = Boolean(
|
||||
core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke")
|
||||
);
|
||||
|
||||
main(
|
||||
appId,
|
||||
privateKey,
|
||||
@@ -26,10 +38,10 @@ main(
|
||||
repositories,
|
||||
core,
|
||||
createAppAuth,
|
||||
request.defaults({
|
||||
baseUrl: process.env["GITHUB_API_URL"],
|
||||
})
|
||||
request,
|
||||
skipTokenRevoke
|
||||
).catch((error) => {
|
||||
/* c8 ignore next 3 */
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
});
|
||||
|
||||
2133
package-lock.json
generated
2133
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -2,24 +2,31 @@
|
||||
"name": "create-github-app-token",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.3.0",
|
||||
"version": "1.8.0",
|
||||
"description": "GitHub Action for creating a GitHub App Installation Access Token",
|
||||
"scripts": {
|
||||
"build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node16.16",
|
||||
"test": "ava tests/index.js"
|
||||
"build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node20.0.0",
|
||||
"test": "c8 --100 ava tests/index.js",
|
||||
"coverage": "c8 report --reporter html",
|
||||
"postcoverage": "open-cli coverage/index.html"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@octokit/auth-app": "^6.0.1",
|
||||
"@octokit/request": "^8.1.2"
|
||||
"@octokit/auth-app": "^6.0.3",
|
||||
"@octokit/request": "^8.1.6",
|
||||
"p-retry": "^6.2.0",
|
||||
"undici": "^6.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^5.3.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.19.4",
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"ava": "^6.1.1",
|
||||
"c8": "^9.1.0",
|
||||
"dotenv": "^16.4.1",
|
||||
"esbuild": "^0.20.0",
|
||||
"execa": "^8.0.1",
|
||||
"undici": "^5.25.2"
|
||||
"open-cli": "^8.0.0",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
8
post.js
8
post.js
@@ -5,12 +5,8 @@ import core from "@actions/core";
|
||||
import { post } from "./lib/post.js";
|
||||
import request from "./lib/request.js";
|
||||
|
||||
post(
|
||||
core,
|
||||
request.defaults({
|
||||
baseUrl: process.env["GITHUB_API_URL"],
|
||||
})
|
||||
).catch((error) => {
|
||||
post(core, request).catch((error) => {
|
||||
/* c8 ignore next 3 */
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
16
tests/action-deprecated-inputs.test.js
Normal file
16
tests/action-deprecated-inputs.test.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,12 @@ const tests = readdirSync("tests").filter((file) => file.endsWith(".test.js"));
|
||||
|
||||
for (const file of tests) {
|
||||
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(stdout, "stdout");
|
||||
});
|
||||
|
||||
13
tests/main-custom-github-api-url.test.js
Normal file
13
tests/main-custom-github-api-url.test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { test, DEFAULT_ENV } from "./main.js";
|
||||
|
||||
// Verify that main works with a custom GitHub API URL passed as `github-api-url` input
|
||||
await test(
|
||||
() => {
|
||||
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
|
||||
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
|
||||
},
|
||||
{
|
||||
...DEFAULT_ENV,
|
||||
"INPUT_GITHUB-API-URL": "https://github.acme-inc.com/api/v3",
|
||||
}
|
||||
);
|
||||
9
tests/main-missing-app-id.test.js
Normal file
9
tests/main-missing-app-id.test.js
Normal 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);
|
||||
}
|
||||
9
tests/main-missing-owner.test.js
Normal file
9
tests/main-missing-owner.test.js
Normal 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);
|
||||
}
|
||||
10
tests/main-missing-private-key.test.js
Normal file
10
tests/main-missing-private-key.test.js
Normal 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);
|
||||
}
|
||||
8
tests/main-missing-repository.test.js
Normal file
8
tests/main-missing-repository.test.js
Normal 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);
|
||||
}
|
||||
56
tests/main-repo-skew.js
Normal file
56
tests/main-repo-skew.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
import { install } from "@sinonjs/fake-timers";
|
||||
|
||||
// Verify `main` retry when the clock has drifted.
|
||||
await test((mockPool) => {
|
||||
process.env.INPUT_OWNER = 'actions'
|
||||
process.env.INPUT_REPOSITORIES = 'failed-repo';
|
||||
const owner = process.env.INPUT_OWNER
|
||||
const repo = process.env.INPUT_REPOSITORIES
|
||||
const mockInstallationId = "123456";
|
||||
|
||||
install({ now: 0, toFake: ["Date"] });
|
||||
|
||||
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(({ headers }) => {
|
||||
const [_, jwt] = (headers.authorization || "").split(" ");
|
||||
const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
|
||||
|
||||
if (payload.iat < 0) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
data: {
|
||||
message: "'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued."
|
||||
},
|
||||
responseOptions: {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"date": new Date(Date.now() + 30000).toUTCString()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
data: {
|
||||
id: mockInstallationId
|
||||
},
|
||||
responseOptions: {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
}
|
||||
};
|
||||
}).times(2);
|
||||
});
|
||||
38
tests/main-token-get-owner-set-repo-fail-response.test.js
Normal file
38
tests/main-token-get-owner-set-repo-fail-response.test.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
// Verify `main` retry when the GitHub API returns a 500 error.
|
||||
await test((mockPool) => {
|
||||
process.env.INPUT_OWNER = "actions";
|
||||
process.env.INPUT_REPOSITORIES = "failed-repo";
|
||||
const owner = process.env.INPUT_OWNER;
|
||||
const repo = process.env.INPUT_REPOSITORIES;
|
||||
const mockInstallationId = "123456";
|
||||
|
||||
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(500, "GitHub API not available");
|
||||
|
||||
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" } }
|
||||
);
|
||||
});
|
||||
7
tests/main-token-get-owner-set-repo-set-to-many.test.js
Normal file
7
tests/main-token-get-owner-set-repo-set-to-many.test.js
Normal 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`;
|
||||
});
|
||||
7
tests/main-token-get-owner-set-repo-set-to-one.test.js
Normal file
7
tests/main-token-get-owner-set-repo-set-to-one.test.js
Normal 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;
|
||||
});
|
||||
25
tests/main-token-get-owner-set-to-org-repo-unset.test.js
Normal file
25
tests/main-token-get-owner-set-to-org-repo-unset.test.js
Normal 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 isn’t 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" } }
|
||||
);
|
||||
});
|
||||
36
tests/main-token-get-owner-set-to-user-fail-response.test.js
Normal file
36
tests/main-token-get-owner-set-to-user-fail-response.test.js
Normal 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 isn’t 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(500, "GitHub API not available");
|
||||
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" } }
|
||||
);
|
||||
});
|
||||
36
tests/main-token-get-owner-set-to-user-repo-unset.test.js
Normal file
36
tests/main-token-get-owner-set-to-user-repo-unset.test.js
Normal 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 isn’t 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" } }
|
||||
);
|
||||
});
|
||||
7
tests/main-token-get-owner-unset-repo-set.test.js
Normal file
7
tests/main-token-get-owner-unset-repo-set.test.js
Normal 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;
|
||||
});
|
||||
25
tests/main-token-get-owner-unset-repo-unset.test.js
Normal file
25
tests/main-token-get-owner-unset-repo-unset.test.js
Normal 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" } }
|
||||
);
|
||||
});
|
||||
104
tests/main.js
Normal file
104
tests/main.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// Base for all `main` tests.
|
||||
// @ts-check
|
||||
import { MockAgent, setGlobalDispatcher } from "undici";
|
||||
|
||||
export const DEFAULT_ENV = {
|
||||
GITHUB_REPOSITORY_OWNER: "actions",
|
||||
GITHUB_REPOSITORY: "actions/create-github-app-token",
|
||||
// inputs are set as environment variables with the prefix INPUT_
|
||||
// https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#example-specifying-inputs
|
||||
"INPUT_GITHUB-API-URL": "https://api.github.com",
|
||||
"INPUT_APP-ID": "123456",
|
||||
// This key is invalidated. It’s from https://github.com/octokit/auth-app.js/issues/465#issuecomment-1564998327.
|
||||
"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-----`,
|
||||
};
|
||||
|
||||
export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
// Set up mocking
|
||||
const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]);
|
||||
const basePath = baseUrl.pathname === '/' ? '' : baseUrl.pathname;
|
||||
const mockAgent = new MockAgent();
|
||||
mockAgent.disableNetConnect();
|
||||
setGlobalDispatcher(mockAgent);
|
||||
const mockPool = mockAgent.get(baseUrl.origin);
|
||||
|
||||
// Calling `auth({ type: "app" })` to obtain a JWT doesn’t make network requests, so no need to intercept.
|
||||
|
||||
// Mock installation id request
|
||||
const mockInstallationId = "123456";
|
||||
const owner = env.INPUT_OWNER ?? env.GITHUB_REPOSITORY_OWNER;
|
||||
const repo = encodeURIComponent(
|
||||
(env.INPUT_REPOSITORIES ?? env.GITHUB_REPOSITORY).split(",")[0]
|
||||
);
|
||||
mockPool
|
||||
.intercept({
|
||||
path: `${basePath}/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. It’s from https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app.
|
||||
const mockExpiresAt = "2016-07-11T22:14:10Z";
|
||||
mockPool
|
||||
.intercept({
|
||||
path: `${basePath}/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, expires_at: mockExpiresAt },
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
// Run the callback
|
||||
cb(mockPool);
|
||||
|
||||
// Run the main script
|
||||
await import("../main.js");
|
||||
}
|
||||
34
tests/post-revoke-token-fail-response.test.js
Normal file
34
tests/post-revoke-token-fail-response.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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_GITHUB-API-URL"] = "https://api.github.com";
|
||||
|
||||
// 1 hour in the future, not expired
|
||||
process.env.STATE_expiresAt = new Date(
|
||||
Date.now() + 1000 * 60 * 60
|
||||
).toISOString();
|
||||
|
||||
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(401);
|
||||
|
||||
await import("../post.js");
|
||||
28
tests/post-token-expired.test.js
Normal file
28
tests/post-token-expired.test.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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";
|
||||
|
||||
// 1 hour in the past, expired
|
||||
process.env.STATE_expiresAt = new Date(Date.now() - 1000 * 60 * 60).toISOString();
|
||||
|
||||
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");
|
||||
@@ -4,6 +4,13 @@ import { MockAgent, setGlobalDispatcher } from "undici";
|
||||
// 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_GITHUB-API-URL"] = "https://api.github.com";
|
||||
|
||||
// 1 hour in the future, not expired
|
||||
process.env.STATE_expiresAt = new Date(Date.now() + 1000 * 60 * 60).toISOString();
|
||||
|
||||
const mockAgent = new MockAgent();
|
||||
|
||||
setGlobalDispatcher(mockAgent);
|
||||
|
||||
29
tests/post-token-skipped.test.js
Normal file
29
tests/post-token-skipped.test.js
Normal 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");
|
||||
@@ -4,6 +4,224 @@ The actual snapshot is saved in `index.js.snap`.
|
||||
|
||||
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-custom-github-api-url.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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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-fail-response.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
`owner and repositories set, creating token for repositories "failed-repo" owned by "actions"␊
|
||||
Failed to create token for "failed-repo" (attempt 1): GitHub API not available␊
|
||||
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## main-token-get-owner-set-to-user-fail-response.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
`repositories not set, creating token for all repositories for given owner "smockle"␊
|
||||
Failed to create token for "smockle" (attempt 1): GitHub API not available␊
|
||||
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## 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␊
|
||||
␊
|
||||
::set-output name=expiresAt::2016-07-11T22:14:10Z`
|
||||
|
||||
## post-revoke-token-fail-response.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
'::warning::Token revocation failed: '
|
||||
|
||||
## post-token-expired.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
'Token expired, skipping token revocation'
|
||||
|
||||
## post-token-set.test.js
|
||||
|
||||
> stderr
|
||||
@@ -14,6 +232,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