Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bert_e/docs/USER_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ and associated tickets. __Bert-E__ helps the participants in a pull request
correct the items that do not follow the rules, by issuing a status report and
specific messages.

__Bert-E__ also maintains a single **status comment** in each pull request,
titled `Bert-E status`. It is posted right after the first greetings, edited in
place (the description of the pull request is never modified) and always shows
the latest state of the pull request (for instance `Conflict`, `Queued` or
`Merged`), the integration branches with their open integration pull requests
and the status of their builds, and the checklist of the `status` command.
Information messages (greetings, help, status report...) do not change the
state shown. Pull requests which were opened before the status comment was
introduced get it at the next run of __Bert-E__ on them, below the existing
comments. The feature can be disabled with the `pr_status_comment` setting.

* There are different stages in the merge of a pull request

* verification that the minimum information required for the process is
Expand Down
28 changes: 28 additions & 0 deletions bert_e/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ class TemplateException(BertE_Exception):
template = None
# whether to re-publish if the message is already in the history
dont_repeat_if_in_history = -1
# whether this message describes the current state of the pull request
# (and thus must be reflected in the pull request's status comment), as
# opposed to a one-shot information or the answer to a command.
reports_state = True
Comment thread
upsun-dispatch[bot] marked this conversation as resolved.
# human readable name of the state, defaults to the class name
state_label = None
# status displayed with the state, defaults to `status`
state_status = None

def __init__(self, **kwargs):
self.kwargs = kwargs
Expand Down Expand Up @@ -59,6 +67,7 @@ class SilentException(BertE_Exception):
# template for informative exceptions
class InformationException(TemplateException):
dont_repeat_if_in_history = NEVER_REPEAT
reports_state = False


# template exceptions
Expand All @@ -71,24 +80,28 @@ class HelpMessage(TemplateException):
code = 101
template = 'help.md'
dont_repeat_if_in_history = 0 # allow repeating if requested by user
reports_state = False


class SuccessMessage(TemplateException):
code = 102
template = 'successful_merge.md'
status = "success"
state_label = "Merged"


class CommandNotImplemented(TemplateException):
code = 103
template = 'not_implemented.md'
dont_repeat_if_in_history = 0 # allow repeating if requested by user
reports_state = False


class StatusReport(TemplateException):
code = 104
template = 'status.md'
dont_repeat_if_in_history = 0 # allow repeating if requested by user
reports_state = False


class IncompatibleSourceBranchPrefix(TemplateException):
Expand Down Expand Up @@ -161,6 +174,7 @@ class AfterPullRequest(TemplateException):
code = 120
template = 'after_pull_request.md'
status = "queued"
state_label = "Waiting for another pull request"


class IntegrationDataCreated(InformationException):
Expand All @@ -170,12 +184,14 @@ class IntegrationDataCreated(InformationException):

class UnknownCommand(TemplateException):
code = 122
reports_state = False
template = 'unknown_command.md'
status = "failure"


class NotEnoughCredentials(TemplateException):
code = 123
reports_state = False
template = "not_enough_credentials.md"
status = "failure"

Expand Down Expand Up @@ -215,6 +231,7 @@ class PartialMerge(TemplateException):
template = 'partial_merge.md'
dont_repeat_if_in_history = 0 # allow repeating as many times as it occurs
status = "success"
state_label = "Partially merged"


class QueueOutOfOrder(TemplateException):
Expand All @@ -232,16 +249,19 @@ class LossyResetWarning(TemplateException):
code = 129
template = "lossy_reset.md"
status = "failure"
reports_state = False


class IncorrectCommandSyntax(TemplateException):
code = 130
reports_state = False
template = "incorrect_command_syntax.md"
status = "failure"


class IncorrectPullRequestNumber(TemplateException):
code = 131
reports_state = False
template = "incorrect_pull_request_number.md"
status = "failure"

Expand All @@ -254,12 +274,14 @@ class SourceBranchTooOld(TemplateException):

class FlakyGitHost(TemplateException):
code = 133
reports_state = False
template = "flaky_git_host.md"
status = "failure"


class NotAuthor(TemplateException):
code = 134
reports_state = False
template = "not_author.md"
status = "failure"

Expand All @@ -274,6 +296,8 @@ class RequestIntegrationBranches(TemplateException):
class QueueBuildFailedMessage(TemplateException):
code = 136
template = "queue_build_failed.md"
state_label = "Queue build failed"
state_status = "failure" # no bot status check is sent for this one


class ForeignCommitsInSourceBranch(TemplateException):
Expand Down Expand Up @@ -572,6 +596,10 @@ class NothingToDo(SilentException):
code = 302


class WaitOptionSet(NothingToDo):
"""Nothing is done on purpose because the wait option is set."""


class BuildInProgress(SilentException):
code = 303
status = "in_progress"
Expand Down
11 changes: 11 additions & 0 deletions bert_e/git_host/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ class AbstractComment(metaclass=ABCMeta):
def delete(self) -> None:
"""Delete the comment."""

@abstractmethod
def edit(self, msg: str) -> None:
"""Replace the comment's contents with `msg`.

The comment keeps its place in the pull request's history.

Args:
- msg: the new raw plaintext of the comment.

"""

