From 325cb70ff25719fcbb257b64532327affbde74f0 Mon Sep 17 00:00:00 2001
From: Justinas <12399634+justinas-b@users.noreply.github.com>
Date: Wed, 4 Jan 2023 10:53:17 +0200
Subject: [PATCH] fix(platform/gitlab): resolve assignees/reviewers from groups
 (#19469)

---
 lib/modules/platform/gitlab/http.ts       | 10 +++++++++-
 lib/modules/platform/gitlab/index.spec.ts | 24 +++++++++++++++++++++++
 lib/modules/platform/gitlab/index.ts      | 19 ++++++++++++++++--
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/lib/modules/platform/gitlab/http.ts b/lib/modules/platform/gitlab/http.ts
index ab327ef20a..07cbd04ed9 100644
--- a/lib/modules/platform/gitlab/http.ts
+++ b/lib/modules/platform/gitlab/http.ts
@@ -1,6 +1,6 @@
 import { logger } from '../../../logger';
 import { GitlabHttp } from '../../../util/http/gitlab';
-import type { GitlabUserStatus } from './types';
+import type { GitLabUser, GitlabUserStatus } from './types';
 
 export const gitlabApi = new GitlabHttp();
 
@@ -10,6 +10,14 @@ export async function getUserID(username: string): Promise<number> {
   ).body[0].id;
 }
 
+export async function getMemberUserIDs(group: string): Promise<number[]> {
+  const groupEncoded = encodeURIComponent(group);
+  const members = (
+    await gitlabApi.getJson<GitLabUser[]>(`groups/${groupEncoded}/members`)
+  ).body;
+  return members.map((u) => u.id);
+}
+
 export async function isUserBusy(user: string): Promise<boolean> {
   try {
     const url = `/users/${user}/status`;
diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts
index be45fcb3ee..41375babfd 100644
--- a/lib/modules/platform/gitlab/index.spec.ts
+++ b/lib/modules/platform/gitlab/index.spec.ts
@@ -1118,12 +1118,36 @@ describe('modules/platform/gitlab/index', () => {
           .get('/api/v4/users?username=someuser')
           .reply(200, [{ id: 10 }])
           .get('/api/v4/users?username=someotheruser')
+          .reply(404)
+          .get('/api/v4/groups/someotheruser/members')
           .reply(404);
 
         await gitlab.addReviewers(42, ['someuser', 'foo', 'someotheruser']);
         expect(scope.isDone()).toBeTrue();
       });
 
+      it('should add gitlab group members as reviewers to MR', async () => {
+        const scope = httpMock
+          .scope(gitlabApiHost)
+          .get(
+            '/api/v4/projects/undefined/merge_requests/42?include_diverged_commits_count=1'
+          )
+          .reply(200, { reviewers: existingReviewers })
+          .get('/api/v4/users?username=someuser')
+          .reply(200, [{ id: 10 }])
+          .get('/api/v4/users?username=somegroup')
+          .reply(404)
+          .get('/api/v4/groups/somegroup/members')
+          .reply(200, [{ id: 11 }, { id: 12 }])
+          .put('/api/v4/projects/undefined/merge_requests/42', {
+            reviewer_ids: [1, 2, 10, 11, 12],
+          })
+          .reply(200);
+
+        await gitlab.addReviewers(42, ['someuser', 'foo', 'somegroup']);
+        expect(scope.isDone()).toBeTrue();
+      });
+
       it('should fail to add reviewers to the MR', async () => {
         const scope = httpMock
           .scope(gitlabApiHost)
diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts
index 47a70abb0a..bc8de972fc 100644
--- a/lib/modules/platform/gitlab/index.ts
+++ b/lib/modules/platform/gitlab/index.ts
@@ -50,7 +50,7 @@ import type {
 } from '../types';
 import { repoFingerprint } from '../util';
 import { smartTruncate } from '../utils/pr-body';
-import { getUserID, gitlabApi, isUserBusy } from './http';
+import { getMemberUserIDs, getUserID, gitlabApi, isUserBusy } from './http';
 import { getMR, updateMR } from './merge-request';
 import type {
   GitLabMergeRequest,
@@ -1013,12 +1013,27 @@ export async function addReviewers(
   // Gather the IDs for all the reviewers we want to add
   let newReviewerIDs: number[];
   try {
-    newReviewerIDs = await p.all(newReviewers.map((r) => () => getUserID(r)));
+    newReviewerIDs = (
+      await p.all(
+        newReviewers.map((r) => async () => {
+          try {
+            return [await getUserID(r)];
+          } catch (err) {
+            // Unable to fetch userId, try resolve as a group
+            return getMemberUserIDs(r);
+          }
+        })
+      )
+    ).flat();
   } catch (err) {
     logger.warn({ err }, 'Failed to get IDs of the new reviewers');
     return;
   }
 
+  // Multiple groups may have the same members, so
+  // filter out non-distinct values
+  newReviewerIDs = [...new Set(newReviewerIDs)];
+
   try {
     await updateMR(config.repository, iid, {
       reviewer_ids: [...existingReviewerIDs, ...newReviewerIDs],
-- 
GitLab