From 7b5485edaa7348686e3b99fc405b12e5af95d515 Mon Sep 17 00:00:00 2001
From: David Straub <Scinvention@gmail.com>
Date: Tue, 14 Jan 2020 22:32:31 -0500
Subject: [PATCH] feat(workers): implement `additionalReviewers` option (#5152)

Closes #5121
---
 docs/usage/configuration-options.md           |  4 ++++
 lib/config/definitions.ts                     |  8 ++++++++
 lib/workers/pr/index.ts                       | 19 +++++++++++++------
 renovate-schema.json                          |  7 +++++++
 .../pr/__snapshots__/index.spec.ts.snap       | 14 ++++++++++++++
 test/workers/pr/index.spec.ts                 |  8 ++++++++
 6 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index e044922481..d5d7863fc5 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 2e7ef9f80c..c10561b22a 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 ca0be895dd..c8f9df166d 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 4bdb19e3d0..43b2586739 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 29bb32942e..a80821eaf3 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 39f83a0ef2..a056263dc6 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;
-- 
GitLab