diff options
-rw-r--r-- | lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap | 354 | ||||
-rw-r--r-- | lib/platform/bitbucket-server/index.spec.ts | 97 | ||||
-rw-r--r-- | lib/platform/bitbucket-server/index.ts | 39 | ||||
-rw-r--r-- | lib/platform/bitbucket-server/utils.ts | 39 |
4 files changed, 519 insertions, 10 deletions
diff --git a/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap b/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap index 4b75af9a009..ee6f150c134 100644 --- a/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap +++ b/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap @@ -396,6 +396,76 @@ Array [ ] `; +exports[`platform/bitbucket-server/index endpoint with no path addReviewers throws on invalid reviewers 1`] = `"Response code 409 (Conflict)"`; + +exports[`platform/bitbucket-server/index endpoint with no path addReviewers throws on invalid reviewers 2`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/branches/default", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"version\\":1,\\"reviewers\\":[{\\"user\\":{\\"name\\":\\"userName2\\"}},{\\"user\\":{\\"name\\":\\"name\\"}}]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "98", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, +] +`; + exports[`platform/bitbucket-server/index endpoint with no path addReviewers throws repository-changed 1`] = ` Array [ Object { @@ -3417,6 +3487,113 @@ Array [ ] `; +exports[`platform/bitbucket-server/index endpoint with no path updatePr() handles invalid users gracefully by retrying without invalid reviewers 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/branches/default", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"description\\":\\"body\\",\\"version\\":1,\\"reviewers\\":[{\\"user\\":{\\"name\\":\\"userName2\\"}}]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "94", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"description\\":\\"body\\",\\"version\\":1,\\"reviewers\\":[]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "65", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, +] +`; + exports[`platform/bitbucket-server/index endpoint with no path updatePr() puts PR 1`] = ` Array [ Object { @@ -4237,6 +4414,76 @@ Array [ ] `; +exports[`platform/bitbucket-server/index endpoint with path addReviewers throws on invalid reviewers 1`] = `"Response code 409 (Conflict)"`; + +exports[`platform/bitbucket-server/index endpoint with path addReviewers throws on invalid reviewers 2`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/branches/default", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"version\\":1,\\"reviewers\\":[{\\"user\\":{\\"name\\":\\"userName2\\"}},{\\"user\\":{\\"name\\":\\"name\\"}}]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "98", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, +] +`; + exports[`platform/bitbucket-server/index endpoint with path addReviewers throws repository-changed 1`] = ` Array [ Object { @@ -7258,6 +7505,113 @@ Array [ ] `; +exports[`platform/bitbucket-server/index endpoint with path updatePr() handles invalid users gracefully by retrying without invalid reviewers 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/branches/default", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"description\\":\\"body\\",\\"version\\":1,\\"reviewers\\":[{\\"user\\":{\\"name\\":\\"userName2\\"}}]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "94", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge", + }, + Object { + "body": "{\\"title\\":\\"title\\",\\"description\\":\\"body\\",\\"version\\":1,\\"reviewers\\":[]}", + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "65", + "content-type": "application/json", + "host": "stash.renovatebot.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "PUT", + "url": "https://stash.renovatebot.com/vcs/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5", + }, +] +`; + exports[`platform/bitbucket-server/index endpoint with path updatePr() puts PR 1`] = ` Array [ Object { diff --git a/lib/platform/bitbucket-server/index.spec.ts b/lib/platform/bitbucket-server/index.spec.ts index 6689a5b1324..925c68fd486 100644 --- a/lib/platform/bitbucket-server/index.spec.ts +++ b/lib/platform/bitbucket-server/index.spec.ts @@ -506,6 +506,46 @@ describe(getName(__filename), () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('throws on invalid reviewers', async () => { + const scope = await initRepo(); + scope + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5` + ) + .reply(200, prMock(url, 'SOME', 'repo')) + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge` + ) + .reply(200, { conflicted: false }) + .put( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5` + ) + .reply(409, { + errors: [ + { + context: 'reviewers', + message: + 'Errors encountered while adding some reviewers to this pull request.', + exceptionName: + 'com.atlassian.bitbucket.pull.InvalidPullRequestReviewersException', + reviewerErrors: [ + { + context: 'name', + message: 'name is not a user.', + exceptionName: null, + }, + ], + validReviewers: [], + }, + ], + }); + + await expect( + bitbucket.addReviewers(5, ['name']) + ).rejects.toThrowErrorMatchingSnapshot(); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('throws', async () => { const scope = await initRepo(); scope @@ -1348,6 +1388,63 @@ describe(getName(__filename), () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('handles invalid users gracefully by retrying without invalid reviewers', async () => { + const scope = await initRepo(); + scope + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5` + ) + .reply(200, prMock(url, 'SOME', 'repo')) + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge` + ) + .reply(200, { conflicted: false }) + .put( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5` + ) + .reply(409, { + errors: [ + { + context: 'reviewers', + message: + 'Errors encountered while adding some reviewers to this pull request.', + exceptionName: + 'com.atlassian.bitbucket.pull.InvalidPullRequestReviewersException', + reviewerErrors: [ + { + context: 'userName2', + message: 'userName2 is not a user.', + exceptionName: null, + }, + ], + validReviewers: [], + }, + ], + }) + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5` + ) + .reply(200, prMock(url, 'SOME', 'repo')) + .get( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/merge` + ) + .reply(200, { conflicted: false }) + .put( + `${urlPath}/rest/api/1.0/projects/SOME/repos/repo/pull-requests/5`, + (body) => body.reviewers.length === 0 + ) + .reply(200, prMock(url, 'SOME', 'repo')); + + await bitbucket.updatePr({ + number: 5, + prTitle: 'title', + prBody: 'body', + state: PrState.Open, + }); + + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('throws repository-changed', async () => { const scope = await initRepo(); scope diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts index 5066ac9f048..609255bf32d 100644 --- a/lib/platform/bitbucket-server/index.ts +++ b/lib/platform/bitbucket-server/index.ts @@ -128,7 +128,7 @@ export async function getJsonFile(fileName: string): Promise<any | null> { return null; } -// Initialize GitLab by getting base branch +// Initialize BitBucket Server by getting base branch export async function initRepo({ repository, localDir, @@ -584,15 +584,15 @@ export async function addReviewers( ); await getPr(prNo, true); } catch (err) { + logger.warn({ err, reviewers, prNo }, `Failed to add reviewers`); if (err.statusCode === 404) { throw new Error(REPOSITORY_NOT_FOUND); - } else if (err.statusCode === 409) { + } else if ( + err.statusCode === 409 && + !utils.isInvalidReviewersResponse(err) + ) { throw new Error(REPOSITORY_CHANGED); } else { - logger.fatal( - { err }, - `Failed to add reviewers '${reviewers.join(', ')}' to #${prNo}` - ); throw err; } } @@ -850,7 +850,10 @@ export async function updatePr({ prTitle: title, prBody: rawDescription, state, -}: UpdatePrConfig): Promise<void> { + bitbucketInvalidReviewers, +}: UpdatePrConfig & { + bitbucketInvalidReviewers: string[] | undefined; +}): Promise<void> { const description = sanitize(rawDescription); logger.debug(`updatePr(${prNo}, title=${title})`); @@ -870,7 +873,11 @@ export async function updatePr({ title, description, version: pr.version, - reviewers: pr.reviewers.map((name: string) => ({ user: { name } })), + reviewers: pr.reviewers + .filter( + (name: string) => !bitbucketInvalidReviewers?.includes(name) + ) + .map((name: string) => ({ user: { name } })), }, } ); @@ -898,12 +905,24 @@ export async function updatePr({ updatePrVersion(pr.number, updatedStatePr.version); } } catch (err) { + logger.debug({ err, prNo }, `Failed to update PR`); if (err.statusCode === 404) { throw new Error(REPOSITORY_NOT_FOUND); } else if (err.statusCode === 409) { - throw new Error(REPOSITORY_CHANGED); + if (utils.isInvalidReviewersResponse(err) && !bitbucketInvalidReviewers) { + // Retry again with invalid reviewers being removed + const invalidReviewers = utils.getInvalidReviewers(err); + await updatePr({ + number: prNo, + prTitle: title, + prBody: rawDescription, + state, + bitbucketInvalidReviewers: invalidReviewers, + }); + } else { + throw new Error(REPOSITORY_CHANGED); + } } else { - logger.fatal({ err }, `Failed to update PR`); throw err; } } diff --git a/lib/platform/bitbucket-server/utils.ts b/lib/platform/bitbucket-server/utils.ts index 9c438dbf0bf..0819ce062cf 100644 --- a/lib/platform/bitbucket-server/utils.ts +++ b/lib/platform/bitbucket-server/utils.ts @@ -1,10 +1,14 @@ // SEE for the reference https://github.com/renovatebot/renovate/blob/c3e9e572b225085448d94aa121c7ec81c14d3955/lib/platform/bitbucket/utils.js import url from 'url'; +import { HTTPError, Response } from 'got'; import { PrState } from '../../types'; import { HttpOptions, HttpPostOptions, HttpResponse } from '../../util/http'; import { BitbucketServerHttp } from '../../util/http/bitbucket-server'; import { BbbsRestPr, BbsPr } from './types'; +const BITBUCKET_INVALID_REVIEWERS_EXCEPTION = + 'com.atlassian.bitbucket.pull.InvalidPullRequestReviewersException'; + const bitbucketServerHttp = new BitbucketServerHttp(); // https://docs.atlassian.com/bitbucket-server/rest/6.0.0/bitbucket-rest.html#idp250 @@ -118,3 +122,38 @@ export interface BitbucketStatus { key: string; state: BitbucketBranchState; } + +interface BitbucketErrorResponse { + errors?: { + exceptionName?: string; + reviewerErrors?: { context?: string }[]; + }[]; +} + +interface BitbucketError extends HTTPError { + readonly response: Response<BitbucketErrorResponse>; +} + +export function isInvalidReviewersResponse(err: BitbucketError): boolean { + const errors = err?.response?.body?.errors || []; + return ( + errors.length > 0 && + errors.every( + (error) => error.exceptionName === BITBUCKET_INVALID_REVIEWERS_EXCEPTION + ) + ); +} + +export function getInvalidReviewers(err: BitbucketError): string[] { + const errors = err?.response?.body?.errors || []; + let invalidReviewers = []; + for (const error of errors) { + if (error.exceptionName === BITBUCKET_INVALID_REVIEWERS_EXCEPTION) { + invalidReviewers = invalidReviewers.concat( + error.reviewerErrors?.map(({ context }) => context) || [] + ); + } + } + + return invalidReviewers; +} |