[IMP] forwardport: ping on CI failure

It's especially important as users / assignees don't get
pinged *during* the forwardport process.

closes #203
This commit is contained in:
Xavier Morel 2019-09-18 08:32:38 +02:00
parent ee8f81be2a
commit 52699d901a
3 changed files with 69 additions and 17 deletions

View File

@ -222,13 +222,23 @@ class PullRequests(models.Model):
def _validate(self, statuses): def _validate(self, statuses):
_logger = logging.getLogger(__name__).getChild('forwardport.next') _logger = logging.getLogger(__name__).getChild('forwardport.next')
super()._validate(statuses) failed = super()._validate(statuses)
# if the PR has a parent and is CI-validated, enqueue the next PR # if the PR has a parent and is CI-validated, enqueue the next PR
for pr in self: for pr in self:
_logger.info('Checking if forward-port %s (%s)', pr, pr.number) _logger.info('Checking if forward-port %s (%s)', pr, pr.number)
if not pr.parent_id or pr.state not in ('validated', 'ready'): if not pr.source_id:
_logger.info('-> no parent or wrong state (%s)', pr.state) _logger.info('-> no parent (%s)', pr)
continue continue
if pr.state not in ['validated', 'ready']:
_logger.info('-> wrong state (%s)', pr.state)
if pr in failed:
self.env['runbot_merge.pull_requests.feedback'].create({
'repository': pr.repository.id,
'pull_request': pr.number,
'message': pr.source_id._pingline() + '\n\nCI failed on this forward-port PR'
})
continue
# if it already has a child, bail # if it already has a child, bail
if self.search_count([('parent_id', '=', self.id)]): if self.search_count([('parent_id', '=', self.id)]):
_logger.info('-> already has a child') _logger.info('-> already has a child')
@ -262,6 +272,7 @@ class PullRequests(models.Model):
'batch_id': batch.id, 'batch_id': batch.id,
'source': 'fp', 'source': 'fp',
}) })
return failed
def _forward_port_sequence(self): def _forward_port_sequence(self):
# risk: we assume disabled branches are still at the correct location # risk: we assume disabled branches are still at the correct location
@ -452,8 +463,6 @@ class PullRequests(models.Model):
] ]
}) })
assignees = (source.author | source.reviewed_by).mapped('github_login')
ping = "Ping %s" % ', '.join('@' + login for login in assignees if login)
if h: if h:
sout = serr = '' sout = serr = ''
if out.strip(): if out.strip():
@ -461,7 +470,7 @@ class PullRequests(models.Model):
if err.strip(): if err.strip():
serr = "\nstderr:\n```\n%s\n```\n" % err serr = "\nstderr:\n```\n%s\n```\n" % err
message = ping + """ message = source._pingline() + """
Cherrypicking %s of source #%d failed Cherrypicking %s of source #%d failed
%s%s %s%s
Either perform the forward-port manually (and push to this branch, proceeding as usual) or close this PR (maybe?). Either perform the forward-port manually (and push to this branch, proceeding as usual) or close this PR (maybe?).
@ -474,7 +483,7 @@ In the former case, you may want to edit this PR message as well.
for p in pr._iter_ancestors() for p in pr._iter_ancestors()
if p.parent_id and p.parent_id != source if p.parent_id and p.parent_id != source
) )
message = ping + """ message = source._pingline() + """
This PR targets %s and is the last of the forward-port chain%s This PR targets %s and is the last of the forward-port chain%s
%s %s
To merge the full chain, say To merge the full chain, say
@ -508,6 +517,14 @@ More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
'active': not has_conflicts, 'active': not has_conflicts,
}) })
def _pingline(self):
assignees = (self.author | self.reviewed_by).mapped('github_login')
return "Ping %s" % ', '.join(
'@' + login
for login in assignees
if login
)
def _create_fp_branch(self, target_branch, fp_branch_name, cleanup): def _create_fp_branch(self, target_branch, fp_branch_name, cleanup):
""" Creates a forward-port for the current PR to ``target_branch`` under """ Creates a forward-port for the current PR to ``target_branch`` under
``fp_branch_name``. ``fp_branch_name``.

View File

