diff --git a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap index a2e14ba769ec51..8484e51d2693da 100644 --- a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap +++ b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap @@ -1611,6 +1611,160 @@ Array [ ] `; +exports[`platform/bitbucket/index updatePr() removes inactive reviewers when updating pr 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5", + }, + Object { + "body": Object { + "description": "body", + "reviewers": Array [ + Object { + "account_id": "456", + "display_name": "Jane Smith", + "uuid": "{90b6646d-1724-4a64-9fd9-539515fe94e9}", + }, + Object { + "account_id": "123", + "display_name": "Bob Smith", + "uuid": "{d2238482-2e9f-48b3-8630-de22ccb9e42f}", + }, + ], + "title": "title", + }, + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "245", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PUT", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/users/456", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/users/123", + }, + Object { + "body": Object { + "description": "body", + "reviewers": Array [ + Object { + "account_id": "456", + "display_name": "Jane Smith", + "uuid": "{90b6646d-1724-4a64-9fd9-539515fe94e9}", + }, + ], + "title": "title", + }, + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "149", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PUT", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5", + }, +] +`; + +exports[`platform/bitbucket/index updatePr() rethrows exception when PR update error not due to inactive reviewers 1`] = `"Response code 400 (Bad Request)"`; + +exports[`platform/bitbucket/index updatePr() rethrows exception when PR update error not due to inactive reviewers 2`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5", + }, + Object { + "body": Object { + "description": "body", + "reviewers": Array [ + Object { + "display_name": "Jane Smith", + "uuid": "{90b6646d-1724-4a64-9fd9-539515fe94e9}", + }, + ], + "title": "title", + }, + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic YWJjOjEyMw==", + "content-length": "130", + "content-type": "application/json", + "host": "api.bitbucket.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "PUT", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests/5", + }, +] +`; + exports[`platform/bitbucket/index updatePr() throws an error on failure to get current list of reviewers 1`] = `"Response code 500 (Internal Server Error)"`; exports[`platform/bitbucket/index updatePr() throws an error on failure to get current list of reviewers 2`] = ` diff --git a/lib/platform/bitbucket/index.spec.ts b/lib/platform/bitbucket/index.spec.ts index 4716c77ab5e5f7..27b42e88955f9a 100644 --- a/lib/platform/bitbucket/index.spec.ts +++ b/lib/platform/bitbucket/index.spec.ts @@ -765,6 +765,69 @@ describe('platform/bitbucket/index', () => { await bitbucket.updatePr({ number: 5, prTitle: 'title', prBody: 'body' }); expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('removes inactive reviewers when updating pr', async () => { + const inactiveReviewer = { + display_name: 'Bob Smith', + uuid: '{d2238482-2e9f-48b3-8630-de22ccb9e42f}', + account_id: '123', + }; + const activeReviewer = { + display_name: 'Jane Smith', + uuid: '{90b6646d-1724-4a64-9fd9-539515fe94e9}', + account_id: '456', + }; + const scope = await initRepoMock(); + scope + .get('/2.0/repositories/some/repo/pullrequests/5') + .reply(200, { reviewers: [activeReviewer, inactiveReviewer] }) + .put('/2.0/repositories/some/repo/pullrequests/5') + .reply(400, { + type: 'error', + error: { + fields: { + reviewers: ['Malformed reviewers list'], + }, + message: 'reviewers: Malformed reviewers list', + }, + }) + .get('/2.0/users/123') + .reply(200, { + account_status: 'inactive', + }) + .get('/2.0/users/456') + .reply(200, { + account_status: 'active', + }) + .put('/2.0/repositories/some/repo/pullrequests/5') + .reply(200); + await bitbucket.updatePr({ number: 5, prTitle: 'title', prBody: 'body' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('rethrows exception when PR update error not due to inactive reviewers', async () => { + const reviewer = { + display_name: 'Jane Smith', + uuid: '{90b6646d-1724-4a64-9fd9-539515fe94e9}', + }; + + const scope = await initRepoMock(); + scope + .get('/2.0/repositories/some/repo/pullrequests/5') + .reply(200, { reviewers: [reviewer] }) + .put('/2.0/repositories/some/repo/pullrequests/5') + .reply(400, { + type: 'error', + error: { + fields: { + reviewers: ['Some other unhandled error'], + }, + message: 'Some other unhandled error', + }, + }); + await expect(() => + bitbucket.updatePr({ number: 5, prTitle: 'title', prBody: 'body' }) + ).rejects.toThrowErrorMatchingSnapshot(); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); it('throws an error on failure to get current list of reviewers', async () => { const scope = await initRepoMock(); scope diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts index 04ff05b3cd90de..aca5adc95bf1e4 100644 --- a/lib/platform/bitbucket/index.ts +++ b/lib/platform/bitbucket/index.ts @@ -30,7 +30,13 @@ import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import * as comments from './comments'; import * as utils from './utils'; -import { PrResponse, RepoInfoBody, mergeBodyTransformer } from './utils'; +import { + PrResponse, + PrReviewer, + RepoInfoBody, + UserResponse, + mergeBodyTransformer, +} from './utils'; const bitbucketHttp = new BitbucketHttp(); @@ -712,16 +718,57 @@ export async function updatePr({ ) ).body; - await bitbucketHttp.putJson( - `/2.0/repositories/${config.repository}/pullrequests/${prNo}`, - { - body: { - title, - description: sanitize(description), - reviewers: pr.reviewers, - }, + try { + await bitbucketHttp.putJson( + `/2.0/repositories/${config.repository}/pullrequests/${prNo}`, + { + body: { + title, + description: sanitize(description), + reviewers: pr.reviewers, + }, + } + ); + } catch (err) { + if ( + err.statusCode === 400 && + err.body?.error?.message.includes('reviewers: Malformed reviewers list') + ) { + logger.warn( + { err }, + 'PR contains inactive reviewer accounts. Will try setting only active reviewers' + ); + + // Bitbucket returns a 400 if any of the PR reviewer accounts are now inactive (ie: disabled/suspended) + const activeReviewers: PrReviewer[] = []; + + // Validate that each previous PR reviewer account is still active + for (const reviewer of pr.reviewers) { + const reviewerUser = ( + await bitbucketHttp.getJson( + `/2.0/users/${reviewer.account_id}` + ) + ).body; + + if (reviewerUser.account_status === 'active') { + activeReviewers.push(reviewer); + } + } + + await bitbucketHttp.putJson( + `/2.0/repositories/${config.repository}/pullrequests/${prNo}`, + { + body: { + title, + description: sanitize(description), + reviewers: activeReviewers, + }, + } + ); + } else { + throw err; } - ); + } if (state === PrState.Closed && pr) { await bitbucketHttp.postJson( diff --git a/lib/platform/bitbucket/utils.ts b/lib/platform/bitbucket/utils.ts index c20b2ec28c9239..b68e42b63b2ec7 100644 --- a/lib/platform/bitbucket/utils.ts +++ b/lib/platform/bitbucket/utils.ts @@ -190,7 +190,7 @@ export interface PrResponse { name: string; }; }; - reviewers: Array; + reviewers: Array; created_on: string; } @@ -208,3 +208,16 @@ export function prInfo(pr: PrResponse): Pr { createdAt: pr.created_on, }; } + +export interface UserResponse { + display_name: string; + account_id: string; + nickname: string; + account_status: string; +} + +export interface PrReviewer { + display_name: string; + account_id: string; + nickname: string; +}