Skip to content

Commit

Permalink
feat: add retry (actions#79)
Browse files Browse the repository at this point in the history
resolves actions#71

- Add p-retry library
- Extract logic to new functions to improve the usage of retry logic
  • Loading branch information
Moser-ss authored Nov 12, 2023
1 parent 9769eb4 commit 0f3b4d7
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 36 deletions.
96 changes: 62 additions & 34 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pRetry from "p-retry";
// @ts-check

/**
Expand Down Expand Up @@ -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,
});
}

Expand All @@ -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;
}
43 changes: 42 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions tests/main-token-get-owner-set-repo-fail-response.test.js
Original file line number Diff line number Diff line change
@@ -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" } }
);

});
36 changes: 36 additions & 0 deletions tests/main-token-get-owner-set-to-user-fail-response.test.js
Original file line number Diff line number Diff line change
@@ -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" } }
);
});
30 changes: 30 additions & 0 deletions tests/snapshots/index.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Binary file modified tests/snapshots/index.js.snap
Binary file not shown.

0 comments on commit 0f3b4d7

Please sign in to comment.