[IMP] runbot: some fixes for ps runbot

- searching on number will search for both pr and branche name
- hooks are now using payload to define repo when not given in url
- fixes .git cleaning in repo
    (remove rstrip since it can fail for repo starting with g, i, t)
- recompute base on prepare if base was not found
- remove local_result form write values if there is a single record
    (instead of raising, makes python step easier to write).
- avoid stucked build/loop after removing a step from a config.
- avoid to send ci for linked base_commit
- add a fallback mechanism for base if no master branch is found
- add option on project to avoid to keep sticky running, usefull
    when using a lots of projects
    WARNING: this is a change of default behaviour, need to update
    existing projects.
- always discover new commits for branch matching base paterns.
    This is especially usefull to discover old versions on project with
    low merge frequency.
- always create a batch, event if there is now trigger. This helps to
    notice that commits are discovered
- add line-through on death branches/pr
- manual trigger are now displayed on main page
This commit is contained in:
Xavier-Do 2021-08-13 12:33:00 +02:00 committed by Christophe Monniez
parent d8b96db1b7
commit fe987cd0f3
13 changed files with 69 additions and 36 deletions

View File

@ -133,8 +133,7 @@ class Runbot(Controller):
for search_elem in search.split("|"): for search_elem in search.split("|"):
if search_elem.isnumeric(): if search_elem.isnumeric():
pr_numbers.append(int(search_elem)) pr_numbers.append(int(search_elem))
else: search_domains.append([('name', 'like', search_elem)])
search_domains.append([('name', 'like', search_elem)])
if pr_numbers: if pr_numbers:
res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)]) res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)])
if res: if res:

View File

@ -12,23 +12,27 @@ _logger = logging.getLogger(__name__)
class Hook(http.Controller): class Hook(http.Controller):
@http.route(['/runbot/hook/<int:remote_id>'], type='http', auth="public", website=True, csrf=False) @http.route(['/runbot/hook', '/runbot/hook/<int:remote_id>'], type='http', auth="public", website=True, csrf=False)
def hook(self, remote_id=None, **_post): def hook(self, remote_id=None, **_post):
event = request.httprequest.headers.get("X-Github-Event") event = request.httprequest.headers.get("X-Github-Event")
payload = json.loads(request.params.get('payload', '{}')) payload = json.loads(request.params.get('payload', '{}'))
if remote_id is None: if remote_id is None:
repo_data = payload.get('repository') repo_data = payload.get('repository')
if repo_data and event in ['push', 'pull_request']: if repo_data:
remote_domain = [ remote_domain = [
'|', '|', ('name', '=', repo_data['ssh_url']), '|', '|', '|',
('name', '=', repo_data['ssh_url']),
('name', '=', repo_data['ssh_url'].replace('.git', '')),
('name', '=', repo_data['clone_url']), ('name', '=', repo_data['clone_url']),
('name', '=', repo_data['clone_url'].rstrip('.git')), ('name', '=', repo_data['clone_url'].replace('.git', '')),
] ]
remote = request.env['runbot.remote'].sudo().search( remote = request.env['runbot.remote'].sudo().search(
remote_domain, limit=1) remote_domain, limit=1)
remote_id = remote.id remote_id = remote.id
if not remote_id:
remote = request.env['runbot.remote'].sudo().browse([remote_id]) _logger.error("Remote %s not found", repo_data['ssh_url'])
remote = request.env['runbot.remote'].sudo().browse(remote_id)
_logger.info('Remote found %s', remote)
# force update of dependencies too in case a hook is lost # force update of dependencies too in case a hook is lost
if not payload or event == 'push': if not payload or event == 'push':

View File

