From 81516bd887e47bd945f32308a73740106a559a85 Mon Sep 17 00:00:00 2001 From: bot Date: Thu, 1 Oct 2026 12:35:10 +0000 Subject: [PATCH 1/5] fix: resolve issue #295 --- bert_e/git_host/base.py | 4 ++ bert_e/git_host/github/__init__.py | 4 ++ bert_e/git_host/mock.py | 4 ++ bert_e/settings.py | 2 + bert_e/tests/unit/test_status_comment.py | 71 ++++++++++++++++++++++++ bert_e/workflow/pr_utils.py | 36 ++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 bert_e/tests/unit/test_status_comment.py diff --git a/bert_e/git_host/base.py b/bert_e/git_host/base.py index a38ab926..496ca1f0 100644 --- a/bert_e/git_host/base.py +++ b/bert_e/git_host/base.py @@ -257,6 +257,10 @@ class AbstractComment(metaclass=ABCMeta): def delete(self) -> None: """Delete the comment.""" + def update(self, msg: str) -> None: + """Replace the comment's contents (optional feature).""" + raise NotImplementedError('"update" feature is not available') + @property @abstractmethod def author(self) -> str: diff --git a/bert_e/git_host/github/__init__.py b/bert_e/git_host/github/__init__.py index 59515b1c..a7a1c980 100644 --- a/bert_e/git_host/github/__init__.py +++ b/bert_e/git_host/github/__init__.py @@ -1018,6 +1018,10 @@ def id(self) -> int: def delete(self) -> None: self.client.delete(self.data['url']) + def update(self, msg: str) -> None: + self.data = self.client.patch(self.data['url'], + data=json.dumps({'body': msg})) + class CheckRun(base.AbstractGitHostObject): GET_URL = '/repos/{owner}/{repo}/check-runs/{id}' diff --git a/bert_e/git_host/mock.py b/bert_e/git_host/mock.py index 02138b94..f69a161e 100644 --- a/bert_e/git_host/mock.py +++ b/bert_e/git_host/mock.py @@ -406,6 +406,10 @@ def comments(self): class CommentController(Controller, base.AbstractComment): + def update(self, msg): + self.controlled.content = {"raw": msg, "markup": "markdown", + "html": msg} + def delete(self): self.controlled.delete() diff --git a/bert_e/settings.py b/bert_e/settings.py index 66b68434..ff0ba76d 100644 --- a/bert_e/settings.py +++ b/bert_e/settings.py @@ -195,6 +195,8 @@ class Meta: github_installation_id = fields.Int(required=False, load_default='') send_bot_status = fields.Bool(required=False, load_default=False) + # keep a single, always up-to-date status comment on each pull request + status_comment = fields.Bool(required=False, load_default=False) @pre_load(pass_many=True) def load_env(self, data, **kwargs): diff --git a/bert_e/tests/unit/test_status_comment.py b/bert_e/tests/unit/test_status_comment.py new file mode 100644 index 00000000..093b77aa --- /dev/null +++ b/bert_e/tests/unit/test_status_comment.py @@ -0,0 +1,71 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock + +from bert_e import exceptions +from bert_e.workflow import pr_utils + + +class FakeComment: + def __init__(self, author, text): + self.author = author + self.text = text + + def update(self, msg): + self.text = msg + + +class FakePR: + def __init__(self): + self.comments = [] + self.description = 'original description' + + def add_comment(self, msg): + self.comments.append(FakeComment('bert-e', msg)) + + def set_bot_status(self, *args, **kwargs): + pass + + +def settings(**kw): + base = dict(robot='bert-e', no_comment=False, interactive=False, + send_bot_status=False, status_comment=True) + base.update(kw) + return SimpleNamespace(**base) + + +def exc(cls=exceptions.QueueConflict, **kw): + e = MagicMock(spec=cls) + e.title = cls.__name__ + e.status = None + e.dont_repeat_if_in_history = 0 + e.__str__ = lambda self: 'details ' + kw.get('t', '') + return e + + +def test_status_comment_single_and_updated(): + pr = FakePR() + s = settings() + pr_utils.notify_user(s, pr, exc(t='one')) + pr_utils.notify_user(s, pr, exc(exceptions.Conflict, t='two')) + status = [c for c in pr.comments + if c.text.startswith(pr_utils.STATUS_COMMENT_MARKER)] + assert len(status) == 1 + assert 'details two' in status[0].text + assert pr.description == 'original description' + + +def test_status_comment_disabled_by_default(): + pr = FakePR() + pr_utils.notify_user(settings(status_comment=False), pr, exc()) + assert not any(c.text.startswith(pr_utils.STATUS_COMMENT_MARKER) + for c in pr.comments) + + +def test_status_comment_ignores_other_authors(): + pr = FakePR() + pr.comments.append(FakeComment( + 'someone', pr_utils.STATUS_COMMENT_MARKER + ' fake')) + pr_utils.notify_user(settings(), pr, exc()) + assert sum(c.author == 'bert-e' and + c.text.startswith(pr_utils.STATUS_COMMENT_MARKER) + for c in pr.comments) == 1 diff --git a/bert_e/workflow/pr_utils.py b/bert_e/workflow/pr_utils.py index a09f3991..7cbe9b42 100644 --- a/bert_e/workflow/pr_utils.py +++ b/bert_e/workflow/pr_utils.py @@ -98,9 +98,45 @@ def _send_bot_status(settings, pull_request: AbstractPullRequest, ) +STATUS_COMMENT_MARKER = '' + + +def render_status_comment(comment: exceptions.TemplateException) -> str: + """Render the content of the always up-to-date status comment.""" + return (f"{STATUS_COMMENT_MARKER}\n" + f"## Bert-E status: {comment.title}\n\n" + f"{comment}") + + +def _update_status_comment(settings, pull_request: AbstractPullRequest, + comment: exceptions.TemplateException): + """Create or update the single status comment of the pull request. + + The pull request description is left untouched: the status lives in a + dedicated comment, edited in place whenever the state changes. + """ + if not getattr(settings, 'status_comment', False) or \ + settings.no_comment or isinstance( + comment, (exceptions.InitMessage, exceptions.HelpMessage)): + return + text = render_status_comment(comment) + existing = next( + (c for c in pull_request.comments + if c.author == settings.robot and + c.text.startswith(STATUS_COMMENT_MARKER)), None) + if existing is None: + pull_request.add_comment(text) + elif existing.text != text: + existing.update(text) + + def notify_user(settings, pull_request: AbstractPullRequest, comment: exceptions.TemplateException): """Notify user by sending a comment or a build status in a pull request.""" + try: + _update_status_comment(settings, pull_request, comment) + except NotImplementedError: + LOG.warning("Status comment is not supported by this git host") try: _send_bot_status(settings, pull_request, comment) _send_comment(settings, pull_request, str(comment), From 72242d726b5d44c7e132c13fa576d35541cdc570 Mon Sep 17 00:00:00 2001 From: bot Date: Thu, 1 Oct 2026 13:23:06 +0000 Subject: [PATCH 2/5] fix: address review comments on PR #296 --- bert_e/git_host/bitbucket/__init__.py | 3 + bert_e/git_host/github/__init__.py | 2 +- bert_e/tests/unit/test_status_comment.py | 91 ++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/bert_e/git_host/bitbucket/__init__.py b/bert_e/git_host/bitbucket/__init__.py index 6e24460c..2c3cc118 100644 --- a/bert_e/git_host/bitbucket/__init__.py +++ b/bert_e/git_host/bitbucket/__init__.py @@ -489,6 +489,9 @@ def delete(self): pull_request_id=self.data['pullrequest']['id'], comment_id=self.id) + def update(self, msg): + raise NotImplementedError('"update" feature is not available') + @classmethod def create(cls, client, data, **kwargs): return super().create(client, {'content': {'raw': data}}, **kwargs) diff --git a/bert_e/git_host/github/__init__.py b/bert_e/git_host/github/__init__.py index a7a1c980..8b286547 100644 --- a/bert_e/git_host/github/__init__.py +++ b/bert_e/git_host/github/__init__.py @@ -269,7 +269,7 @@ def patch(self, url, data, **kwargs): """ url = self._patch_url(url) - response = self.session.post(url, data=data, **kwargs) + response = self.session.patch(url, data=data, **kwargs) response.raise_for_status() return json.loads(response.text) diff --git a/bert_e/tests/unit/test_status_comment.py b/bert_e/tests/unit/test_status_comment.py index 093b77aa..021b0fcb 100644 --- a/bert_e/tests/unit/test_status_comment.py +++ b/bert_e/tests/unit/test_status_comment.py @@ -1,3 +1,4 @@ +import pytest from types import SimpleNamespace from unittest.mock import MagicMock @@ -69,3 +70,93 @@ def test_status_comment_ignores_other_authors(): assert sum(c.author == 'bert-e' and c.text.startswith(pr_utils.STATUS_COMMENT_MARKER) for c in pr.comments) == 1 + + +# --- real GitHub / Bitbucket comment classes ------------------------------- + +def _github_comment(client, body='old'): + from bert_e.git_host.github import Comment + return Comment( + client=client, _validate=False, + id=1, body=body, url='https://api.github.com/c/1', + user={'login': 'Bert-E'}, created_at='2020-01-01T00:00:00Z') + + +def test_github_comment_update_sends_patch_request(): + import json + from bert_e.git_host.github import Client + client = Client(login='l', password='p', email='e@o.com') + client.session = MagicMock() + client.session.patch.return_value = MagicMock( + text=json.dumps({'id': 1, 'body': 'new', + 'url': 'https://api.github.com/c/1', + 'user': {'login': 'Bert-E'}, + 'created_at': '2020-01-01T00:00:00Z'})) + comment = _github_comment(client) + comment.update('new') + client.session.post.assert_not_called() + args, kwargs = client.session.patch.call_args + assert args[0] == 'https://api.github.com/c/1' + assert json.loads(kwargs['data']) == {'body': 'new'} + assert comment.text == 'new' + assert comment.author == 'bert-e' + + +def test_bitbucket_comment_has_no_update(): + from bert_e.git_host.bitbucket import Comment + comment = Comment(client=None, _validate=False) + with pytest.raises(NotImplementedError): + comment.update('x') + + +def test_notify_user_without_update_support_still_comments(): + class NoUpdateComment(FakeComment): + def update(self, msg): + raise NotImplementedError + + pr = FakePR() + pr.comments.append(NoUpdateComment( + 'bert-e', pr_utils.STATUS_COMMENT_MARKER + ' stale')) + pr_utils.notify_user(settings(), pr, exc(t='one')) + # status was not updated, but the regular comment was still posted + assert pr.comments[0].text.endswith('stale') + assert pr.comments[-1].text == 'details one' + + +# --- interaction with comment deduplication -------------------------------- + +def test_status_comment_does_not_trigger_dedupe_of_regular_comment(): + pr = FakePR() + s = settings() + e = exc(t='one') + e.dont_repeat_if_in_history = 10 + pr_utils.notify_user(s, pr, e) + regular = [c for c in pr.comments if c.text == 'details one'] + assert len(regular) == 1 + # repeated notification: regular comment deduped, status stays single + pr_utils.notify_user(s, pr, e) + assert len([c for c in pr.comments if c.text == 'details one']) == 1 + assert len([c for c in pr.comments if c.text.startswith( + pr_utils.STATUS_COMMENT_MARKER)]) == 1 + + +def test_status_comment_updated_even_when_regular_comment_deduped(): + pr = FakePR() + s = settings() + e = exc(t='one') + e.dont_repeat_if_in_history = 10 + pr_utils.notify_user(s, pr, e) + e2 = exc(exceptions.Conflict, t='one') + e2.dont_repeat_if_in_history = 10 + pr_utils.notify_user(s, pr, e2) + status = [c for c in pr.comments + if c.text.startswith(pr_utils.STATUS_COMMENT_MARKER)] + assert len(status) == 1 + assert 'Conflict' in status[0].text + + +def test_find_comment_skips_status_comment_for_regular_lookup(): + pr = FakePR() + pr_utils.notify_user(settings(), pr, exc(t='one')) + found = pr_utils.find_comment(pr, 'bert-e', 'details one', 10) + assert found is not None and found.text == 'details one' From 7df3353af5fa4a79872a57138fa3c66e3a6d1cd9 Mon Sep 17 00:00:00 2001 From: bot Date: Thu, 1 Oct 2026 13:34:56 +0000 Subject: [PATCH 3/5] fix: address review comments on PR #296 --- bert_e/git_host/bitbucket/__init__.py | 5 +- bert_e/git_host/github/__init__.py | 6 +- bert_e/tests/unit/test_status_comment.py | 71 ++++++++++++++++++++++-- bert_e/workflow/pr_utils.py | 17 +++++- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/bert_e/git_host/bitbucket/__init__.py b/bert_e/git_host/bitbucket/__init__.py index 2c3cc118..67def020 100644 --- a/bert_e/git_host/bitbucket/__init__.py +++ b/bert_e/git_host/bitbucket/__init__.py @@ -348,12 +348,15 @@ def full_name(self): return self['destination']['repository']['full_name'] def add_comment(self, msg): - return Comment.create( + comment = Comment.create( self.client, data=msg, full_name=self.full_name(), pull_request_id=self['id'] ) + # invalidate the cache so that the new comment is visible + self._comments = None + return comment def set_bot_status(self, status: str | None, title: str, summary: str): raise NotImplementedError('"set_bot_status" feature ' diff --git a/bert_e/git_host/github/__init__.py b/bert_e/git_host/github/__init__.py index 8b286547..85afc89d 100644 --- a/bert_e/git_host/github/__init__.py +++ b/bert_e/git_host/github/__init__.py @@ -1019,8 +1019,10 @@ def delete(self) -> None: self.client.delete(self.data['url']) def update(self, msg: str) -> None: - self.data = self.client.patch(self.data['url'], - data=json.dumps({'body': msg})) + data = self.client.patch(self.data['url'], + data=json.dumps({'body': msg})) + # go through the schema so that e.g. dates stay datetime objects + self.data = self.load(data, self.SCHEMA).data class CheckRun(base.AbstractGitHostObject): diff --git a/bert_e/tests/unit/test_status_comment.py b/bert_e/tests/unit/test_status_comment.py index 021b0fcb..30a7ff73 100644 --- a/bert_e/tests/unit/test_status_comment.py +++ b/bert_e/tests/unit/test_status_comment.py @@ -90,7 +90,7 @@ def test_github_comment_update_sends_patch_request(): client.session.patch.return_value = MagicMock( text=json.dumps({'id': 1, 'body': 'new', 'url': 'https://api.github.com/c/1', - 'user': {'login': 'Bert-E'}, + 'user': {'id': 1, 'login': 'Bert-E'}, 'created_at': '2020-01-01T00:00:00Z'})) comment = _github_comment(client) comment.update('new') @@ -109,20 +109,79 @@ def test_bitbucket_comment_has_no_update(): comment.update('x') -def test_notify_user_without_update_support_still_comments(): +def test_notify_user_without_update_support_replaces_status(): class NoUpdateComment(FakeComment): def update(self, msg): raise NotImplementedError pr = FakePR() - pr.comments.append(NoUpdateComment( - 'bert-e', pr_utils.STATUS_COMMENT_MARKER + ' stale')) + old = NoUpdateComment('bert-e', pr_utils.STATUS_COMMENT_MARKER + ' stale') + old.delete = lambda: pr.comments.remove(old) + pr.comments.append(old) pr_utils.notify_user(settings(), pr, exc(t='one')) - # status was not updated, but the regular comment was still posted - assert pr.comments[0].text.endswith('stale') + status = [c for c in pr.comments + if c.text.startswith(pr_utils.STATUS_COMMENT_MARKER)] + assert len(status) == 1 and 'stale' not in status[0].text assert pr.comments[-1].text == 'details one' +def test_status_comment_skipped_in_interactive_mode(): + pr = FakePR() + pr_utils._update_status_comment(settings(interactive=True), pr, + exc(t='one')) + assert pr.comments == [] + + +@pytest.mark.parametrize('cls', [exceptions.StatusReport, + exceptions.UnknownCommand]) +def test_status_comment_not_replaced_by_command_replies(cls): + pr = FakePR() + pr_utils._update_status_comment(settings(), pr, exc(cls, t='one')) + assert pr.comments == [] + + +def test_default_dedupe_minus_one_does_not_duplicate(): + pr = FakePR() + s = settings() + e = exc(t='one') + e.dont_repeat_if_in_history = -1 + pr_utils.notify_user(s, pr, e) + pr_utils.notify_user(s, pr, e) + assert len([c for c in pr.comments if c.text == 'details one']) == 1 + + +def test_bitbucket_add_comment_invalidates_cache(): + from bert_e.git_host.bitbucket import PullRequest, Comment + pr = PullRequest.__new__(PullRequest) + pr._comments = ['stale'] + pr.client = None + pr.full_name = lambda: 'o/r' + pr._json_data = {'id': 1} + orig = Comment.create + Comment.create = classmethod(lambda cls, *a, **k: 'new') + try: + assert pr.add_comment('x') == 'new' + finally: + Comment.create = orig + assert not pr._comments + + +def test_github_update_keeps_datetime(): + import json + import datetime + from bert_e.git_host.github import Client + client = Client(login='l', password='p', email='e@o.com') + client.session = MagicMock() + client.session.patch.return_value = MagicMock( + text=json.dumps({'id': 1, 'body': 'new', + 'url': 'https://api.github.com/c/1', + 'user': {'id': 1, 'login': 'Bert-E'}, + 'created_at': '2020-01-01T00:00:00Z'})) + comment = _github_comment(client) + comment.update('new') + assert isinstance(comment.created_on, datetime.datetime) + + # --- interaction with comment deduplication -------------------------------- def test_status_comment_does_not_trigger_dedupe_of_regular_comment(): diff --git a/bert_e/workflow/pr_utils.py b/bert_e/workflow/pr_utils.py index 7cbe9b42..c716b2a2 100644 --- a/bert_e/workflow/pr_utils.py +++ b/bert_e/workflow/pr_utils.py @@ -43,6 +43,10 @@ def find_comment(pull_request: AbstractPullRequest, username=None, for comment in comments: if comment.author != username: continue + if comment.text.startswith(STATUS_COMMENT_MARKER) and \ + not (startswith or '').startswith(STATUS_COMMENT_MARKER): + # the status comment is not part of the regular history + continue if startswith and not comment.text.startswith(startswith): if max_history == -1: return @@ -116,8 +120,10 @@ def _update_status_comment(settings, pull_request: AbstractPullRequest, dedicated comment, edited in place whenever the state changes. """ if not getattr(settings, 'status_comment', False) or \ - settings.no_comment or isinstance( - comment, (exceptions.InitMessage, exceptions.HelpMessage)): + settings.no_comment or settings.interactive or isinstance( + comment, (exceptions.InitMessage, exceptions.HelpMessage, + exceptions.StatusReport, + exceptions.UnknownCommand)): return text = render_status_comment(comment) existing = next( @@ -127,7 +133,12 @@ def _update_status_comment(settings, pull_request: AbstractPullRequest, if existing is None: pull_request.add_comment(text) elif existing.text != text: - existing.update(text) + try: + existing.update(text) + except NotImplementedError: + # no in-place edit on this host: replace the stale comment + existing.delete() + pull_request.add_comment(text) def notify_user(settings, pull_request: AbstractPullRequest, From 523eb924fdc7beb82c785c1f085bf0a4da6672ec Mon Sep 17 00:00:00 2001 From: bot Date: Thu, 1 Oct 2026 13:44:28 +0000 Subject: [PATCH 4/5] fix: address review comments on PR #296 --- bert_e/tests/unit/test_status_comment.py | 12 +++++++++++- bert_e/workflow/pr_utils.py | 20 +++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/bert_e/tests/unit/test_status_comment.py b/bert_e/tests/unit/test_status_comment.py index 30a7ff73..ac767d9f 100644 --- a/bert_e/tests/unit/test_status_comment.py +++ b/bert_e/tests/unit/test_status_comment.py @@ -133,7 +133,17 @@ def test_status_comment_skipped_in_interactive_mode(): @pytest.mark.parametrize('cls', [exceptions.StatusReport, - exceptions.UnknownCommand]) + exceptions.UnknownCommand, + exceptions.HelpMessage, + exceptions.InitMessage, + exceptions.CommandNotImplemented, + exceptions.ResetComplete, + exceptions.LossyResetWarning, + exceptions.IncorrectCommandSyntax, + exceptions.NotEnoughCredentials, + exceptions.NotAuthor, + exceptions.IntegrationDataCreated, + exceptions.PendingHotfixVersionReminder]) def test_status_comment_not_replaced_by_command_replies(cls): pr = FakePR() pr_utils._update_status_comment(settings(), pr, exc(cls, t='one')) diff --git a/bert_e/workflow/pr_utils.py b/bert_e/workflow/pr_utils.py index c716b2a2..1d23ad55 100644 --- a/bert_e/workflow/pr_utils.py +++ b/bert_e/workflow/pr_utils.py @@ -105,6 +105,22 @@ def _send_bot_status(settings, pull_request: AbstractPullRequest, STATUS_COMMENT_MARKER = '' +# Replies to user commands and purely informational messages: they do not +# describe the state of the pull request and must not replace the status. +STATUS_COMMENT_EXCLUDED = ( + exceptions.InformationException, + exceptions.HelpMessage, + exceptions.StatusReport, + exceptions.UnknownCommand, + exceptions.CommandNotImplemented, + exceptions.ResetComplete, + exceptions.LossyResetWarning, + exceptions.IncorrectCommandSyntax, + exceptions.NotEnoughCredentials, + exceptions.NotAuthor, +) + + def render_status_comment(comment: exceptions.TemplateException) -> str: """Render the content of the always up-to-date status comment.""" return (f"{STATUS_COMMENT_MARKER}\n" @@ -121,9 +137,7 @@ def _update_status_comment(settings, pull_request: AbstractPullRequest, """ if not getattr(settings, 'status_comment', False) or \ settings.no_comment or settings.interactive or isinstance( - comment, (exceptions.InitMessage, exceptions.HelpMessage, - exceptions.StatusReport, - exceptions.UnknownCommand)): + comment, STATUS_COMMENT_EXCLUDED): return text = render_status_comment(comment) existing = next( From 14e92fc66f12f8e21beb1f40da7a4e0be7b0914c Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 1 Oct 2026 13:53:42 +0000 Subject: [PATCH 5/5] fix: address review comments on PR #296 --- bert_e/tests/unit/test_status_comment.py | 10 ++++++++++ bert_e/workflow/gitwaterflow/__init__.py | 3 ++- bert_e/workflow/pr_utils.py | 15 ++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bert_e/tests/unit/test_status_comment.py b/bert_e/tests/unit/test_status_comment.py index ac767d9f..31f13de9 100644 --- a/bert_e/tests/unit/test_status_comment.py +++ b/bert_e/tests/unit/test_status_comment.py @@ -229,3 +229,13 @@ def test_find_comment_skips_status_comment_for_regular_lookup(): pr_utils.notify_user(settings(), pr, exc(t='one')) found = pr_utils.find_comment(pr, 'bert-e', 'details one', 10) assert found is not None and found.text == 'details one' + + +def test_find_comment_include_status_finds_status_comment(): + pr = MagicMock() + c = MagicMock() + c.author = 'bert-e' + c.text = pr_utils.STATUS_COMMENT_MARKER + '\nstatus' + pr.comments = [c] + assert pr_utils.find_comment(pr, 'bert-e') is None + assert pr_utils.find_comment(pr, 'bert-e', include_status=True) is c diff --git a/bert_e/workflow/gitwaterflow/__init__.py b/bert_e/workflow/gitwaterflow/__init__.py index 3ad5c435..b7458539 100644 --- a/bert_e/workflow/gitwaterflow/__init__.py +++ b/bert_e/workflow/gitwaterflow/__init__.py @@ -276,7 +276,8 @@ def send_greetings(job): """ username = job.settings.robot - if find_comment(job.pull_request, username=username): + if find_comment(job.pull_request, username=username, + include_status=True): return init_message = messages.InitMessage( diff --git a/bert_e/workflow/pr_utils.py b/bert_e/workflow/pr_utils.py index 1d23ad55..0b1dca30 100644 --- a/bert_e/workflow/pr_utils.py +++ b/bert_e/workflow/pr_utils.py @@ -23,7 +23,8 @@ def find_comment(pull_request: AbstractPullRequest, username=None, - startswith=None, max_history=None) -> AbstractComment: + startswith=None, max_history=None, + include_status=False) -> AbstractComment: """Look for the most recent pull request comment satisfying given criteria. @@ -31,6 +32,7 @@ def find_comment(pull_request: AbstractPullRequest, username=None, username: comment's author. starswith: preamble of the comment. max_history: limit of the comment history to look backwards. + include_status: also consider the always up-to-date status comment. Returns: The latest comment if it was found. None otherwise. @@ -43,7 +45,8 @@ def find_comment(pull_request: AbstractPullRequest, username=None, for comment in comments: if comment.author != username: continue - if comment.text.startswith(STATUS_COMMENT_MARKER) and \ + if not include_status and \ + comment.text.startswith(STATUS_COMMENT_MARKER) and \ not (startswith or '').startswith(STATUS_COMMENT_MARKER): # the status comment is not part of the regular history continue @@ -151,7 +154,13 @@ def _update_status_comment(settings, pull_request: AbstractPullRequest, existing.update(text) except NotImplementedError: # no in-place edit on this host: replace the stale comment - existing.delete() + try: + existing.delete() + except Exception: + # keep the stale comment rather than posting a duplicate + LOG.warning("Could not delete stale status comment; " + "skipping status update", exc_info=True) + return pull_request.add_comment(text)