feat: add retry (#79)
resolves #71 - Add p-retry library - Extract logic to new functions to improve the usage of retry logic
This commit is contained in:
+62
-34
@@ -1,3 +1,4 @@
|
|||||||
|
import pRetry from "p-retry";
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,47 +76,26 @@ export async function main(
|
|||||||
|
|
||||||
let authentication;
|
let authentication;
|
||||||
// If at least one repository is set, get installation ID from that repository
|
// 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) {
|
if (parsedRepositoryNames) {
|
||||||
const response = await request("GET /repos/{owner}/{repo}/installation", {
|
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames), {
|
||||||
owner: parsedOwner,
|
onFailedAttempt: (error) => {
|
||||||
repo: parsedRepositoryNames.split(",")[0],
|
core.info(
|
||||||
headers: {
|
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||||
authorization: `bearer ${appAuthentication.token}`,
|
);
|
||||||
},
|
},
|
||||||
|
retries: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get token for given repositories
|
|
||||||
authentication = await auth({
|
|
||||||
type: "installation",
|
|
||||||
installationId: response.data.id,
|
|
||||||
repositoryNames: parsedRepositoryNames.split(","),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise get the installation for the owner, which can either be an organization or a user account
|
// 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
|
authentication = await pRetry(() => getTokenFromOwner(request, auth, appAuthentication, parsedOwner), {
|
||||||
const response = await request("GET /orgs/{org}/installation", {
|
onFailedAttempt: (error) => {
|
||||||
org: parsedOwner,
|
core.info(
|
||||||
headers: {
|
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||||
authorization: `bearer ${appAuthentication.token}`,
|
);
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
retries: 3,
|
||||||
/* 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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,3 +109,51 @@ export async function main(
|
|||||||
core.saveState("token", authentication.token);
|
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;
|
||||||
|
}
|
||||||
Generated
+42
-1
@@ -11,7 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@octokit/auth-app": "^6.0.1",
|
"@octokit/auth-app": "^6.0.1",
|
||||||
"@octokit/request": "^8.1.4"
|
"@octokit/request": "^8.1.4",
|
||||||
|
"p-retry": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^5.3.1",
|
"ava": "^5.3.1",
|
||||||
@@ -853,6 +854,11 @@
|
|||||||
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==",
|
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.10.0",
|
"version": "8.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||||
@@ -2407,6 +2413,17 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -3098,6 +3115,22 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/p-timeout": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz",
|
||||||
@@ -3465,6 +3498,14 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/reusify": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@octokit/auth-app": "^6.0.1",
|
"@octokit/auth-app": "^6.0.1",
|
||||||
"@octokit/request": "^8.1.4"
|
"@octokit/request": "^8.1.4",
|
||||||
|
"p-retry": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^5.3.1",
|
"ava": "^5.3.1",
|
||||||
|
|||||||
@@ -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" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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" } }
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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
|
## main-token-get-owner-set-repo-set-to-many.test.js
|
||||||
|
|
||||||
> stderr
|
> stderr
|
||||||
@@ -98,6 +113,21 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||||
::save-state 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
|
## main-token-get-owner-set-to-user-repo-unset.test.js
|
||||||
|
|
||||||
> stderr
|
> stderr
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user