Skip to content
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

PTFE-1482 introduce bot status report #167

Merged
merged 9 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
34 changes: 34 additions & 0 deletions bert_e/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Literal
from bert_e.lib.template_loader import render

# When dont_repeat_if_in_history is None, Bert-E will look for the message
Expand All @@ -27,6 +28,7 @@ class BertE_Exception(Exception):
class TemplateException(BertE_Exception):
code = -2
template = None
status: Literal[None, "in_progress", "success", "failure"] = None
# whether to re-publish if the message is already in the history
dont_repeat_if_in_history = -1

Expand All @@ -40,6 +42,11 @@ def __init__(self, **kwargs):
self.msg = render(self.template, code=self.code, **kwargs)
super(TemplateException, self).__init__(self.msg)

@property
def title(self) -> str:
# Return the exception class name as the title
return self.__class__.__name__


class InternalException(BertE_Exception):
code = 1
Expand Down Expand Up @@ -69,6 +76,7 @@ class HelpMessage(TemplateException):
class SuccessMessage(TemplateException):
code = 102
template = 'successful_merge.md'
status = "success"


class CommandNotImplemented(TemplateException):
Expand All @@ -86,61 +94,73 @@ class StatusReport(TemplateException):
class IncompatibleSourceBranchPrefix(TemplateException):
code = 106
template = 'incompatible_source_branch_prefix.md'
status = "failure"


class MissingJiraId(TemplateException):
code = 107
template = 'missing_jira_id.md'
status = "failure"


class JiraIssueNotFound(TemplateException):
code = 108
template = 'jira_issue_not_found.md'
status = "failure"


class IssueTypeNotSupported(TemplateException):
code = 109
template = 'issue_type_not_supported.md'
status = "failure"


class IncorrectJiraProject(TemplateException):
code = 110
template = 'incorrect_jira_project.md'
status = "failure"


class MismatchPrefixIssueType(TemplateException):
code = 111
template = 'mismatch_prefix_issue_type.md'
status = "failure"


class IncorrectFixVersion(TemplateException):
code = 112
template = 'incorrect_fix_version.md'
status = "failure"


class BranchHistoryMismatch(TemplateException):
code = 113
template = 'history_mismatch.md'
status = "failure"


class Conflict(TemplateException):
code = 114
template = 'conflict.md'
status = "failure"


class ApprovalRequired(TemplateException):
code = 115
template = 'need_approval.md'
status = "in_progress"


class BuildFailed(TemplateException):
code = 118
template = 'build_failed.md'
status = "failure"


class AfterPullRequest(TemplateException):
code = 120
template = 'after_pull_request.md'
status = "in_progress"


class IntegrationDataCreated(InformationException):
Expand All @@ -151,21 +171,25 @@ class IntegrationDataCreated(InformationException):
class UnknownCommand(TemplateException):
code = 122
template = 'unknown_command.md'
status = "failure"


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


class QueueConflict(TemplateException):
code = 124
template = "queue_conflict.md"
status = "failure"


class Queued(TemplateException):
code = 125
template = 'queued.md'
status = "in_progress"

def __init__(self, branches, ignored, issue, author, active_options):
"""Save args for later use by tests."""
Expand All @@ -187,11 +211,13 @@ class PartialMerge(TemplateException):
code = 126
template = 'partial_merge.md'
dont_repeat_if_in_history = 0 # allow repeating as many times as it occurs
status = "success"


class QueueOutOfOrder(TemplateException):
code = 127
template = "queue_out_of_order.md"
status = "failure"


class ResetComplete(TemplateException):
Expand All @@ -202,36 +228,44 @@ class ResetComplete(TemplateException):
class LossyResetWarning(TemplateException):
code = 129
template = "lossy_reset.md"
status = "failure"


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


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


class SourceBranchTooOld(TemplateException):
code = 132
template = "source_branch_too_old.md"
status = "failure"


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


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


class RequestIntegrationBranches(TemplateException):
code = 135
template = "request_integration_branches.md"
# TODO: review if it should be failure.
status = "in_progress"


# internal exceptions
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 @@ -350,6 +350,17 @@ def approve(self):
def decline(self):
"""Decline this pull request."""

@abstractmethod
def set_bot_status(self, status: str | None, title: str,
summary: str) -> None:
"""Set a status check reporting its advancement regarding Bert-E's checks

Args:
- status: the status of the check.
- title: the title of the check.
- summary: the summary of the check.
"""

@property
@abstractmethod
def id(self) -> str:
Expand Down
4 changes: 4 additions & 0 deletions bert_e/git_host/bitbucket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ def add_comment(self, msg):
pull_request_id=self['id']
)

def set_bot_status(self, status: str | None, title: str, summary: str):
raise NotImplementedError('"set_bot_status" feature '
'is not available in bitbucket')

