diff --git a/README.md b/README.md index f969916..4337932 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,28 @@ jobs: body: "Hello, World!" ``` +### Create a token for an enterprise installation + +```yaml +on: [workflow_dispatch] + +jobs: + hello-world: + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + enterprise: my-enterprise-slug + - name: Call enterprise management REST API with gh + run: | + gh api /enterprises/my-enterprise-slug/apps/installable_organizations + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} +``` + ### Create a token with specific permissions > [!NOTE] @@ -335,6 +357,13 @@ steps: > [!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. +### `enterprise` + +**Optional:** The slug of the enterprise to generate a token for enterprise-level app installations. + +> [!NOTE] +> The `enterprise` input is mutually exclusive with `owner` and `repositories`. GitHub Apps can be installed on enterprise accounts with permissions that let them call enterprise management APIs. Enterprise installations do not grant access to organization or repository resources. + ### `permission-` **Optional:** The permissions to grant to the token. By default, the token inherits all of the installation's permissions. We recommend to explicitly list the permissions that are required for a use case. This follows GitHub's own recommendation to [control permissions of `GITHUB_TOKEN` in workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token). The documentation also lists all available permissions, just prefix the permission key with `permission-` (e.g., `pull-requests` → `permission-pull-requests`). diff --git a/action.yml b/action.yml index ab7d7f3..6ed61cf 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,9 @@ inputs: repositories: description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)" required: false + enterprise: + description: "Enterprise slug for enterprise-level app installations (cannot be used with 'owner' or 'repositories')" + required: false skip-token-revoke: description: "If true, the token will not be revoked when the current job is complete" required: false diff --git a/dist/main.cjs b/dist/main.cjs index 905c9fb..2931c58 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -42523,39 +42523,46 @@ async function pRetry(input, options) { } // lib/main.js -async function main(appId2, privateKey2, owner2, repositories2, permissions2, core3, createAppAuth2, request2, skipTokenRevoke2) { +async function main(appId2, privateKey2, enterprise2, owner2, repositories2, permissions2, core3, createAppAuth2, request2, skipTokenRevoke2) { + if (enterprise2 && (owner2 || repositories2.length > 0)) { + throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs"); + } let parsedOwner = ""; let parsedRepositoryNames = []; - if (!owner2 && repositories2.length === 0) { - const [owner3, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); - parsedOwner = owner3; - parsedRepositoryNames = [repo]; - core3.info( - `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner3}/${repo}).` - ); - } - if (owner2 && repositories2.length === 0) { - parsedOwner = owner2; - core3.info( - `Input 'repositories' is not set. Creating token for all repositories owned by ${owner2}.` - ); - } - if (!owner2 && repositories2.length > 0) { - parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); - parsedRepositoryNames = repositories2; - core3.info( - `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories2.map((repo) => ` + if (!enterprise2) { + if (!owner2 && repositories2.length === 0) { + const [owner3, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); + parsedOwner = owner3; + parsedRepositoryNames = [repo]; + core3.info( + `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner3}/${repo}).` + ); + } + if (owner2 && repositories2.length === 0) { + parsedOwner = owner2; + core3.info( + `Input 'repositories' is not set. Creating token for all repositories owned by ${owner2}.` + ); + } + if (!owner2 && repositories2.length > 0) { + parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); + parsedRepositoryNames = repositories2; + core3.info( + `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories2.map((repo) => ` - ${parsedOwner}/${repo}`).join("")}` - ); - } - if (owner2 && repositories2.length > 0) { - parsedOwner = owner2; - parsedRepositoryNames = repositories2; - core3.info( - `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: - ${repositories2.map((repo) => ` + ); + } + if (owner2 && repositories2.length > 0) { + parsedOwner = owner2; + parsedRepositoryNames = repositories2; + core3.info( + `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: + ${repositories2.map((repo) => ` - ${parsedOwner}/${repo}`).join("")}` - ); + ); + } + } else { + core3.info(`Creating enterprise installation token for enterprise "${enterprise2}".`); } const auth5 = createAppAuth2({ appId: appId2, @@ -42563,7 +42570,20 @@ async function main(appId2, privateKey2, owner2, repositories2, permissions2, co request: request2 }); let authentication, installationId, appSlug; - if (parsedRepositoryNames.length > 0) { + if (enterprise2) { + ({ authentication, installationId, appSlug } = await pRetry( + () => getTokenFromEnterprise(request2, auth5, enterprise2, permissions2), + { + shouldRetry: (error) => error.status >= 500, + onFailedAttempt: (error) => { + core3.info( + `Failed to create token for enterprise "${enterprise2}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3 + } + )); + } else if (parsedRepositoryNames.length > 0) { ({ authentication, installationId, appSlug } = await pRetry( () => getTokenFromRepository( request2, @@ -42640,6 +42660,27 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; } +async function getTokenFromEnterprise(request2, auth5, enterprise2, permissions2) { + const response = await request2("GET /app/installations", { + request: { + hook: auth5.hook + } + }); + const enterpriseInstallation = response.data.find( + (installation) => installation.target_type === "Enterprise" && installation.account?.slug === enterprise2 + ); + if (!enterpriseInstallation) { + throw new Error(`No enterprise installation found matching the name ${enterprise2}. Available installations: ${response.data.map((i) => `${i.target_type}:${i.account?.login || "N/A"}`).join(", ")}`); + } + const authentication = await auth5({ + type: "installation", + installationId: enterpriseInstallation.id, + permissions: permissions2 + }); + const installationId = enterpriseInstallation.id; + const appSlug = enterpriseInstallation["app_slug"]; + return { authentication, installationId, appSlug }; +} // lib/request.js var import_core = __toESM(require_core(), 1); @@ -42677,6 +42718,7 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) { } var appId = import_core2.default.getInput("app-id"); var privateKey = import_core2.default.getInput("private-key"); +var enterprise = import_core2.default.getInput("enterprise"); var owner = import_core2.default.getInput("owner"); var repositories = import_core2.default.getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== ""); var skipTokenRevoke = import_core2.default.getBooleanInput("skip-token-revoke"); @@ -42684,6 +42726,7 @@ var permissions = getPermissionsFromInputs(process.env); var main_default = main( appId, privateKey, + enterprise, owner, repositories, permissions, diff --git a/lib/main.js b/lib/main.js index 3ec39b5..e9c83b0 100644 --- a/lib/main.js +++ b/lib/main.js @@ -4,6 +4,7 @@ import pRetry from "p-retry"; /** * @param {string} appId * @param {string} privateKey + * @param {string} enterprise * @param {string} owner * @param {string[]} repositories * @param {undefined | Record} permissions @@ -15,58 +16,70 @@ import pRetry from "p-retry"; export async function main( appId, privateKey, + enterprise, owner, repositories, permissions, core, createAppAuth, request, - skipTokenRevoke -) { - let parsedOwner = ""; - let parsedRepositoryNames = []; - - // If neither owner nor repositories are set, default to current repository - if (!owner && repositories.length === 0) { - const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); - parsedOwner = owner; - parsedRepositoryNames = [repo]; - - core.info( - `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).` - ); - } + skipTokenRevoke, - // If only an owner is set, default to all repositories from that owner - if (owner && repositories.length === 0) { - parsedOwner = owner; - - core.info( - `Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.` - ); - } - - // If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER` - if (!owner && repositories.length > 0) { - parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); - parsedRepositoryNames = repositories; - - core.info( - `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories - .map((repo) => `\n- ${parsedOwner}/${repo}`) - .join("")}` - ); +) { + // Validate mutual exclusivity of enterprise with owner/repositories + if (enterprise && (owner || repositories.length > 0)) { + throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs"); } - // If both owner and repositories are set, use those values - if (owner && repositories.length > 0) { - parsedOwner = owner; - parsedRepositoryNames = repositories; + let parsedOwner = ""; + let parsedRepositoryNames = []; - core.info( - `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: - ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` - ); + // Skip owner/repository parsing if enterprise is set + if (!enterprise) { + // If neither owner nor repositories are set, default to current repository + if (!owner && repositories.length === 0) { + const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); + parsedOwner = owner; + parsedRepositoryNames = [repo]; + + core.info( + `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).` + ); + } + + // If only an owner is set, default to all repositories from that owner + if (owner && repositories.length === 0) { + parsedOwner = owner; + + core.info( + `Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.` + ); + } + + // If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER` + if (!owner && repositories.length > 0) { + parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); + parsedRepositoryNames = repositories; + + core.info( + `No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories + .map((repo) => `\n- ${parsedOwner}/${repo}`) + .join("")}` + ); + } + + // If both owner and repositories are set, use those values + if (owner && repositories.length > 0) { + parsedOwner = owner; + parsedRepositoryNames = repositories; + + core.info( + `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: + ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` + ); + } + } else { + core.info(`Creating enterprise installation token for enterprise "${enterprise}".`); } const auth = createAppAuth({ @@ -76,9 +89,22 @@ export async function main( }); let authentication, installationId, appSlug; - // If at least one repository is set, get installation ID from that repository - - if (parsedRepositoryNames.length > 0) { + + // If enterprise is set, get installation ID from the enterprise + if (enterprise) { + ({ authentication, installationId, appSlug } = await pRetry( + () => getTokenFromEnterprise(request, auth, enterprise, permissions), + { + shouldRetry: (error) => error.status >= 500, + onFailedAttempt: (error) => { + core.info( + `Failed to create token for enterprise "${enterprise}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3, + } + )); + } else if (parsedRepositoryNames.length > 0) { ({ authentication, installationId, appSlug } = await pRetry( () => getTokenFromRepository( @@ -181,3 +207,37 @@ async function getTokenFromRepository( return { authentication, installationId, appSlug }; } + +async function getTokenFromEnterprise(request, auth, enterprise, permissions) { + // Get all installations and find the enterprise one + // https://docs.github.com/rest/apps/apps#list-installations-for-the-authenticated-app + // Note: Currently we do not have a way to get the installation for an enterprise directly, + // so as a workaround we need to list all installations and filter for the enterprise one. + const response = await request("GET /app/installations", { + request: { + hook: auth.hook, + }, + }); + + // Find the enterprise installation + const enterpriseInstallation = response.data.find( + installation => installation.target_type === "Enterprise" && + installation.account?.slug === enterprise + ); + + if (!enterpriseInstallation) { + throw new Error(`No enterprise installation found matching the name ${enterprise}. Available installations: ${response.data.map(i => `${i.target_type}:${i.account?.login || 'N/A'}`).join(', ')}`); + } + + // Get token for the enterprise installation + const authentication = await auth({ + type: "installation", + installationId: enterpriseInstallation.id, + permissions, + }); + + const installationId = enterpriseInstallation.id; + const appSlug = enterpriseInstallation["app_slug"]; + + return { authentication, installationId, appSlug }; +} diff --git a/main.js b/main.js index 7670378..a53f740 100644 --- a/main.js +++ b/main.js @@ -17,6 +17,7 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) { const appId = core.getInput("app-id"); const privateKey = core.getInput("private-key"); +const enterprise = core.getInput("enterprise"); const owner = core.getInput("owner"); const repositories = core .getInput("repositories") @@ -32,6 +33,7 @@ const permissions = getPermissionsFromInputs(process.env); export default main( appId, privateKey, + enterprise, owner, repositories, permissions, diff --git a/package.json b/package.json index 89a8e93..6a6de42 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "create-github-app-token", "private": true, "type": "module", - "version": "2.0.6", + "version": "2.0.7", "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=node20.0.0 --packages=bundle", diff --git a/tests/main-enterprise-installation-not-found.test.js b/tests/main-enterprise-installation-not-found.test.js new file mode 100644 index 0000000..c5a492d --- /dev/null +++ b/tests/main-enterprise-installation-not-found.test.js @@ -0,0 +1,40 @@ +import { test } from "./main.js"; + + +// Verify `main` handles when no enterprise installation is found. +await test((mockPool) => { + delete process.env.INPUT_OWNER; + delete process.env.INPUT_REPOSITORIES; + process.env.INPUT_ENTERPRISE = "test-enterprise"; + + + // Mock the /app/installations endpoint to return only non-enterprise installations + mockPool + .intercept({ + path: "/app/installations", + 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: "111111", + app_slug: "github-actions", + target_type: "Organization", + account: { login: "some-org" } + }, + { + id: "222222", + app_slug: "github-actions", + target_type: "User", + account: { login: "some-user" } + } + ], + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main-enterprise-mutual-exclusivity-both.test.js b/tests/main-enterprise-mutual-exclusivity-both.test.js new file mode 100644 index 0000000..f4b5e3b --- /dev/null +++ b/tests/main-enterprise-mutual-exclusivity-both.test.js @@ -0,0 +1,16 @@ +import { DEFAULT_ENV } from "./main.js"; + +// Verify `main` exits with an error when `enterprise` is used with both `owner` and `repositories` inputs. +try { + // Set up environment with enterprise, owner, and repositories all set + for (const [key, value] of Object.entries(DEFAULT_ENV)) { + process.env[key] = value; + } + process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env.INPUT_OWNER = "test-owner"; + process.env.INPUT_REPOSITORIES = "repo1,repo2"; + + await import("../main.js"); +} catch (error) { + console.error(error.message); +} diff --git a/tests/main-enterprise-mutual-exclusivity-owner.test.js b/tests/main-enterprise-mutual-exclusivity-owner.test.js new file mode 100644 index 0000000..59ec637 --- /dev/null +++ b/tests/main-enterprise-mutual-exclusivity-owner.test.js @@ -0,0 +1,15 @@ +import { DEFAULT_ENV } from "./main.js"; + +// Verify `main` exits with an error when `enterprise` is used with `owner` input. +try { + // Set up environment with enterprise and owner both set + for (const [key, value] of Object.entries(DEFAULT_ENV)) { + process.env[key] = value; + } + process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env.INPUT_OWNER = "test-owner"; + + await import("../main.js"); +} catch (error) { + console.error(error.message); +} diff --git a/tests/main-enterprise-mutual-exclusivity-repositories.test.js b/tests/main-enterprise-mutual-exclusivity-repositories.test.js new file mode 100644 index 0000000..893c7de --- /dev/null +++ b/tests/main-enterprise-mutual-exclusivity-repositories.test.js @@ -0,0 +1,15 @@ +import { DEFAULT_ENV } from "./main.js"; + +// Verify `main` exits with an error when `enterprise` is used with `repositories` input. +try { + // Set up environment with enterprise and repositories both set + for (const [key, value] of Object.entries(DEFAULT_ENV)) { + process.env[key] = value; + } + process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env.INPUT_REPOSITORIES = "repo1,repo2"; + + await import("../main.js"); +} catch (error) { + console.error(error.message); +} diff --git a/tests/main-enterprise-only-success.test.js b/tests/main-enterprise-only-success.test.js new file mode 100644 index 0000000..dae89bc --- /dev/null +++ b/tests/main-enterprise-only-success.test.js @@ -0,0 +1,34 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when only the `enterprise` input is set. +await test((mockPool) => { + process.env.INPUT_ENTERPRISE = "test-enterprise"; + delete process.env.INPUT_OWNER; + delete process.env.INPUT_REPOSITORIES; + + // Mock the /app/installations endpoint to return an enterprise installation + const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; + mockPool + .intercept({ + path: "/app/installations", + 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, + app_slug: mockAppSlug, + target_type: "Enterprise", + account: { login: "test-enterprise" } + } + ], + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main-enterprise-token-success.test.js b/tests/main-enterprise-token-success.test.js new file mode 100644 index 0000000..197348e --- /dev/null +++ b/tests/main-enterprise-token-success.test.js @@ -0,0 +1,34 @@ +import { test } from "./main.js"; + +// Verify `main` successfully generates enterprise token with basic functionality. +await test((mockPool) => { + process.env.INPUT_ENTERPRISE = "test-enterprise"; + delete process.env.INPUT_OWNER; + delete process.env.INPUT_REPOSITORIES; + + // Mock the /app/installations endpoint to return an enterprise installation + const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; + mockPool + .intercept({ + path: "/app/installations", + 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, + app_slug: mockAppSlug, + target_type: "Enterprise", + account: { login: "test-enterprise" } + } + ], + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/main-enterprise-token-with-permissions.test.js b/tests/main-enterprise-token-with-permissions.test.js new file mode 100644 index 0000000..ab2a160 --- /dev/null +++ b/tests/main-enterprise-token-with-permissions.test.js @@ -0,0 +1,36 @@ +import { test } from "./main.js"; + +// Verify `main` successfully generates enterprise token with specific permissions. +await test((mockPool) => { + process.env.INPUT_ENTERPRISE = "test-enterprise"; + delete process.env.INPUT_OWNER; + delete process.env.INPUT_REPOSITORIES; + process.env["INPUT_PERMISSION-ENTERPRISE-ORGANIZATIONS"] = "read"; + process.env["INPUT_PERMISSION-ENTERPRISE-PEOPLE"] = "write"; + + // Mock the /app/installations endpoint to return an enterprise installation + const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; + mockPool + .intercept({ + path: "/app/installations", + 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, + app_slug: mockAppSlug, + target_type: "Enterprise", + account: { login: "test-enterprise" } + } + ], + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index e419536..dfd7473 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -39,6 +39,99 @@ Generated by [AVA](https://avajs.dev). POST /api/v3/app/installations/123456/access_tokens␊ {"repositories":["create-github-app-token"]}` +## main-enterprise-only-success.test.js + +> stderr + + '' + +> stdout + + `Creating enterprise installation token for enterprise "test-enterprise".␊ + ### Found enterprise installation: {␊ + "id": "123456",␊ + "app_slug": "github-actions",␊ + "target_type": "Enterprise",␊ + "account": {␊ + "login": "test-enterprise"␊ + }␊ + }␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=installation-id::123456␊ + ␊ + ::set-output name=app-slug::github-actions␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ + --- REQUESTS ---␊ + GET /app/installations␊ + POST /app/installations/123456/access_tokens␊ + null` + +## main-enterprise-token-success.test.js + +> stderr + + '' + +> stdout + + `Creating enterprise installation token for enterprise "test-enterprise".␊ + ### Found enterprise installation: {␊ + "id": "123456",␊ + "app_slug": "github-actions",␊ + "target_type": "Enterprise",␊ + "account": {␊ + "login": "test-enterprise"␊ + }␊ + }␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=installation-id::123456␊ + ␊ + ::set-output name=app-slug::github-actions␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ + --- REQUESTS ---␊ + GET /app/installations␊ + POST /app/installations/123456/access_tokens␊ + null` + +## main-enterprise-token-with-permissions.test.js + +> stderr + + '' + +> stdout + + `Creating enterprise installation token for enterprise "test-enterprise".␊ + ### Found enterprise installation: {␊ + "id": "123456",␊ + "app_slug": "github-actions",␊ + "target_type": "Enterprise",␊ + "account": {␊ + "login": "test-enterprise"␊ + }␊ + }␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=installation-id::123456␊ + ␊ + ::set-output name=app-slug::github-actions␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=expiresAt::2016-07-11T22:14:10Z␊ + --- REQUESTS ---␊ + GET /app/installations␊ + POST /app/installations/123456/access_tokens␊ + {"permissions":{"enterprise_organizations":"read","enterprise_people":"write"}}` + ## main-missing-owner.test.js > stderr diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index 773f4b1..edde164 100644 Binary files a/tests/snapshots/index.js.snap and b/tests/snapshots/index.js.snap differ