diff --git a/README.md b/README.md index 3d43472..2790c6d 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,14 @@ jobs: GitHub App installation access token. +### `installation-id` + +GitHub App installation ID. + +### `app-slug` + +GitHub App slug. + ## How it works The action creates an installation access token using [the `POST /app/installations/{installation_id}/access_tokens` endpoint](https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app). By default, diff --git a/action.yml b/action.yml index e150b71..1d2909e 100644 --- a/action.yml +++ b/action.yml @@ -40,6 +40,10 @@ inputs: outputs: token: description: "GitHub installation access token" + installation-id: + description: "GitHub App installation ID" + app-slug: + description: "GitHub App slug" runs: using: "node20" main: "dist/main.cjs" diff --git a/dist/main.cjs b/dist/main.cjs index 0f42fa1..a8f5b87 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -29923,28 +29923,30 @@ async function main(appId2, privateKey2, owner2, repositories2, core3, createApp privateKey: privateKey2, request: request2 }); - let authentication; + let authentication, installationId, appSlug; if (parsedRepositoryNames) { - authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames), { + ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames), { onFailedAttempt: (error) => { core3.info( `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3 - }); + })); } else { - authentication = await pRetry(() => getTokenFromOwner(request2, auth, parsedOwner), { + ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromOwner(request2, auth, parsedOwner), { onFailedAttempt: (error) => { core3.info( `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3 - }); + })); } core3.setSecret(authentication.token); core3.setOutput("token", authentication.token); + core3.setOutput("installation-id", installationId); + core3.setOutput("app-slug", appSlug); if (!skipTokenRevoke2) { core3.saveState("token", authentication.token); core3.setOutput("expiresAt", authentication.expiresAt); @@ -29970,7 +29972,9 @@ async function getTokenFromOwner(request2, auth, parsedOwner) { type: "installation", installationId: response.data.id }); - return authentication; + const installationId = response.data.id; + const appSlug = response.data["app_slug"]; + return { authentication, installationId, appSlug }; } async function getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames) { const response = await request2("GET /repos/{owner}/{repo}/installation", { @@ -29985,7 +29989,9 @@ async function getTokenFromRepository(request2, auth, parsedOwner, parsedReposit installationId: response.data.id, repositoryNames: parsedRepositoryNames.split(",") }); - return authentication; + const installationId = response.data.id; + const appSlug = response.data["app_slug"]; + return { authentication, installationId, appSlug }; } // lib/request.js diff --git a/lib/main.js b/lib/main.js index b97329d..d685277 100644 --- a/lib/main.js +++ b/lib/main.js @@ -70,35 +70,36 @@ export async function main( request, }); - let authentication; + let authentication, installationId, appSlug; // If at least one repository is set, get installation ID from that repository if (parsedRepositoryNames) { - authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), { + ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), { onFailedAttempt: (error) => { core.info( `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3, - }); - + })); } else { // Otherwise get the installation for the owner, which can either be an organization or a user account - authentication = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), { + ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), { onFailedAttempt: (error) => { core.info( `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` ); }, retries: 3, - }); + })); } // Register the token with the runner as a secret to ensure it is masked in logs core.setSecret(authentication.token); core.setOutput("token", authentication.token); + core.setOutput("installation-id", installationId); + core.setOutput("app-slug", appSlug); // Make token accessible to post function (so we can invalidate it) if (!skipTokenRevoke) { @@ -132,7 +133,11 @@ async function getTokenFromOwner(request, auth, parsedOwner) { type: "installation", installationId: response.data.id, }); - return authentication; + + const installationId = response.data.id; + const appSlug = response.data['app_slug']; + + return { authentication, installationId, appSlug }; } async function getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames) { @@ -152,5 +157,8 @@ async function getTokenFromRepository(request, auth, parsedOwner, parsedReposito repositoryNames: parsedRepositoryNames.split(","), }); - return authentication; -} \ No newline at end of file + const installationId = response.data.id; + const appSlug = response.data['app_slug']; + + return { authentication, installationId, appSlug }; + } \ No newline at end of file diff --git a/tests/main-repo-skew.js b/tests/main-repo-skew.js index a3554ad..e35a531 100644 --- a/tests/main-repo-skew.js +++ b/tests/main-repo-skew.js @@ -9,6 +9,7 @@ await test((mockPool) => { const owner = process.env.INPUT_OWNER const repo = process.env.INPUT_REPOSITORIES const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; install({ now: 0, toFake: ["Date"] }); @@ -44,7 +45,8 @@ await test((mockPool) => { return { statusCode: 200, data: { - id: mockInstallationId + id: mockInstallationId, + "app_slug": mockAppSlug }, responseOptions: { headers: { diff --git a/tests/main-token-get-owner-set-repo-fail-response.test.js b/tests/main-token-get-owner-set-repo-fail-response.test.js index 14c5811..f97cf26 100644 --- a/tests/main-token-get-owner-set-repo-fail-response.test.js +++ b/tests/main-token-get-owner-set-repo-fail-response.test.js @@ -7,6 +7,7 @@ await test((mockPool) => { const owner = process.env.INPUT_OWNER; const repo = process.env.INPUT_REPOSITORIES; const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; mockPool .intercept({ @@ -32,7 +33,7 @@ await test((mockPool) => { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-token-get-owner-set-to-org-repo-unset.test.js b/tests/main-token-get-owner-set-to-org-repo-unset.test.js index 1fa0e1f..87245f7 100644 --- a/tests/main-token-get-owner-set-to-org-repo-unset.test.js +++ b/tests/main-token-get-owner-set-to-org-repo-unset.test.js @@ -5,8 +5,9 @@ await test((mockPool) => { process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; delete process.env.INPUT_REPOSITORIES; - // Mock installation id request + // Mock installation id and app slug request const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; mockPool .intercept({ path: `/orgs/${process.env.INPUT_OWNER}/installation`, @@ -19,7 +20,7 @@ await test((mockPool) => { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-token-get-owner-set-to-user-fail-response.test.js b/tests/main-token-get-owner-set-to-user-fail-response.test.js index e9fba9b..318b8dc 100644 --- a/tests/main-token-get-owner-set-to-user-fail-response.test.js +++ b/tests/main-token-get-owner-set-to-user-fail-response.test.js @@ -5,8 +5,9 @@ await test((mockPool) => { process.env.INPUT_OWNER = "smockle"; delete process.env.INPUT_REPOSITORIES; - // Mock installation id request + // Mock installation ID and app slug request const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; mockPool .intercept({ path: `/orgs/${process.env.INPUT_OWNER}/installation`, @@ -30,7 +31,7 @@ await test((mockPool) => { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-token-get-owner-set-to-user-repo-unset.test.js b/tests/main-token-get-owner-set-to-user-repo-unset.test.js index bbb802c..a73c3d7 100644 --- a/tests/main-token-get-owner-set-to-user-repo-unset.test.js +++ b/tests/main-token-get-owner-set-to-user-repo-unset.test.js @@ -5,8 +5,9 @@ await test((mockPool) => { process.env.INPUT_OWNER = "smockle"; delete process.env.INPUT_REPOSITORIES; - // Mock installation id request + // Mock installation ID and app slug request const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; mockPool .intercept({ path: `/orgs/${process.env.INPUT_OWNER}/installation`, @@ -30,7 +31,7 @@ await test((mockPool) => { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main-token-get-owner-unset-repo-unset.test.js b/tests/main-token-get-owner-unset-repo-unset.test.js index ccc20dd..e284aae 100644 --- a/tests/main-token-get-owner-unset-repo-unset.test.js +++ b/tests/main-token-get-owner-unset-repo-unset.test.js @@ -5,8 +5,9 @@ await test((mockPool) => { delete process.env.INPUT_OWNER; delete process.env.INPUT_REPOSITORIES; - // Mock installation id request + // Mock installation ID and app slug request const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; mockPool .intercept({ path: `/repos/${process.env.GITHUB_REPOSITORY}/installation`, @@ -19,7 +20,7 @@ await test((mockPool) => { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); }); diff --git a/tests/main.js b/tests/main.js index eb755b7..3e52f69 100644 --- a/tests/main.js +++ b/tests/main.js @@ -54,8 +54,9 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) { // Calling `auth({ type: "app" })` to obtain a JWT doesn’t make network requests, so no need to intercept. - // Mock installation id request + // Mock installation ID and app slug request const mockInstallationId = "123456"; + const mockAppSlug = "github-actions"; const owner = env.INPUT_OWNER ?? env.GITHUB_REPOSITORY_OWNER; const repo = encodeURIComponent( (env.INPUT_REPOSITORIES ?? env.GITHUB_REPOSITORY).split(",")[0] @@ -72,7 +73,7 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) { }) .reply( 200, - { id: mockInstallationId }, + { id: mockInstallationId, "app_slug": mockAppSlug }, { headers: { "content-type": "application/json" } } ); diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index b42248a..21918c0 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -28,6 +28,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -85,6 +89,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -101,6 +109,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -117,6 +129,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -133,6 +149,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -150,6 +170,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -166,6 +190,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -182,6 +210,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` @@ -198,6 +230,10 @@ Generated by [AVA](https://avajs.dev). ::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␊ ␊ ::set-output name=expiresAt::2016-07-11T22:14:10Z` diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index 46c364e..53720f3 100644 Binary files a/tests/snapshots/index.js.snap and b/tests/snapshots/index.js.snap differ