def get_comments(self, deleted=False):
return sorted(
(comment
Expand Down
23 changes: 21 additions & 2 deletions bert_e/git_host/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,27 @@ def add_comment(self, msg: str):
url = self.data['comments_url']
return Comment.create(self.client, {'body': msg}, url=url)

def add_checkrun(
self, name: str, status: str, conclusion: str,
def set_bot_status(self, status: str | None, title: str, summary: str):
if self.client and self.client.is_app is False:
LOG.error("Cannot set bot status without a GitHub App")
return
conclusion: str | None = None
if status == "success":
conclusion = "success"
status = "completed"
elif status == "in_progress":
conclusion = None
elif status == "failure":
conclusion = "failure"
status = "completed"

self._add_checkrun(
name='bert-e', status=status, conclusion=conclusion,
title=title, summary=summary
)

def _add_checkrun(
self, name: str, status: str, conclusion: str | None,
title: str, summary: str):
return CheckRun.create(
client=self.client,
Expand Down
27 changes: 21 additions & 6 deletions bert_e/git_host/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,15 @@ def __init__(self, client, owner, repo_slug, scm='git', is_private=True):
self.size = 76182262
self.is_private = is_private
self.uuid = "{9970a9b6-2d86-413f-8555-da8e1ac0e542}"

# ###############
# revisions will be used to store the build status
# It is a dict that will return the build status for a given
# revision and key.
# The value of revisions will be consistent across
# every instance of Repository even if the class is
# instantiated multiple times, therefore it needs to be
# cached at the class level.
if not hasattr(Repository, 'revisions'):
Repository.revisions = {}

def delete(self):
super().delete()
Expand All @@ -180,7 +187,6 @@ def delete(self):
def create(self):
self.gitrepo = GitRepository(None)
self.gitrepo.cmd('git init --bare')
self.gitrepo.revisions = {} # UGLY
Repository.repos[(self.repo_owner, self.repo_slug)] = self.gitrepo
return super().create()

Expand Down Expand Up @@ -235,11 +241,11 @@ def get_commit_url(self, revision):

def get_build_url(self, revision, key):
key = '{}-build'.format(revision)
return self.gitrepo.revisions.get((revision, key), None)
return self.revisions.get((revision, key), None)

def get_build_status(self, revision, key):
try:
return self.gitrepo.revisions[(revision, key)]
return self.revisions[(revision, key)]
except KeyError:
return 'NOTSTARTED'

Expand All @@ -248,7 +254,7 @@ def invalidate_build_status_cache(self):

def set_build_status(self, revision, key, state, **kwargs):
self.get_git_url()
self.gitrepo.revisions[(revision, key)] = state
self.revisions[(revision, key)] = state

@property
def owner(self):
Expand Down Expand Up @@ -351,6 +357,15 @@ def decline(self):
except Exception:
pass

def set_bot_status(self, status: str, title: str, summary: str):

self['repo'].set_build_status(
revision=self.src_commit,
key="bert-e",
state=status,
description=f"# {title}\n{summary}"
)

@property
def id(self):
return self['id']
Expand Down
2 changes: 2 additions & 0 deletions bert_e/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ class Meta:
github_private_key = fields.Str(required=False, load_default='')
github_installation_id = fields.Int(required=False, load_default='')

send_bot_status = fields.Bool(required=False, load_default=False)

@pre_load(pass_many=True)
def load_env(self, data, **kwargs):
"""Load environment variables"""
Expand Down
15 changes: 15 additions & 0 deletions bert_e/tests/test_bert_e.py
Original file line number Diff line number Diff line change
Expand Up @@ -4936,6 +4936,21 @@ def test_init_message(self):
for command in ['help', 'reset']:
assert command in init_message

def test_set_bot_status(self):
"""Test Bert-E's capability to its own status on PRs"""
settings = DEFAULT_SETTINGS + "send_bot_status: true"
pr = self.create_pr('bugfix/TEST-01', 'development/4.3')
self.handle(pr.id)
assert self.get_build_status(
pr.src_commit, key="bert-e") == "NOTSTARTED"
self.handle(pr.id, settings=settings)
assert self.get_build_status(
pr.src_commit, key="bert-e") == "failure"
self.handle(pr.id, settings=settings, options=["bypass_jira_check"])
assert self.get_build_status(
pr.src_commit, key="bert-e") == "in_progress"
self.handle(pr.id, settings=settings, options=self.bypass_all)


class TestQueueing(RepositoryTests):
"""Tests which validate all things related to the merge queue.
Expand Down
8 changes: 7 additions & 1 deletion bert_e/tests/unit/test_github_app_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ def test_github_auth_app(client_app):
def test_github_check_run(client_app):
repository = client_app.get_repository('octo-org', 'Hello-World')
pr = repository.get_pull_request(1)
check_run = pr.add_checkrun(
check_run = pr._add_checkrun(
'bert-e', 'completed', 'success', 'title', 'summary')
assert check_run.name == check_run.data['name']


def test_github_set_status(client_app):
repository = client_app.get_repository('octo-org', 'Hello-World')
pr = repository.get_pull_request(1)
pr.set_bot_status('success', 'title', 'summary')
Loading
Loading