From bb368d6a108e1f8f77914270decfd728d4a44138 Mon Sep 17 00:00:00 2001 From: Grant Birkinbine Date: Thu, 2 Nov 2023 23:23:07 -0600 Subject: [PATCH 1/4] General Improvements (#70) # General Improvements > This is a classic @GrantBirki drive-by PR :car: This pull request does the following: - Lightly updates and formats a few existing Actions workflows - Adds a new `package-check` workflow to validate the contents of the `dist/` directory have been properly built - Uses a `.node-version` file so that local development and Actions remain on the same pinned version of node - Adds status badges to the readme for visual effect :star: :art: --- .github/workflows/release.yml | 9 ++++++--- .github/workflows/test.yml | 7 +++++-- .node-version | 1 + README.md | 4 +++- badges/coverage.svg | 25 +++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 7 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 .node-version create mode 100644 badges/coverage.svg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d487c79..dbce170 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,6 @@ name: release -"on": + +on: push: branches: - main @@ -18,10 +19,12 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false + - uses: actions/setup-node@v4 with: - node-version: 20 - cache: "npm" + node-version-file: .node-version + cache: 'npm' + - run: npm ci - run: npm run build - uses: ./ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48fbfef..40ec55b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,5 @@ name: test + on: push: branches: @@ -15,10 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 20 - cache: "npm" + node-version-file: .node-version + cache: 'npm' + - run: npm ci - run: npm test diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..e88320d --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20.0.0 diff --git a/README.md b/README.md index a1de0c6..1f122ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Create GitHub App Token +[![test](https://github.com/actions/create-github-app-token/actions/workflows/test.yml/badge.svg)](https://github.com/actions/create-github-app-token/actions/workflows/test.yml) + GitHub Action for creating a GitHub App installation access token. ## Usage @@ -46,7 +48,7 @@ jobs: # required app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.PRIVATE_KEY }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ steps.app-token.outputs.token }} ref: ${{ github.head_ref }} diff --git a/badges/coverage.svg b/badges/coverage.svg new file mode 100644 index 0000000..5c93d2c --- /dev/null +++ b/badges/coverage.svg @@ -0,0 +1,25 @@ + + Coverage: 100% + + + + + + + + + + + + + + + Coverage + + 100% + + diff --git a/package-lock.json b/package-lock.json index da3ae85..5bf8b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-github-app-token", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-github-app-token", - "version": "1.5.0", + "version": "1.5.1", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 1e35a78..a9f1578 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "1.5.1", "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=node16.16", + "build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node20.0.0", "test": "c8 --100 ava tests/index.js", "coverage": "c8 report --reporter html", "postcoverage": "open-cli coverage/index.html" From 9769eb4076a1785cd28c7f83b17b1e855fb092b8 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:58:23 -0800 Subject: [PATCH 2/4] build(.node-version): use latest LTS version (#78) this should resolve https://github.com/actions/create-github-app-token/actions/runs/6741605908/job/18326430392 --- .node-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.node-version b/.node-version index e88320d..f3f52b4 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.0.0 +20.9.0 From 0f3b4d7df99b1af7cb8596ba4f855d6de4155aa5 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sun, 12 Nov 2023 16:00:38 +0000 Subject: [PATCH 3/4] feat: add retry (#79) resolves #71 - Add p-retry library - Extract logic to new functions to improve the usage of retry logic --- lib/main.js | 96 +++++++++++------- package-lock.json | 43 +++++++- package.json | 3 +- ...n-get-owner-set-repo-fail-response.test.js | 39 +++++++ ...et-owner-set-to-user-fail-response.test.js | 36 +++++++ tests/snapshots/index.js.md | 30 ++++++ tests/snapshots/index.js.snap | Bin 857 -> 955 bytes 7 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 tests/main-token-get-owner-set-repo-fail-response.test.js create mode 100644 tests/main-token-get-owner-set-to-user-fail-response.test.js diff --git a/lib/main.js b/lib/main.js index 7b32a09..9dfe730 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,3 +1,4 @@ +import pRetry from "p-retry"; // @ts-check /** @@ -75,47 +76,26 @@ export async function main( let authentication; // If at least one repository is set, get installation ID from that repository - // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app + if (parsedRepositoryNames) { - const response = await request("GET /repos/{owner}/{repo}/installation", { - owner: parsedOwner, - repo: parsedRepositoryNames.split(",")[0], - headers: { - authorization: `bearer ${appAuthentication.token}`, + authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames), { + onFailedAttempt: (error) => { + core.info( + `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` + ); }, + retries: 3, }); - // Get token for given repositories - authentication = await auth({ - type: "installation", - installationId: response.data.id, - repositoryNames: parsedRepositoryNames.split(","), - }); } else { // Otherwise get the installation for the owner, which can either be an organization or a user account - // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app - const response = await request("GET /orgs/{org}/installation", { - org: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}`, + authentication = await pRetry(() => getTokenFromOwner(request, auth, appAuthentication, parsedOwner), { + onFailedAttempt: (error) => { + core.info( + `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` + ); }, - }).catch((error) => { - /* c8 ignore next */ - if (error.status !== 404) throw error; - - // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app - return request("GET /users/{username}/installation", { - username: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}`, - }, - }); - }); - - // Get token for for all repositories of the given installation - authentication = await auth({ - type: "installation", - installationId: response.data.id, + retries: 3, }); } @@ -129,3 +109,51 @@ export async function main( core.saveState("token", authentication.token); } } + +async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) { + // https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app + const response = await request("GET /orgs/{org}/installation", { + org: parsedOwner, + headers: { + authorization: `bearer ${appAuthentication.token}`, + }, + }).catch((error) => { + /* c8 ignore next */ + if (error.status !== 404) throw error; + + // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app + return request("GET /users/{username}/installation", { + username: parsedOwner, + headers: { + authorization: `bearer ${appAuthentication.token}`, + }, + }); + }); + + // Get token for for all repositories of the given installation + const authentication = await auth({ + type: "installation", + installationId: response.data.id, + }); + return authentication; +} + +async function getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames) { + // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app + const response = await request("GET /repos/{owner}/{repo}/installation", { + owner: parsedOwner, + repo: parsedRepositoryNames.split(",")[0], + headers: { + authorization: `bearer ${appAuthentication.token}`, + }, + }); + + // Get token for given repositories + const authentication = await auth({ + type: "installation", + installationId: response.data.id, + repositoryNames: parsedRepositoryNames.split(","), + }); + + return authentication; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5bf8b49..3ce99ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@actions/core": "^1.10.1", "@octokit/auth-app": "^6.0.1", - "@octokit/request": "^8.1.4" + "@octokit/request": "^8.1.4", + "p-retry": "^6.1.0" }, "devDependencies": { "ava": "^5.3.1", @@ -853,6 +854,11 @@ "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", "dev": true }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==" + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2407,6 +2413,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-network-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.0.tgz", + "integrity": "sha512-P3fxi10Aji2FZmHTrMPSNFbNC6nnp4U5juPAIjXPHkUNubi4+qK7vvdsaNpAUwXslhYm9oyjEYTxs1xd/+Ph0w==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3098,6 +3115,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.1.0.tgz", + "integrity": "sha512-fJLEQ2KqYBJRuaA/8cKMnqhulqNM+bpcjYtXNex2t3mOXKRYPitAJt9NacSf8XAFzcYahSAbKpobiWDSqHSh2g==", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-timeout": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", @@ -3465,6 +3498,14 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index a9f1578..23c723e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "dependencies": { "@actions/core": "^1.10.1", "@octokit/auth-app": "^6.0.1", - "@octokit/request": "^8.1.4" + "@octokit/request": "^8.1.4", + "p-retry": "^6.1.0" }, "devDependencies": { "ava": "^5.3.1", 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 new file mode 100644 index 0000000..9729376 --- /dev/null +++ b/tests/main-token-get-owner-set-repo-fail-response.test.js @@ -0,0 +1,39 @@ +import { test } from "./main.js"; + +// Verify `main` retry when the GitHub API returns a 500 error. +await test((mockPool) => { + process.env.INPUT_OWNER = 'actions' + process.env.INPUT_REPOSITORIES = 'failed-repo'; + const owner = process.env.INPUT_OWNER + const repo = process.env.INPUT_REPOSITORIES + const mockInstallationId = "123456"; + + mockPool + .intercept({ + path: `/repos/${owner}/${repo}/installation`, + 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(500, 'GitHub API not available') + + mockPool + .intercept({ + path: `/repos/${owner}/${repo}/installation`, + 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 }, + { 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 new file mode 100644 index 0000000..d1edf81 --- /dev/null +++ b/tests/main-token-get-owner-set-to-user-fail-response.test.js @@ -0,0 +1,36 @@ +import { test } from "./main.js"; + +// Verify `main` successfully obtains a token when the `owner` input is set (to a user), but the `repositories` input isn’t set. +await test((mockPool) => { + process.env.INPUT_OWNER = "smockle"; + delete process.env.INPUT_REPOSITORIES; + + // Mock installation id request + const mockInstallationId = "123456"; + mockPool + .intercept({ + path: `/orgs/${process.env.INPUT_OWNER}/installation`, + 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(500, 'GitHub API not available') + mockPool + .intercept({ + path: `/orgs/${process.env.INPUT_OWNER}/installation`, + 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 }, + { headers: { "content-type": "application/json" } } + ); +}); diff --git a/tests/snapshots/index.js.md b/tests/snapshots/index.js.md index 4d9a2ec..e7c6e86 100644 --- a/tests/snapshots/index.js.md +++ b/tests/snapshots/index.js.md @@ -56,6 +56,21 @@ Generated by [AVA](https://avajs.dev). '' +## main-token-get-owner-set-repo-fail-response.test.js + +> stderr + + '' + +> stdout + + `owner and repositories set, creating token for repositories "failed-repo" owned by "actions"␊ + Failed to create token for "failed-repo" (attempt 1): GitHub API not available␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + ## main-token-get-owner-set-repo-set-to-many.test.js > stderr @@ -98,6 +113,21 @@ Generated by [AVA](https://avajs.dev). ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` +## main-token-get-owner-set-to-user-fail-response.test.js + +> stderr + + '' + +> stdout + + `repositories not set, creating token for all repositories for given owner "smockle"␊ + Failed to create token for "smockle" (attempt 1): GitHub API not available␊ + ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ␊ + ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ + ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a` + ## main-token-get-owner-set-to-user-repo-unset.test.js > stderr diff --git a/tests/snapshots/index.js.snap b/tests/snapshots/index.js.snap index 902533097181cd76e356d0ea1eb75af80c2738c1..f7989c60e0237f307e83e20af6fca8e5ef7c89d1 100644 GIT binary patch literal 955 zcmV;s14R5mRzVoKTjEBE5QwO+dqKWh^uDP#S^}T)N zwuY!yN*{{|00000000B!nB7hjK@`WS#+ZK!<0LiL3_D>R5FJ1njv??6-+&{O|5 z>bd58v9+9M! zkx3kf3<956y)nGA3L@p~uH%fKMj*0{?Ks9Zc4`N^hcD{;+i&;Y)eiRdkL!Ey-)!%b zzS8SBq7hjgU-SO1rx^*7$%8<)MHyB zO#u|ukcqKHT3ooD7PKq19O}jeqLl*DI3o*o!ITTLugra&BqIeWA@|uuiU{2pN%^5q zb{Os^4YKvNW~zerR8QInVb=F)5O`fGJHFpOk@fQ0%POo^UR5?K&9#kkWxHA}FE`g$ z)>l@mTTrg9zgVTS>M5N?m15xW%wU3 zt7J0MsD#}vyhJUun1|YZhuU1h!C^7ew@y_vCVqwwk1EUVAdvqNu;{IvTd;K zz&W}~II^o=A3~VC`wcq%mpfnfS!clcpPzw4$kRpU@mMkj#Bf8)LuAt-;`Mj3^C)cl zDC>W?g{c?DW+DVc*jHK{E}0LfjF@*3vD~w$ilu_!b zA!8p>E*eMooVV1u1CwI==_45bT_GL1!{QjSKX_am{*e>@{wREt>gh#JsXB(*C}Z8+ dNx8{cm`4vBFSo20HY`LZ{x4O4(}UF$005uT#yS81 literal 857 zcmV-f1E%~zRzVV7OoOn1FO%@9Igy?TSYp?Sm~#)B@T~ zVnj>ZG9QZw00000000B!m(gz$F&xK1V@#GT(Kq83*X#wI92*SBxJ(mg=Ej50;1;(h zQn(*bIoh-BG52Qt1AOuyaDSSAiM^J=HRHy{#l#>lTuSfz{r2~1OZ%(c^jWJXeq4Yg z(uc1Iw)neAVJ8&XF&wTf#@%>7LC$==I6%z%%Z z;J1bJfOFH~G>hY1?$on52pYsgzrX!NIrE%DM4+)J!qh`&#P?AX5Qjb+s5U|oM(t3B z9MAx`P%#VWSb)}Wv<4Ltuvb_JI2kCNMi0)7OROQC)lkSWLYID`M}mfAJ)jK^1NCiO zuQaFgj^rFI=Zxfr3C3k-p}b_d)W?K6eIf**9mf6dt9}Yj(jt=Xmjl)mx_v&b< zmPh?i$f%$KB$5o}cC`Mh+-y;i6*Y!GIk0O5&QGmmOmw!4D^^Ah0TVcguJ|y%p6%RWF)G6!+n! zcD#jzQosXngjC6AQkV#F<3FH=GJ)1LP&NO1FS5d)YwvzO(blrNAT^IowpV$4^t<{|>faGh@bo({SetB%Ku6SyBp);(G jC&p!Bnn$rTDJSO16D!LDlZA{XqUHYw4)n3>`wsvBe7vCy From e995b4e40ace2eb5bf13137d9abe242c98f3aab6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 16 Nov 2023 22:53:33 +0000 Subject: [PATCH 4/4] build(release): 1.6.0 [skip ci] # [1.6.0](https://github.com/actions/create-github-app-token/compare/v1.5.1...v1.6.0) (2023-11-16) ### Features * add retry ([#79](https://github.com/actions/create-github-app-token/issues/79)) ([0f3b4d7](https://github.com/actions/create-github-app-token/commit/0f3b4d7df99b1af7cb8596ba4f855d6de4155aa5)), closes [#71](https://github.com/actions/create-github-app-token/issues/71) --- dist/main.cjs | 444 ++++++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 399 insertions(+), 47 deletions(-) diff --git a/dist/main.cjs b/dist/main.cjs index fb2c167..42b48da 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -7664,7 +7664,7 @@ var require_lodash = __commonJS({ } var objectProto = Object.prototype; var hasOwnProperty = objectProto.hasOwnProperty; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; var propertyIsEnumerable = objectProto.propertyIsEnumerable; var nativeKeys = overArg(Object.keys, Object); var nativeMax = Math.max; @@ -7708,7 +7708,7 @@ var require_lodash = __commonJS({ return isString(collection) ? fromIndex <= length && collection.indexOf(value, fromIndex) > -1 : !!length && baseIndexOf(collection, value, fromIndex) > -1; } function isArguments(value) { - return isArrayLikeObject(value) && hasOwnProperty.call(value, "callee") && (!propertyIsEnumerable.call(value, "callee") || objectToString.call(value) == argsTag); + return isArrayLikeObject(value) && hasOwnProperty.call(value, "callee") && (!propertyIsEnumerable.call(value, "callee") || objectToString2.call(value) == argsTag); } var isArray = Array.isArray; function isArrayLike(value) { @@ -7718,7 +7718,7 @@ var require_lodash = __commonJS({ return isObjectLike(value) && isArrayLike(value); } function isFunction(value) { - var tag = isObject(value) ? objectToString.call(value) : ""; + var tag = isObject(value) ? objectToString2.call(value) : ""; return tag == funcTag || tag == genTag; } function isLength(value) { @@ -7732,10 +7732,10 @@ var require_lodash = __commonJS({ return !!value && typeof value == "object"; } function isString(value) { - return typeof value == "string" || !isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag; + return typeof value == "string" || !isArray(value) && isObjectLike(value) && objectToString2.call(value) == stringTag; } function isSymbol(value) { - return typeof value == "symbol" || isObjectLike(value) && objectToString.call(value) == symbolTag; + return typeof value == "symbol" || isObjectLike(value) && objectToString2.call(value) == symbolTag; } function toFinite(value) { if (!value) { @@ -7785,9 +7785,9 @@ var require_lodash2 = __commonJS({ "node_modules/lodash.isboolean/index.js"(exports, module2) { var boolTag = "[object Boolean]"; var objectProto = Object.prototype; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; function isBoolean(value) { - return value === true || value === false || isObjectLike(value) && objectToString.call(value) == boolTag; + return value === true || value === false || isObjectLike(value) && objectToString2.call(value) == boolTag; } function isObjectLike(value) { return !!value && typeof value == "object"; @@ -7809,7 +7809,7 @@ var require_lodash3 = __commonJS({ var reIsOctal = /^0o[0-7]+$/i; var freeParseInt = parseInt; var objectProto = Object.prototype; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; function isInteger(value) { return typeof value == "number" && value == toInteger(value); } @@ -7821,7 +7821,7 @@ var require_lodash3 = __commonJS({ return !!value && typeof value == "object"; } function isSymbol(value) { - return typeof value == "symbol" || isObjectLike(value) && objectToString.call(value) == symbolTag; + return typeof value == "symbol" || isObjectLike(value) && objectToString2.call(value) == symbolTag; } function toFinite(value) { if (!value) { @@ -7865,12 +7865,12 @@ var require_lodash4 = __commonJS({ "node_modules/lodash.isnumber/index.js"(exports, module2) { var numberTag = "[object Number]"; var objectProto = Object.prototype; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; function isObjectLike(value) { return !!value && typeof value == "object"; } function isNumber(value) { - return typeof value == "number" || isObjectLike(value) && objectToString.call(value) == numberTag; + return typeof value == "number" || isObjectLike(value) && objectToString2.call(value) == numberTag; } module2.exports = isNumber; } @@ -7900,13 +7900,13 @@ var require_lodash5 = __commonJS({ var funcToString = funcProto.toString; var hasOwnProperty = objectProto.hasOwnProperty; var objectCtorString = funcToString.call(Object); - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; var getPrototype = overArg(Object.getPrototypeOf, Object); function isObjectLike(value) { return !!value && typeof value == "object"; } function isPlainObject(value) { - if (!isObjectLike(value) || objectToString.call(value) != objectTag || isHostObject(value)) { + if (!isObjectLike(value) || objectToString2.call(value) != objectTag || isHostObject(value)) { return false; } var proto = getPrototype(value); @@ -7925,13 +7925,13 @@ var require_lodash6 = __commonJS({ "node_modules/lodash.isstring/index.js"(exports, module2) { var stringTag = "[object String]"; var objectProto = Object.prototype; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; var isArray = Array.isArray; function isObjectLike(value) { return !!value && typeof value == "object"; } function isString(value) { - return typeof value == "string" || !isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag; + return typeof value == "string" || !isArray(value) && isObjectLike(value) && objectToString2.call(value) == stringTag; } module2.exports = isString; } @@ -7951,7 +7951,7 @@ var require_lodash7 = __commonJS({ var reIsOctal = /^0o[0-7]+$/i; var freeParseInt = parseInt; var objectProto = Object.prototype; - var objectToString = objectProto.toString; + var objectToString2 = objectProto.toString; function before(n, func) { var result; if (typeof func != "function") { @@ -7979,7 +7979,7 @@ var require_lodash7 = __commonJS({ return !!value && typeof value == "object"; } function isSymbol(value) { - return typeof value == "symbol" || isObjectLike(value) && objectToString.call(value) == symbolTag; + return typeof value == "symbol" || isObjectLike(value) && objectToString2.call(value) == symbolTag; } function toFinite(value) { if (!value) { @@ -10009,10 +10009,340 @@ var require_dist_node12 = __commonJS({ } }); +// node_modules/retry/lib/retry_operation.js +var require_retry_operation = __commonJS({ + "node_modules/retry/lib/retry_operation.js"(exports, module2) { + function RetryOperation(timeouts, options) { + if (typeof options === "boolean") { + options = { forever: options }; + } + this._originalTimeouts = JSON.parse(JSON.stringify(timeouts)); + this._timeouts = timeouts; + this._options = options || {}; + this._maxRetryTime = options && options.maxRetryTime || Infinity; + this._fn = null; + this._errors = []; + this._attempts = 1; + this._operationTimeout = null; + this._operationTimeoutCb = null; + this._timeout = null; + this._operationStart = null; + this._timer = null; + if (this._options.forever) { + this._cachedTimeouts = this._timeouts.slice(0); + } + } + module2.exports = RetryOperation; + RetryOperation.prototype.reset = function() { + this._attempts = 1; + this._timeouts = this._originalTimeouts.slice(0); + }; + RetryOperation.prototype.stop = function() { + if (this._timeout) { + clearTimeout(this._timeout); + } + if (this._timer) { + clearTimeout(this._timer); + } + this._timeouts = []; + this._cachedTimeouts = null; + }; + RetryOperation.prototype.retry = function(err) { + if (this._timeout) { + clearTimeout(this._timeout); + } + if (!err) { + return false; + } + var currentTime = (/* @__PURE__ */ new Date()).getTime(); + if (err && currentTime - this._operationStart >= this._maxRetryTime) { + this._errors.push(err); + this._errors.unshift(new Error("RetryOperation timeout occurred")); + return false; + } + this._errors.push(err); + var timeout = this._timeouts.shift(); + if (timeout === void 0) { + if (this._cachedTimeouts) { + this._errors.splice(0, this._errors.length - 1); + timeout = this._cachedTimeouts.slice(-1); + } else { + return false; + } + } + var self = this; + this._timer = setTimeout(function() { + self._attempts++; + if (self._operationTimeoutCb) { + self._timeout = setTimeout(function() { + self._operationTimeoutCb(self._attempts); + }, self._operationTimeout); + if (self._options.unref) { + self._timeout.unref(); + } + } + self._fn(self._attempts); + }, timeout); + if (this._options.unref) { + this._timer.unref(); + } + return true; + }; + RetryOperation.prototype.attempt = function(fn, timeoutOps) { + this._fn = fn; + if (timeoutOps) { + if (timeoutOps.timeout) { + this._operationTimeout = timeoutOps.timeout; + } + if (timeoutOps.cb) { + this._operationTimeoutCb = timeoutOps.cb; + } + } + var self = this; + if (this._operationTimeoutCb) { + this._timeout = setTimeout(function() { + self._operationTimeoutCb(); + }, self._operationTimeout); + } + this._operationStart = (/* @__PURE__ */ new Date()).getTime(); + this._fn(this._attempts); + }; + RetryOperation.prototype.try = function(fn) { + console.log("Using RetryOperation.try() is deprecated"); + this.attempt(fn); + }; + RetryOperation.prototype.start = function(fn) { + console.log("Using RetryOperation.start() is deprecated"); + this.attempt(fn); + }; + RetryOperation.prototype.start = RetryOperation.prototype.try; + RetryOperation.prototype.errors = function() { + return this._errors; + }; + RetryOperation.prototype.attempts = function() { + return this._attempts; + }; + RetryOperation.prototype.mainError = function() { + if (this._errors.length === 0) { + return null; + } + var counts = {}; + var mainError = null; + var mainErrorCount = 0; + for (var i = 0; i < this._errors.length; i++) { + var error = this._errors[i]; + var message = error.message; + var count = (counts[message] || 0) + 1; + counts[message] = count; + if (count >= mainErrorCount) { + mainError = error; + mainErrorCount = count; + } + } + return mainError; + }; + } +}); + +// node_modules/retry/lib/retry.js +var require_retry = __commonJS({ + "node_modules/retry/lib/retry.js"(exports) { + var RetryOperation = require_retry_operation(); + exports.operation = function(options) { + var timeouts = exports.timeouts(options); + return new RetryOperation(timeouts, { + forever: options && (options.forever || options.retries === Infinity), + unref: options && options.unref, + maxRetryTime: options && options.maxRetryTime + }); + }; + exports.timeouts = function(options) { + if (options instanceof Array) { + return [].concat(options); + } + var opts = { + retries: 10, + factor: 2, + minTimeout: 1 * 1e3, + maxTimeout: Infinity, + randomize: false + }; + for (var key in options) { + opts[key] = options[key]; + } + if (opts.minTimeout > opts.maxTimeout) { + throw new Error("minTimeout is greater than maxTimeout"); + } + var timeouts = []; + for (var i = 0; i < opts.retries; i++) { + timeouts.push(this.createTimeout(i, opts)); + } + if (options && options.forever && !timeouts.length) { + timeouts.push(this.createTimeout(i, opts)); + } + timeouts.sort(function(a, b) { + return a - b; + }); + return timeouts; + }; + exports.createTimeout = function(attempt, opts) { + var random = opts.randomize ? Math.random() + 1 : 1; + var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt)); + timeout = Math.min(timeout, opts.maxTimeout); + return timeout; + }; + exports.wrap = function(obj, options, methods) { + if (options instanceof Array) { + methods = options; + options = null; + } + if (!methods) { + methods = []; + for (var key in obj) { + if (typeof obj[key] === "function") { + methods.push(key); + } + } + } + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + var original = obj[method]; + obj[method] = function retryWrapper(original2) { + var op = exports.operation(options); + var args = Array.prototype.slice.call(arguments, 1); + var callback = args.pop(); + args.push(function(err) { + if (op.retry(err)) { + return; + } + if (err) { + arguments[0] = op.mainError(); + } + callback.apply(this, arguments); + }); + op.attempt(function() { + original2.apply(obj, args); + }); + }.bind(obj, original); + obj[method].options = options; + } + }; + } +}); + +// node_modules/retry/index.js +var require_retry2 = __commonJS({ + "node_modules/retry/index.js"(exports, module2) { + module2.exports = require_retry(); + } +}); + // main.js var import_core = __toESM(require_core(), 1); var import_auth_app = __toESM(require_dist_node12(), 1); +// node_modules/p-retry/index.js +var import_retry = __toESM(require_retry2(), 1); + +// node_modules/is-network-error/index.js +var objectToString = Object.prototype.toString; +var isError = (value) => objectToString.call(value) === "[object Error]"; +var errorMessages = /* @__PURE__ */ new Set([ + "Failed to fetch", + // Chrome + "NetworkError when attempting to fetch resource.", + // Firefox + "The Internet connection appears to be offline.", + // Safari 16 + "Load failed", + // Safari 17+ + "Network request failed", + // `cross-fetch` + "fetch failed" + // Undici (Node.js) +]); +function isNetworkError(error) { + const isValid = error && isError(error) && error.name === "TypeError" && typeof error.message === "string"; + if (!isValid) { + return false; + } + if (error.message === "Load failed") { + return error.stack === void 0; + } + return errorMessages.has(error.message); +} + +// node_modules/p-retry/index.js +var AbortError = class extends Error { + constructor(message) { + super(); + if (message instanceof Error) { + this.originalError = message; + ({ message } = message); + } else { + this.originalError = new Error(message); + this.originalError.stack = this.stack; + } + this.name = "AbortError"; + this.message = message; + } +}; +var decorateErrorWithCounts = (error, attemptNumber, options) => { + const retriesLeft = options.retries - (attemptNumber - 1); + error.attemptNumber = attemptNumber; + error.retriesLeft = retriesLeft; + return error; +}; +async function pRetry(input, options) { + return new Promise((resolve, reject) => { + options = { + onFailedAttempt() { + }, + retries: 10, + ...options + }; + const operation = import_retry.default.operation(options); + const abortHandler = () => { + operation.stop(); + reject(options.signal?.reason); + }; + if (options.signal && !options.signal.aborted) { + options.signal.addEventListener("abort", abortHandler, { once: true }); + } + const cleanUp = () => { + options.signal?.removeEventListener("abort", abortHandler); + operation.stop(); + }; + operation.attempt(async (attemptNumber) => { + try { + const result = await input(attemptNumber); + cleanUp(); + resolve(result); + } catch (error) { + try { + if (!(error instanceof Error)) { + throw new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`); + } + if (error instanceof AbortError) { + throw error.originalError; + } + if (error instanceof TypeError && !isNetworkError(error)) { + throw error; + } + await options.onFailedAttempt(decorateErrorWithCounts(error, attemptNumber, options)); + if (!operation.retry(error)) { + throw operation.mainError(); + } + } catch (finalError) { + decorateErrorWithCounts(finalError, attemptNumber, options); + cleanUp(); + reject(finalError); + } + } + }); + }); +} + // lib/main.js async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2, skipTokenRevoke2) { let parsedOwner = ""; @@ -10055,37 +10385,22 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp }); let authentication; if (parsedRepositoryNames) { - const response = await request2("GET /repos/{owner}/{repo}/installation", { - owner: parsedOwner, - repo: parsedRepositoryNames.split(",")[0], - headers: { - authorization: `bearer ${appAuthentication.token}` - } - }); - authentication = await auth({ - type: "installation", - installationId: response.data.id, - repositoryNames: parsedRepositoryNames.split(",") + authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames), { + onFailedAttempt: (error) => { + core2.info( + `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3 }); } else { - const response = await request2("GET /orgs/{org}/installation", { - org: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}` - } - }).catch((error) => { - if (error.status !== 404) - throw error; - return request2("GET /users/{username}/installation", { - username: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}` - } - }); - }); - authentication = await auth({ - type: "installation", - installationId: response.data.id + authentication = await pRetry(() => getTokenFromOwner(request2, auth, appAuthentication, parsedOwner), { + onFailedAttempt: (error) => { + core2.info( + `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` + ); + }, + retries: 3 }); } core2.setSecret(authentication.token); @@ -10094,6 +10409,43 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp core2.saveState("token", authentication.token); } } +async function getTokenFromOwner(request2, auth, appAuthentication, parsedOwner) { + const response = await request2("GET /orgs/{org}/installation", { + org: parsedOwner, + headers: { + authorization: `bearer ${appAuthentication.token}` + } + }).catch((error) => { + if (error.status !== 404) + throw error; + return request2("GET /users/{username}/installation", { + username: parsedOwner, + headers: { + authorization: `bearer ${appAuthentication.token}` + } + }); + }); + const authentication = await auth({ + type: "installation", + installationId: response.data.id + }); + return authentication; +} +async function getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames) { + const response = await request2("GET /repos/{owner}/{repo}/installation", { + owner: parsedOwner, + repo: parsedRepositoryNames.split(",")[0], + headers: { + authorization: `bearer ${appAuthentication.token}` + } + }); + const authentication = await auth({ + type: "installation", + installationId: response.data.id, + repositoryNames: parsedRepositoryNames.split(",") + }); + return authentication; +} // lib/request.js var import_request = __toESM(require_dist_node5(), 1); diff --git a/package.json b/package.json index 23c723e..8e7ab96 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "create-github-app-token", "private": true, "type": "module", - "version": "1.5.1", + "version": "1.6.0", "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",