From af3cd372aa779073284d38b8001889a7b65a8c96 Mon Sep 17 00:00:00 2001
From: Gilbert Gilb's <gilbsgilbs@users.noreply.github.com>
Date: Sun, 22 Sep 2019 09:19:26 +0200
Subject: [PATCH] feat(workers): add option to randomize assignees and
 reviewers. (#4517)

Closes #4516
---
 docs/usage/configuration-options.md |  8 ++++++++
 lib/config/definitions.ts           | 12 ++++++++++++
 lib/workers/pr/index.js             | 20 ++++++++++++++------
 renovate-schema.json                | 10 ++++++++++
 test/workers/pr/index.spec.js       | 17 +++++++++++++++++
 5 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 05eb77c664..d3b4b10519 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -33,6 +33,10 @@ By default, Renovate will not assign reviewers and assignees if the PR is to be
 
 Must be valid usernames.
 
+## assigneesSampleSize
+
+Take a random sample of given size from assignees.
+
 ## automerge
 
 By default, Renovate raises PRs but leaves them to someone/something else to merge them. By configuring this setting, you can enable Renovate to automerge branches or PRs itself, therefore reducing the amount of human intervention required.
@@ -1028,6 +1032,10 @@ Similar to `ignoreUnstable`, this option controls whether to update to versions
 
 Must be valid usernames. If on GitHub and assigning a team to review, use the prefix `team:`, e.g. provide a value like `team:someteam`.
 
+## reviewersSampleSize
+
+Take a random sample of given size from reviewers.
+
 ## rollbackPrs
 
 Set this to false either globally, per-language, or per-package if you want to disable Renovate's behaviour of generating rollback PRs when it can't find the current version on the registry anymore.
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index e139a3876c..1af8fa2f18 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -1241,6 +1241,12 @@ const options: RenovateOptions[] = [
     type: 'array',
     subType: 'string',
   },
+  {
+    name: 'assigneesSampleSize',
+    description: 'Take a random sample of given size from assignees.',
+    type: 'integer',
+    default: null,
+  },
   {
     name: 'assignAutomerge',
     description:
@@ -1255,6 +1261,12 @@ const options: RenovateOptions[] = [
     type: 'array',
     subType: 'string',
   },
+  {
+    name: 'reviewersSampleSize',
+    description: 'Take a random sample of given size from reviewers.',
+    type: 'integer',
+    default: null,
+  },
   {
     name: 'fileMatch',
     description: 'JS RegExp pattern for matching manager files',
diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js
index 052d739efb..dc953c5432 100644
--- a/lib/workers/pr/index.js
+++ b/lib/workers/pr/index.js
@@ -1,3 +1,5 @@
+const sampleSize = require('lodash/sampleSize');
+
 const { logger } = require('../../logger');
 const changelogHelper = require('./changelog');
 const { getPrBody } = require('./pr-body');
@@ -339,15 +341,18 @@ async function ensurePr(prConfig) {
 async function addAssigneesReviewers(config, pr) {
   if (config.assignees.length > 0) {
     try {
-      const assignees = config.assignees.map(assignee =>
+      let assignees = config.assignees.map(assignee =>
         assignee.length && assignee[0] === '@' ? assignee.slice(1) : assignee
       );
+      if (config.assigneesSampleSize !== null) {
+        assignees = sampleSize(assignees, config.assigneesSampleSize);
+      }
       // istanbul ignore if
       if (config.dryRun) {
         logger.info('DRY-RUN: Would add assignees to PR #' + pr.number);
       } else {
         await platform.addAssignees(pr.number, assignees);
-        logger.info({ assignees: config.assignees }, 'Added assignees');
+        logger.info({ assignees }, 'Added assignees');
       }
     } catch (err) {
       logger.info(
@@ -358,19 +363,22 @@ async function addAssigneesReviewers(config, pr) {
   }
   if (config.reviewers.length > 0) {
     try {
-      const reviewers = config.reviewers.map(reviewer =>
+      let reviewers = config.reviewers.map(reviewer =>
         reviewer.length && reviewer[0] === '@' ? reviewer.slice(1) : reviewer
       );
+      if (config.reviewersSampleSize !== null) {
+        reviewers = sampleSize(reviewers, config.reviewersSampleSize);
+      }
       // istanbul ignore if
       if (config.dryRun) {
-        logger.info('DRY-RUN: Would add assignees to PR #' + pr.number);
+        logger.info('DRY-RUN: Would add reviewers to PR #' + pr.number);
       } else {
         await platform.addReviewers(pr.number, reviewers);
-        logger.info({ reviewers: config.reviewers }, 'Added reviewers');
+        logger.info({ reviewers }, 'Added reviewers');
       }
     } catch (err) {
       logger.info(
-        { assignees: config.assignees, err },
+        { reviewers: config.reviewers, err },
         'Failed to add reviewers'
       );
     }
diff --git a/renovate-schema.json b/renovate-schema.json
index 04b676f41c..c06ef58622 100644
--- a/renovate-schema.json
+++ b/renovate-schema.json
@@ -784,6 +784,11 @@
         "type": "string"
       }
     },
+    "assigneesSampleSize": {
+      "description": "Take a random sample of given size from assignees.",
+      "type": "integer",
+      "default": null
+    },
     "assignAutomerge": {
       "description": "Assign reviewers and assignees even if the PR is to be automerged",
       "type": "boolean",
@@ -796,6 +801,11 @@
         "type": "string"
       }
     },
+    "reviewersSampleSize": {
+      "description": "Take a random sample of given size from reviewers.",
+      "type": "integer",
+      "default": null
+    },
     "fileMatch": {
       "description": "JS RegExp pattern for matching manager files",
       "type": "array",
diff --git a/test/workers/pr/index.spec.js b/test/workers/pr/index.spec.js
index f766722870..3bbf13823b 100644
--- a/test/workers/pr/index.spec.js
+++ b/test/workers/pr/index.spec.js
@@ -300,6 +300,23 @@ describe('workers/pr', () => {
       expect(platform.addAssignees).toHaveBeenCalledTimes(1);
       expect(platform.addReviewers).toHaveBeenCalledTimes(1);
     });
+    it('should add random sample of assignees and reviewers to new PR', async () => {
+      config.assignees = ['foo', 'bar', 'baz'];
+      config.assigneesSampleSize = 2;
+      config.reviewers = ['baz', 'boo', 'bor'];
+      config.reviewersSampleSize = 2;
+      await prWorker.ensurePr(config);
+
+      expect(platform.addAssignees).toHaveBeenCalledTimes(1);
+      const assignees = platform.addAssignees.mock.calls[0][1];
+      expect(assignees.length).toEqual(2);
+      expect(config.assignees).toEqual(expect.arrayContaining(assignees));
+
+      expect(platform.addReviewers).toHaveBeenCalledTimes(1);
+      const reviewers = platform.addReviewers.mock.calls[0][1];
+      expect(reviewers.length).toEqual(2);
+      expect(config.reviewers).toEqual(expect.arrayContaining(reviewers));
+    });
     it('should return unmodified existing PR', async () => {
       platform.getBranchPr.mockReturnValueOnce(existingPr);
       config.semanticCommitScope = null;
-- 
GitLab