From 6cf7b5f22acbf06cc6851840d0f030709a312f26 Mon Sep 17 00:00:00 2001 From: Stefan Petrushevski Date: Thu, 28 Aug 2025 10:23:07 +0200 Subject: [PATCH] update tests with enterprise-slug --- .gitignore | 1 + dist/main.cjs | 32 ++++--- lib/main.js | 22 ++--- main.js | 9 +- package-lock.json | 4 +- ...-enterprise-installation-not-found.test.js | 2 +- ...enterprise-mutual-exclusivity-both.test.js | 6 +- ...nterprise-mutual-exclusivity-owner.test.js | 6 +- ...se-mutual-exclusivity-repositories.test.js | 6 +- tests/main-enterprise-only-success.test.js | 6 +- tests/main-enterprise-token-success.test.js | 4 +- ...-enterprise-token-with-permissions.test.js | 4 +- tests/snapshots/index.js.md | 88 +++++++++++++----- tests/snapshots/index.js.snap | Bin 1589 -> 2003 bytes 14 files changed, 118 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index b443287..d5a9b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env coverage node_modules/ +.DS_Store \ No newline at end of file diff --git a/dist/main.cjs b/dist/main.cjs index 2931c58..c00f94b 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -42523,13 +42523,13 @@ async function pRetry(input, options) { } // lib/main.js -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"); +async function main(appId2, privateKey2, enterpriseSlug2, owner2, repositories2, permissions2, core3, createAppAuth2, request2, skipTokenRevoke2) { + if (enterpriseSlug2 && (owner2 || repositories2.length > 0)) { + throw new Error("Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs"); } let parsedOwner = ""; let parsedRepositoryNames = []; - if (!enterprise2) { + if (!enterpriseSlug2) { if (!owner2 && repositories2.length === 0) { const [owner3, repo] = String(process.env.GITHUB_REPOSITORY).split("/"); parsedOwner = owner3; @@ -42557,12 +42557,12 @@ async function main(appId2, privateKey2, enterprise2, owner2, repositories2, per parsedRepositoryNames = repositories2; core3.info( `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: - ${repositories2.map((repo) => ` + ${repositories2.map((repo) => ` - ${parsedOwner}/${repo}`).join("")}` ); } } else { - core3.info(`Creating enterprise installation token for enterprise "${enterprise2}".`); + core3.info(`Creating enterprise installation token for enterprise "${enterpriseSlug2}".`); } const auth5 = createAppAuth2({ appId: appId2, @@ -42570,14 +42570,14 @@ async function main(appId2, privateKey2, enterprise2, owner2, repositories2, per request: request2 }); let authentication, installationId, appSlug; - if (enterprise2) { + if (enterpriseSlug2) { ({ authentication, installationId, appSlug } = await pRetry( - () => getTokenFromEnterprise(request2, auth5, enterprise2, permissions2), + () => getTokenFromEnterprise(request2, auth5, enterpriseSlug2, permissions2), { shouldRetry: (error) => error.status >= 500, onFailedAttempt: (error) => { core3.info( - `Failed to create token for enterprise "${enterprise2}" (attempt ${error.attemptNumber}): ${error.message}` + `Failed to create token for enterprise "${enterpriseSlug2}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3 @@ -42660,17 +42660,17 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; } -async function getTokenFromEnterprise(request2, auth5, enterprise2, permissions2) { +async function getTokenFromEnterprise(request2, auth5, enterpriseSlug2, 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 + (installation) => installation.target_type === "Enterprise" && installation.account?.slug === enterpriseSlug2 ); 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(", ")}`); + throw new Error(`No enterprise installation found matching the name ${enterpriseSlug2}. Available installations: ${response.data.map((i) => `${i.target_type}:${i.account?.login || "N/A"}`).join(", ")}`); } const authentication = await auth5({ type: "installation", @@ -42718,7 +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 enterpriseSlug = import_core2.default.getInput("enterprise-slug"); 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"); @@ -42726,7 +42726,7 @@ var permissions = getPermissionsFromInputs(process.env); var main_default = main( appId, privateKey, - enterprise, + enterpriseSlug, owner, repositories, permissions, @@ -42736,7 +42736,9 @@ var main_default = main( skipTokenRevoke ).catch((error) => { console.error(error); - import_core2.default.setFailed(error.message); + if (process.env.GITHUB_OUTPUT !== void 0) { + import_core2.default.setFailed(error.message); + } }); /*! Bundled license information: diff --git a/lib/main.js b/lib/main.js index 4b3d352..8256f81 100644 --- a/lib/main.js +++ b/lib/main.js @@ -34,8 +34,8 @@ export async function main( let parsedOwner = ""; let parsedRepositoryNames = []; - // Skip owner/repository parsing if enterprise is set - if (!enterprise) { + // Skip owner/repository parsing if enterprise-slug is set + if (!enterpriseSlug) { // 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("/"); @@ -75,11 +75,11 @@ export async function main( core.info( `Inputs 'owner' and 'repositories' are set. Creating token for the following repositories: - ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` + ${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}` ); } } else { - core.info(`Creating enterprise installation token for enterprise "${enterprise}".`); + core.info(`Creating enterprise installation token for enterprise "${enterpriseSlug}".`); } const auth = createAppAuth({ @@ -90,15 +90,15 @@ export async function main( let authentication, installationId, appSlug; - // If enterprise is set, get installation ID from the enterprise - if (enterprise) { + // If enterprise-slug is set, get installation ID from the enterprise + if (enterpriseSlug) { ({ authentication, installationId, appSlug } = await pRetry( - () => getTokenFromEnterprise(request, auth, enterprise, permissions), + () => getTokenFromEnterprise(request, auth, enterpriseSlug, permissions), { shouldRetry: (error) => error.status >= 500, onFailedAttempt: (error) => { core.info( - `Failed to create token for enterprise "${enterprise}" (attempt ${error.attemptNumber}): ${error.message}` + `Failed to create token for enterprise "${enterpriseSlug}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3, @@ -208,7 +208,7 @@ async function getTokenFromRepository( return { authentication, installationId, appSlug }; } -async function getTokenFromEnterprise(request, auth, enterprise, permissions) { +async function getTokenFromEnterprise(request, auth, enterpriseSlug, 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, @@ -222,11 +222,11 @@ async function getTokenFromEnterprise(request, auth, enterprise, permissions) { // Find the enterprise installation const enterpriseInstallation = response.data.find( installation => installation.target_type === "Enterprise" && - installation.account?.slug === enterprise + installation.account?.slug === enterpriseSlug ); 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(', ')}`); + throw new Error(`No enterprise installation found matching the name ${enterpriseSlug}. Available installations: ${response.data.map(i => `${i.target_type}:${i.account?.login || 'N/A'}`).join(', ')}`); } // Get token for the enterprise installation diff --git a/main.js b/main.js index a53f740..8850149 100644 --- a/main.js +++ b/main.js @@ -17,7 +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 enterpriseSlug = core.getInput("enterprise-slug"); const owner = core.getInput("owner"); const repositories = core .getInput("repositories") @@ -33,7 +33,7 @@ const permissions = getPermissionsFromInputs(process.env); export default main( appId, privateKey, - enterprise, + enterpriseSlug, owner, repositories, permissions, @@ -44,5 +44,8 @@ export default main( ).catch((error) => { /* c8 ignore next 3 */ console.error(error); - core.setFailed(error.message); + // Don't set failed in test mode (when GITHUB_OUTPUT is undefined) + if (process.env.GITHUB_OUTPUT !== undefined) { + core.setFailed(error.message); + } }); diff --git a/package-lock.json b/package-lock.json index d8d12f8..d65d35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-github-app-token", - "version": "2.0.6", + "version": "2.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-github-app-token", - "version": "2.0.6", + "version": "2.0.7", "license": "MIT", "dependencies": { "@actions/core": "^1.11.1", diff --git a/tests/main-enterprise-installation-not-found.test.js b/tests/main-enterprise-installation-not-found.test.js index c5a492d..cc5be81 100644 --- a/tests/main-enterprise-installation-not-found.test.js +++ b/tests/main-enterprise-installation-not-found.test.js @@ -5,7 +5,7 @@ import { test } from "./main.js"; await test((mockPool) => { delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; // Mock the /app/installations endpoint to return only non-enterprise installations diff --git a/tests/main-enterprise-mutual-exclusivity-both.test.js b/tests/main-enterprise-mutual-exclusivity-both.test.js index f4b5e3b..521f6e5 100644 --- a/tests/main-enterprise-mutual-exclusivity-both.test.js +++ b/tests/main-enterprise-mutual-exclusivity-both.test.js @@ -1,12 +1,12 @@ import { DEFAULT_ENV } from "./main.js"; -// Verify `main` exits with an error when `enterprise` is used with both `owner` and `repositories` inputs. +// Verify `main` exits with an error when `enterprise-slug` is used with both `owner` and `repositories` inputs. try { - // Set up environment with enterprise, owner, and repositories all set + // Set up environment with enterprise-slug, 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_ENTERPRISE-SLUG"] = "test-enterprise"; process.env.INPUT_OWNER = "test-owner"; process.env.INPUT_REPOSITORIES = "repo1,repo2"; diff --git a/tests/main-enterprise-mutual-exclusivity-owner.test.js b/tests/main-enterprise-mutual-exclusivity-owner.test.js index 59ec637..eaa36da 100644 --- a/tests/main-enterprise-mutual-exclusivity-owner.test.js +++ b/tests/main-enterprise-mutual-exclusivity-owner.test.js @@ -1,12 +1,12 @@ import { DEFAULT_ENV } from "./main.js"; -// Verify `main` exits with an error when `enterprise` is used with `owner` input. +// Verify `main` exits with an error when `enterprise-slug` is used with `owner` input. try { - // Set up environment with enterprise and owner both set + // Set up environment with enterprise-slug and owner set for (const [key, value] of Object.entries(DEFAULT_ENV)) { process.env[key] = value; } - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; process.env.INPUT_OWNER = "test-owner"; await import("../main.js"); diff --git a/tests/main-enterprise-mutual-exclusivity-repositories.test.js b/tests/main-enterprise-mutual-exclusivity-repositories.test.js index 893c7de..c69f0f0 100644 --- a/tests/main-enterprise-mutual-exclusivity-repositories.test.js +++ b/tests/main-enterprise-mutual-exclusivity-repositories.test.js @@ -1,12 +1,12 @@ import { DEFAULT_ENV } from "./main.js"; -// Verify `main` exits with an error when `enterprise` is used with `repositories` input. +// Verify `main` exits with an error when `enterprise-slug` is used with `repositories` input. try { - // Set up environment with enterprise and repositories both set + // Set up environment with enterprise-slug and repositories set for (const [key, value] of Object.entries(DEFAULT_ENV)) { process.env[key] = value; } - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; process.env.INPUT_REPOSITORIES = "repo1,repo2"; await import("../main.js"); diff --git a/tests/main-enterprise-only-success.test.js b/tests/main-enterprise-only-success.test.js index dae89bc..68fcce7 100644 --- a/tests/main-enterprise-only-success.test.js +++ b/tests/main-enterprise-only-success.test.js @@ -1,8 +1,8 @@ import { test } from "./main.js"; -// Verify `main` successfully obtains a token when only the `enterprise` input is set. +// Verify `main` successfully obtains a token when only the `enterprise-slug` input is set. await test((mockPool) => { - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; @@ -26,7 +26,7 @@ await test((mockPool) => { id: mockInstallationId, app_slug: mockAppSlug, target_type: "Enterprise", - account: { login: "test-enterprise" } + account: { login: "test-enterprise", slug: "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 index 197348e..69e2b5c 100644 --- a/tests/main-enterprise-token-success.test.js +++ b/tests/main-enterprise-token-success.test.js @@ -2,7 +2,7 @@ import { test } from "./main.js"; // Verify `main` successfully generates enterprise token with basic functionality. await test((mockPool) => { - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; @@ -26,7 +26,7 @@ await test((mockPool) => { id: mockInstallationId, app_slug: mockAppSlug, target_type: "Enterprise", - account: { login: "test-enterprise" } + account: { login: "test-enterprise", slug: "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 index ab2a160..3ffae88 100644 --- a/tests/main-enterprise-token-with-permissions.test.js +++ b/tests/main-enterprise-token-with-permissions.test.js @@ -2,7 +2,7 @@ import { test } from "./main.js"; // Verify `main` successfully generates enterprise token with specific permissions. await test((mockPool) => { - process.env.INPUT_ENTERPRISE = "test-enterprise"; + process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; process.env["INPUT_PERMISSION-ENTERPRISE-ORGANIZATIONS"] = "read"; @@ -28,7 +28,7 @@ await test((mockPool) => { id: mockInstallationId, app_slug: mockAppSlug, target_type: "Enterprise", - account: { login: "test-enterprise" } + account: { login: "test-enterprise", slug: "test-enterprise" } } ], { headers: { "content-type": "application/json" } } diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index dfd7473..b991609 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -39,6 +39,70 @@ Generated by [AVA](https://avajs.dev). POST /api/v3/app/installations/123456/access_tokens␊ {"repositories":["create-github-app-token"]}` +## main-enterprise-installation-not-found.test.js + +> stderr + + `Error: No enterprise installation found matching the name test-enterprise. Available installations: Organization:some-org, User:some-user␊ + at getTokenFromEnterprise (file:///Users/s/dev/create-github-app-token/lib/main.js:229:11)␊ + at process.processTicksAndRejections (node:internal/process/task_queues:105:5)␊ + at async RetryOperation._fn (file:///Users/s/dev/create-github-app-token/node_modules/p-retry/index.js:55:20) {␊ + attemptNumber: 1,␊ + retriesLeft: 3␊ + }` + +> stdout + + `Creating enterprise installation token for enterprise "test-enterprise".␊ + Failed to create token for enterprise "test-enterprise" (attempt 1): No enterprise installation found matching the name test-enterprise. Available installations: Organization:some-org, User:some-user␊ + --- REQUESTS ---␊ + GET /app/installations` + +## main-enterprise-mutual-exclusivity-both.test.js + +> stderr + + `Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊ + at main (file:///Users/s/dev/create-github-app-token/lib/main.js:31:11)␊ + at file:///Users/s/dev/create-github-app-token/main.js:33:16␊ + at ModuleJob.run (node:internal/modules/esm/module_job:274:25)␊ + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)␊ + at async file:///Users/s/dev/create-github-app-token/tests/main-enterprise-mutual-exclusivity-both.test.js:13:3` + +> stdout + + '' + +## main-enterprise-mutual-exclusivity-owner.test.js + +> stderr + + `Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊ + at main (file:///Users/s/dev/create-github-app-token/lib/main.js:31:11)␊ + at file:///Users/s/dev/create-github-app-token/main.js:33:16␊ + at ModuleJob.run (node:internal/modules/esm/module_job:274:25)␊ + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)␊ + at async file:///Users/s/dev/create-github-app-token/tests/main-enterprise-mutual-exclusivity-owner.test.js:12:3` + +> stdout + + '' + +## main-enterprise-mutual-exclusivity-repositories.test.js + +> stderr + + `Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊ + at main (file:///Users/s/dev/create-github-app-token/lib/main.js:31:11)␊ + at file:///Users/s/dev/create-github-app-token/main.js:33:16␊ + at ModuleJob.run (node:internal/modules/esm/module_job:274:25)␊ + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)␊ + at async file:///Users/s/dev/create-github-app-token/tests/main-enterprise-mutual-exclusivity-repositories.test.js:12:3` + +> stdout + + '' + ## main-enterprise-only-success.test.js > stderr @@ -48,14 +112,6 @@ Generated by [AVA](https://avajs.dev). > 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␊ @@ -79,14 +135,6 @@ Generated by [AVA](https://avajs.dev). > 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␊ @@ -110,14 +158,6 @@ Generated by [AVA](https://avajs.dev). > 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␊ diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index edde1648817a03f4f23319d5c9aa3aedc6eb310c..87ac80eaa0ee9482e7a53f48a4b593fc9d34e0af 100644 GIT binary patch literal 2003 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 n9&@E&;ALVo?