@ -140,6 +140,9 @@ class Batch(models.Model):
def _prepare(self, auto_rebase=False): def _prepare(self, auto_rebase=False):
_logger.info('Preparing batch %s', self.id) _logger.info('Preparing batch %s', self.id)
if not self.bundle_id.base_id:
# in some case the base can be detected lately. If a bundle has no base, recompute the base before preparing
self.bundle_id._compute_base_id()
for level, message in self.bundle_id.consistency_warning(): for level, message in self.bundle_id.consistency_warning():
if level == "warning": if level == "warning":
self.warning("Bundle warning: %s" % message) self.warning("Bundle warning: %s" % message)

View File

@ -316,7 +316,11 @@ class BuildResult(models.Model):
build_by_old_values[record.local_state] += record build_by_old_values[record.local_state] += record
local_result = values.get('local_result') local_result = values.get('local_result')
for build in self: for build in self:
assert not local_result or local_result == self._get_worst_result([build.local_result, local_result]) # dont write ok on a warn/error build if local_result and local_result != self._get_worst_result([build.local_result, local_result]): # dont write ok on a warn/error build
if len(self) == 1:
values.pop('local_result')
else:
raise ValidationError('Local result cannot be set to a less critical level')
res = super(BuildResult, self).write(values) res = super(BuildResult, self).write(values)
if 'log_counter' in values: # not 100% usefull but more correct ( see test_ir_logging) if 'log_counter' in values: # not 100% usefull but more correct ( see test_ir_logging)
self.flush() self.flush()
@ -1009,7 +1013,13 @@ class BuildResult(models.Model):
# means that a step has been run manually without using config # means that a step has been run manually without using config
return {'active_step': False, 'local_state': 'done'} return {'active_step': False, 'local_state': 'done'}
next_index = step_ids.index(self.active_step) + 1 if self.active_step else 0 if not self.active_step:
next_index = 0
else:
if self.active_step not in step_ids:
self._log('run', 'Config was modified and current step does not exists anymore, skipping.', level='ERROR')
return {'active_step': False, 'local_state': 'done', 'local_result': self._get_worst_result([self.local_result, 'ko'])}
next_index = step_ids.index(self.active_step) + 1
while True: while True:
if next_index >= len(step_ids): # final job, build is done if next_index >= len(step_ids): # final job, build is done
@ -1132,5 +1142,5 @@ class BuildResult(models.Model):
if trigger.ci_context: if trigger.ci_context:
for build_commit in self.params_id.commit_link_ids: for build_commit in self.params_id.commit_link_ids:
commit = build_commit.commit_id commit = build_commit.commit_id
if build_commit.match_type != 'default' and commit.repo_id in trigger.repo_ids: if 'base_' not in build_commit.match_type and commit.repo_id in trigger.repo_ids:
commit._github_status(build, trigger.ci_context, state, target_url, desc, post_commit) commit._github_status(build, trigger.ci_context, state, target_url, desc, post_commit)

View File

