Skip to content
Draft
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/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ class QueueBuildFailedMessage(TemplateException):
template = "queue_build_failed.md"


class GitHubChecksFailed(TemplateException):
code = 137
template = "github_checks_failed.md"
status = "failure"


# internal exceptions
class UnableToSendEmail(InternalException):
code = 201
Expand Down Expand Up @@ -585,3 +591,8 @@ class JobFailure(SilentException):

class QueueBuildFailed(SilentException):
code = 309


class GitHubChecksInProgress(SilentException):
code = 310
status = "in_progress"
3 changes: 3 additions & 0 deletions bert_e/git_host/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ def get_pull_request(self, pull_request_id: int) -> AbstractPullRequest:
- pr_id: id of the pull request to get.
"""

def get_check_runs(self, ref):
return []

def create_pull_request(self, title: str, src_branch: str, dst_branch: str,
description: str, **kwargs) -> AbstractPullRequest:
"""Create a new pull request
Expand Down
15 changes: 15 additions & 0 deletions bert_e/git_host/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,16 @@ def get_build_url(self, revision: str, key: str) -> str:
if status:
return status.url

def get_check_runs(self, ref):
try:
result = AggregatedCheckRuns.get(
self.client, owner=self.owner, repo=self.slug, ref=ref)
return result.data.get('check_runs', [])
except HTTPError as err:
if err.response.status_code == 404:
return []
raise

def get_build_description(self, revision: str, key: str) -> str:
status = cache.BUILD_STATUS_CACHE[key].get(revision, None)
if status:
Expand Down Expand Up @@ -737,6 +747,11 @@ def __str__(self) -> str:
return self.state


class AggregatedCheckRuns(base.AbstractGitHostObject):
GET_URL = "/repos/{owner}/{repo}/commits/{ref}/check-runs"
SCHEMA = schema.AggregateCheckRuns


class PullRequest(base.AbstractGitHostObject, base.AbstractPullRequest):
LIST_URL = '/repos/{owner}/{repo}/pulls'
GET_URL = '/repos/{owner}/{repo}/pulls/{number}'
Expand Down
6 changes: 6 additions & 0 deletions bert_e/git_host/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ def set_build_status(self, revision, key, state, **kwargs):
self.get_git_url()
self.revisions[(revision, key)] = state

def get_check_runs(self, ref):
return self.revisions.get((ref, '_check_runs'), [])

def set_check_runs(self, ref, check_runs):
self.revisions[(ref, '_check_runs')] = check_runs

@property
def owner(self):
return self.repo_owner
Expand Down
1 change: 1 addition & 0 deletions bert_e/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class PrAuthorsOptions(fields.Dict):
'bypass_incompatible_branch',
'bypass_peer_approval',
'bypass_leader_approval',
'bypass_github_checks',
]

def serialize(self, value, attr=None, obj=None, **kwargs):
Expand Down
147 changes: 146 additions & 1 deletion bert_e/tests/test_bert_e.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,8 @@ class RepositoryTests(unittest.TestCase):
'bypass_incompatible_branch',
'bypass_jira_check',
'bypass_peer_approval',
'bypass_leader_approval'
'bypass_leader_approval',
'bypass_github_checks',
]

def get_last_pr_comment(self, pr):
Expand Down Expand Up @@ -4779,6 +4780,150 @@ def test_admin_self_bypass(self):
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, backtrace=True)

def test_github_checks_pass(self):
pr = self.create_pr('bugfix/TEST-00090', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00090',
'w/10.0/bugfix/TEST-00090',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
self.robot_bb.set_check_runs(sha, [
{'name': 'policy-check', 'status': 'completed',
'conclusion': 'success', 'html_url': ''},
])
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status', 'bypass_github_checks']),
backtrace=True)

def test_github_checks_failed(self):
pr = self.create_pr('bugfix/TEST-00091', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00091',
'w/10.0/bugfix/TEST-00091',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
sha_last = self.gitrepo.cmd(
f'git rev-parse origin/{wbranches[-1]}').rstrip()
self.robot_bb.set_check_runs(sha_last, [
{'name': 'commit-msg-check', 'status': 'completed',
'conclusion': 'failure', 'html_url': 'https://example.com/check'},
])
with self.assertRaises(exns.GitHubChecksFailed):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status', 'bypass_github_checks']),
backtrace=True)

def test_github_checks_in_progress(self):
pr = self.create_pr('bugfix/TEST-00092', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00092',
'w/10.0/bugfix/TEST-00092',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
sha_last = self.gitrepo.cmd(
f'git rev-parse origin/{wbranches[-1]}').rstrip()
self.robot_bb.set_check_runs(sha_last, [
{'name': 'policy-check', 'status': 'in_progress',
'conclusion': None, 'html_url': ''},
])
with self.assertRaises(exns.GitHubChecksInProgress):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status', 'bypass_github_checks']),
backtrace=True)

