diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index e044922481a7dfcfcedac8a66d43045d4d047c5f..d5d7863fc5b85d48b1497a580bbfca5e3d8544e3 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -21,6 +21,10 @@ Also, be sure to check out Renovate's [shareable config presets](/config-presets If you have any questions about the below config options, or would like to get help/feedback about a config, please post it as an issue in [renovatebot/config-help](https://github.com/renovatebot/config-help) where we will do our best to answer your question. +## additionalReviewers + +In contrast to `reviewers`, this option adds to the existing reviewer list, rather than replacing it. This makes it suitable for augmenting a preset or base list without displacing the original, for example when adding focused reviewers for a specific package group. + ## aliases The `aliases` object is used for configuring registry aliases. Currently it is needed/supported for the `helm-requiremenets` manager only. diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index 2e7ef9f80c27f7e040e53b7f156deafdcb7ee46f..c10561b22ad48dc90267783127f909aeef922746 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -1335,6 +1335,14 @@ const options: RenovateOptions[] = [ type: 'integer', default: null, }, + { + name: 'additionalReviewers', + description: + 'Additional reviewers for Pull Requests (in contrast to `reviewers`, this option adds to the existing reviewer list, rather than replacing it)', + type: 'array', + subType: 'string', + mergeable: true, + }, { name: 'fileMatch', description: 'RegEx (re2) pattern for matching manager files', diff --git a/lib/workers/pr/index.ts b/lib/workers/pr/index.ts index ca0be895dde68e8c0dd59542f57f6ee3e6d4916f..c8f9df166dd2f4c2c6b430293fd833c34825dab3 100644 --- a/lib/workers/pr/index.ts +++ b/lib/workers/pr/index.ts @@ -1,4 +1,5 @@ import sampleSize from 'lodash/sampleSize'; +import uniq from 'lodash/uniq'; import { logger } from '../../logger'; import { getChangeLogJSON } from './changelog'; import { getPrBody } from './body'; @@ -15,12 +16,14 @@ function noWhitespace(input: string): string { return input.replace(/\r?\n|\r|\s/g, ''); } +function noLeadingAtSymbol(input: string): string { + return input.length && input[0] === '@' ? input.slice(1) : input; +} + async function addAssigneesReviewers(config, pr: Pr): Promise<void> { if (config.assignees.length > 0) { try { - let assignees = config.assignees.map(assignee => - assignee.length && assignee[0] === '@' ? assignee.slice(1) : assignee - ); + let assignees = config.assignees.map(noLeadingAtSymbol); if (config.assigneesSampleSize !== null) { assignees = sampleSize(assignees, config.assigneesSampleSize); } @@ -40,9 +43,13 @@ async function addAssigneesReviewers(config, pr: Pr): Promise<void> { } if (config.reviewers.length > 0) { try { - let reviewers = config.reviewers.map(reviewer => - reviewer.length && reviewer[0] === '@' ? reviewer.slice(1) : reviewer - ); + let reviewers = config.reviewers.map(noLeadingAtSymbol); + if (config.additionalReviewers.length > 0) { + const additionalReviewers = config.additionalReviewers.map( + noLeadingAtSymbol + ); + reviewers = uniq(reviewers.concat(additionalReviewers)); + } if (config.reviewersSampleSize !== null) { reviewers = sampleSize(reviewers, config.reviewersSampleSize); } diff --git a/renovate-schema.json b/renovate-schema.json index 4bdb19e3d077184ac1a9caf88ec023be4ceacdbc..43b258673983a13ae56c94c99687cfc67894683a 100644 --- a/renovate-schema.json +++ b/renovate-schema.json @@ -855,6 +855,13 @@ "type": "integer", "default": null }, + "additionalReviewers": { + "description": "Additional reviewers for Pull Requests (in contrast to `reviewers`, this option adds to the existing reviewer list, rather than replacing it)", + "type": "array", + "items": { + "type": "string" + } + }, "fileMatch": { "description": "RegEx (re2) pattern for matching manager files", "type": "array", diff --git a/test/workers/pr/__snapshots__/index.spec.ts.snap b/test/workers/pr/__snapshots__/index.spec.ts.snap index 29bb32942e8dd8ee8be2e02bf0edbc18ee3ba6d9..a80821eaf3ee717ccfd1a4b725e5ef3c0bafc07c 100644 --- a/test/workers/pr/__snapshots__/index.spec.ts.snap +++ b/test/workers/pr/__snapshots__/index.spec.ts.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`workers/pr ensurePr should add and deduplicate additionalReviewers on new PR 1`] = ` +Array [ + Array [ + undefined, + Array [ + "foo", + "bar", + "baz", + "boo", + ], + ], +] +`; + exports[`workers/pr ensurePr should add assignees and reviewers to new PR 1`] = ` Array [ Array [ diff --git a/test/workers/pr/index.spec.ts b/test/workers/pr/index.spec.ts index 39f83a0ef2d5c6cf34b039a91d7fdf86a10d27c3..a056263dc6198dd4f1b1f2a0fb667a8915f98f7e 100644 --- a/test/workers/pr/index.spec.ts +++ b/test/workers/pr/index.spec.ts @@ -325,6 +325,14 @@ describe('workers/pr', () => { expect(reviewers.length).toEqual(2); expect(config.reviewers).toEqual(expect.arrayContaining(reviewers)); }); + it('should add and deduplicate additionalReviewers on new PR', async () => { + config.reviewers = ['@foo', 'bar']; + config.additionalReviewers = ['bar', 'baz', '@boo']; + const pr = await prWorker.ensurePr(config); + expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); + expect(platform.addReviewers).toHaveBeenCalledTimes(1); + expect(platform.addReviewers.mock.calls).toMatchSnapshot(); + }); it('should return unmodified existing PR', async () => { platform.getBranchPr.mockResolvedValueOnce(existingPr); config.semanticCommitScope = null;