From c86267762306b9b9a984175c6ade050d631c1b47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Brauer?= <zaubernerd@zaubernerd.de>
Date: Tue, 12 May 2020 23:21:58 +0200
Subject: [PATCH] feat(platform): remove comments by content (#6181)

---
 lib/platform/azure/index.spec.ts              |  66 ++++++----
 lib/platform/azure/index.ts                   |  49 ++++---
 .../__snapshots__/index.spec.ts.snap          |  36 +++++-
 lib/platform/bitbucket-server/index.spec.ts   |  15 ++-
 lib/platform/bitbucket-server/index.ts        |  25 +++-
 .../__snapshots__/comments.spec.ts.snap       |  11 +-
 lib/platform/bitbucket/comments.spec.ts       |  11 +-
 lib/platform/bitbucket/comments.ts            |  27 ++--
 lib/platform/bitbucket/index.ts               |   3 +-
 lib/platform/common.ts                        |  15 ++-
 lib/platform/gitea/index.spec.ts              |  16 ++-
 lib/platform/gitea/index.ts                   |  23 +++-
 .../github/__snapshots__/index.spec.ts.snap   | 122 +++++++++++++++++-
 lib/platform/github/index.spec.ts             |  20 ++-
 lib/platform/github/index.ts                  |  24 +++-
 lib/platform/gitlab/index.spec.ts             |  12 +-
 lib/platform/gitlab/index.ts                  |  32 +++--
 17 files changed, 409 insertions(+), 98 deletions(-)

diff --git a/lib/platform/azure/index.spec.ts b/lib/platform/azure/index.spec.ts
index 0762a6bbd9..e01cc79edb 100644
--- a/lib/platform/azure/index.spec.ts
+++ b/lib/platform/azure/index.spec.ts
@@ -777,40 +777,50 @@ describe('platform/azure', () => {
   });
 
   describe('ensureCommentRemoval', () => {
-    it('deletes comment if found', async () => {
+    let gitApiMock;
+    beforeEach(() => {
+      gitApiMock = {
+        getThreads: jest.fn(() => [
+          {
+            comments: [{ content: '### some-subject\n\nblabla' }],
+            id: 123,
+          },
+          {
+            comments: [{ content: 'some-content\n' }],
+            id: 124,
+          },
+        ]),
+        updateThread: jest.fn(),
+      };
+      azureApi.gitApi.mockImplementation(() => gitApiMock);
+    });
+    it('deletes comment by topic if found', async () => {
       await initRepo({ repository: 'some/repo' });
-      azureApi.gitApi.mockImplementation(
-        () =>
-          ({
-            getThreads: jest.fn(() => [
-              {
-                comments: [{ content: '### some-subject\n\nblabla' }],
-                id: 123,
-              },
-            ]),
-            updateThread: jest.fn(),
-          } as any)
-      );
       await azure.ensureCommentRemoval({ number: 42, topic: 'some-subject' });
-      expect(azureApi.gitApi).toHaveBeenCalledTimes(3);
+      expect(gitApiMock.getThreads).toHaveBeenCalledWith('1', 42);
+      expect(gitApiMock.updateThread).toHaveBeenCalledWith(
+        { status: 4 },
+        '1',
+        42,
+        123
+      );
     });
-    it('nothing should happen, no number', async () => {
-      await azure.ensureCommentRemoval({ number: 0, topic: 'test' });
-      expect(azureApi.gitApi).toHaveBeenCalledTimes(0);
+    it('deletes comment by content if found', async () => {
+      await initRepo({ repository: 'some/repo' });
+      await azure.ensureCommentRemoval({ number: 42, content: 'some-content' });
+      expect(gitApiMock.getThreads).toHaveBeenCalledWith('1', 42);
+      expect(gitApiMock.updateThread).toHaveBeenCalledWith(
+        { status: 4 },
+        '1',
+        42,
+        124
+      );
     });
     it('comment not found', async () => {
       await initRepo({ repository: 'some/repo' });
-      azureApi.gitApi.mockImplementation(
-        () =>
-          ({
-            getThreads: jest.fn(() => [
-              { comments: [{ content: 'stupid comment' }], id: 123 },
-            ]),
-            updateThread: jest.fn(),
-          } as any)
-      );
-      await azure.ensureCommentRemoval({ number: 42, topic: 'some-subject' });
-      expect(azureApi.gitApi).toHaveBeenCalledTimes(3);
+      await azure.ensureCommentRemoval({ number: 42, topic: 'does-not-exist' });
+      expect(gitApiMock.getThreads).toHaveBeenCalledWith('1', 42);
+      expect(gitApiMock.updateThread).not.toHaveBeenCalled();
     });
   });
 
diff --git a/lib/platform/azure/index.ts b/lib/platform/azure/index.ts
index 9bb3700045..52414ad774 100644
--- a/lib/platform/azure/index.ts
+++ b/lib/platform/azure/index.ts
@@ -1,5 +1,6 @@
 import {
   GitPullRequest,
+  GitPullRequestCommentThread,
   GitPullRequestMergeStrategy,
 } from 'azure-devops-node-api/interfaces/GitInterfaces';
 import { RenovateConfig } from '../../config/common';
@@ -572,29 +573,37 @@ export async function ensureComment({
 export async function ensureCommentRemoval({
   number: issueNo,
   topic,
+  content,
 }: EnsureCommentRemovalConfig): Promise<void> {
-  logger.debug(`ensureCommentRemoval(issueNo, topic)(${issueNo}, ${topic})`);
-  if (issueNo) {
-    const azureApiGit = await azureApi.gitApi();
-    const threads = await azureApiGit.getThreads(config.repoId, issueNo);
-    let threadIdFound = null;
+  logger.debug(
+    `Ensuring comment "${topic || content}" in #${issueNo} is removed`
+  );
 
-    threads.forEach((thread) => {
-      if (thread.comments[0].content.startsWith(`### ${topic}\n\n`)) {
-        threadIdFound = thread.id;
-      }
-    });
+  const azureApiGit = await azureApi.gitApi();
+  const threads = await azureApiGit.getThreads(config.repoId, issueNo);
 
-    if (threadIdFound) {
-      await azureApiGit.updateThread(
-        {
-          status: 4, // close
-        },
-        config.repoId,
-        issueNo,
-        threadIdFound
-      );
-    }
+  const byTopic = (thread: GitPullRequestCommentThread): boolean =>
+    thread.comments[0].content.startsWith(`### ${topic}\n\n`);
+  const byContent = (thread: GitPullRequestCommentThread): boolean =>
+    thread.comments[0].content.trim() === content;
+
+  let threadIdFound: number | null = null;
+
+  if (topic) {
+    threadIdFound = threads.find(byTopic)?.id;
+  } else if (content) {
+    threadIdFound = threads.find(byContent)?.id;
+  }
+
+  if (threadIdFound) {
+    await azureApiGit.updateThread(
+      {
+        status: 4, // close
+      },
+      config.repoId,
+      issueNo,
+      threadIdFound
+    );
   }
 }
 
diff --git a/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap b/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap
index 918f2a460b..35d7d34f24 100644
--- a/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap
+++ b/lib/platform/bitbucket-server/__snapshots__/index.spec.ts.snap
@@ -439,7 +439,23 @@ Array [
 ]
 `;
 
-exports[`platform/bitbucket-server endpoint with no path ensureCommentRemoval() deletes comment if found 1`] = `
+exports[`platform/bitbucket-server endpoint with no path ensureCommentRemoval() deletes comment by content if found 1`] = `
+Array [
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100",
+    undefined,
+  ],
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100&start=1",
+    undefined,
+  ],
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/comments/22",
+  ],
+]
+`;
+
+exports[`platform/bitbucket-server endpoint with no path ensureCommentRemoval() deletes comment by topic if found 1`] = `
 Array [
   Array [
     "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100",
@@ -2002,7 +2018,23 @@ Array [
 ]
 `;
 
-exports[`platform/bitbucket-server endpoint with path ensureCommentRemoval() deletes comment if found 1`] = `
+exports[`platform/bitbucket-server endpoint with path ensureCommentRemoval() deletes comment by content if found 1`] = `
+Array [
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100",
+    undefined,
+  ],
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100&start=1",
+    undefined,
+  ],
+  Array [
+    "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/comments/22",
+  ],
+]
+`;
+
+exports[`platform/bitbucket-server endpoint with path ensureCommentRemoval() deletes comment by topic if found 1`] = `
 Array [
   Array [
     "./rest/api/1.0/projects/SOME/repos/repo/pull-requests/5/activities?limit=100",
diff --git a/lib/platform/bitbucket-server/index.spec.ts b/lib/platform/bitbucket-server/index.spec.ts
index 2c90f2b2b1..9dc79ed5a5 100644
--- a/lib/platform/bitbucket-server/index.spec.ts
+++ b/lib/platform/bitbucket-server/index.spec.ts
@@ -491,7 +491,7 @@ describe('platform/bitbucket-server', () => {
           expect(api.get.mock.calls).toMatchSnapshot();
         });
 
-        it('deletes comment if found', async () => {
+        it('deletes comment by topic if found', async () => {
           expect.assertions(2);
           await initRepo();
           api.get.mockClear();
@@ -504,6 +504,19 @@ describe('platform/bitbucket-server', () => {
           expect(api.delete).toHaveBeenCalledTimes(1);
         });
 
+        it('deletes comment by content if found', async () => {
+          expect.assertions(2);
+          await initRepo();
+          api.get.mockClear();
+
+          await bitbucket.ensureCommentRemoval({
+            number: 5,
+            content: '!merge',
+          });
+          expect(api.get.mock.calls).toMatchSnapshot();
+          expect(api.delete).toHaveBeenCalledTimes(1);
+        });
+
         it('deletes nothing', async () => {
           expect.assertions(2);
           await initRepo();
diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts
index d5af777596..ca495e9c05 100644
--- a/lib/platform/bitbucket-server/index.ts
+++ b/lib/platform/bitbucket-server/index.ts
@@ -867,16 +867,27 @@ export async function ensureComment({
 export async function ensureCommentRemoval({
   number: prNo,
   topic,
+  content,
 }: EnsureCommentRemovalConfig): Promise<void> {
   try {
-    logger.debug(`Ensuring comment "${topic}" in #${prNo} is removed`);
+    logger.debug(
+      `Ensuring comment "${topic || content}" in #${prNo} is removed`
+    );
     const comments = await getComments(prNo);
-    let commentId: number;
-    comments.forEach((comment) => {
-      if (comment.text.startsWith(`### ${topic}\n\n`)) {
-        commentId = comment.id;
-      }
-    });
+
+    const byTopic = (comment: Comment): boolean =>
+      comment.text.startsWith(`### ${topic}\n\n`);
+    const byContent = (comment: Comment): boolean =>
+      comment.text.trim() === content;
+
+    let commentId: number | null = null;
+
+    if (topic) {
+      commentId = comments.find(byTopic)?.id;
+    } else if (content) {
+      commentId = comments.find(byContent)?.id;
+    }
+
     if (commentId) {
       await deleteComment(prNo, commentId);
     }
diff --git a/lib/platform/bitbucket/__snapshots__/comments.spec.ts.snap b/lib/platform/bitbucket/__snapshots__/comments.spec.ts.snap
index 0e7a5aeef0..bb1301f380 100644
--- a/lib/platform/bitbucket/__snapshots__/comments.spec.ts.snap
+++ b/lib/platform/bitbucket/__snapshots__/comments.spec.ts.snap
@@ -63,7 +63,16 @@ Array [
 ]
 `;
 
-exports[`platform/comments ensureCommentRemoval() deletes comment if found 1`] = `
+exports[`platform/comments ensureCommentRemoval() deletes comment by content if found 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureCommentRemoval() deletes comment by topic if found 1`] = `
 Array [
   Array [
     "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
diff --git a/lib/platform/bitbucket/comments.spec.ts b/lib/platform/bitbucket/comments.spec.ts
index bd42fd8c41..6488be7873 100644
--- a/lib/platform/bitbucket/comments.spec.ts
+++ b/lib/platform/bitbucket/comments.spec.ts
@@ -147,7 +147,7 @@ describe('platform/comments', () => {
       expect(api.get.mock.calls).toMatchSnapshot();
     });
 
-    it('deletes comment if found', async () => {
+    it('deletes comment by topic if found', async () => {
       expect.assertions(2);
       api.get.mockClear();
 
@@ -156,6 +156,15 @@ describe('platform/comments', () => {
       expect(api.delete).toHaveBeenCalledTimes(1);
     });
 
+    it('deletes comment by content if found', async () => {
+      expect.assertions(2);
+      api.get.mockClear();
+
+      await comments.ensureCommentRemoval(config, 5, undefined, '!merge');
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.delete).toHaveBeenCalledTimes(1);
+    });
+
     it('deletes nothing', async () => {
       expect.assertions(2);
       api.get.mockClear();
diff --git a/lib/platform/bitbucket/comments.ts b/lib/platform/bitbucket/comments.ts
index 1c86bd1040..86a1c40086 100644
--- a/lib/platform/bitbucket/comments.ts
+++ b/lib/platform/bitbucket/comments.ts
@@ -115,17 +115,28 @@ export async function ensureComment({
 export async function ensureCommentRemoval(
   config: CommentsConfig,
   prNo: number,
-  topic: string
+  topic?: string,
+  content?: string
 ): Promise<void> {
   try {
-    logger.debug(`Ensuring comment "${topic}" in #${prNo} is removed`);
+    logger.debug(
+      `Ensuring comment "${topic || content}" in #${prNo} is removed`
+    );
     const comments = await getComments(config, prNo);
-    let commentId: number;
-    comments.forEach((comment) => {
-      if (comment.content.raw.startsWith(`### ${topic}\n\n`)) {
-        commentId = comment.id;
-      }
-    });
+
+    const byTopic = (comment: Comment): boolean =>
+      comment.content.raw.startsWith(`### ${topic}\n\n`);
+    const byContent = (comment: Comment): boolean =>
+      comment.content.raw.trim() === content;
+
+    let commentId: number | null = null;
+
+    if (topic) {
+      commentId = comments.find(byTopic)?.id;
+    } else if (content) {
+      commentId = comments.find(byContent)?.id;
+    }
+
     if (commentId) {
       await deleteComment(config, prNo, commentId);
     }
diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts
index 39f1fa85a0..bba4830baf 100644
--- a/lib/platform/bitbucket/index.ts
+++ b/lib/platform/bitbucket/index.ts
@@ -710,8 +710,9 @@ export function ensureComment({
 export function ensureCommentRemoval({
   number: prNo,
   topic,
+  content,
 }: EnsureCommentRemovalConfig): Promise<void> {
-  return comments.ensureCommentRemoval(config, prNo, topic);
+  return comments.ensureCommentRemoval(config, prNo, topic, content);
 }
 
 // Creates PR and returns PR number
diff --git a/lib/platform/common.ts b/lib/platform/common.ts
index 6690e178b4..38bc903153 100644
--- a/lib/platform/common.ts
+++ b/lib/platform/common.ts
@@ -166,10 +166,19 @@ export interface EnsureCommentConfig {
   topic: string;
   content: string;
 }
+
+export interface EnsureCommentRemovalConfigByTopic {
+  number: number;
+  topic: string;
+}
+export interface EnsureCommentRemovalConfigByContent {
+  number: number;
+  content: string;
+}
 export interface EnsureCommentRemovalConfig {
   number: number;
-  topic?: string;
   content?: string;
+  topic?: string;
 }
 
 export type EnsureIssueResult = 'updated' | 'created';
@@ -207,7 +216,9 @@ export interface Platform {
     context: string
   ): Promise<BranchStatus | null>;
   ensureCommentRemoval(
-    ensureCommentRemoval: EnsureCommentRemovalConfig
+    ensureCommentRemoval:
+      | EnsureCommentRemovalConfigByTopic
+      | EnsureCommentRemovalConfigByContent
   ): Promise<void>;
   deleteBranch(branchName: string, closePr?: boolean): Promise<void>;
   ensureComment(ensureComment: EnsureCommentConfig): Promise<boolean>;
diff --git a/lib/platform/gitea/index.spec.ts b/lib/platform/gitea/index.spec.ts
index 49ddde3a4e..1ab1a02d09 100644
--- a/lib/platform/gitea/index.spec.ts
+++ b/lib/platform/gitea/index.spec.ts
@@ -1322,16 +1322,22 @@ index 0000000..2173594
   });
 
   describe('ensureCommentRemoval', () => {
-    it('should remove existing comment', async () => {
+    it('should remove existing comment by topic', async () => {
       helper.getComments.mockResolvedValueOnce(mockComments);
       await initFakeRepo();
       await gitea.ensureCommentRemoval({ number: 1, topic: 'some-topic' });
 
       expect(helper.deleteComment).toHaveBeenCalledTimes(1);
-      expect(helper.deleteComment).toHaveBeenCalledWith(
-        mockRepo.full_name,
-        expect.any(Number)
-      );
+      expect(helper.deleteComment).toHaveBeenCalledWith(mockRepo.full_name, 3);
+    });
+
+    it('should remove existing comment by content', async () => {
+      helper.getComments.mockResolvedValueOnce(mockComments);
+      await initFakeRepo();
+      await gitea.ensureCommentRemoval({ number: 1, content: 'some-body' });
+
+      expect(helper.deleteComment).toHaveBeenCalledTimes(1);
+      expect(helper.deleteComment).toHaveBeenCalledWith(mockRepo.full_name, 1);
     });
 
     it('should gracefully fail with warning', async () => {
diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts
index ff00f8d0aa..bc47b23f13 100644
--- a/lib/platform/gitea/index.ts
+++ b/lib/platform/gitea/index.ts
@@ -128,6 +128,13 @@ function findCommentByTopic(
   return comments.find((c) => c.body.startsWith(`### ${topic}\n\n`));
 }
 
+function findCommentByContent(
+  comments: helper.Comment[],
+  content: string
+): helper.Comment | null {
+  return comments.find((c) => c.body.trim() === content);
+}
+
 async function isPRModified(
   repoPath: string,
   branchName: string
@@ -820,13 +827,23 @@ const platform: Platform = {
   async ensureCommentRemoval({
     number: issue,
     topic,
+    content,
   }: EnsureCommentRemovalConfig): Promise<void> {
+    logger.debug(
+      `Ensuring comment "${topic || content}" in #${issue} is removed`
+    );
     const commentList = await helper.getComments(config.repository, issue);
-    const comment = findCommentByTopic(commentList, topic);
+    let comment: helper.Comment | null = null;
+
+    if (topic) {
+      comment = findCommentByTopic(commentList, topic);
+    } else if (content) {
+      comment = findCommentByContent(commentList, content);
+    }
 
     // Abort and do nothing if no matching comment was found
     if (!comment) {
-      return null;
+      return;
     }
 
     // Attempt to delete comment
@@ -835,8 +852,6 @@ const platform: Platform = {
     } catch (err) {
       logger.warn({ err, issue, subject: topic }, 'Error deleting comment');
     }
-
-    return null;
   },
 
   async getBranchPr(branchName: string): Promise<Pr | null> {
diff --git a/lib/platform/github/__snapshots__/index.spec.ts.snap b/lib/platform/github/__snapshots__/index.spec.ts.snap
index 7dea1c622b..9b6a588d17 100644
--- a/lib/platform/github/__snapshots__/index.spec.ts.snap
+++ b/lib/platform/github/__snapshots__/index.spec.ts.snap
@@ -802,7 +802,127 @@ Array [
 ]
 `;
 
-exports[`platform/github ensureCommentRemoval deletes comment if found 1`] = `
+exports[`platform/github ensureCommentRemoval deletes comment by content if found 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc123",
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.github.com/repos/some/repo",
+  },
+  Object {
+    "graphql": Object {
+      "query": Object {
+        "repository": Object {
+          "__args": Object {
+            "name": "repo",
+            "owner": "some",
+          },
+          "pullRequests": Object {
+            "__args": Object {
+              "first": "100",
+            },
+            "nodes": Object {
+              "comments": Object {
+                "__args": Object {
+                  "last": "100",
+                },
+                "nodes": Object {
+                  "body": null,
+                  "databaseId": null,
+                },
+              },
+              "headRefName": null,
+              "number": null,
+              "state": null,
+              "title": null,
+            },
+          },
+        },
+      },
+    },
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc123",
+      "content-length": 513,
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "POST",
+    "url": "https://api.github.com/graphql",
+  },
+  Object {
+    "graphql": Object {
+      "query": Object {
+        "repository": Object {
+          "__args": Object {
+            "name": "repo",
+            "owner": "some",
+          },
+          "pullRequests": Object {
+            "__args": Object {
+              "first": "25",
+            },
+            "nodes": Object {
+              "comments": Object {
+                "__args": Object {
+                  "last": "100",
+                },
+                "nodes": Object {
+                  "body": null,
+                  "databaseId": null,
+                },
+              },
+              "headRefName": null,
+              "number": null,
+              "state": null,
+              "title": null,
+            },
+          },
+        },
+      },
+    },
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc123",
+      "content-length": 512,
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "POST",
+    "url": "https://api.github.com/graphql",
+  },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc123",
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.github.com/repos/some/repo/issues/42/comments?per_page=100",
+  },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token abc123",
+      "host": "api.github.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "DELETE",
+    "url": "https://api.github.com/repos/some/repo/issues/comments/1234",
+  },
+]
+`;
+
+exports[`platform/github ensureCommentRemoval deletes comment by topic if found 1`] = `
 Array [
   Object {
     "headers": Object {
diff --git a/lib/platform/github/index.spec.ts b/lib/platform/github/index.spec.ts
index c651112481..bf32b2283d 100644
--- a/lib/platform/github/index.spec.ts
+++ b/lib/platform/github/index.spec.ts
@@ -1426,7 +1426,7 @@ describe('platform/github', () => {
     });
   });
   describe('ensureCommentRemoval', () => {
-    it('deletes comment if found', async () => {
+    it('deletes comment by topic if found', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
@@ -1441,6 +1441,24 @@ describe('platform/github', () => {
       await github.ensureCommentRemoval({ number: 42, topic: 'some-subject' });
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
+    it('deletes comment by content if found', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope
+        .post('/graphql')
+        .twice()
+        .reply(200, {})
+        .get('/repos/some/repo/issues/42/comments?per_page=100')
+        .reply(200, [{ id: 1234, body: 'some-content' }])
+        .delete('/repos/some/repo/issues/comments/1234')
+        .reply(200);
+      await github.initRepo({ repository: 'some/repo', token: 'token' } as any);
+      await github.ensureCommentRemoval({
+        number: 42,
+        content: 'some-content',
+      });
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
   });
   describe('findPr(branchName, prTitle, state)', () => {
     it('returns true if no title and all state', async () => {
diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts
index 636d2fe811..108fb64602 100644
--- a/lib/platform/github/index.ts
+++ b/lib/platform/github/index.ts
@@ -1568,15 +1568,25 @@ export async function ensureComment({
 export async function ensureCommentRemoval({
   number: issueNo,
   topic,
+  content,
 }: EnsureCommentRemovalConfig): Promise<void> {
-  logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`);
+  logger.debug(
+    `Ensuring comment "${topic || content}" in #${issueNo} is removed`
+  );
   const comments = await getComments(issueNo);
-  let commentId: number;
-  comments.forEach((comment) => {
-    if (comment.body.startsWith(`### ${topic}\n\n`)) {
-      commentId = comment.id;
-    }
-  });
+  let commentId: number | null = null;
+
+  const byTopic = (comment: Comment): boolean =>
+    comment.body.startsWith(`### ${topic}\n\n`);
+  const byContent = (comment: Comment): boolean =>
+    comment.body.trim() === content;
+
+  if (topic) {
+    commentId = comments.find(byTopic)?.id;
+  } else if (content) {
+    commentId = comments.find(byContent)?.id;
+  }
+
   try {
     if (commentId) {
       await deleteComment(commentId);
diff --git a/lib/platform/gitlab/index.spec.ts b/lib/platform/gitlab/index.spec.ts
index 87b0787e72..40f748ca9f 100644
--- a/lib/platform/gitlab/index.spec.ts
+++ b/lib/platform/gitlab/index.spec.ts
@@ -850,7 +850,7 @@ describe('platform/gitlab', () => {
     });
   });
   describe('ensureCommentRemoval', () => {
-    it('deletes comment if found', async () => {
+    it('deletes comment by topic if found', async () => {
       await initRepo({ repository: 'some/repo', token: 'token' });
       api.get.mockResolvedValueOnce(
         partial<GotResponse>({
@@ -860,6 +860,16 @@ describe('platform/gitlab', () => {
       await gitlab.ensureCommentRemoval({ number: 42, topic: 'some-subject' });
       expect(api.delete).toHaveBeenCalledTimes(1);
     });
+    it('deletes comment by content if found', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      api.get.mockResolvedValueOnce(
+        partial<GotResponse>({
+          body: [{ id: 1234, body: 'some-body\n' }],
+        })
+      );
+      await gitlab.ensureCommentRemoval({ number: 42, content: 'some-body' });
+      expect(api.delete).toHaveBeenCalledTimes(1);
+    });
   });
   describe('findPr(branchName, prTitle, state)', () => {
     it('returns true if no title and all state', async () => {
diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts
index 0e88681bb7..2e0a380576 100644
--- a/lib/platform/gitlab/index.ts
+++ b/lib/platform/gitlab/index.ts
@@ -848,7 +848,7 @@ export async function deleteLabel(
   }
 }
 
-async function getComments(issueNo: number): Promise<any[]> {
+async function getComments(issueNo: number): Promise<GitlabComment[]> {
   // GET projects/:owner/:repo/merge_requests/:number/notes
   logger.debug(`Getting comments for #${issueNo}`);
   const url = `projects/${config.repository}/merge_requests/${issueNo}/notes`;
@@ -942,18 +942,34 @@ export async function ensureComment({
   return true;
 }
 
+type GitlabComment = {
+  body: string;
+  id: number;
+};
+
 export async function ensureCommentRemoval({
   number: issueNo,
   topic,
+  content,
 }: EnsureCommentRemovalConfig): Promise<void> {
-  logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`);
+  logger.debug(
+    `Ensuring comment "${topic || content}" in #${issueNo} is removed`
+  );
+
   const comments = await getComments(issueNo);
-  let commentId: number;
-  comments.forEach((comment: { body: string; id: number }) => {
-    if (comment.body.startsWith(`### ${topic}\n\n`)) {
-      commentId = comment.id;
-    }
-  });
+  let commentId: number | null = null;
+
+  const byTopic = (comment: GitlabComment): boolean =>
+    comment.body.startsWith(`### ${topic}\n\n`);
+  const byContent = (comment: GitlabComment): boolean =>
+    comment.body.trim() === content;
+
+  if (topic) {
+    commentId = comments.find(byTopic)?.id;
+  } else if (content) {
+    commentId = comments.find(byContent)?.id;
+  }
+
   if (commentId) {
     await deleteComment(issueNo, commentId);
   }
-- 
GitLab