diff --git a/lib/api/github.js b/lib/api/github.js index ff64a0422a69056921fbf3081948b2ccae7034a2..53dcc4805f47109410452c4df29b85ce0cbba426 100644 --- a/lib/api/github.js +++ b/lib/api/github.js @@ -1,6 +1,34 @@ let logger = require('../logger'); const ghGot = require('gh-got'); +async function ghGotRetry(path, opts, retries = 3) { + try { + const res = await ghGot(path, opts); + return res; + } catch (err) { + if (err.statusCode === 502 && retries > 0) { + return ghGotRetry(path, opts, retries - 1); + } + throw err; + } +} + +const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete']; + +for (const x of helpers) { + ghGotRetry[x] = async (path, opts, retries = 3) => { + try { + const res = await ghGot[x](path, opts); + return res; + } catch (err) { + if (err.statusCode === 502 && retries > 0) { + return ghGotRetry[x](path, opts, retries - 1); + } + throw err; + } + }; +} + const config = {}; module.exports = { @@ -55,7 +83,7 @@ async function getInstallations(appToken) { authorization: `Bearer ${appToken}`, }, }; - const res = await ghGot(url, options); + const res = await ghGotRetry(url, options); logger.debug(`Returning ${res.body.length} results`); return res.body; } catch (err) { @@ -75,7 +103,7 @@ async function getInstallationToken(appToken, installationId) { authorization: `Bearer ${appToken}`, }, }; - const res = await ghGot.post(url, options); + const res = await ghGotRetry.post(url, options); return res.body.token; } catch (err) { logger.error({ err }, `GitHub getInstallationToken error`); @@ -94,7 +122,7 @@ async function getInstallationRepositories(userToken) { authorization: `token ${userToken}`, }, }; - const res = await ghGot(url, options); + const res = await ghGotRetry(url, options); logger.debug( `Returning ${res.body.repositories.length} results from a total of ${res .body.total_count}` @@ -118,7 +146,7 @@ async function getRepos(token, endpoint) { process.env.GITHUB_ENDPOINT = endpoint; } try { - const res = await ghGot('user/repos'); + const res = await ghGotRetry('user/repos'); return res.body.map(repo => repo.full_name); } catch (err) /* istanbul ignore next */ { logger.error({ err }, `GitHub getRepos error`); @@ -144,7 +172,7 @@ async function initRepo(repoName, token, endpoint, repoLogger) { config.repoName = repoName; const platformConfig = {}; try { - const res = await ghGot(`repos/${repoName}`); + const res = await ghGotRetry(`repos/${repoName}`); config.privateRepo = res.body.private === true; config.owner = res.body.owner.login; logger.debug(`${repoName} owner = ${config.owner}`); @@ -194,7 +222,7 @@ async function initRepo(repoName, token, endpoint, repoLogger) { } async function getBranchProtection(branchName) { - const res = await ghGot( + const res = await ghGotRetry( `repos/${config.repoName}/branches/${branchName}/protection/required_status_checks`, { headers: { @@ -217,7 +245,7 @@ async function setBaseBranch(branchName) { // Returns an array of file paths in current repo matching the fileName async function findFilePaths(fileName) { - const res = await ghGot( + const res = await ghGotRetry( `search/code?q=repo:${config.repoName}+filename:${fileName}` ); const exactMatches = res.body.items.filter(item => item.name === fileName); @@ -232,7 +260,7 @@ async function findFilePaths(fileName) { async function branchExists(branchName) { logger.debug(`Checking if branch exists: ${branchName}`); try { - const res = await ghGot( + const res = await ghGotRetry( `repos/${config.repoName}/git/refs/heads/${branchName}` ); if (Array.isArray(res.body)) { @@ -262,8 +290,9 @@ async function branchExists(branchName) { async function getAllRenovateBranches() { logger.trace('getAllRenovateBranches'); - const allBranches = (await ghGot(`repos/${config.repoName}/git/refs/heads`)) - .body; + const allBranches = (await ghGotRetry( + `repos/${config.repoName}/git/refs/heads` + )).body; return allBranches.reduce((arr, branch) => { if (branch.ref.indexOf('refs/heads/renovate/') === 0) { arr.push(branch.ref.substring('refs/heads/'.length)); @@ -291,7 +320,7 @@ async function getBranchPr(branchName) { const gotString = `repos/${config.repoName}/pulls?` + `state=open&base=${config.baseBranch}&head=${config.owner}:${branchName}`; - const res = await ghGot(gotString); + const res = await ghGotRetry(gotString); if (!res.body.length) { return null; } @@ -313,12 +342,14 @@ async function getBranchStatus(branchName, requiredStatusChecks) { } const gotString = `repos/${config.repoName}/commits/${branchName}/status`; logger.debug(gotString); - const res = await ghGot(gotString); + const res = await ghGotRetry(gotString); return res.body.state; } async function deleteBranch(branchName) { - await ghGot.delete(`repos/${config.repoName}/git/refs/heads/${branchName}`); + await ghGotRetry.delete( + `repos/${config.repoName}/git/refs/heads/${branchName}` + ); } async function mergeBranch(branchName, mergeType) { @@ -331,7 +362,7 @@ async function mergeBranch(branchName, mergeType) { }, }; try { - await ghGot.patch(url, options); + await ghGotRetry.patch(url, options); } catch (err) { logger.error({ err }, `Error pushing branch merge for ${branchName}`); throw new Error('branch-push failed'); @@ -345,7 +376,7 @@ async function mergeBranch(branchName, mergeType) { }, }; try { - await ghGot.post(url, options); + await ghGotRetry.post(url, options); } catch (err) { logger.error({ err }, `Error pushing branch merge for ${branchName}`); throw new Error('branch-push failed'); @@ -363,16 +394,19 @@ async function mergeBranch(branchName, mergeType) { async function addAssignees(issueNo, assignees) { logger.debug(`Adding assignees ${assignees} to #${issueNo}`); - await ghGot.post(`repos/${config.repoName}/issues/${issueNo}/assignees`, { - body: { - assignees, - }, - }); + await ghGotRetry.post( + `repos/${config.repoName}/issues/${issueNo}/assignees`, + { + body: { + assignees, + }, + } + ); } async function addReviewers(issueNo, reviewers) { logger.debug(`Adding reviewers ${reviewers} to #${issueNo}`); - await ghGot.post( + await ghGotRetry.post( `repos/${config.repoName}/pulls/${issueNo}/requested_reviewers`, { headers: { @@ -387,7 +421,7 @@ async function addReviewers(issueNo, reviewers) { async function addLabels(issueNo, labels) { logger.debug(`Adding labels ${labels} to #${issueNo}`); - await ghGot.post(`repos/${config.repoName}/issues/${issueNo}/labels`, { + await ghGotRetry.post(`repos/${config.repoName}/issues/${issueNo}/labels`, { body: labels, }); } @@ -396,7 +430,7 @@ async function findPr(branchName, prTitle, state = 'all') { logger.debug(`findPr(${branchName}, ${state})`); const urlString = `repos/${config.repoName}/pulls?head=${config.owner}:${branchName}&state=${state}`; logger.debug(`findPr urlString: ${urlString}`); - const res = await ghGot(urlString); + const res = await ghGotRetry(urlString); let pr = null; res.body.forEach(result => { if (!prTitle || result.title === prTitle) { @@ -414,7 +448,7 @@ async function findPr(branchName, prTitle, state = 'all') { async function checkForClosedPr(branchName, prTitle) { logger.debug(`checkForClosedPr(${branchName}, ${prTitle})`); const url = `repos/${config.repoName}/pulls?state=closed&head=${config.owner}:${branchName}`; - const res = await ghGot(url); + const res = await ghGotRetry(url); // Return true if any of the titles match exactly return res.body.some( pr => @@ -425,7 +459,7 @@ async function checkForClosedPr(branchName, prTitle) { // Creates PR and returns PR number async function createPr(branchName, title, body, useDefaultBranch) { const base = useDefaultBranch ? config.defaultBranch : config.baseBranch; - const pr = (await ghGot.post(`repos/${config.repoName}/pulls`, { + const pr = (await ghGotRetry.post(`repos/${config.repoName}/pulls`, { body: { title, head: branchName, @@ -442,7 +476,7 @@ async function getPr(prNo) { if (!prNo) { return null; } - const pr = (await ghGot(`repos/${config.repoName}/pulls/${prNo}`)).body; + const pr = (await ghGotRetry(`repos/${config.repoName}/pulls/${prNo}`)).body; if (!pr) { return null; } @@ -463,7 +497,7 @@ async function getPr(prNo) { } else { // Check if only one author of all commits logger.debug('Checking all commits'); - const prCommits = (await ghGot( + const prCommits = (await ghGotRetry( `repos/${config.repoName}/pulls/${prNo}/commits` )).body; const authors = prCommits.reduce((arr, commit) => { @@ -495,7 +529,8 @@ async function getPr(prNo) { } async function getAllPrs() { - const all = (await ghGot(`repos/${config.repoName}/pulls?state=open`)).body; + const all = (await ghGotRetry(`repos/${config.repoName}/pulls?state=open`)) + .body; return all.map(pr => ({ number: pr.number, branchName: pr.head.ref, @@ -503,7 +538,7 @@ async function getAllPrs() { } async function updatePr(prNo, title, body) { - await ghGot.patch(`repos/${config.repoName}/pulls/${prNo}`, { + await ghGotRetry.patch(`repos/${config.repoName}/pulls/${prNo}`, { body: { title, body }, }); } @@ -518,7 +553,7 @@ async function mergePr(pr) { options.body.merge_method = config.mergeMethod; try { logger.debug({ options, url }, `mergePr`); - await ghGot.put(url, options); + await ghGotRetry.put(url, options); } catch (err) { logger.error({ err }, `Failed to ${options.body.merge_method} PR`); return; @@ -528,13 +563,13 @@ async function mergePr(pr) { options.body.merge_method = 'rebase'; try { logger.debug({ options, url }, `mergePr`); - await ghGot.put(url, options); + await ghGotRetry.put(url, options); } catch (err1) { logger.debug({ err: err1 }, `Failed to ${options.body.merge_method} PR}`); try { options.body.merge_method = 'squash'; logger.debug({ options, url }, `mergePr`); - await ghGot.put(url, options); + await ghGotRetry.put(url, options); } catch (err2) { logger.debug( { err: err2 }, @@ -543,7 +578,7 @@ async function mergePr(pr) { try { options.body.merge_method = 'merge'; logger.debug({ options, url }, `mergePr`); - await ghGot.put(url, options); + await ghGotRetry.put(url, options); } catch (err3) { logger.debug( { err: err3 }, @@ -564,7 +599,7 @@ async function mergePr(pr) { // Generic File operations async function getFile(filePath, branchName = config.baseBranch) { - const res = await ghGot( + const res = await ghGotRetry( `repos/${config.repoName}/contents/${filePath}?ref=${branchName}` ); return res.body.content; @@ -600,7 +635,7 @@ async function getFileJson(filePath, branchName) { async function getSubDirectories(path) { logger.trace(`getSubDirectories(path=${path})`); - const res = await ghGot(`repos/${config.repoName}/contents/${path}`); + const res = await ghGotRetry(`repos/${config.repoName}/contents/${path}`); const directoryList = []; res.body.forEach(item => { if (item.type === 'dir') { @@ -646,7 +681,7 @@ async function commitFilesToBranch( // Creates a new branch with provided commit async function createBranch(branchName, commit = config.baseCommitSHA) { - await ghGot.post(`repos/${config.repoName}/git/refs`, { + await ghGotRetry.post(`repos/${config.repoName}/git/refs`, { body: { ref: `refs/heads/${branchName}`, sha: commit, @@ -657,12 +692,15 @@ async function createBranch(branchName, commit = config.baseCommitSHA) { // Internal: Updates an existing branch to new commit sha async function updateBranch(branchName, commit) { logger.debug(`Updating branch ${branchName} with commit ${commit}`); - await ghGot.patch(`repos/${config.repoName}/git/refs/heads/${branchName}`, { - body: { - sha: commit, - force: true, - }, - }); + await ghGotRetry.patch( + `repos/${config.repoName}/git/refs/heads/${branchName}`, + { + body: { + sha: commit, + force: true, + }, + } + ); } // Low-level commit operations @@ -670,7 +708,7 @@ async function updateBranch(branchName, commit) { // Create a blob with fileContents and return sha async function createBlob(fileContents) { logger.debug('Creating blob'); - return (await ghGot.post(`repos/${config.repoName}/git/blobs`, { + return (await ghGotRetry.post(`repos/${config.repoName}/git/blobs`, { body: { encoding: 'base64', content: new Buffer(fileContents).toString('base64'), @@ -680,21 +718,24 @@ async function createBlob(fileContents) { // Return the commit SHA for a branch async function getBranchCommit(branchName) { - return (await ghGot(`repos/${config.repoName}/git/refs/heads/${branchName}`)) - .body.object.sha; + return (await ghGotRetry( + `repos/${config.repoName}/git/refs/heads/${branchName}` + )).body.object.sha; } async function getCommitDetails(commit) { logger.debug(`getCommitDetails(${commit})`); - const results = await ghGot(`repos/${config.repoName}/git/commits/${commit}`); + const results = await ghGotRetry( + `repos/${config.repoName}/git/commits/${commit}` + ); return results.body; } // Return the tree SHA for a commit async function getCommitTree(commit) { logger.debug(`getCommitTree(${commit})`); - return (await ghGot(`repos/${config.repoName}/git/commits/${commit}`)).body - .tree.sha; + return (await ghGotRetry(`repos/${config.repoName}/git/commits/${commit}`)) + .body.tree.sha; } // Create a tree and return SHA @@ -713,14 +754,14 @@ async function createTree(baseTree, files) { }); }); logger.trace({ body }, 'createTree body'); - return (await ghGot.post(`repos/${config.repoName}/git/trees`, { body })).body - .sha; + return (await ghGotRetry.post(`repos/${config.repoName}/git/trees`, { body })) + .body.sha; } // Create a commit and return commit SHA async function createCommit(parent, tree, message) { logger.debug(`createCommit(${parent}, ${tree}, ${message})`); - return (await ghGot.post(`repos/${config.repoName}/git/commits`, { + return (await ghGotRetry.post(`repos/${config.repoName}/git/commits`, { body: { message, parents: [parent], @@ -732,7 +773,7 @@ async function createCommit(parent, tree, message) { async function getCommitMessages() { logger.debug('getCommitMessages'); try { - const res = await ghGot(`repos/${config.repoName}/commits`); + const res = await ghGotRetry(`repos/${config.repoName}/commits`); return res.body.map(commit => commit.commit.message); } catch (err) { logger.error({ err }, `getCommitMessages error`); diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap index 10fda155e8759a46201e7080a7f294daa29b0a80..d135ab6a0c3e27233025e60244e0f7395fb633df 100644 --- a/test/api/__snapshots__/github.spec.js.snap +++ b/test/api/__snapshots__/github.spec.js.snap @@ -53,9 +53,11 @@ exports[`api/github branchExists(branchName) should propagate unknown errors 1`] Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -67,6 +69,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -75,9 +78,11 @@ exports[`api/github branchExists(branchName) should return false if a 404 is ret Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -89,6 +94,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -97,9 +103,11 @@ exports[`api/github branchExists(branchName) should return false if the branch d Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -111,6 +119,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -119,9 +128,11 @@ exports[`api/github branchExists(branchName) should return false if the branch d Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -133,6 +144,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -141,9 +153,11 @@ exports[`api/github branchExists(branchName) should return true if the branch ex Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -155,6 +169,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -163,9 +178,11 @@ exports[`api/github branchExists(branchName) should return true if the branch ex Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -177,6 +194,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -185,9 +203,11 @@ exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -199,12 +219,15 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/git/commits/1111", + undefined, ], Array [ "repos/some/repo/git/refs/heads/package.json", + undefined, ], ] `; @@ -266,9 +289,11 @@ exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -280,12 +305,15 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/git/commits/1111", + undefined, ], Array [ "repos/some/repo/git/refs/heads/package.json", + undefined, ], ] `; @@ -396,9 +424,11 @@ exports[`api/github findFilePaths(fileName) should return empty array if none fo Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -410,6 +440,7 @@ Array [ ], Array [ "search/code?q=repo:some/repo+filename:package.json", + undefined, ], ] `; @@ -418,9 +449,11 @@ exports[`api/github findFilePaths(fileName) should return the files matching the Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -432,6 +465,7 @@ Array [ ], Array [ "search/code?q=repo:some/repo+filename:package.json", + undefined, ], ] `; @@ -448,9 +482,11 @@ exports[`api/github findPr(branchName, prTitle, state) should return a PR object Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -462,6 +498,7 @@ Array [ ], Array [ "repos/some/repo/pulls?head=theowner:master&state=all", + undefined, ], ] `; @@ -479,9 +516,11 @@ exports[`api/github findPr(branchName, prTitle, state) should return null if no Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -493,6 +532,7 @@ Array [ ], Array [ "repos/some/repo/pulls?head=theowner:master&state=all", + undefined, ], ] `; @@ -501,9 +541,11 @@ exports[`api/github findPr(branchName, prTitle, state) should set the isClosed a Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -515,6 +557,7 @@ Array [ ], Array [ "repos/some/repo/pulls?head=theowner:master&state=all", + undefined, ], ] `; @@ -557,9 +600,11 @@ exports[`api/github getBranchPr(branchName) should return null if no PR exists 1 Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -571,6 +616,7 @@ Array [ ], Array [ "repos/some/repo/pulls?state=open&base=master&head=theowner:somebranch", + undefined, ], ] `; @@ -579,9 +625,11 @@ exports[`api/github getBranchPr(branchName) should return the PR object 1`] = ` Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -593,9 +641,11 @@ Array [ ], Array [ "repos/some/repo/pulls?state=open&base=master&head=theowner:somebranch", + undefined, ], Array [ "repos/some/repo/pulls/91", + undefined, ], ] `; @@ -625,9 +675,11 @@ exports[`api/github getFile(filePatch, branchName) should return the encoded fil Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -639,6 +691,7 @@ Array [ ], Array [ "repos/some/repo/contents/package.json?ref=master", + undefined, ], ] `; @@ -647,9 +700,11 @@ exports[`api/github getFileContent(filePatch, branchName) should return null if Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -661,6 +716,7 @@ Array [ ], Array [ "repos/some/repo/contents/package.json?ref=master", + undefined, ], ] `; @@ -669,9 +725,11 @@ exports[`api/github getFileContent(filePatch, branchName) should return the enco Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -683,6 +741,7 @@ Array [ ], Array [ "repos/some/repo/contents/package.json?ref=master", + undefined, ], ] `; @@ -691,9 +750,11 @@ exports[`api/github getFileJson(filePatch, branchName) should return null if inv Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -705,6 +766,7 @@ Array [ ], Array [ "repos/some/repo/contents/package.json?ref=master", + undefined, ], ] `; @@ -713,9 +775,11 @@ exports[`api/github getFileJson(filePatch, branchName) should return the file co Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -727,6 +791,7 @@ Array [ ], Array [ "repos/some/repo/contents/package.json?ref=master", + undefined, ], ] `; @@ -761,10 +826,83 @@ Object { } `; +exports[`api/github getInstallationToken should retry posts 1`] = `Array []`; + +exports[`api/github getInstallationToken should retry posts 2`] = `"aUserToken"`; + exports[`api/github getInstallationToken should return an installation token 1`] = `Array []`; exports[`api/github getInstallationToken should return an installation token 2`] = `"aUserToken"`; +exports[`api/github getInstallations should retry 502s once 1`] = ` +Array [ + Array [ + "app/installations", + Object { + "headers": Object { + "accept": "application/vnd.github.machine-man-preview+json", + "authorization": "Bearer sometoken", + }, + }, + ], + Array [ + "app/installations", + Object { + "headers": Object { + "accept": "application/vnd.github.machine-man-preview+json", + "authorization": "Bearer sometoken", + }, + }, + ], +] +`; + +exports[`api/github getInstallations should retry 502s once 2`] = ` +Array [ + "a", + "b", +] +`; + +exports[`api/github getInstallations should retry 502s until success 1`] = ` +Array [ + Array [ + "app/installations", + Object { + "headers": Object { + "accept": "application/vnd.github.machine-man-preview+json", + "authorization": "Bearer sometoken", + }, + }, + ], + Array [ + "app/installations", + Object { + "headers": Object { + "accept": "application/vnd.github.machine-man-preview+json", + "authorization": "Bearer sometoken", + }, + }, + ], + Array [ + "app/installations", + Object { + "headers": Object { + "accept": "application/vnd.github.machine-man-preview+json", + "authorization": "Bearer sometoken", + }, + }, + ], +] +`; + +exports[`api/github getInstallations should retry 502s until success 2`] = ` +Array [ + "a", + "b", +] +`; + exports[`api/github getInstallations should return an array of installations 1`] = ` Array [ Array [ @@ -860,6 +998,7 @@ exports[`api/github getRepos should return an array of repos 1`] = ` Array [ Array [ "user/repos", + undefined, ], ] `; @@ -875,6 +1014,7 @@ exports[`api/github getRepos should support a custom endpoint 1`] = ` Array [ Array [ "user/repos", + undefined, ], ] `; @@ -890,9 +1030,11 @@ exports[`api/github getSubDirectories(path) should return subdirectories 1`] = ` Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -904,6 +1046,7 @@ Array [ ], Array [ "repos/some/repo/contents/some-path", + undefined, ], ] `; @@ -936,9 +1079,11 @@ exports[`api/github initRepo should initialise the config for the repo - 0 1`] = Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -961,9 +1106,11 @@ exports[`api/github initRepo should initialise the config for the repo - 1 1`] = Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -986,9 +1133,11 @@ exports[`api/github initRepo should initialise the config for the repo - 2 1`] = Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1035,9 +1184,11 @@ exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-m Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1049,6 +1200,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], ] `; @@ -1075,6 +1227,7 @@ exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-m Array [ Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -1083,9 +1236,11 @@ exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-p Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1097,9 +1252,11 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], ] `; @@ -1125,6 +1282,7 @@ exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-p Array [ Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -1135,9 +1293,11 @@ exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-me Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1176,9 +1336,11 @@ exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-pu Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1190,6 +1352,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/thebranchname", + undefined, ], ] `; @@ -1219,9 +1382,11 @@ exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown m Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1246,9 +1411,11 @@ exports[`api/github setBaseBranch(branchName) sets the base branch 1`] = ` Array [ Array [ "repos/some/repo", + undefined, ], Array [ "repos/some/repo/git/refs/heads/master", + undefined, ], Array [ "repos/some/repo/branches/master/protection/required_status_checks", @@ -1260,6 +1427,7 @@ Array [ ], Array [ "repos/some/repo/git/refs/heads/some-branch", + undefined, ], ] `; diff --git a/test/api/github.spec.js b/test/api/github.spec.js index 8711c4235eff9ed33b743c8134500c8ef2447724..7c0d2fb59164af6812b86cda46b7c69bf3721041 100644 --- a/test/api/github.spec.js +++ b/test/api/github.spec.js @@ -24,17 +24,103 @@ describe('api/github', () => { expect(ghGot.mock.calls).toMatchSnapshot(); expect(installations).toMatchSnapshot(); }); - it('should return an error if given one', async () => { - ghGot.mockImplementationOnce(() => { - throw new Error('error'); - }); + it('should return a 404', async () => { + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 404, + }) + ); let err; try { await github.getInstallations('sometoken'); } catch (e) { err = e; } - expect(err.message).toBe('error'); + expect(err.statusCode).toBe(404); + }); + it('should retry 502s once', async () => { + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => ({ + body: ['a', 'b'], + })); + const installations = await github.getInstallations('sometoken'); + expect(ghGot.mock.calls).toMatchSnapshot(); + expect(installations).toMatchSnapshot(); + }); + it('should retry 502s until success', async () => { + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => ({ + body: ['a', 'b'], + })); + const installations = await github.getInstallations('sometoken'); + expect(ghGot.mock.calls).toMatchSnapshot(); + expect(installations).toMatchSnapshot(); + }); + it('should retry 502s until failure', async () => { + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 404, + }) + ); + let err; + try { + await github.getInstallations('sometoken'); + } catch (e) { + err = e; + } + expect(err.statusCode).toBe(404); + }); + it('should give up after 3 retries', async () => { + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + let err; + try { + await github.getInstallations('sometoken'); + } catch (e) { + err = e; + } + expect(err.statusCode).toBe(502); }); }); @@ -52,6 +138,24 @@ describe('api/github', () => { expect(ghGot.mock.calls).toMatchSnapshot(); expect(installationToken).toMatchSnapshot(); }); + it('should retry posts', async () => { + ghGot.post.mockImplementationOnce(() => + Promise.reject({ + statusCode: 502, + }) + ); + ghGot.post.mockImplementationOnce(() => ({ + body: { + token: 'aUserToken', + }, + })); + const installationToken = await github.getInstallationToken( + 'sometoken', + 123456 + ); + expect(ghGot.mock.calls).toMatchSnapshot(); + expect(installationToken).toMatchSnapshot(); + }); it('should return an error if given one', async () => { ghGot.post.mockImplementationOnce(() => { throw new Error('error');