@ -93,14 +93,17 @@ class Bundle(models.Model):
continue continue
project_id = bundle.project_id.id project_id = bundle.project_id.id
master_base = False master_base = False
fallback = False
for bid, bname in self._get_base_ids(project_id): for bid, bname in self._get_base_ids(project_id):
if bundle.name.startswith('%s-' % bname): if bundle.name.startswith('%s-' % bname):
bundle.base_id = self.browse(bid) bundle.base_id = self.browse(bid)
break break
elif bname == 'master': elif bname == 'master':
master_base = self.browse(bid) master_base = self.browse(bid)
elif not fallback or fallback.id < bid:
fallback = self.browse(bid)
else: else:
bundle.base_id = master_base bundle.base_id = master_base or fallback
@tools.ormcache('project_id') @tools.ormcache('project_id')
def _get_base_ids(self, project_id): def _get_base_ids(self, project_id):
@ -213,14 +216,17 @@ class Bundle(models.Model):
if self.defined_base_id: if self.defined_base_id:
return [('info', 'This bundle has a forced base: %s' % self.defined_base_id.name)] return [('info', 'This bundle has a forced base: %s' % self.defined_base_id.name)]
warnings = [] warnings = []
for branch in self.branch_ids: if not self.base_id:
if branch.is_pr and branch.target_branch_name != self.base_id.name: warnings.append(('warning', 'No base defined on this bundle'))
if branch.target_branch_name.startswith(self.base_id.name): else:
warnings.append(('info', 'PR %s targeting a non base branch: %s' % (branch.dname, branch.target_branch_name))) for branch in self.branch_ids:
else: if branch.is_pr and branch.target_branch_name != self.base_id.name:
warnings.append(('warning' if branch.alive else 'info', 'PR %s targeting wrong version: %s (expecting %s)' % (branch.dname, branch.target_branch_name, self.base_id.name))) if branch.target_branch_name.startswith(self.base_id.name):
elif not branch.is_pr and not branch.name.startswith(self.base_id.name) and not self.defined_base_id: warnings.append(('info', 'PR %s targeting a non base branch: %s' % (branch.dname, branch.target_branch_name)))
warnings.append(('warning', 'Branch %s not starting with version name (%s)' % (branch.dname, self.base_id.name))) else:
warnings.append(('warning' if branch.alive else 'info', 'PR %s targeting wrong version: %s (expecting %s)' % (branch.dname, branch.target_branch_name, self.base_id.name)))
elif not branch.is_pr and not branch.name.startswith(self.base_id.name) and not self.defined_base_id:
warnings.append(('warning', 'Branch %s not starting with version name (%s)' % (branch.dname, self.base_id.name)))
return warnings return warnings
def branch_groups(self): def branch_groups(self):

View File

@ -7,7 +7,7 @@ class Project(models.Model):
name = fields.Char('Project name', required=True, unique=True) name = fields.Char('Project name', required=True, unique=True)
group_ids = fields.Many2many('res.groups', string='Required groups') group_ids = fields.Many2many('res.groups', string='Required groups')
keep_sticky_running = fields.Boolean('Keep last sticky builds running')
trigger_ids = fields.One2many('runbot.trigger', 'project_id', string='Triggers') trigger_ids = fields.One2many('runbot.trigger', 'project_id', string='Triggers')
dockerfile_id = fields.Many2one('runbot.dockerfile', index=True, help="Project Default Dockerfile") dockerfile_id = fields.Many2one('runbot.dockerfile', index=True, help="Project Default Dockerfile")

View File

@ -365,7 +365,7 @@ class Repo(models.Model):
if not git_refs: if not git_refs:
return [] return []
refs = [tuple(field for field in line.split('\x00')) for line in git_refs.split('\n')] refs = [tuple(field for field in line.split('\x00')) for line in git_refs.split('\n')]
refs = [r for r in refs if dateutil.parser.parse(r[2][:19]) + datetime.timedelta(days=max_age) > datetime.datetime.now()] refs = [r for r in refs if dateutil.parser.parse(r[2][:19]) + datetime.timedelta(days=max_age) > datetime.datetime.now() or self.env['runbot.branch'].match_is_base(r[0])]
if ignore: if ignore:
refs = [r for r in refs if r[0].split('/')[-1] not in ignore] refs = [r for r in refs if r[0].split('/')[-1] not in ignore]
return refs return refs
@ -441,9 +441,6 @@ class Repo(models.Model):
message = branch.remote_id.repo_id.invalid_branch_message or message message = branch.remote_id.repo_id.invalid_branch_message or message
branch.head._github_status(False, "Branch naming", 'failure', False, message) branch.head._github_status(False, "Branch naming", 'failure', False, message)
if not self.trigger_ids:
continue
bundle = branch.bundle_id bundle = branch.bundle_id
if bundle.no_build: if bundle.no_build:
continue continue

View File

