From 55b8c24e8dc0675d17bf34be98016c9a258907c3 Mon Sep 17 00:00:00 2001 From: Stefan Petrushevski Date: Tue, 8 Jul 2025 17:05:18 +0200 Subject: [PATCH] tests; update README --- README.md | 29 ++++++ ...-enterprise-installation-not-found.test.js | 39 ++++++++ ...enterprise-mutual-exclusivity-both.test.js | 16 +++ ...nterprise-mutual-exclusivity-owner.test.js | 15 +++ ...se-mutual-exclusivity-repositories.test.js | 15 +++ tests/main-enterprise-only-success.test.js | 34 +++++++ tests/main-enterprise-token-success.test.js | 34 +++++++ ...-enterprise-token-with-permissions.test.js | 36 +++++++ tests/snapshots/index.js.md | 93 ++++++++++++++++++ tests/snapshots/index.js.snap | Bin 1388 -> 1589 bytes 10 files changed, 311 insertions(+) create mode 100644 tests/main-enterprise-installation-not-found.test.js create mode 100644 tests/main-enterprise-mutual-exclusivity-both.test.js create mode 100644 tests/main-enterprise-mutual-exclusivity-owner.test.js create mode 100644 tests/main-enterprise-mutual-exclusivity-repositories.test.js create mode 100644 tests/main-enterprise-only-success.test.js create mode 100644 tests/main-enterprise-token-success.test.js create mode 100644 tests/main-enterprise-token-with-permissions.test.js 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/tests/main-enterprise-installation-not-found.test.js b/tests/main-enterprise-installation-not-found.test.js new file mode 100644 index 0000000..ca27b31 --- /dev/null +++ b/tests/main-enterprise-installation-not-found.test.js @@ -0,0 +1,39 @@ +import { test } from "./main.js"; +delete process.env.INPUT_OWNER; +delete process.env.INPUT_REPOSITORIES; + +// Verify `main` handles when no enterprise installation is found. +await test((mockPool) => { + 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 773f4b18b4e1aa064fd7944d242c0276c527e30b..edde1648817a03f4f23319d5c9aa3aedc6eb310c 100644 GIT binary patch literal 1589 zcmV-52Fm$CRzVP6nIDS?00000000B+n%!<2MHI)=AcV9C3GNUe7)^_ug2#@XkdIYCv{jnsf~F*? zNLy9bc=y;Iyq;NRX6>3NLPFvii5u>a+Be_{cmp1Q2SDNhxZ?uNtUq>B+ex!YTea(} z#GX0l%*_9HX3y+AYSaVawbieWK{Fk|Lqt8z1SdX(5zE3s%>b1Ey=FI29>vQkSK|t#eG?ey1%GnJ>q=#N{hcZsQNNLd_O{QB>ozRex zNCv0(dw2e)WRv;s{FWUFDu^BjSwNKgs30K}ifJJkr~;xAkOEyo*CkNRcoS*S296p+ zBHaSi5J4b%=KEwn*FpByO?0c#13h;BcbiFImL{*C0d56cE9KK2eeNl11~sr8NKZ{7A@S zUnn`iguoLmu!$fM9xgc-FJ449M8y4RmRxk;AcUEZU4-LV!$tEA4a1sIkNG~WpZIr8 zWfOEw_d_sEHpX7#?w%(iu5oN1AsmP%|zPj$Hioe5JVUNc5zQK4ft^K!`FM3FzbXjiLC~v(zL&sWaYy8=GNZLJJ)JE z8@IQ2xAwMo?$)-yxwWx_2G)wG6Df`Awv0eppu!bv|BJ%uDx;55p^hFp`_tpSnTpqa zBHm*oEnhKiP8)EXy+X;l=5e?U{Wx)f@@NP?;m`{hhfLXvrLxz(6P9&c=ozIQQ{#%R zj~0$8ATF2k{Bd6SBSQuEOiH(|6}YiMR2zEJLHsu*)4%5mTS`UXBdQC?3n=TLg#x3x zaCrnGXfs`=k#3oh zJV2XF-;C<$>g}!AgrP(Am4?pXe+kHEjRr{I8rAz~X%Q*#1ou@5?SPj3__X7@2$V*K zWgEb-iw9`1^@49uDorHzhtyPr&+;IqL0JI4U9X(kXTxk_%F~=aP1}r2C+to;f{Ayk z_~E)~=G=&pnAta-i3Al2t{}@!)lKQX8SDP2)R~&OH_;$SX&jk2eM7412(KML$+=+% zYxeLsl_h%PK_iS7sMgR4HL6^8lUc#|rd`ScS`Q#cNbjQ4)K|VBS6lg0li3)?E7be!UvyyL~b@Qw{E$fowM4vhD z=5OnAfj>uqOPaFqow81}iPHfDX=H!!<`wyO2 n9&@E&;ALVo?@o0`5|6xdj==`ve% znh@Rir5}q300000000B+TG4JBMHEef5RwrRydgj^S|M!;89R1D(u@istlvcL44oHp1qSjxjxw6#Gs6s6}^~$Z?{c(RD$GLw^EzL@e2LTv!?E0I^ z120>DkKOgA4CUQ>IFa!mImY*dF+#SfBji-3A8u!PJioEMb8zGK)#JUbTf6%^2fKSm z$Gbn=+}b0fXiKD*7$i-{WP*zLS6uvW{PT(PvmDgZc@L-Ky^+Jqp2B-3(%Ka}%y|J< z9pG4tzO6_XhIAmXL%{?*hLB1aL_$IVM>|>&IcK@^jLsqz&TtoD#-=hPi)V27QnmDw zi}I2JCh}6;qwffAV^GtD!E_=1lcVY1bN-Go350~BPlAYx9$EASNB`2P5V<5f5}^&+ zl2L;JnG&@F1Vs-Bb~w6U+!)Yyl4QUQ5iW4Ga`#&mpy~>&@dP_|Cilp;z#B=EY~I>Q zLs)f~p+V@4erq71Xth9tMA96Rm1SZeP%<=CvInS#>4N)H0tSiIvIB6^B>=*MKqV3( zv{I^0RP4{WRKzdyUZBNA1fi=}so9sK*-XmwI(?qD1vQcaybicxq*t-rlH@ zM;19E^kO9SODyXOc5L3~BI3;mN{L*QL=pqq)JD~qAd4CQKbI6_5=9f~zA}~WZY_IZ zoc}Aw`9EKk*U!>K)*MP*luO5RFC81RwPe4bqCGB$CmWt3iuxm`sK>9vlaz4_R4VEU z%=YO(&{YYRntAiLNK4+R{@#*zQuk@1sXw!5O0kq;`Janr`I*F`iY+XIoK`xogH}oz zyzw%a!uT&|Gd_`)cLm7)ZjS8#&cE2b_$aaEbKwi#K%Wr!0yfB#3FvrtYPv-T_~b*- zo9fhQ-6=1Lwd#uy!Ya9K4`@gTEuKW^NT!KDnz}!6Pdm$eOV=%3cbel0woatc*;4 zImbavv8B}u3!qxdp;}C#jKVGv(-m<%mf;xU#Do6~utM%}_;C)@d*l3cAv`lD4s)TL ue32Xelc&mKUnv-kG7-&(88iiN4-P-gVR=8rGTw#d#{UoZe*3L3DF6V^n5cyS