[IMP] runbot_merge, forwardport: move required statuses to repository

Allows more flexibility in project composition as different
repositories can each have their own CI passes.
This commit is contained in:
Xavier Morel 2020-01-21 14:00:11 +01:00 committed by xmo-odoo
parent b2f9bd697c
commit 7dfa973b57
9 changed files with 99 additions and 25 deletions

View File

@ -25,7 +25,6 @@ def make_basic(env, config, make_repo, *, fp_token, fp_remote):
'github_token': config['github']['token'],
'github_prefix': 'hansen',
'fp_github_token': fp_token and config['github']['token'],
'required_statuses': 'legal/cla,ci/runbot',
'branch_ids': [
(0, 0, {'name': 'a', 'fp_sequence': 2, 'fp_target': True}),
(0, 0, {'name': 'b', 'fp_sequence': 1, 'fp_target': True}),
@ -59,6 +58,7 @@ def make_basic(env, config, make_repo, *, fp_token, fp_remote):
project.write({
'repo_ids': [(0, 0, {
'name': prod.name,
'required_statuses': 'legal/cla,ci/runbot',
'fp_remote_target': fp_remote and other.name,
})],
})

View File

@ -55,7 +55,6 @@ def make_basic(env, config, make_repo, *, reponame='proj', project_name='myproje
'github_token': config['github']['token'],
'github_prefix': 'hansen',
'fp_github_token': config['github']['token'],
'required_statuses': 'legal/cla,ci/runbot',
'branch_ids': [
(0, 0, {'name': 'a', 'fp_sequence': 2, 'fp_target': True}),
(0, 0, {'name': 'b', 'fp_sequence': 1, 'fp_target': True}),
@ -89,6 +88,7 @@ def make_basic(env, config, make_repo, *, reponame='proj', project_name='myproje
project.write({
'repo_ids': [(0, 0, {
'name': prod.name,
'required_statuses': 'legal/cla,ci/runbot',
'fp_remote_target': other.name,
})],
})

View File

@ -1,5 +1,6 @@
{
'name': 'merge bot',
'version': '1.1',
'depends': ['contacts', 'website'],
'data': [
'security/security.xml',

View File

@ -0,0 +1,17 @@
def migrate(cr, version):
""" Moved the required_statuses field from the project to the repository so
different repos can have different CI requirements within a project
"""
# create column on repo
cr.execute("ALTER TABLE runbot_merge_repository ADD COLUMN required_statuses varchar")
# copy data from project
cr.execute("""
UPDATE runbot_merge_repository r
SET required_statuses = (
SELECT required_statuses
FROM runbot_merge_project
WHERE id = r.project_id
)
""")
# drop old column on project
cr.execute("ALTER TABLE runbot_merge_project DROP COLUMN required_statuses")

View File

@ -41,11 +41,6 @@ class Project(models.Model):
"target branches of PR this project handles."
)
required_statuses = fields.Char(
help="Comma-separated list of status contexts which must be "\
"`success` for a PR or staging to be valid",
default='legal/cla,ci/runbot'
)
ci_timeout = fields.Integer(
default=60, required=True,
help="Delay (in minutes) before a staging is considered timed out and failed"
@ -217,6 +212,11 @@ class Repository(models.Model):
name = fields.Char(required=True)
project_id = fields.Many2one('runbot_merge.project', required=True)
required_statuses = fields.Char(
help="Comma-separated list of status contexts which must be "\
"`success` for a PR or staging to be valid",
default='legal/cla,ci/runbot'
)
def github(self, token_field='github_token'):
return github.GH(self.project_id[token_field], self.name)
@ -586,7 +586,7 @@ class PullRequests(models.Model):
for pr in self:
pr.blocked = pr.id not in stageable
@api.depends('head', 'repository.project_id.required_statuses')
@api.depends('head', 'repository.required_statuses')
def _compute_statuses(self):
Commits = self.env['runbot_merge.commit']
for s in self:
@ -599,7 +599,7 @@ class PullRequests(models.Model):
s.statuses = pprint.pformat(statuses)
st = 'success'
for ci in s.repository.project_id.required_statuses.split(','):
for ci in s.repository.required_statuses.split(','):
v = state_(statuses, ci) or 'pending'
if v in ('error', 'failure'):
st = 'failure'
@ -851,7 +851,7 @@ class PullRequests(models.Model):
# targets
failed = self.browse(())
for pr in self:
required = filter(None, pr.repository.project_id.required_statuses.split(','))
required = filter(None, pr.repository.required_statuses.split(','))
success = True
for ci in required:
@ -1416,18 +1416,23 @@ class Stagings(models.Model):
if s.state != 'pending':
continue
heads = [
head for repo, head in json.loads(s.heads).items()
repos = {
repo.name: repo
for repo in self.env['runbot_merge.repository'].search([])
}
repomap = {
head: repos[repo]
for repo, head in json.loads(s.heads).items()
if not repo.endswith('^')
]
}
commits = Commits.search([
('sha', 'in', heads)
('sha', 'in', list(repomap))
])
update_timeout_limit = False
reqs = [r.strip() for r in s.target.project_id.required_statuses.split(',')]
st = 'success'
for c in commits:
reqs = [r.strip() for r in repomap[c.sha].required_statuses.split(',')]
statuses = json.loads(c.statuses)
for v in map(lambda n: state_(statuses, n), reqs):
if st == 'failure' or v in ('error', 'failure'):
@ -1441,7 +1446,7 @@ class Stagings(models.Model):
assert v == 'success'
# mark failure as soon as we find a failed status, but wait until
# all commits are known & not pending to mark a success
if st == 'success' and len(commits) < len(heads):
if st == 'success' and len(commits) < len(repomap):
st = 'pending'
vals = {'state': st}

View File

@ -36,5 +36,4 @@ def project(env, config):
'github_token': config['github']['token'],
'github_prefix': 'hansen',
'branch_ids': [(0, 0, {'name': 'master'})],
'required_statuses': 'legal/cla,ci/runbot',
})

View File

@ -15,7 +15,10 @@ from test_utils import re_matches, get_partner, _simple_init
@pytest.fixture
def repo(project, make_repo):
r = make_repo('repo')
project.write({'repo_ids': [(0, 0, {'name': r.name})]})
project.write({'repo_ids': [(0, 0, {
'name': r.name,
'required_statuses': 'legal/cla,ci/runbot'
})]})
return r
def test_trivial_flow(env, repo, page, users, config):
@ -937,7 +940,7 @@ def test_reopen_state(env, repo):
def test_no_required_statuses(env, repo, config):
""" check that mergebot can work on a repo with no CI at all
"""
env['runbot_merge.project'].search([]).required_statuses = ''
env['runbot_merge.repository'].search([('name', '=', repo.name)]).required_statuses = ''
with repo:
m = repo.make_commit(None, 'initial', None, tree={'0': '0'})
repo.make_ref('heads/master', m)

View File

@ -14,19 +14,28 @@ from test_utils import re_matches, get_partner
@pytest.fixture
def repo_a(project, make_repo):
repo = make_repo('a')
project.write({'repo_ids': [(0, 0, {'name': repo.name})]})
project.write({'repo_ids': [(0, 0, {
'name': repo.name,
'required_statuses': 'legal/cla,ci/runbot'
})]})
return repo
@pytest.fixture
def repo_b(project, make_repo):
repo = make_repo('b')
project.write({'repo_ids': [(0, 0, {'name': repo.name})]})
project.write({'repo_ids': [(0, 0, {
'name': repo.name,
'required_statuses': 'legal/cla,ci/runbot'
})]})
return repo
@pytest.fixture
def repo_c(project, make_repo):
repo = make_repo('c')
project.write({'repo_ids': [(0, 0, {'name': repo.name})]})
project.write({'repo_ids': [(0, 0, {
'name': repo.name,
'required_statuses': 'legal/cla,ci/runbot'
})]})
return repo
def make_pr(repo, prefix, trees, *, target='master', user,
@ -137,6 +146,48 @@ def test_stage_match(env, project, repo_a, repo_b, config):
assert 'Related: {}#{}'.format(repo_b.name, pr_b.number) in repo_a.commit('master').message
assert 'Related: {}#{}'.format(repo_a.name, pr_a.number) in repo_b.commit('master').message
def test_stage_different_statuses(env, project, repo_a, repo_b, config):
project.batch_limit = 1
env['runbot_merge.repository'].search([
('name', '=', repo_b.name)
]).write({
'required_statuses': 'foo/bar',
})
with repo_a:
make_branch(repo_a, 'master', 'initial', {'a': 'a_0'})
pr_a = make_pr(
repo_a, 'do-a-thing', [{'a': 'a_1'}],
user=config['role_user']['token'],
reviewer=config['role_reviewer']['token'],
)
repo_a.post_status(pr_a.head, 'success', 'foo/bar')
with repo_b:
make_branch(repo_b, 'master', 'initial', {'a': 'b_0'})
pr_b = make_pr(repo_b, 'do-a-thing', [{'a': 'b_1'}],
user=config['role_user']['token'],
reviewer=config['role_reviewer']['token'],
)
env.run_crons()
# since the labels are the same but the statuses on pr_b are not the
# expected ones, pr_a should be blocked on pr_b, which should be approved
# but not validated / ready
pr_a_id = to_pr(env, pr_a)
pr_b_id = to_pr(env, pr_b)
assert pr_a_id.state == 'ready'
assert not pr_a_id.staging_id
assert pr_a_id.blocked
assert pr_b_id.state == 'approved'
assert not pr_b_id.staging_id
with repo_b:
repo_b.post_status(pr_b.head, 'success', 'foo/bar')
env.run_crons()
assert pr_a_id.state == pr_b_id.state == 'ready'
assert pr_a_id.staging_id == pr_b_id.staging_id
def test_unmatch_patch(env, project, repo_a, repo_b, config):
""" When editing files via the UI for a project you don't have write
access to, a branch called patch-XXX is automatically created in your

View File

@ -19,9 +19,6 @@
<group>
<field name="github_prefix" string="bot name"/>
</group>
<group>
<field name="required_statuses"/>
</group>
</group>
<group>
<group>
@ -38,6 +35,7 @@
<field name="repo_ids">
<tree editable="bottom">
<field name="name"/>
<field name="required_statuses"/>
</tree>
</field>
<separator string="Branches"/>