mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[IMP] runbot_merge: split status configuration into own object
Having the required statuses be a mere list of contexts has become a bit too limiting for our needs as it doesn't allow e.g. adding new required statuses on only some branches of a repository (e.g. only master), nor does it allow putting checks on only branches, or only stagings, which would be useful for overridable checks and the like, or for checks which only make sense linked to a specific revision range (e.g. "incremental" linting which would only check whatever's been modified in a PR). Split the required statuses into a separate set of objects, any of which can be separately marked as applying only to some branches (no branch = all branches). Fixes #382
This commit is contained in:
parent
916fb30e75
commit
be228a5681
@ -1087,7 +1087,6 @@ class Model:
|
|||||||
return self._env(self._model, name, self._ids, *args, **kwargs)
|
return self._env(self._model, name, self._ids, *args, **kwargs)
|
||||||
|
|
||||||
def __setattr__(self, fieldname, value):
|
def __setattr__(self, fieldname, value):
|
||||||
assert self._fields[fieldname]['type'] not in ('many2one', 'one2many', 'many2many')
|
|
||||||
self._env(self._model, 'write', self._ids, {fieldname: value})
|
self._env(self._model, 'write', self._ids, {fieldname: value})
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import base64
|
import base64
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
@ -10,7 +11,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from itertools import takewhile
|
from itertools import takewhile
|
||||||
@ -65,9 +65,6 @@ class Project(models.Model):
|
|||||||
"will lead to webhook rejection. Should only use ASCII."
|
"will lead to webhook rejection. Should only use ASCII."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_stagings(self, commit=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _check_stagings(self, commit=False):
|
def _check_stagings(self, commit=False):
|
||||||
for staging in self.search([]).mapped('branch_ids.active_staging_id'):
|
for staging in self.search([]).mapped('branch_ids.active_staging_id'):
|
||||||
staging.check_status()
|
staging.check_status()
|
||||||
@ -217,6 +214,20 @@ class Project(models.Model):
|
|||||||
""", (self.id, name))
|
""", (self.id, name))
|
||||||
return bool(self.env.cr.rowcount)
|
return bool(self.env.cr.rowcount)
|
||||||
|
|
||||||
|
class StatusConfiguration(models.Model):
|
||||||
|
_name = 'runbot_merge.repository.status'
|
||||||
|
_description = "required statuses on repositories"
|
||||||
|
_rec_name = 'context'
|
||||||
|
_log_access = False
|
||||||
|
|
||||||
|
context = fields.Char(required=True)
|
||||||
|
repo_id = fields.Many2one('runbot_merge.repository', required=True, ondelete='cascade')
|
||||||
|
branch_ids = fields.Many2many('runbot_merge.branch', 'runbot_merge_repository_status_branch', 'status_id', 'branch_id')
|
||||||
|
|
||||||
|
def _for_branch(self, branch):
|
||||||
|
assert branch._name == 'runbot_merge.branch', \
|
||||||
|
f'Expected branch, got {branch}'
|
||||||
|
return self.filtered(lambda st: not st.branch_ids or branch in st.branch_ids)
|
||||||
class Repository(models.Model):
|
class Repository(models.Model):
|
||||||
_name = _description = 'runbot_merge.repository'
|
_name = _description = 'runbot_merge.repository'
|
||||||
_order = 'sequence, id'
|
_order = 'sequence, id'
|
||||||
@ -224,11 +235,8 @@ class Repository(models.Model):
|
|||||||
sequence = fields.Integer(default=50)
|
sequence = fields.Integer(default=50)
|
||||||
name = fields.Char(required=True)
|
name = fields.Char(required=True)
|
||||||
project_id = fields.Many2one('runbot_merge.project', required=True)
|
project_id = fields.Many2one('runbot_merge.project', required=True)
|
||||||
required_statuses = fields.Char(
|
status_ids = fields.One2many('runbot_merge.repository.status', 'repo_id', string="Required Statuses")
|
||||||
help="Comma-separated list of status contexts which must be "\
|
|
||||||
"`success` for a PR or staging to be valid",
|
|
||||||
default='legal/cla,ci/runbot'
|
|
||||||
)
|
|
||||||
branch_filter = fields.Char(default='[(1, "=", 1)]', help="Filter branches valid for this repository")
|
branch_filter = fields.Char(default='[(1, "=", 1)]', help="Filter branches valid for this repository")
|
||||||
substitutions = fields.Text(
|
substitutions = fields.Text(
|
||||||
"label substitutions",
|
"label substitutions",
|
||||||
@ -237,6 +245,22 @@ class Repository(models.Model):
|
|||||||
All substitutions are tentatively applied sequentially to the input.
|
All substitutions are tentatively applied sequentially to the input.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
if 'status_ids' in vals:
|
||||||
|
return super().create(vals)
|
||||||
|
|
||||||
|
st = vals.pop('required_statuses', 'legal/cla,ci/runbot')
|
||||||
|
if st:
|
||||||
|
vals['status_ids'] = [(0, 0, {'context': c}) for c in st.split(',')]
|
||||||
|
return super().create(vals)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
st = vals.pop('required_statuses', None)
|
||||||
|
if st:
|
||||||
|
vals['status_ids'] = [(5, 0, {})] + [(0, 0, {'context': c}) for c in st.split(',')]
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
def github(self, token_field='github_token'):
|
def github(self, token_field='github_token'):
|
||||||
return github.GH(self.project_id[token_field], self.name)
|
return github.GH(self.project_id[token_field], self.name)
|
||||||
|
|
||||||
@ -662,27 +686,27 @@ class PullRequests(models.Model):
|
|||||||
pr.blocked = 'linked pr %s is not ready' % unready.display_name
|
pr.blocked = 'linked pr %s is not ready' % unready.display_name
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@api.depends('head', 'repository.required_statuses')
|
@api.depends('head', 'repository.status_ids')
|
||||||
def _compute_statuses(self):
|
def _compute_statuses(self):
|
||||||
Commits = self.env['runbot_merge.commit']
|
Commits = self.env['runbot_merge.commit']
|
||||||
for s in self:
|
for pr in self:
|
||||||
c = Commits.search([('sha', '=', s.head)])
|
c = Commits.search([('sha', '=', pr.head)])
|
||||||
if not (c and c.statuses):
|
if not (c and c.statuses):
|
||||||
s.status = s.statuses = False
|
pr.status = pr.statuses = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
statuses = json.loads(c.statuses)
|
statuses = json.loads(c.statuses)
|
||||||
s.statuses = pprint.pformat(statuses)
|
pr.statuses = pprint.pformat(statuses)
|
||||||
|
|
||||||
st = 'success'
|
st = 'success'
|
||||||
for ci in filter(None, (s.repository.required_statuses or '').split(',')):
|
for ci in pr.repository.status_ids._for_branch(pr.target):
|
||||||
v = state_(statuses, ci) or 'pending'
|
v = state_(statuses, ci.context) or 'pending'
|
||||||
if v in ('error', 'failure'):
|
if v in ('error', 'failure'):
|
||||||
st = 'failure'
|
st = 'failure'
|
||||||
break
|
break
|
||||||
if v == 'pending':
|
if v == 'pending':
|
||||||
st = 'pending'
|
st = 'pending'
|
||||||
s.status = st
|
pr.status = st
|
||||||
|
|
||||||
@api.depends('batch_ids.active')
|
@api.depends('batch_ids.active')
|
||||||
def _compute_active_batch(self):
|
def _compute_active_batch(self):
|
||||||
@ -944,7 +968,7 @@ class PullRequests(models.Model):
|
|||||||
# targets
|
# targets
|
||||||
failed = self.browse(())
|
failed = self.browse(())
|
||||||
for pr in self:
|
for pr in self:
|
||||||
required = filter(None, (pr.repository.required_statuses or '').split(','))
|
required = pr.repository.status_ids._for_branch(pr.target).mapped('context')
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
for ci in required:
|
for ci in required:
|
||||||
@ -1438,7 +1462,7 @@ class Commit(models.Model):
|
|||||||
def _auto_init(self):
|
def _auto_init(self):
|
||||||
res = super(Commit, self)._auto_init()
|
res = super(Commit, self)._auto_init()
|
||||||
self._cr.execute("""
|
self._cr.execute("""
|
||||||
CREATE INDEX IF NOT EXISTS runbot_merge_unique_statuses
|
CREATE INDEX IF NOT EXISTS runbot_merge_unique_statuses
|
||||||
ON runbot_merge_commit
|
ON runbot_merge_commit
|
||||||
USING hash (sha)
|
USING hash (sha)
|
||||||
""")
|
""")
|
||||||
@ -1523,10 +1547,9 @@ class Stagings(models.Model):
|
|||||||
}
|
}
|
||||||
# maps commits to the statuses they need
|
# maps commits to the statuses they need
|
||||||
required_statuses = [
|
required_statuses = [
|
||||||
(head, repos[repo].required_statuses.split(','))
|
(head, repos[repo].status_ids._for_branch(s.target).mapped('context'))
|
||||||
for repo, head in json.loads(s.heads).items()
|
for repo, head in json.loads(s.heads).items()
|
||||||
if not repo.endswith('^')
|
if not repo.endswith('^')
|
||||||
if repos[repo].required_statuses
|
|
||||||
]
|
]
|
||||||
# maps commits to their statuses
|
# maps commits to their statuses
|
||||||
cmap = {
|
cmap = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_runbot_merge_project_admin,Admin access to project,model_runbot_merge_project,runbot_merge.group_admin,1,1,1,1
|
access_runbot_merge_project_admin,Admin access to project,model_runbot_merge_project,runbot_merge.group_admin,1,1,1,1
|
||||||
access_runbot_merge_repository_admin,Admin access to repo,model_runbot_merge_repository,runbot_merge.group_admin,1,1,1,1
|
access_runbot_merge_repository_admin,Admin access to repo,model_runbot_merge_repository,runbot_merge.group_admin,1,1,1,1
|
||||||
|
access_runbot_merge_repository_status_admin,Admin access to repo statuses,model_runbot_merge_repository_status,runbot_merge.group_admin,1,1,1,1
|
||||||
access_runbot_merge_branch_admin,Admin access to branches,model_runbot_merge_branch,runbot_merge.group_admin,1,1,1,1
|
access_runbot_merge_branch_admin,Admin access to branches,model_runbot_merge_branch,runbot_merge.group_admin,1,1,1,1
|
||||||
access_runbot_merge_pull_requests_admin,Admin access to PR,model_runbot_merge_pull_requests,runbot_merge.group_admin,1,1,1,1
|
access_runbot_merge_pull_requests_admin,Admin access to PR,model_runbot_merge_pull_requests,runbot_merge.group_admin,1,1,1,1
|
||||||
access_runbot_merge_pull_requests_tagging_admin,Admin access to tagging,model_runbot_merge_pull_requests_tagging,runbot_merge.group_admin,1,1,1,1
|
access_runbot_merge_pull_requests_tagging_admin,Admin access to tagging,model_runbot_merge_pull_requests_tagging,runbot_merge.group_admin,1,1,1,1
|
||||||
|
|
@ -982,7 +982,7 @@ class TestNoRequiredStatus:
|
|||||||
def test_basic(self, env, repo, config):
|
def test_basic(self, env, repo, config):
|
||||||
""" check that mergebot can work on a repo with no CI at all
|
""" check that mergebot can work on a repo with no CI at all
|
||||||
"""
|
"""
|
||||||
env['runbot_merge.repository'].search([('name', '=', repo.name)]).required_statuses = False
|
env['runbot_merge.repository'].search([('name', '=', repo.name)]).status_ids = False
|
||||||
with repo:
|
with repo:
|
||||||
m = repo.make_commit(None, 'initial', None, tree={'0': '0'})
|
m = repo.make_commit(None, 'initial', None, tree={'0': '0'})
|
||||||
repo.make_ref('heads/master', m)
|
repo.make_ref('heads/master', m)
|
||||||
@ -1004,7 +1004,7 @@ class TestNoRequiredStatus:
|
|||||||
assert pr.state == 'merged'
|
assert pr.state == 'merged'
|
||||||
|
|
||||||
def test_updated(self, env, repo, config):
|
def test_updated(self, env, repo, config):
|
||||||
env['runbot_merge.repository'].search([('name', '=', repo.name)]).required_statuses = False
|
env['runbot_merge.repository'].search([('name', '=', repo.name)]).status_ids = False
|
||||||
with repo:
|
with repo:
|
||||||
m = repo.make_commit(None, 'initial', None, tree={'0': '0'})
|
m = repo.make_commit(None, 'initial', None, tree={'0': '0'})
|
||||||
repo.make_ref('heads/master', m)
|
repo.make_ref('heads/master', m)
|
||||||
|
96
runbot_merge/tests/test_by_branch.py
Normal file
96
runbot_merge/tests/test_by_branch.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils import Commit
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(env, project, make_repo, users, setreviewers):
|
||||||
|
r = make_repo('repo')
|
||||||
|
project.write({
|
||||||
|
'repo_ids': [(0, 0, {
|
||||||
|
'name': r.name,
|
||||||
|
'status_ids': [
|
||||||
|
(0, 0, {'context': 'ci'}),
|
||||||
|
# require the lint status on master
|
||||||
|
(0, 0, {
|
||||||
|
'context': 'lint',
|
||||||
|
'branch_ids': [(4, project.branch_ids.id, False)]
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
})],
|
||||||
|
})
|
||||||
|
setreviewers(*project.repo_ids)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def test_status_applies(env, repo, config):
|
||||||
|
""" If branches are associated with a repo status, only those branch should
|
||||||
|
require the status on their PRs & stagings
|
||||||
|
"""
|
||||||
|
with repo:
|
||||||
|
m = repo.make_commits(None, Commit('root', tree={'a': '1'}), ref='heads/master')
|
||||||
|
|
||||||
|
[c] = repo.make_commits(m, Commit('pr', tree={'a': '2'}), ref='heads/change')
|
||||||
|
pr = repo.make_pr(target='master', title="super change", head='change')
|
||||||
|
pr_id = env['runbot_merge.pull_requests'].search([
|
||||||
|
('repository.name', '=', repo.name),
|
||||||
|
('number', '=', pr.number)
|
||||||
|
])
|
||||||
|
assert pr_id.state == 'opened'
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
repo.post_status(c, 'success', 'ci')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert pr_id.state == 'opened'
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
repo.post_status(c, 'success', 'lint')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert pr_id.state == 'validated'
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
st = env['runbot_merge.stagings'].search([])
|
||||||
|
assert st.state == 'pending'
|
||||||
|
with repo:
|
||||||
|
repo.post_status('staging.master', 'success', 'ci')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert st.state == 'pending'
|
||||||
|
with repo:
|
||||||
|
repo.post_status('staging.master', 'success', 'lint')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert st.state == 'success'
|
||||||
|
|
||||||
|
def test_status_skipped(env, project, repo, config):
|
||||||
|
""" Branches not associated with a repo status should not require the status
|
||||||
|
on their PRs or stagings
|
||||||
|
"""
|
||||||
|
# add a second branch for which the lint status doesn't apply
|
||||||
|
project.write({'branch_ids': [(0, 0, {'name': 'maintenance'})]})
|
||||||
|
with repo:
|
||||||
|
m = repo.make_commits(None, Commit('root', tree={'a': '1'}), ref='heads/maintenance')
|
||||||
|
|
||||||
|
[c] = repo.make_commits(m, Commit('pr', tree={'a': '2'}), ref='heads/change')
|
||||||
|
pr = repo.make_pr(target='maintenance', title="super change", head='change')
|
||||||
|
pr_id = env['runbot_merge.pull_requests'].search([
|
||||||
|
('repository.name', '=', repo.name),
|
||||||
|
('number', '=', pr.number)
|
||||||
|
])
|
||||||
|
assert pr_id.state == 'opened'
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
repo.post_status(c, 'success', 'ci')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert pr_id.state == 'validated'
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
st = env['runbot_merge.stagings'].search([])
|
||||||
|
assert st.state == 'pending'
|
||||||
|
with repo:
|
||||||
|
repo.post_status('staging.maintenance', 'success', 'ci')
|
||||||
|
env.run_crons('runbot_merge.process_updated_commits')
|
||||||
|
assert st.state == 'success'
|
@ -5,13 +5,6 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<sheet>
|
<sheet>
|
||||||
<!--
|
|
||||||
<div class="oe_button_box" name="button_box">
|
|
||||||
<button class="oe_stat_button" name="action_see_attachments" type="object" icon="fa-book" attrs="{'invisible': ['|', ('state', '=', 'confirmed'), ('type', '=', 'routing')]}">
|
|
||||||
<field string="Attachments" name="mrp_document_count" widget="statinfo"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1><field name="name" placeholder="Name"/></h1>
|
<h1><field name="name" placeholder="Name"/></h1>
|
||||||
</div>
|
</div>
|
||||||
@ -33,11 +26,11 @@
|
|||||||
|
|
||||||
<separator string="Repositories"/>
|
<separator string="Repositories"/>
|
||||||
<field name="repo_ids">
|
<field name="repo_ids">
|
||||||
<tree editable="bottom">
|
<tree>
|
||||||
<field name="sequence" widget="handle"/>
|
<field name="sequence" widget="handle"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="required_statuses"/>
|
|
||||||
<field name="branch_filter"/>
|
<field name="branch_filter"/>
|
||||||
|
<field name="status_ids" widget="many2many_tags"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
<separator string="Branches"/>
|
<separator string="Branches"/>
|
||||||
@ -53,6 +46,32 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="form_repository" model="ir.ui.view">
|
||||||
|
<field name="name">Repository form</field>
|
||||||
|
<field name="model">runbot_merge.repository</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1><field name="name"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="branch_filter"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<separator string="Required Statuses"/>
|
||||||
|
<field name="status_ids">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="context"/>
|
||||||
|
<field name="branch_ids" widget="many2many_tags"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="runbot_merge_action_projects" model="ir.actions.act_window">
|
<record id="runbot_merge_action_projects" model="ir.actions.act_window">
|
||||||
<field name="name">Projects</field>
|
<field name="name">Projects</field>
|
||||||
<field name="res_model">runbot_merge.project</field>
|
<field name="res_model">runbot_merge.project</field>
|
||||||
|
Loading…
Reference in New Issue
Block a user