@ -254,7 +254,7 @@ More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
# check that we didn't just smash the original trees # check that we didn't just smash the original trees
assert prod.read_tree(prod.commit('a')) != b_tree != c_tree assert prod.read_tree(prod.commit('a')) != b_tree != c_tree
def test_update_pr(env, config, make_repo): def test_update_pr(env, config, make_repo, users):
""" Even for successful cherrypicks, it's possible that e.g. CI doesn't """ Even for successful cherrypicks, it's possible that e.g. CI doesn't
pass or the reviewer finds out they need to update the code. pass or the reviewer finds out they need to update the code.
@ -282,10 +282,41 @@ def test_update_pr(env, config, make_repo):
env.run_crons() env.run_crons()
pr0, pr1 = env['runbot_merge.pull_requests'].search([], order='number') pr0, pr1 = env['runbot_merge.pull_requests'].search([], order='number')
with prod: # FP to c as well
prod.post_status(pr1.head, 'success', 'ci/runbot') fp_intermediate = (users['user'], '''\
This PR targets b and is part of the forward-port chain. Further PRs will be created up to c.
More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
''')
ci_warning = (users['user'], 'Ping @%(user)s, @%(reviewer)s\n\nCI failed on this forward-port PR' % users)
# oh no CI of the first FP PR failed!
with prod:
prod.post_status(pr1.head, 'failure', 'ci/runbot')
prod.post_status(pr1.head, 'success', 'legal/cla') prod.post_status(pr1.head, 'success', 'legal/cla')
env.run_crons() env.run_crons()
# check that FP did not resume & we have a ping on the PR
assert env['runbot_merge.pull_requests'].search([], order='number') == pr0 | pr1,\
"forward port should not continue on CI failure"
pr1_remote = prod.get_pr(pr1.number)
assert pr1_remote.comments == [fp_intermediate, ci_warning]
# it was a false positive, rebuild... it fails again!
with prod:
prod.post_status(pr1.head, 'failure', 'ci/runbot')
env.run_crons()
# check that FP did not resume & we have a ping on the PR
assert env['runbot_merge.pull_requests'].search([], order='number') == pr0 | pr1,\
"ensure it still hasn't restarted"
assert pr1_remote.comments == [fp_intermediate, ci_warning, ci_warning]
# nb: updating the head would detach the PR and not put it in the warning
# path anymore
# rebuild again, finally passes
with prod:
prod.post_status(pr1.head, 'success', 'ci/runbot')
env.run_crons()
pr0, pr1, pr2 = env['runbot_merge.pull_requests'].search([], order='number') pr0, pr1, pr2 = env['runbot_merge.pull_requests'].search([], order='number')
assert pr1.parent_id == pr0 assert pr1.parent_id == pr0

View File

@ -810,6 +810,7 @@ class PullRequests(models.Model):
# could have two PRs (e.g. one open and one closed) at least # could have two PRs (e.g. one open and one closed) at least
# temporarily on the same head, or on the same head with different # temporarily on the same head, or on the same head with different
# targets # targets
failed = self.browse(())
for pr in self: for pr in self:
required = pr.repository.project_id.required_statuses.split(',') required = pr.repository.project_id.required_statuses.split(',')
@ -820,13 +821,15 @@ class PullRequests(models.Model):
continue continue
success = False success = False
# only report an issue of the PR is already approved (r+'d) if st in ('error', 'failure'):
if st in ('error', 'failure') and pr.state == 'approved': failed |= pr
self.env['runbot_merge.pull_requests.feedback'].create({ # only report an issue of the PR is already approved (r+'d)
'repository': pr.repository.id, if pr.state == 'approved':
'pull_request': pr.number, self.env['runbot_merge.pull_requests.feedback'].create({
'message': "%r failed on this reviewed PR." % ci, 'repository': pr.repository.id,
}) 'pull_request': pr.number,
'message': "%r failed on this reviewed PR." % ci,
})
if success: if success:
oldstate = pr.state oldstate = pr.state
@ -834,6 +837,7 @@ class PullRequests(models.Model):
pr.state = 'validated' pr.state = 'validated'
elif oldstate == 'approved': elif oldstate == 'approved':
pr.state = 'ready' pr.state = 'ready'
return failed
def _auto_init(self): def _auto_init(self):
super(PullRequests, self)._auto_init() super(PullRequests, self)._auto_init()