def test_github_checks_bert_e_excluded(self):
pr = self.create_pr('bugfix/TEST-00093', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00093',
'w/10.0/bugfix/TEST-00093',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
self.robot_bb.set_check_runs(sha, [
{'name': 'bert-e', 'status': 'completed',
'conclusion': 'failure', 'html_url': ''},
])
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status', 'bypass_github_checks']),
backtrace=True)

def test_github_checks_bypass(self):
pr = self.create_pr('bugfix/TEST-00094', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00094',
'w/10.0/bugfix/TEST-00094',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
self.robot_bb.set_check_runs(sha, [
{'name': 'policy-check', 'status': 'completed',
'conclusion': 'failure', 'html_url': ''},
])
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status']),
backtrace=True)

def test_github_checks_neutral_skipped(self):
pr = self.create_pr('bugfix/TEST-00095', 'development/4.3')
self.set_build_status_on_pr_id(pr.id, 'SUCCESSFUL')
self.gitrepo.cmd('git fetch --all')
wbranches = [
'w/5.1/bugfix/TEST-00095',
'w/10.0/bugfix/TEST-00095',
]
for branch in wbranches:
sha = self.gitrepo.cmd(
f'git rev-parse origin/{branch}').rstrip()
self.set_build_status(sha, 'SUCCESSFUL')
sha_first = self.gitrepo.cmd(
f'git rev-parse origin/{wbranches[0]}').rstrip()
sha_last = self.gitrepo.cmd(
f'git rev-parse origin/{wbranches[-1]}').rstrip()
self.robot_bb.set_check_runs(sha_first, [
{'name': 'check-neutral', 'status': 'completed',
'conclusion': 'neutral', 'html_url': ''},
])
self.robot_bb.set_check_runs(sha_last, [
{'name': 'check-skipped', 'status': 'completed',
'conclusion': 'skipped', 'html_url': ''},
])
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, options=self.bypass_all_but(
['bypass_build_status', 'bypass_github_checks']),
backtrace=True)

def test_github_checks_no_check_runs(self):
pr = self.create_pr('bugfix/TEST-00096', 'development/4.3')
with self.assertRaises(exns.SuccessMessage):
self.handle(pr.id, options=self.bypass_all,
backtrace=True)


class TestQueueing(RepositoryTests):
"""Tests which validate all things related to the merge queue.
Expand Down
48 changes: 47 additions & 1 deletion bert_e/workflow/gitwaterflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
)
from .utils import (
bypass_incompatible_branch, bypass_peer_approval,
bypass_author_approval, bypass_leader_approval, bypass_build_status
bypass_author_approval, bypass_leader_approval, bypass_build_status,
bypass_github_checks
)
from .commands import setup # noqa
from .integration import (check_integration_branches,
Expand Down Expand Up @@ -197,6 +198,7 @@ def _handle_pull_request(job: PullRequestJob):

check_approvals(job)
check_build_status(job, wbranches)
check_github_checks(job, wbranches)

interactive = job.settings.interactive
if interactive and not confirm('Do you want to merge/queue?'):
Expand Down Expand Up @@ -667,3 +669,47 @@ def status(branch):
elif worst_status == 'INPROGRESS':
raise messages.BuildInProgress()
assert worst_status == 'SUCCESSFUL'


def check_github_checks(job, wbranches):
if bypass_github_checks(job):
return

failed_checks = []
in_progress = False

for wbranch in wbranches:
sha = wbranch.get_latest_commit()
check_runs = job.project_repo.get_check_runs(sha)

for cr in check_runs:
if cr.get('name') == 'bert-e':
continue

if cr.get('status') != 'completed':
in_progress = True
continue

conclusion = cr.get('conclusion')
if conclusion not in ('success', 'neutral', 'skipped'):
failed_checks.append({
'name': cr.get('name', 'unknown'),
'conclusion': conclusion,
'html_url': cr.get('html_url', ''),
'branch': wbranch.name,
})

if failed_checks:
first = failed_checks[0]
raise messages.GitHubChecksFailed(
branch=first['branch'],
dst_branch=wbranches[0].dst_branch.name,
failed_checks=failed_checks,
githost=job.settings.repository_host,
owner=job.settings.repository_owner,
slug=job.settings.repository_slug,
active_options=job.active_options,
)

if in_progress:
raise messages.GitHubChecksInProgress()
5 changes: 5 additions & 0 deletions bert_e/workflow/gitwaterflow/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ def setup(defaults={}):
"Bypass the pull request leaders' approval",
privileged=True,
default=defaults.get("bypass_leader_approval", False))
Reactor.add_option(
"bypass_github_checks",
"Bypass the GitHub check runs validation",
privileged=True,
default=defaults.get("bypass_github_checks", False))

# Other options
Reactor.add_option(
Expand Down
5 changes: 5 additions & 0 deletions bert_e/workflow/gitwaterflow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ def bypass_build_status(job):
def bypass_jira_check(job):
return (job.settings.bypass_jira_check or
job.author_bypass.get('bypass_jira_check', False))


def bypass_github_checks(job):
return (job.settings.bypass_github_checks or
job.author_bypass.get('bypass_github_checks', False))
Loading