From 4f9eeddfa73c23e60664e388323fa48ac177f136 Mon Sep 17 00:00:00 2001 From: Parker Brown <17183625+parkerbxyz@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:14:05 -0700 Subject: [PATCH] Use direct enterprise installation route Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/main.js | 42 +++++++++---------- tests/index.js.snapshot | 16 +++---- ...-enterprise-installation-not-found.test.js | 25 +++-------- tests/main-enterprise-only-success.test.js | 16 +++---- tests/main-enterprise-token-success.test.js | 16 +++---- ...-enterprise-token-with-permissions.test.js | 16 +++---- 6 files changed, 51 insertions(+), 80 deletions(-) diff --git a/lib/main.js b/lib/main.js index 5a2c4c5..85d276e 100644 --- a/lib/main.js +++ b/lib/main.js @@ -208,37 +208,35 @@ async function getTokenFromRepository( return { authentication, installationId, appSlug }; } -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, - // 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, - }, - }); +async function getTokenFromEnterprise(request, auth, enterpriseSlug, permissions) { + let response; + try { + response = await request("GET /enterprises/{enterprise}/installation", { + enterprise: enterpriseSlug, + request: { + hook: auth.hook, + }, + }); + } catch (error) { + /* c8 ignore next 8 */ + if (error.status === 404) { + throw new Error( + `No enterprise installation found matching the name ${enterpriseSlug}.` + ); + } - // Find the enterprise installation - const enterpriseInstallation = response.data.find( - installation => installation.target_type === "Enterprise" && - installation.account?.slug === enterpriseSlug - ); - - /* c8 ignore next 3 */ - if (!enterpriseInstallation) { - 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(', ')}`); + throw error; } // Get token for the enterprise installation const authentication = await auth({ type: "installation", - installationId: enterpriseInstallation.id, + installationId: response.data.id, permissions, }); - const installationId = enterpriseInstallation.id; - const appSlug = enterpriseInstallation["app_slug"]; + const installationId = response.data.id; + const appSlug = response.data["app_slug"]; return { authentication, installationId, appSlug }; } diff --git a/tests/index.js.snapshot b/tests/index.js.snapshot index c455214..0681c49 100644 --- a/tests/index.js.snapshot +++ b/tests/index.js.snapshot @@ -18,20 +18,20 @@ POST /api/v3/app/installations/123456/access_tokens `; exports[`main-enterprise-installation-not-found.test.js > stderr 1`] = ` -Error: No enterprise installation found matching the name test-enterprise. Available installations: Organization:some-org, User:some-user - at getTokenFromEnterprise (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:230:11) +Error: No enterprise installation found matching the name test-enterprise. + at getTokenFromEnterprise (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:223:13)  at process.processTicksAndRejections (node:internal/process/task_queues:104:5) at async pRetry (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/node_modules/p-retry/index.js:197:19) at async main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:95:52) at async test (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main.js:111:3) - at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-installation-not-found.test.js:5:1 + at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-installation-not-found.test.js:4:1 `; exports[`main-enterprise-installation-not-found.test.js > stdout 1`] = ` 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 +Failed to create token for enterprise "test-enterprise" (attempt 1): No enterprise installation found matching the name test-enterprise. --- REQUESTS --- -GET /app/installations +GET /enterprises/test-enterprise/installation `; exports[`main-enterprise-mutual-exclusivity-both.test.js > stderr 1`] = ` @@ -76,7 +76,7 @@ Creating enterprise installation token for enterprise "test-enterprise". ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::save-state name=expiresAt::2016-07-11T22:14:10Z --- REQUESTS --- -GET /app/installations +GET /enterprises/test-enterprise/installation POST /app/installations/123456/access_tokens null `; @@ -93,7 +93,7 @@ Creating enterprise installation token for enterprise "test-enterprise". ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::save-state name=expiresAt::2016-07-11T22:14:10Z --- REQUESTS --- -GET /app/installations +GET /enterprises/test-enterprise/installation POST /app/installations/123456/access_tokens null `; @@ -110,7 +110,7 @@ Creating enterprise installation token for enterprise "test-enterprise". ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a ::save-state name=expiresAt::2016-07-11T22:14:10Z --- REQUESTS --- -GET /app/installations +GET /enterprises/test-enterprise/installation POST /app/installations/123456/access_tokens {"permissions":{"enterprise_organizations":"read","enterprise_people":"write"}} `; diff --git a/tests/main-enterprise-installation-not-found.test.js b/tests/main-enterprise-installation-not-found.test.js index cc5be81..0c36a26 100644 --- a/tests/main-enterprise-installation-not-found.test.js +++ b/tests/main-enterprise-installation-not-found.test.js @@ -1,17 +1,15 @@ 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; + delete process.env.INPUT_REPOSITORIES; process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise"; - - // Mock the /app/installations endpoint to return only non-enterprise installations + // Mock the enterprise installation endpoint to return no matching installation mockPool .intercept({ - path: "/app/installations", + path: "/enterprises/test-enterprise/installation", method: "GET", headers: { accept: "application/vnd.github.v3+json", @@ -20,21 +18,8 @@ await test((mockPool) => { }, }) .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" } - } - ], + 404, + { message: "Not Found" }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-enterprise-only-success.test.js b/tests/main-enterprise-only-success.test.js index 68fcce7..7f696ef 100644 --- a/tests/main-enterprise-only-success.test.js +++ b/tests/main-enterprise-only-success.test.js @@ -6,12 +6,12 @@ await test((mockPool) => { delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; - // Mock the /app/installations endpoint to return an enterprise installation + // Mock the enterprise installation endpoint const mockInstallationId = "123456"; const mockAppSlug = "github-actions"; mockPool .intercept({ - path: "/app/installations", + path: "/enterprises/test-enterprise/installation", method: "GET", headers: { accept: "application/vnd.github.v3+json", @@ -21,14 +21,10 @@ await test((mockPool) => { }) .reply( 200, - [ - { - id: mockInstallationId, - app_slug: mockAppSlug, - target_type: "Enterprise", - account: { login: "test-enterprise", slug: "test-enterprise" } - } - ], + { + id: mockInstallationId, + app_slug: mockAppSlug, + }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-enterprise-token-success.test.js b/tests/main-enterprise-token-success.test.js index 69e2b5c..2128b7a 100644 --- a/tests/main-enterprise-token-success.test.js +++ b/tests/main-enterprise-token-success.test.js @@ -6,12 +6,12 @@ await test((mockPool) => { delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; - // Mock the /app/installations endpoint to return an enterprise installation + // Mock the enterprise installation endpoint const mockInstallationId = "123456"; const mockAppSlug = "github-actions"; mockPool .intercept({ - path: "/app/installations", + path: "/enterprises/test-enterprise/installation", method: "GET", headers: { accept: "application/vnd.github.v3+json", @@ -21,14 +21,10 @@ await test((mockPool) => { }) .reply( 200, - [ - { - id: mockInstallationId, - app_slug: mockAppSlug, - target_type: "Enterprise", - account: { login: "test-enterprise", slug: "test-enterprise" } - } - ], + { + id: mockInstallationId, + app_slug: mockAppSlug, + }, { 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 3ffae88..9f9c0cc 100644 --- a/tests/main-enterprise-token-with-permissions.test.js +++ b/tests/main-enterprise-token-with-permissions.test.js @@ -8,12 +8,12 @@ await test((mockPool) => { process.env["INPUT_PERMISSION-ENTERPRISE-ORGANIZATIONS"] = "read"; process.env["INPUT_PERMISSION-ENTERPRISE-PEOPLE"] = "write"; - // Mock the /app/installations endpoint to return an enterprise installation + // Mock the enterprise installation endpoint const mockInstallationId = "123456"; const mockAppSlug = "github-actions"; mockPool .intercept({ - path: "/app/installations", + path: "/enterprises/test-enterprise/installation", method: "GET", headers: { accept: "application/vnd.github.v3+json", @@ -23,14 +23,10 @@ await test((mockPool) => { }) .reply( 200, - [ - { - id: mockInstallationId, - app_slug: mockAppSlug, - target_type: "Enterprise", - account: { login: "test-enterprise", slug: "test-enterprise" } - } - ], + { + id: mockInstallationId, + app_slug: mockAppSlug, + }, { headers: { "content-type": "application/json" } } ); });