@property
@abstractmethod
def author(self) -> str:
Expand Down
17 changes: 16 additions & 1 deletion bert_e/git_host/bitbucket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,16 @@ 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']
)
# keep the cache of comments (see `comments`) consistent
if getattr(self, '_comments', None):
self._comments.append(comment)
return comment

def set_bot_status(self, status: str | None, title: str, summary: str):
raise NotImplementedError('"set_bot_status" feature '
Expand Down Expand Up @@ -489,6 +493,17 @@ def delete(self):
pull_request_id=self.data['pullrequest']['id'],
comment_id=self.id)

def edit(self, msg):
url = self.GET_URL.format(
full_name=self.full_name(),
pull_request_id=self.data['pullrequest']['id'],
comment_id=self.id)
response = self.client.put(
url, data=json.dumps({'content': {'raw': msg}}))
response.raise_for_status()
# the pull request caches its comments: keep them up to date
self.data['content']['raw'] = msg

@classmethod
def create(cls, client, data, **kwargs):
return super().create(client, {'content': {'raw': data}}, **kwargs)
Expand Down
9 changes: 8 additions & 1 deletion bert_e/git_host/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -1015,6 +1015,13 @@ def text(self) -> str:
def id(self) -> int:
return self.data['id']

def edit(self, msg: str) -> None:
self.client.patch(
self.data['url'],
data=json.dumps({'body': msg})
)
self.data['body'] = msg

def delete(self) -> None:
self.client.delete(self.data['url'])

Expand Down
5 changes: 5 additions & 0 deletions bert_e/git_host/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@ def comments(self):

class CommentController(Controller, base.AbstractComment):

def edit(self, msg):
self.controlled.content = {"raw": msg, "markup": "markdown",
"html": msg}
self.controlled.updated_on = datetime.now()

def delete(self):
self.controlled.delete()

Expand Down
14 changes: 14 additions & 0 deletions bert_e/server/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@

def handle_bitbucket_pr_event(bert_e, event, json_data):
"""Handle a Bitbucket webhook sent on a pull request event."""
commenter = (json_data.get('comment') or {}).get('user') or {}
if event.startswith('comment') and bert_e.settings.robot in (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings.robot is a UserDict, and its __eq__ raises ValueError when compared with None. Bitbucket no longer sends username in user objects (and nickname/account_id can be missing), so robot in (..., None, ...) raises on every comment written by a human. The webhook then fails and the command is never handled. The tests miss this because test_server.py sets robot = 'robot' (a plain str). Drop the empty values before comparing, and add a test with a real UserDict.

— Claude Code

commenter.get('nickname'), commenter.get('username'),
commenter.get('account_id')):
# Bert-E's own comments (including its status comment) must not
# trigger new runs
LOG.debug('Comment written by the robot, ignoring event')
return

Check warning on line 70 in bert_e/server/webhook.py

View check run for this annotation

Codecov / codecov/patch

bert_e/server/webhook.py#L69-L70

Added lines #L69 - L70 were not covered by tests
pr_id = json_data['pullrequest']['id']
pr = PullRequest(bert_e.client, **json_data['pullrequest'])
LOG.info('The pull request <%s> has been updated', pr_id)
Expand All @@ -78,6 +86,12 @@

def handle_github_issue_comment(bert_e, json_data):
"""Handle a GitHub webhook sent on an issue comment event."""
sender = (json_data.get('comment') or {}).get('user') or {}
if sender.get('login') == bert_e.settings.robot:

Check warning on line 90 in bert_e/server/webhook.py

View check run for this annotation

Codecov / codecov/patch

bert_e/server/webhook.py#L89-L90

Added lines #L89 - L90 were not covered by tests
Comment thread
upsun-dispatch[bot] marked this conversation as resolved.
# Bert-E's own comments (including its status comment) must not
# trigger new runs
LOG.debug('Comment written by the robot, ignoring event')
return

Check warning on line 94 in bert_e/server/webhook.py

View check run for this annotation

Codecov / codecov/patch

bert_e/server/webhook.py#L93-L94

Added lines #L93 - L94 were not covered by tests
event = github.IssueCommentEvent(client=bert_e.client, **json_data)
pr = event.pull_request
if pr:
Expand Down
3 changes: 3 additions & 0 deletions bert_e/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ class Meta:

frontend_url = fields.Str(required=False, load_default='')

# Maintain an always up-to-date status comment in the pull requests
pr_status_comment = fields.Bool(required=False, load_default=True)

repository_owner = fields.Str(required=True)
repository_slug = fields.Str(required=False, load_default=None)

Expand Down
27 changes: 27 additions & 0 deletions bert_e/templates/pr_status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends "message.md" %}

{% block title -%}
Bert-E status
{% endblock %}

{% block message %}
{{ icon }} **{{ label }}**{% if code %} (message {{ code }}){% endif %}

{% if integration is not none %}
{% if integration %}
integration branch | pull request | build
-------------------|--------------|------
{% for item in integration -%}
`{{ item.branch }}` | {% if item.pr_id %}#{{ item.pr_id }}{% else %}-{% endif %} | {% if item.build %}{{ item.build }}{% else %}-{% endif %}
{% endfor %}
{% else %}
*No integration branch.*
{% endif %}
{% endif %}

{% if status %}
{% include 'status_report.md' %}
{% endif %}

*This comment is kept up to date by Bert-E.*
{% endblock %}
Loading
Loading