mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[ADD] forwardport: ability to skip CI when forward porting
Provides a `skipci` command to PR reviewers. This makes it so the followup PRs (after the first one) get created immediately, without waiting for CI to succeed on a given forward-port PR. This can be useful if for some reason a change *must* be merged in branch N+1 before it can be merged in branch N. Fixes #363
This commit is contained in:
parent
49c1d960fa
commit
d99d1c2ad6
@ -189,6 +189,12 @@ class PullRequests(models.Model):
|
|||||||
source_id = fields.Many2one('runbot_merge.pull_requests', index=True, help="the original source of this FP even if parents were detached along the way")
|
source_id = fields.Many2one('runbot_merge.pull_requests', index=True, help="the original source of this FP even if parents were detached along the way")
|
||||||
reminder_backoff_factor = fields.Integer(default=-4)
|
reminder_backoff_factor = fields.Integer(default=-4)
|
||||||
|
|
||||||
|
fw_policy = fields.Selection([
|
||||||
|
('ci', "Normal"),
|
||||||
|
('skipci', "Skip CI"),
|
||||||
|
# ('skipmerge', "Skip merge"),
|
||||||
|
], required=True, default="ci")
|
||||||
|
|
||||||
refname = fields.Char(compute='_compute_refname')
|
refname = fields.Char(compute='_compute_refname')
|
||||||
@api.depends('label')
|
@api.depends('label')
|
||||||
def _compute_refname(self):
|
def _compute_refname(self):
|
||||||
@ -250,6 +256,12 @@ class PullRequests(models.Model):
|
|||||||
self.env['forwardport.branch_remover'].create({
|
self.env['forwardport.branch_remover'].create({
|
||||||
'pr_id': p.id,
|
'pr_id': p.id,
|
||||||
})
|
})
|
||||||
|
# if we change the policy to skip CI, schedule followups on existing FPs
|
||||||
|
if vals.get('fw_policy') == 'skipci' and self.state == 'merged':
|
||||||
|
self.env['runbot_merge.pull_requests'].search([
|
||||||
|
('source_id', '=', self.id),
|
||||||
|
('state', 'not in', ('closed', 'merged')),
|
||||||
|
])._schedule_fp_followup()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def _try_closing(self, by):
|
def _try_closing(self, by):
|
||||||
@ -280,6 +292,16 @@ class PullRequests(models.Model):
|
|||||||
if token is None:
|
if token is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
close = False
|
||||||
|
msg = None
|
||||||
|
if token in ('ci', 'skipci'):
|
||||||
|
pr = (self.source_id or self)
|
||||||
|
if pr._pr_acl(author).is_reviewer:
|
||||||
|
pr.fw_policy = token
|
||||||
|
msg = "Not waiting for CI to create followup forward-ports, sure." if token == 'skipci' else "Waiting for CI to create followup forward-ports, sure."
|
||||||
|
else:
|
||||||
|
msg = "I don't trust you enough to do that @{}.".format(login)
|
||||||
|
|
||||||
if token == 'ignore': # replace 'ignore' by 'up to <pr_branch>'
|
if token == 'ignore': # replace 'ignore' by 'up to <pr_branch>'
|
||||||
token = 'up'
|
token = 'up'
|
||||||
tokens = itertools.chain(['to', self.target.name], tokens)
|
tokens = itertools.chain(['to', self.target.name], tokens)
|
||||||
@ -298,19 +320,10 @@ class PullRequests(models.Model):
|
|||||||
# only the author is delegated explicitely on the
|
# only the author is delegated explicitely on the
|
||||||
pr._parse_commands(author, merge_bot + ' r+', login)
|
pr._parse_commands(author, merge_bot + ' r+', login)
|
||||||
elif token == 'close':
|
elif token == 'close':
|
||||||
close = False
|
msg = "I'm sorry, @{}. I can't close this PR for you."
|
||||||
message = "I'm sorry, @{}. I can't close this PR for you."
|
|
||||||
if self.source_id._pr_acl(author).is_reviewer:
|
if self.source_id._pr_acl(author).is_reviewer:
|
||||||
close = True
|
close = True
|
||||||
message = None
|
msg = None
|
||||||
|
|
||||||
Feedback.create({
|
|
||||||
'repository': self.repository.id,
|
|
||||||
'pull_request': self.number,
|
|
||||||
'message': message,
|
|
||||||
'close': close,
|
|
||||||
'token_field': 'fp_github_token',
|
|
||||||
})
|
|
||||||
elif token == 'up' and next(tokens, None) == 'to':
|
elif token == 'up' and next(tokens, None) == 'to':
|
||||||
limit = next(tokens, None)
|
limit = next(tokens, None)
|
||||||
if not self._pr_acl(author).is_author:
|
if not self._pr_acl(author).is_author:
|
||||||
@ -345,11 +358,16 @@ class PullRequests(models.Model):
|
|||||||
msg = "Forward-porting to %r." % limit_id.name
|
msg = "Forward-porting to %r." % limit_id.name
|
||||||
self.limit_id = limit_id
|
self.limit_id = limit_id
|
||||||
|
|
||||||
_logger.info("%s: %s", author, msg)
|
if msg or close:
|
||||||
|
if msg:
|
||||||
|
_logger.info("%s [%s]: %s", self.display_name, login, msg)
|
||||||
|
else:
|
||||||
|
_logger.info("%s [%s]: closing", self.display_name, login)
|
||||||
self.env['runbot_merge.pull_requests.feedback'].create({
|
self.env['runbot_merge.pull_requests.feedback'].create({
|
||||||
'repository': self.repository.id,
|
'repository': self.repository.id,
|
||||||
'pull_request': self.number,
|
'pull_request': self.number,
|
||||||
'message': msg,
|
'message': msg,
|
||||||
|
'close': close,
|
||||||
'token_field': 'fp_github_token',
|
'token_field': 'fp_github_token',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -370,16 +388,20 @@ class PullRequests(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def _validate(self, statuses):
|
def _validate(self, statuses):
|
||||||
_logger = logging.getLogger(__name__).getChild('forwardport.next')
|
|
||||||
failed = super()._validate(statuses)
|
failed = super()._validate(statuses)
|
||||||
|
self._schedule_fp_followup()
|
||||||
|
return failed
|
||||||
|
|
||||||
|
def _schedule_fp_followup(self):
|
||||||
|
_logger = logging.getLogger(__name__).getChild('forwardport.next')
|
||||||
# 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.display_name, pr)
|
||||||
if not pr.parent_id:
|
if not pr.parent_id:
|
||||||
_logger.info('-> no parent (%s)', pr)
|
_logger.info('-> no parent %s (%s)', pr.display_name, pr.parent_id)
|
||||||
continue
|
continue
|
||||||
if pr.state not in ['validated', 'ready']:
|
if self.source_id.fw_policy != 'skipci' and pr.state not in ['validated', 'ready']:
|
||||||
_logger.info('-> wrong state (%s)', pr.state)
|
_logger.info('-> wrong state %s (%s)', pr.display_name, pr.state)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check if we've already forward-ported this branch:
|
# check if we've already forward-ported this branch:
|
||||||
@ -391,10 +413,11 @@ class PullRequests(models.Model):
|
|||||||
# if the batch is inactive, the forward-port has been done *or*
|
# if the batch is inactive, the forward-port has been done *or*
|
||||||
# the PR's own forward port is in error, so bail
|
# the PR's own forward port is in error, so bail
|
||||||
if not batch.active:
|
if not batch.active:
|
||||||
|
_logger.info('-> forward port done or in error (%s.active=%s)', batch, batch.active)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# otherwise check if we already have a pending forward port
|
# otherwise check if we already have a pending forward port
|
||||||
_logger.info("%s %s %s", pr, batch, batch.prs)
|
_logger.info("%s %s %s", pr.display_name, batch, ', '.join(batch.mapped('prs.display_name')))
|
||||||
if self.env['forwardport.batches'].search_count([('batch_id', '=', batch.id)]):
|
if self.env['forwardport.batches'].search_count([('batch_id', '=', batch.id)]):
|
||||||
_logger.warning('-> already recorded')
|
_logger.warning('-> already recorded')
|
||||||
continue
|
continue
|
||||||
@ -402,8 +425,8 @@ class PullRequests(models.Model):
|
|||||||
# check if batch-mate are all valid
|
# check if batch-mate are all valid
|
||||||
mates = batch.prs
|
mates = batch.prs
|
||||||
# wait until all of them are validated or ready
|
# wait until all of them are validated or ready
|
||||||
if any(pr.state not in ('validated', 'ready') for pr in mates):
|
if any(pr.source_id.fw_policy != 'skipci' and pr.state not in ('validated', 'ready') for pr in mates):
|
||||||
_logger.warning("-> not ready (%s)", [(pr.number, pr.state) for pr in mates])
|
_logger.warning("-> not ready (%s)", [(pr.display_name, pr.state) for pr in mates])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check that there's no weird-ass state
|
# check that there's no weird-ass state
|
||||||
@ -419,7 +442,6 @@ class PullRequests(models.Model):
|
|||||||
'batch_id': batch.id,
|
'batch_id': batch.id,
|
||||||
'source': 'fp',
|
'source': 'fp',
|
||||||
})
|
})
|
||||||
return failed
|
|
||||||
|
|
||||||
def _find_next_target(self, reference):
|
def _find_next_target(self, reference):
|
||||||
""" Finds the branch between target and limit_id which follows
|
""" Finds the branch between target and limit_id which follows
|
||||||
@ -711,11 +733,15 @@ This PR targets %s and is part of the forward-port chain. Further PRs will be cr
|
|||||||
# (with the entire batch). If there are conflict then create a
|
# (with the entire batch). If there are conflict then create a
|
||||||
# deactivated batch so the interface is coherent but we don't pickup
|
# deactivated batch so the interface is coherent but we don't pickup
|
||||||
# an active batch we're never going to deactivate.
|
# an active batch we're never going to deactivate.
|
||||||
return self.env['runbot_merge.batch'].create({
|
b = self.env['runbot_merge.batch'].create({
|
||||||
'target': target.id,
|
'target': target.id,
|
||||||
'prs': [(6, 0, new_batch.ids)],
|
'prs': [(6, 0, new_batch.ids)],
|
||||||
'active': not has_conflicts,
|
'active': not has_conflicts,
|
||||||
})
|
})
|
||||||
|
# if we're not waiting for CI, schedule followup immediately
|
||||||
|
if any(p.source_id.fw_policy == 'skipci' for p in b.prs):
|
||||||
|
b.prs[0]._schedule_fp_followup()
|
||||||
|
return b
|
||||||
|
|
||||||
def _pingline(self):
|
def _pingline(self):
|
||||||
assignees = (self.author | self.reviewed_by).mapped('github_login')
|
assignees = (self.author | self.reviewed_by).mapped('github_login')
|
||||||
|
@ -558,7 +558,7 @@ def test_new_intermediate_branch(env, config, make_repo):
|
|||||||
'x': 'x',
|
'x': 'x',
|
||||||
}, "check that new got all the updates (should be in the same state as c really)"
|
}, "check that new got all the updates (should be in the same state as c really)"
|
||||||
|
|
||||||
def test_author_can_close_via_fwbot(env, config, make_repo, users):
|
def test_author_can_close_via_fwbot(env, config, make_repo):
|
||||||
project, prod, xxx = make_basic(env, config, make_repo, fp_token=True, fp_remote=True)
|
project, prod, xxx = make_basic(env, config, make_repo, fp_token=True, fp_remote=True)
|
||||||
other_user = config['role_other']
|
other_user = config['role_other']
|
||||||
other_token = other_user['token']
|
other_token = other_user['token']
|
||||||
@ -598,3 +598,64 @@ def test_author_can_close_via_fwbot(env, config, make_repo, users):
|
|||||||
env.run_crons()
|
env.run_crons()
|
||||||
assert pr1.state == 'closed'
|
assert pr1.state == 'closed'
|
||||||
assert pr1_id.state == 'closed'
|
assert pr1_id.state == 'closed'
|
||||||
|
|
||||||
|
def test_skip_ci_all(env, config, make_repo):
|
||||||
|
project, prod, _ = make_basic(env, config, make_repo, fp_token=True, fp_remote=True)
|
||||||
|
|
||||||
|
with prod:
|
||||||
|
prod.make_commits('a', Commit('x', tree={'x': '0'}), ref='heads/change')
|
||||||
|
pr = prod.make_pr(target='a', head='change')
|
||||||
|
prod.post_status(pr.head, 'success', 'legal/cla')
|
||||||
|
prod.post_status(pr.head, 'success', 'ci/runbot')
|
||||||
|
pr.post_comment('%s skipci' % project.fp_github_name, config['role_reviewer']['token'])
|
||||||
|
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||||
|
env.run_crons()
|
||||||
|
assert env['runbot_merge.pull_requests'].search([
|
||||||
|
('repository.name', '=', prod.name),
|
||||||
|
('number', '=', pr.number)
|
||||||
|
]).fw_policy == 'skipci'
|
||||||
|
|
||||||
|
with prod:
|
||||||
|
prod.post_status('staging.a', 'success', 'legal/cla')
|
||||||
|
prod.post_status('staging.a', 'success', 'ci/runbot')
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
# run cron a few more times for the fps
|
||||||
|
env.run_crons()
|
||||||
|
env.run_crons()
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
pr0_id, pr1_id, pr2_id = env['runbot_merge.pull_requests'].search([], order='number')
|
||||||
|
assert pr1_id.state == 'opened'
|
||||||
|
assert pr1_id.source_id == pr0_id
|
||||||
|
assert pr2_id.state == 'opened'
|
||||||
|
assert pr2_id.source_id == pr0_id
|
||||||
|
|
||||||
|
def test_skip_ci_next(env, config, make_repo):
|
||||||
|
project, prod, _ = make_basic(env, config, make_repo, fp_token=True, fp_remote=True)
|
||||||
|
|
||||||
|
with prod:
|
||||||
|
prod.make_commits('a', Commit('x', tree={'x': '0'}), ref='heads/change')
|
||||||
|
pr = prod.make_pr(target='a', head='change')
|
||||||
|
prod.post_status(pr.head, 'success', 'legal/cla')
|
||||||
|
prod.post_status(pr.head, 'success', 'ci/runbot')
|
||||||
|
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
with prod:
|
||||||
|
prod.post_status('staging.a', 'success', 'legal/cla')
|
||||||
|
prod.post_status('staging.a', 'success', 'ci/runbot')
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
pr0_id, pr1_id = env['runbot_merge.pull_requests'].search([], order='number')
|
||||||
|
with prod:
|
||||||
|
prod.get_pr(pr1_id.number).post_comment(
|
||||||
|
'%s skipci' % project.fp_github_name,
|
||||||
|
config['role_user']['token']
|
||||||
|
)
|
||||||
|
assert pr0_id.fw_policy == 'skipci'
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
_, _, pr2_id = env['runbot_merge.pull_requests'].search([], order='number')
|
||||||
|
assert pr1_id.state == 'opened'
|
||||||
|
assert pr2_id.state == 'opened'
|
||||||
|
Loading…
Reference in New Issue
Block a user