diff --git a/dist/main.cjs b/dist/main.cjs index 266e78e..0d6d1a4 100644 --- a/dist/main.cjs +++ b/dist/main.cjs @@ -10390,12 +10390,9 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp privateKey: privateKey2, request: request2 }); - const appAuthentication = await auth({ - type: "app" - }); let authentication; if (parsedRepositoryNames) { - authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames), { + authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames), { onFailedAttempt: (error) => { core2.info( `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` @@ -10404,7 +10401,7 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp retries: 3 }); } else { - authentication = await pRetry(() => getTokenFromOwner(request2, auth, appAuthentication, parsedOwner), { + authentication = await pRetry(() => getTokenFromOwner(request2, auth, parsedOwner), { onFailedAttempt: (error) => { core2.info( `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` @@ -10419,19 +10416,19 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp core2.saveState("token", authentication.token); } } -async function getTokenFromOwner(request2, auth, appAuthentication, parsedOwner) { +async function getTokenFromOwner(request2, auth, parsedOwner) { const response = await request2("GET /orgs/{org}/installation", { org: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}` + request: { + hook: auth.hook } }).catch((error) => { if (error.status !== 404) throw error; return request2("GET /users/{username}/installation", { username: parsedOwner, - headers: { - authorization: `bearer ${appAuthentication.token}` + request: { + hook: auth.hook } }); }); @@ -10441,12 +10438,12 @@ async function getTokenFromOwner(request2, auth, appAuthentication, parsedOwner) }); return authentication; } -async function getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames) { +async function getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames) { const response = await request2("GET /repos/{owner}/{repo}/installation", { owner: parsedOwner, repo: parsedRepositoryNames.split(",")[0], - headers: { - authorization: `bearer ${appAuthentication.token}` + request: { + hook: auth.hook } }); const authentication = await auth({ diff --git a/lib/main.js b/lib/main.js index 9dfe730..233be3d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -70,15 +70,11 @@ export async function main( request, }); - const appAuthentication = await auth({ - type: "app", - }); - let authentication; // If at least one repository is set, get installation ID from that repository if (parsedRepositoryNames) { - authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames), { + authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), { onFailedAttempt: (error) => { core.info( `Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` @@ -89,7 +85,7 @@ export async function main( } else { // Otherwise get the installation for the owner, which can either be an organization or a user account - authentication = await pRetry(() => getTokenFromOwner(request, auth, appAuthentication, parsedOwner), { + authentication = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), { onFailedAttempt: (error) => { core.info( `Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}` @@ -110,12 +106,12 @@ export async function main( } } -async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) { +async function getTokenFromOwner(request, auth, 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}`, + request: { + hook: auth.hook, }, }).catch((error) => { /* c8 ignore next */ @@ -124,8 +120,8 @@ async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) // 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}`, + request: { + hook: auth.hook, }, }); }); @@ -138,13 +134,13 @@ async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) return authentication; } -async function getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames) { +async function getTokenFromRepository(request, auth, parsedOwner, 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}`, + request: { + hook: auth.hook, }, }); diff --git a/package-lock.json b/package-lock.json index 2ef6e47..664e29d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-github-app-token", - "version": "1.6.0", + "version": "1.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-github-app-token", - "version": "1.6.0", + "version": "1.6.1", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", @@ -15,6 +15,7 @@ "p-retry": "^6.1.0" }, "devDependencies": { + "@sinonjs/fake-timers": "^11.2.2", "ava": "^5.3.1", "c8": "^8.0.1", "dotenv": "^16.3.1", @@ -811,6 +812,24 @@ "@octokit/openapi-types": "^19.0.0" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -4089,6 +4108,15 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", diff --git a/package.json b/package.json index e3b0b3f..b6459aa 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "p-retry": "^6.1.0" }, "devDependencies": { + "@sinonjs/fake-timers": "^11.2.2", "ava": "^5.3.1", "c8": "^8.0.1", "dotenv": "^16.3.1", diff --git a/tests/main-repo-skew.js b/tests/main-repo-skew.js new file mode 100644 index 0000000..a3554ad --- /dev/null +++ b/tests/main-repo-skew.js @@ -0,0 +1,56 @@ +import { test } from "./main.js"; + +import { install } from "@sinonjs/fake-timers"; + +// Verify `main` retry when the clock has drifted. +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"; + + install({ now: 0, toFake: ["Date"] }); + + 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(({ headers }) => { + const [_, jwt] = (headers.authorization || "").split(" "); + const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString()); + + if (payload.iat < 0) { + return { + statusCode: 401, + data: { + message: "'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued." + }, + responseOptions: { + headers: { + "content-type": "application/json", + "date": new Date(Date.now() + 30000).toUTCString() + } + } + }; + } + + return { + statusCode: 200, + data: { + id: mockInstallationId + }, + responseOptions: { + headers: { + "content-type": "application/json" + } + } + }; + }).times(2); +});