[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("|"):
if search_elem.isnumeric():
pr_numbers.append(int(search_elem))
else:
search_domains.append([('name', 'like', search_elem)])
search_domains.append([('name', 'like', search_elem)])
if pr_numbers:
res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)])
if res:

View File

@ -12,23 +12,27 @@ _logger = logging.getLogger(__name__)
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):
event = request.httprequest.headers.get("X-Github-Event")
payload = json.loads(request.params.get('payload', '{}'))
if remote_id is None:
repo_data = payload.get('repository')
if repo_data and event in ['push', 'pull_request']:
if repo_data:
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'].rstrip('.git')),
('name', '=', repo_data['clone_url'].replace('.git', '')),
]
remote = request.env['runbot.remote'].sudo().search(
remote_domain, limit=1)
remote_id = remote.id
remote = request.env['runbot.remote'].sudo().browse([remote_id])
if not 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
if not payload or event == 'push':

View File

@ -140,6 +140,9 @@ class Batch(models.Model):
def _prepare(self, auto_rebase=False):
_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():
if level == "warning":
self.warning("Bundle warning: %s" % message)

View File

@ -316,7 +316,11 @@ class BuildResult(models.Model):
build_by_old_values[record.local_state] += record
local_result = values.get('local_result')
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)
if 'log_counter' in values: # not 100% usefull but more correct ( see test_ir_logging)
self.flush()
@ -1009,7 +1013,13 @@ class BuildResult(models.Model):
# means that a step has been run manually without using config
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:
if next_index >= len(step_ids): # final job, build is done
@ -1132,5 +1142,5 @@ class BuildResult(models.Model):
if trigger.ci_context:
for build_commit in self.params_id.commit_link_ids:
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)

View File

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

View File

@ -7,7 +7,7 @@ class Project(models.Model):
name = fields.Char('Project name', required=True, unique=True)
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')
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:
return []
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:
refs = [r for r in refs if r[0].split('/')[-1] not in ignore]
return refs
@ -441,9 +441,6 @@ class Repo(models.Model):
message = branch.remote_id.repo_id.invalid_branch_message or message
branch.head._github_status(False, "Branch naming", 'failure', False, message)
if not self.trigger_ids:
continue
bundle = branch.bundle_id
if bundle.no_build:
continue

View File

@ -90,7 +90,7 @@ class Runbot(models.AbstractModel):
domain_host = self.build_domain_host(host)
Build = self.env['runbot.build']
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 += [
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 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 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">
<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>

View File

@ -94,7 +94,7 @@
<div class="batch_slots">
<t t-foreach="batch.slot_ids" t-as="slot">
<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>
</t>
@ -120,7 +120,3 @@
</data>
</odoo>

View File

@ -67,7 +67,7 @@
<t t-if="triggers">
<input type="hidden" name="update_triggers" t-att-value="project.id"/>
<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"/>
<label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/>
</div>
@ -305,7 +305,9 @@
<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-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>
</div>
</template>

View File

@ -4,7 +4,7 @@ import datetime
from unittest.mock import patch
from odoo import fields
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
from .common import RunbotCase, RunbotCaseMinimalSetup
@ -183,9 +183,12 @@ class TestBuildResult(RunbotCase):
'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'
builds = self.Build.browse([build.id, other.id])
with self.assertRaises(AssertionError):
with self.assertRaises(ValidationError):
builds.write({'local_result': 'ok'})
def test_markdown_description(self):

View File

@ -7,6 +7,7 @@
<form string="Projects">
<group>
<field name="name"/>
<field name="keep_sticky_running"/>
<field name="dockerfile_id"/>
<field name="group_ids"/>
<field name="trigger_ids"/>
@ -15,6 +16,18 @@
</field>
</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">
<field name="model">runbot.bundle</field>