Skip to content

Commit 0f3b4d7

Browse files
authored
feat: add retry (#79)
resolves #71 - Add p-retry library - Extract logic to new functions to improve the usage of retry logic
1 parent 9769eb4 commit 0f3b4d7

7 files changed

+211
-36
lines changed

lib/main.js

+62-34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pRetry from "p-retry";
12
// @ts-check
23

34
/**
@@ -75,47 +76,26 @@ export async function main(
7576

7677
let authentication;
7778
// If at least one repository is set, get installation ID from that repository
78-
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
79+
7980
if (parsedRepositoryNames) {
80-
const response = await request("GET /repos/{owner}/{repo}/installation", {
81-
owner: parsedOwner,
82-
repo: parsedRepositoryNames.split(",")[0],
83-
headers: {
84-
authorization: `bearer ${appAuthentication.token}`,
81+
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames), {
82+
onFailedAttempt: (error) => {
83+
core.info(
84+
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}`
85+
);
8586
},
87+
retries: 3,
8688
});
8789

88-
// Get token for given repositories
89-
authentication = await auth({
90-
type: "installation",
91-
installationId: response.data.id,
92-
repositoryNames: parsedRepositoryNames.split(","),
93-
});
9490
} else {
9591
// Otherwise get the installation for the owner, which can either be an organization or a user account
96-
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
97-
const response = await request("GET /orgs/{org}/installation", {
98-
org: parsedOwner,
99-
headers: {
100-
authorization: `bearer ${appAuthentication.token}`,
92+
authentication = await pRetry(() => getTokenFromOwner(request, auth, appAuthentication, parsedOwner), {
93+
onFailedAttempt: (error) => {
94+
core.info(
95+
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
96+
);
10197
},
102-
}).catch((error) => {
103-
/* c8 ignore next */
104-
if (error.status !== 404) throw error;
105-
106-
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
107-
return request("GET /users/{username}/installation", {
108-
username: parsedOwner,
109-
headers: {
110-
authorization: `bearer ${appAuthentication.token}`,
111-
},
112-
});
113-
});
114-
115-
// Get token for for all repositories of the given installation
116-
authentication = await auth({
117-
type: "installation",
118-
installationId: response.data.id,
98+
retries: 3,
11999
});
120100
}
121101

@@ -129,3 +109,51 @@ export async function main(
129109
core.saveState("token", authentication.token);
130110
}
131111
}
112+
113+
async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) {
114+
// https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app
115+
const response = await request("GET /orgs/{org}/installation", {
116+
org: parsedOwner,
117+
headers: {
118+
authorization: `bearer ${appAuthentication.token}`,
119+
},
120+
}).catch((error) => {
121+
/* c8 ignore next */
122+
if (error.status !== 404) throw error;
123+
124+
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
125+
return request("GET /users/{username}/installation", {
126+
username: parsedOwner,
127+
headers: {
128+
authorization: `bearer ${appAuthentication.token}`,
129+
},
130+
});
131+
});
132+
133+
// Get token for for all repositories of the given installation
134+
const authentication = await auth({
135+
type: "installation",
136+
installationId: response.data.id,
137+
});
138+
return authentication;
139+
}
140+
141+
async function getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames) {
142+
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
143+
const response = await request("GET /repos/{owner}/{repo}/installation", {
144+
owner: parsedOwner,
145+
repo: parsedRepositoryNames.split(",")[0],
146+
headers: {
147+
authorization: `bearer ${appAuthentication.token}`,
148+
},
149+
});
150+
151+
// Get token for given repositories
152+
const authentication = await auth({
153+
type: "installation",
154+
installationId: response.data.id,
155+
repositoryNames: parsedRepositoryNames.split(","),
156+
});
157+
158+
return authentication;
159+
}

package-lock.json

+42-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"dependencies": {
1515
"@actions/core": "^1.10.1",
1616
"@octokit/auth-app": "^6.0.1",
17-
"@octokit/request": "^8.1.4"
17+
"@octokit/request": "^8.1.4",
18+
"p-retry": "^6.1.0"
1819
},
1920
"devDependencies": {
2021
"ava": "^5.3.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` retry when the GitHub API returns a 500 error.
4+
await test((mockPool) => {
5+
process.env.INPUT_OWNER = 'actions'
6+
process.env.INPUT_REPOSITORIES = 'failed-repo';
7+
const owner = process.env.INPUT_OWNER
8+
const repo = process.env.INPUT_REPOSITORIES
9+
const mockInstallationId = "123456";
10+
11+
mockPool
12+
.intercept({
13+
path: `/repos/${owner}/${repo}/installation`,
14+
method: "GET",
15+
headers: {
16+
accept: "application/vnd.github.v3+json",
17+
"user-agent": "actions/create-github-app-token",
18+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
19+
},
20+
})
21+
.reply(500, 'GitHub API not available')
22+
23+
mockPool
24+
.intercept({
25+
path: `/repos/${owner}/${repo}/installation`,
26+
method: "GET",
27+
headers: {
28+
accept: "application/vnd.github.v3+json",
29+
"user-agent": "actions/create-github-app-token",
30+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
31+
},
32+
})
33+
.reply(
34+
200,
35+
{ id: mockInstallationId },
36+
{ headers: { "content-type": "application/json" } }
37+
);
38+
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` input is set (to a user), but the `repositories` input isn’t set.
4+
await test((mockPool) => {
5+
process.env.INPUT_OWNER = "smockle";
6+
delete process.env.INPUT_REPOSITORIES;
7+
8+
// Mock installation id request
9+
const mockInstallationId = "123456";
10+
mockPool
11+
.intercept({
12+
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
13+
method: "GET",
14+
headers: {
15+
accept: "application/vnd.github.v3+json",
16+
"user-agent": "actions/create-github-app-token",
17+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
18+
},
19+
})
20+
.reply(500, 'GitHub API not available')
21+
mockPool
22+
.intercept({
23+
path: `/orgs/${process.env.INPUT_OWNER}/installation`,
24+
method: "GET",
25+
headers: {
26+
accept: "application/vnd.github.v3+json",
27+
"user-agent": "actions/create-github-app-token",
28+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
29+
},
30+
})
31+
.reply(
32+
200,
33+
{ id: mockInstallationId },
34+
{ headers: { "content-type": "application/json" } }
35+
);
36+
});

tests/snapshots/index.js.md

+30
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ Generated by [AVA](https://avajs.dev).
5656
5757
''
5858

59+
## main-token-get-owner-set-repo-fail-response.test.js
60+
61+
> stderr
62+
63+
''
64+
65+
> stdout
66+
67+
`owner and repositories set, creating token for repositories "failed-repo" owned by "actions"␊
68+
Failed to create token for "failed-repo" (attempt 1): GitHub API not available␊
69+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
70+
71+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
72+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
73+
5974
## main-token-get-owner-set-repo-set-to-many.test.js
6075

6176
> stderr
@@ -98,6 +113,21 @@ Generated by [AVA](https://avajs.dev).
98113
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
99114
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
100115

116+
## main-token-get-owner-set-to-user-fail-response.test.js
117+
118+
> stderr
119+
120+
''
121+
122+
> stdout
123+
124+
`repositories not set, creating token for all repositories for given owner "smockle"␊
125+
Failed to create token for "smockle" (attempt 1): GitHub API not available␊
126+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
127+
128+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
129+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a`
130+
101131
## main-token-get-owner-set-to-user-repo-unset.test.js
102132

103133
> stderr

tests/snapshots/index.js.snap

98 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)