@ -90,7 +90,7 @@ class Runbot(models.AbstractModel):
domain_host = self.build_domain_host(host) domain_host = self.build_domain_host(host)
Build = self.env['runbot.build'] Build = self.env['runbot.build']
cannot_be_killed_ids = Build.search(domain_host + [('keep_running', '=', True)]).ids cannot_be_killed_ids = Build.search(domain_host + [('keep_running', '=', True)]).ids
sticky_bundles = self.env['runbot.bundle'].search([('sticky', '=', True)]) sticky_bundles = self.env['runbot.bundle'].search([('sticky', '=', True), ('project_id.keep_sticky_running', '=', True)])
cannot_be_killed_ids += [ cannot_be_killed_ids += [
build.id build.id
for build in sticky_bundles.mapped('last_batchs.slot_ids.build_id') for build in sticky_bundles.mapped('last_batchs.slot_ids.build_id')

View File

@ -47,7 +47,7 @@
<a t-att-href="branch.branch_url" class="btn btn-default text-left" title="View Branch on Github"><i class="fa fa-github"/></a> <a t-att-href="branch.branch_url" class="btn btn-default text-left" title="View Branch on Github"><i class="fa fa-github"/></a>
<a groups="runbot.group_runbot_admin" class="btn btn-default fa fa-list text-left" t-attf-href="/web/#id={{branch.id}}&amp;view_type=form&amp;model=runbot.branch" target="new" title="View Branch in Backend"/> <a groups="runbot.group_runbot_admin" class="btn btn-default fa fa-list text-left" t-attf-href="/web/#id={{branch.id}}&amp;view_type=form&amp;model=runbot.branch" target="new" title="View Branch in Backend"/>
<a href="#" t-esc="branch.remote_id.short_name" class="btn btn-default disabled text-left"/> <a href="#" t-esc="branch.remote_id.short_name" class="btn btn-default disabled text-left"/>
<a t-attf-href="/runbot/branch/{{branch.id}}" class="btn btn-default text-left" title="View Branch Details"><t t-esc="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/></a> <a t-attf-href="/runbot/branch/{{branch.id}}" class="btn btn-default text-left" title="View Branch Details"><span t-att-class="'' if branch.alive else 'line-through'" t-esc="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/></a>
<t t-if="len(group[1]) == 1 and not branch.is_pr"> <t t-if="len(group[1]) == 1 and not branch.is_pr">
<a t-attf-href="https://{{group[0].main_remote_id.base_url}}/compare/{{bundle.version_id.name}}...{{branch.remote_id.owner}}:{{branch.name}}?expand=1" class="btn btn-default text-left" title="Create pr"><i class="fa fa-code-fork"/> Create pr</a> <a t-attf-href="https://{{group[0].main_remote_id.base_url}}/compare/{{bundle.version_id.name}}...{{branch.remote_id.owner}}:{{branch.name}}?expand=1" class="btn btn-default text-left" title="Create pr"><i class="fa fa-code-fork"/> Create pr</a>
</t> </t>

View File

@ -94,7 +94,7 @@
<div class="batch_slots"> <div class="batch_slots">
<t t-foreach="batch.slot_ids" t-as="slot"> <t t-foreach="batch.slot_ids" t-as="slot">
<t t-if="slot.build_id"> <t t-if="slot.build_id">
<div t-if="not slot.trigger_id.manual and ((not slot.trigger_id.hide and trigger_display is None) or (trigger_display and slot.trigger_id.id in trigger_display))or slot.build_id.global_result == 'ko'" <div t-if="((not slot.trigger_id.hide and trigger_display is None) or (trigger_display and slot.trigger_id.id in trigger_display)) or slot.build_id.global_result == 'ko'"
t-call="runbot.slot_button" class="slot_container"/> t-call="runbot.slot_button" class="slot_container"/>
</t> </t>
</t> </t>
@ -120,7 +120,3 @@
</data> </data>
</odoo> </odoo>

View File

@ -67,7 +67,7 @@
<t t-if="triggers"> <t t-if="triggers">
<input type="hidden" name="update_triggers" t-att-value="project.id"/> <input type="hidden" name="update_triggers" t-att-value="project.id"/>
<t t-foreach="triggers" t-as="trigger"> <t t-foreach="triggers" t-as="trigger">
<div t-if="not trigger.manual" class="text-nowrap"> <div class="text-nowrap">
<input type="checkbox" t-attf-name="trigger_{{trigger.id}}" t-attf-id="trigger_{{trigger.id}}" t-att-checked="trigger_display is None or trigger.id in trigger_display"/> <input type="checkbox" t-attf-name="trigger_{{trigger.id}}" t-attf-id="trigger_{{trigger.id}}" t-att-checked="trigger_display is None or trigger.id in trigger_display"/>
<label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/> <label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/>
</div> </div>
@ -305,7 +305,9 @@
<div class="dropdown-menu" role="menu"> <div class="dropdown-menu" role="menu">
<t t-foreach="bundle.branch_ids.sorted(key=lambda b: (b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr))" t-as="branch"> <t t-foreach="bundle.branch_ids.sorted(key=lambda b: (b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr))" t-as="branch">
<t t-set="link_title" t-value="'View %s %s on Github' % ('PR' if branch.is_pr else 'Branch', branch.name)"/> <t t-set="link_title" t-value="'View %s %s on Github' % ('PR' if branch.is_pr else 'Branch', branch.name)"/>
<a t-att-href="branch.branch_url" class="dropdown-item" t-att-title="link_title"><span class="font-italic text-muted"><t t-esc="branch.remote_id.short_name"/></span> <t t-esc="branch.name"/></a> <a t-att-href="branch.branch_url" class="dropdown-item" t-att-title="link_title">
<span class="font-italic text-muted" t-esc="branch.remote_id.short_name"/> <span t-att-class="'' if branch.alive else 'line-through'" t-esc="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/>
</a>
</t> </t>
</div> </div>
</template> </template>

View File

@ -4,7 +4,7 @@ import datetime
from unittest.mock import patch from unittest.mock import patch
from odoo import fields from odoo import fields
from odoo.exceptions import UserError from odoo.exceptions import UserError, ValidationError
from .common import RunbotCase, RunbotCaseMinimalSetup from .common import RunbotCase, RunbotCaseMinimalSetup
@ -183,9 +183,12 @@ class TestBuildResult(RunbotCase):
'local_result': 'ko' 'local_result': 'ko'
}) })
other.write({'local_result': 'ok'})
self.assertEqual(other.local_result, 'ko')
# test a bulk write, that one cannot change from 'ko' to 'ok' # test a bulk write, that one cannot change from 'ko' to 'ok'
builds = self.Build.browse([build.id, other.id]) builds = self.Build.browse([build.id, other.id])
with self.assertRaises(AssertionError): with self.assertRaises(ValidationError):
builds.write({'local_result': 'ok'}) builds.write({'local_result': 'ok'})
def test_markdown_description(self): def test_markdown_description(self):

View File

@ -7,6 +7,7 @@
<form string="Projects"> <form string="Projects">
<group> <group>
<field name="name"/> <field name="name"/>
<field name="keep_sticky_running"/>
<field name="dockerfile_id"/> <field name="dockerfile_id"/>
<field name="group_ids"/> <field name="group_ids"/>
<field name="trigger_ids"/> <field name="trigger_ids"/>
@ -15,6 +16,18 @@
</field> </field>
</record> </record>
<record id="view_runbot_project_tree" model="ir.ui.view">
<field name="model">runbot.project</field>
<field name="arch" type="xml">
<tree string="Projects">
<field name="name"/>
<field name="keep_sticky_running"/>
<field name="dockerfile_id"/>
<field name="group_ids"/>
<field name="trigger_ids"/>
</tree>
</field>
</record>
<record id="view_runbot_bundle" model="ir.ui.view"> <record id="view_runbot_bundle" model="ir.ui.view">
<field name="model">runbot.bundle</field> <field name="model">runbot.bundle</field>