-
Notifications
You must be signed in to change notification settings - Fork 2
fix: resolve issue #293 - Reduce verbosity #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,9 @@ 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 is a transient status that should edit the | ||||||
| # previous status comment instead of posting a new one | ||||||
| updatable = False | ||||||
|
|
||||||
| def __init__(self, **kwargs): | ||||||
| self.kwargs = kwargs | ||||||
|
|
@@ -93,78 +96,91 @@ class StatusReport(TemplateException): | |||||
|
|
||||||
| class IncompatibleSourceBranchPrefix(TemplateException): | ||||||
| code = 106 | ||||||
| updatable = True | ||||||
| template = 'incompatible_source_branch_prefix.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class MissingJiraId(TemplateException): | ||||||
| code = 107 | ||||||
| updatable = True | ||||||
| template = 'missing_jira_id.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class JiraIssueNotFound(TemplateException): | ||||||
| code = 108 | ||||||
| updatable = True | ||||||
| template = 'jira_issue_not_found.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class IssueTypeNotSupported(TemplateException): | ||||||
| code = 109 | ||||||
| updatable = True | ||||||
| template = 'issue_type_not_supported.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class IncorrectJiraProject(TemplateException): | ||||||
| code = 110 | ||||||
| updatable = True | ||||||
| template = 'incorrect_jira_project.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class MismatchPrefixIssueType(TemplateException): | ||||||
| code = 111 | ||||||
| updatable = True | ||||||
| template = 'mismatch_prefix_issue_type.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class IncorrectFixVersion(TemplateException): | ||||||
| code = 112 | ||||||
| updatable = True | ||||||
| template = 'incorrect_fix_version.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class BranchHistoryMismatch(TemplateException): | ||||||
| code = 113 | ||||||
| updatable = True | ||||||
| template = 'history_mismatch.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class Conflict(TemplateException): | ||||||
| code = 114 | ||||||
| updatable = True | ||||||
| template = 'conflict.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class ApprovalRequired(TemplateException): | ||||||
| code = 115 | ||||||
| updatable = True | ||||||
| template = 'need_approval.md' | ||||||
| status = "queued" | ||||||
|
|
||||||
|
|
||||||
| class BuildFailed(TemplateException): | ||||||
| code = 118 | ||||||
| updatable = True | ||||||
| template = 'build_failed.md' | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
||||||
| class AfterPullRequest(TemplateException): | ||||||
| code = 120 | ||||||
| updatable = True | ||||||
| template = 'after_pull_request.md' | ||||||
| status = "queued" | ||||||
|
|
||||||
|
|
||||||
| class IntegrationDataCreated(InformationException): | ||||||
| code = 121 | ||||||
| updatable = True | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| template = 'integration_data_created.md' | ||||||
|
|
||||||
|
|
||||||
|
|
@@ -182,6 +198,7 @@ class NotEnoughCredentials(TemplateException): | |||||
|
|
||||||
| class QueueConflict(TemplateException): | ||||||
| code = 124 | ||||||
| updatable = True | ||||||
| template = "queue_conflict.md" | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
@@ -225,11 +242,13 @@ class QueueOutOfOrder(TemplateException): | |||||
|
|
||||||
| class ResetComplete(TemplateException): | ||||||
| code = 128 | ||||||
| updatable = True | ||||||
| template = "reset_complete.md" | ||||||
|
|
||||||
|
|
||||||
| class LossyResetWarning(TemplateException): | ||||||
| code = 129 | ||||||
| updatable = True | ||||||
| template = "lossy_reset.md" | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
@@ -248,6 +267,7 @@ class IncorrectPullRequestNumber(TemplateException): | |||||
|
|
||||||
| class SourceBranchTooOld(TemplateException): | ||||||
| code = 132 | ||||||
| updatable = True | ||||||
| template = "source_branch_too_old.md" | ||||||
| status = "failure" | ||||||
|
|
||||||
|
|
@@ -266,6 +286,7 @@ class NotAuthor(TemplateException): | |||||
|
|
||||||
| class RequestIntegrationBranches(TemplateException): | ||||||
| code = 135 | ||||||
| updatable = True | ||||||
| template = "request_integration_branches.md" | ||||||
| # TODO: review if it should be failure. | ||||||
| status = "queued" | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1018,6 +1018,10 @@ | |
| def delete(self) -> None: | ||
| self.client.delete(self.data['url']) | ||
|
|
||
| def update(self, msg: str) -> None: | ||
| self.data = self.client.patch( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Minor — A failed edit aborts the job instead of falling back to a new comment.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| self.data['url'], data=json.dumps({'body': msg})) | ||
|
|
||
|
|
||
| class CheckRun(base.AbstractGitHostObject): | ||
| GET_URL = '/repos/{owner}/{repo}/check-runs/{id}' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| """Transient status messages edit the previous status comment.""" | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
|
|
||
| from bert_e import exceptions | ||
| from bert_e.workflow import pr_utils | ||
| from bert_e.workflow.pr_utils import STATUS_MARKER, notify_user | ||
|
|
||
|
|
||
| class FakeComment: | ||
| def __init__(self, author, text, editable=True): | ||
| self.author = author | ||
| self.text = text | ||
| self.editable = editable | ||
|
|
||
| def update(self, msg): | ||
| if not self.editable: | ||
| raise NotImplementedError | ||
| self.text = msg | ||
|
|
||
|
|
||
| class FakePR: | ||
| def __init__(self, editable=True): | ||
| self.comments = [] | ||
| self.editable = editable | ||
|
|
||
| def add_comment(self, msg): | ||
| self.comments.append(FakeComment('bert-e', msg, self.editable)) | ||
|
|
||
| def set_bot_status(self, *args, **kwargs): | ||
| pass | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def settings(): | ||
| return SimpleNamespace(robot='bert-e', no_comment=False, | ||
| interactive=False, send_bot_status=False) | ||
|
|
||
|
|
||
| class FakeStatus(Exception): | ||
| status = None | ||
| title = 'title' | ||
| dont_repeat_if_in_history = 0 | ||
| updatable = True | ||
|
|
||
| def __init__(self, text): | ||
| super().__init__(text) | ||
| self.text = text | ||
|
|
||
| def __str__(self): | ||
| return self.text | ||
|
|
||
|
|
||
| def test_updatable_flag(): | ||
| assert exceptions.BranchHistoryMismatch.updatable | ||
| assert exceptions.Conflict.updatable | ||
| assert not exceptions.HelpMessage.updatable | ||
| assert not exceptions.SuccessMessage.updatable | ||
|
|
||
|
|
||
| def test_status_comment_is_edited_in_place(settings): | ||
| pr = FakePR() | ||
| pr_utils._send_comment(settings, pr, 'first', 0, updatable=True) | ||
| pr_utils._send_comment(settings, pr, 'second', 0, updatable=True) | ||
| assert len(pr.comments) == 1 | ||
| assert pr.comments[0].text == f'second\n{STATUS_MARKER}' | ||
|
|
||
|
|
||
| def test_same_status_is_not_republished(settings): | ||
| pr = FakePR() | ||
| pr_utils._send_comment(settings, pr, 'same', 0, updatable=True) | ||
| with pytest.raises(exceptions.CommentAlreadyExists): | ||
| pr_utils._send_comment(settings, pr, 'same', 0, updatable=True) | ||
| assert len(pr.comments) == 1 | ||
|
|
||
|
|
||
| def test_fallback_to_new_comment_when_edit_unsupported(settings): | ||
| pr = FakePR(editable=False) | ||
| pr_utils._send_comment(settings, pr, 'first', 0, updatable=True) | ||
| pr_utils._send_comment(settings, pr, 'second', 0, updatable=True) | ||
| assert len(pr.comments) == 2 | ||
|
|
||
|
|
||
| def test_non_updatable_messages_still_create_comments(settings): | ||
| pr = FakePR() | ||
| pr_utils._send_comment(settings, pr, 'one', 0) | ||
| pr_utils._send_comment(settings, pr, 'two', 0) | ||
| assert [c.text for c in pr.comments] == ['one', 'two'] | ||
|
|
||
|
|
||
| def test_other_authors_comments_are_not_edited(settings): | ||
| pr = FakePR() | ||
| pr.comments.append(FakeComment('someone', f'x\n{STATUS_MARKER}')) | ||
| pr_utils._send_comment(settings, pr, 'new', 0, updatable=True) | ||
| assert len(pr.comments) == 2 | ||
| assert pr.comments[0].text == f'x\n{STATUS_MARKER}' | ||
|
|
||
|
|
||
| def test_notify_user_uses_exception_flag(settings): | ||
| pr = FakePR() | ||
| notify_user(settings, pr, FakeStatus('history conflict')) | ||
| notify_user(settings, pr, FakeStatus('reset')) | ||
| assert len(pr.comments) == 1 | ||
| assert pr.comments[0].text.startswith('reset') | ||
|
|
||
|
|
||
| def test_status_comment_not_edited_after_user_comment(settings): | ||
| """Commands posted after the status must still be seen as new.""" | ||
| pr = FakePR() | ||
| pr_utils._send_comment(settings, pr, 'first', 0, updatable=True) | ||
| pr.comments.append(FakeComment('user', '@bert-e reset')) | ||
| pr_utils._send_comment(settings, pr, 'second', 0, updatable=True) | ||
| assert len(pr.comments) == 3 | ||
| assert pr.comments[0].text == f'first\n{STATUS_MARKER}' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,9 @@ | |
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
| # Hidden marker identifying the single, continuously edited status comment. | ||
| STATUS_MARKER = '<!-- bert-e-status -->' | ||
|
|
||
|
|
||
| def find_comment(pull_request: AbstractPullRequest, username=None, | ||
| startswith=None, max_history=None) -> AbstractComment: | ||
|
|
@@ -51,7 +54,7 @@ def find_comment(pull_request: AbstractPullRequest, username=None, | |
|
|
||
|
|
||
| def _send_comment(settings, pull_request: AbstractPullRequest, msg: str, | ||
| dont_repeat_if_in_history=10) -> None: | ||
| dont_repeat_if_in_history=10, updatable=False) -> None: | ||
| """Comment a pull request. | ||
|
|
||
| Before posting: | ||
|
|
@@ -80,6 +83,30 @@ def _send_comment(settings, pull_request: AbstractPullRequest, msg: str, | |
| if not confirm('Do you want to send this comment?'): | ||
| return | ||
|
|
||
| if updatable: | ||
| msg = f'{msg}\n{STATUS_MARKER}' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Warning — Bitbucket users see stray HTML markup in every Bert-E status comment. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The marker is added even when the host cannot edit comments. Bitbucket's |
||
| # Only edit the status comment if it is the last comment of the | ||
| # pull request: commands are looked up in the comments posted after | ||
| # Bert-E's last one, so editing an older comment would make already | ||
| # handled commands look new again. | ||
| comments = pull_request.comments | ||
| previous = comments[-1] if comments else None | ||
| if previous is not None: | ||
| is_status = (previous.author == settings.robot and | ||
| STATUS_MARKER in previous.text) | ||
| if not is_status: | ||
| previous = None | ||
| if previous is not None: | ||
| if previous.text == msg: | ||
| raise exceptions.CommentAlreadyExists( | ||
| "The status comment is already up to date.") | ||
| try: | ||
| LOG.debug('UPDATING MESSAGE %s', msg) | ||
| previous.update(msg) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Warning — Authors silently miss build failures and conflicts that block their PR. When the previous status is the last comment, a new status such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GitHub does not send notifications when a comment is edited. With this change, a PR author will no longer be notified when the status moves to Build failed, Conflict, History mismatch or Waiting for approval, which are the statuses they need to act on. |
||
| return | ||
| except NotImplementedError: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| pass | ||
|
|
||
| LOG.debug('SENDING MESSAGE %s', msg) | ||
| pull_request.add_comment(msg) | ||
|
|
||
|
|
@@ -104,6 +131,7 @@ def notify_user(settings, pull_request: AbstractPullRequest, | |
| try: | ||
| _send_bot_status(settings, pull_request, comment) | ||
| _send_comment(settings, pull_request, str(comment), | ||
| comment.dont_repeat_if_in_history) | ||
| comment.dont_repeat_if_in_history, | ||
| getattr(comment, 'updatable', False)) | ||
| except exceptions.CommentAlreadyExists: | ||
| LOG.info("Comment '%s' already posted", comment.__class__.__name__) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Warning — Users lose the list of integration branches and PRs Bert-E created.
IntegrationDataCreatedis markedupdatable, but it is sent fromnotify_integration_datajust beforecheck_approvalsandcheck_build_statusrun in the same handle. Those almost always raiseApprovalRequired,BuildFailedorAfterPullRequest, which are also updatable, and they immediately overwrite it. So the one message listing the newly created integration branches and child PRs is erased within the same run. That is why the PR had to drop the'Integration data created'assertion intest_creation_integration_branch_by_approve. The message is informational (NEVER_REPEAT), not a transient status, and it is only sent when branches are newly created, so it will never be shown again.