runbot/runbot/models/bundle.py
Xavier-Do c513edc6ad [IMP] runbot: allow to use dev foreigh branches
Using dev branch from foreign project to fill missing commits looks like
a bad idea, mainly because the lifecycle of the branches is not the same

In some case, it can be useful to allow that to test a branch with a
future change in the base project that will be needed to make the branch
work. As an example introducing a small api change in odoo to make an
override easier, or introducing a module that may be needed to use
the feature.

This commit changes that by allowing to configure on the project or
bundle if we allow to use foreign bundle as reference *before* checking
the base bundle.
2024-01-24 09:30:10 +01:00

283 lines
12 KiB
Python

import time
import logging
import datetime
import subprocess
from collections import defaultdict
from odoo import models, fields, api, tools
from ..common import dt2time, s2human_long
class Bundle(models.Model):
_name = 'runbot.bundle'
_description = "Bundle"
_inherit = 'mail.thread'
name = fields.Char('Bundle name', required=True, help="Name of the base branch")
project_id = fields.Many2one('runbot.project', required=True, index=True)
branch_ids = fields.One2many('runbot.branch', 'bundle_id')
# custom behaviour
no_build = fields.Boolean('No build')
no_auto_run = fields.Boolean('No run')
build_all = fields.Boolean('Force all triggers')
always_use_foreign = fields.Boolean('Use foreigh bundle', help='By default, check for the same bundle name in another project to fill missing commits.', default=lambda self: self.project_id.always_use_foreign)
modules = fields.Char("Modules to install", help="Comma-separated list of modules to install and test.")
batch_ids = fields.One2many('runbot.batch', 'bundle_id')
last_batch = fields.Many2one('runbot.batch', index=True, domain=lambda self: [('category_id', '=', self.env.ref('runbot.default_category').id)])
last_batchs = fields.Many2many('runbot.batch', 'Last batchs', compute='_compute_last_batchs')
last_done_batch = fields.Many2many('runbot.batch', 'Last batchs', compute='_compute_last_done_batch')
sticky = fields.Boolean('Sticky', compute='_compute_sticky', store=True, index=True)
is_base = fields.Boolean('Is base', index=True)
defined_base_id = fields.Many2one('runbot.bundle', 'Forced base bundle', domain="[('project_id', '=', project_id), ('is_base', '=', True)]")
base_id = fields.Many2one('runbot.bundle', 'Base bundle', compute='_compute_base_id', store=True)
to_upgrade = fields.Boolean('To upgrade', compute='_compute_to_upgrade', store=True, index=False)
version_id = fields.Many2one('runbot.version', 'Version', compute='_compute_version_id', store=True, recursive=True)
version_number = fields.Char(related='version_id.number', store=True, index=True)
previous_major_version_base_id = fields.Many2one('runbot.bundle', 'Previous base bundle', compute='_compute_relations_base_id')
intermediate_version_base_ids = fields.Many2many('runbot.bundle', 'Intermediate base bundles', compute='_compute_relations_base_id')
priority = fields.Boolean('Build priority', default=False, groups="runbot.group_runbot_admin")
# Custom parameters
trigger_custom_ids = fields.One2many('runbot.bundle.trigger.custom', 'bundle_id')
host_id = fields.Many2one('runbot.host', compute="_compute_host_id", store=True)
dockerfile_id = fields.Many2one('runbot.dockerfile', index=True, help="Use a custom Dockerfile")
commit_limit = fields.Integer("Commit limit")
file_limit = fields.Integer("File limit")
disable_codeowner = fields.Boolean("Disable codeowners", tracking=True)
# extra_info
for_next_freeze = fields.Boolean('Should be in next freeze')
@api.depends('name')
def _compute_host_id(self):
assigned_only = None
runbots = {}
for bundle in self:
bundle.host_id = False
elems = (bundle.name or '').split('-')
for elem in elems:
if elem.startswith('runbot'):
if elem.replace('runbot', '') == '_x':
if assigned_only is None:
assigned_only = self.env['runbot.host'].search([('assigned_only', '=', True)], limit=1)
bundle.host_id = assigned_only or False
elif elem.replace('runbot', '').isdigit():
if elem not in runbots:
runbots[elem] = self.env['runbot.host'].search([('name', 'like', '%s%%' % elem)], limit=1)
bundle.host_id = runbots[elem] or False
@api.depends('sticky')
def _compute_make_stats(self):
for bundle in self:
bundle.make_stats = bundle.sticky
@api.depends('is_base')
def _compute_sticky(self):
for bundle in self:
bundle.sticky = bundle.is_base
@api.depends('is_base')
def _compute_to_upgrade(self):
for bundle in self:
bundle.to_upgrade = bundle.is_base
@api.depends('name', 'is_base', 'defined_base_id', 'base_id.is_base', 'project_id')
def _compute_base_id(self):
for bundle in self:
if bundle.is_base:
bundle.base_id = bundle
continue
if bundle.defined_base_id:
bundle.base_id = bundle.defined_base_id
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 or fallback
@tools.ormcache('project_id')
def _get_base_ids(self, project_id):
return [(b.id, b.name) for b in self.search([('is_base', '=', True), ('project_id', '=', project_id)])]
@api.depends('is_base', 'base_id.version_id')
def _compute_version_id(self):
for bundle in self.sorted(key='is_base', reverse=True):
if not bundle.is_base:
bundle.version_id = bundle.base_id.version_id
continue
bundle.version_id = self.env['runbot.version']._get(bundle.name)
@api.depends('version_id')
def _compute_relations_base_id(self):
for bundle in self:
bundle = bundle.with_context(project_id=bundle.project_id.id)
bundle.previous_major_version_base_id = bundle.version_id.previous_major_version_id.base_bundle_id
bundle.intermediate_version_base_ids = bundle.version_id.intermediate_version_ids.mapped('base_bundle_id')
@api.depends_context('category_id')
def _compute_last_batchs(self):
batch_ids = defaultdict(list)
if self.ids:
category_id = self.env.context.get('category_id', self.env['ir.model.data']._xmlid_to_res_id('runbot.default_category'))
self.env.cr.execute("""
SELECT
id
FROM (
SELECT
batch.id AS id,
row_number() OVER (PARTITION BY batch.bundle_id order by batch.id desc) AS row
FROM
runbot_bundle bundle INNER JOIN runbot_batch batch ON bundle.id=batch.bundle_id
WHERE
bundle.id in %s
AND batch.category_id = %s
) AS bundle_batch
WHERE
row <= 4
ORDER BY row, id desc
""", [tuple(self.ids), category_id]
)
batchs = self.env['runbot.batch'].browse([r[0] for r in self.env.cr.fetchall()])
for batch in batchs:
batch_ids[batch.bundle_id.id].append(batch.id)
for bundle in self:
bundle.last_batchs = [(6, 0, batch_ids[bundle.id])] if bundle.id in batch_ids else False
@api.depends_context('category_id')
def _compute_last_done_batch(self):
if self:
self.env['runbot.batch'].flush_model()
self.env['runbot.bundle'].flush_model()
# self.env['runbot.batch'].flush()
for bundle in self:
bundle.last_done_batch = False
category_id = self.env.context.get('category_id', self.env['ir.model.data']._xmlid_to_res_id('runbot.default_category'))
self.env.cr.execute("""
SELECT
id
FROM (
SELECT
batch.id AS id,
row_number() OVER (PARTITION BY batch.bundle_id order by batch.id desc) AS row
FROM
runbot_bundle bundle INNER JOIN runbot_batch batch ON bundle.id=batch.bundle_id
WHERE
bundle.id in %s
AND batch.state = 'done'
AND batch.category_id = %s
) AS bundle_batch
WHERE
row = 1
ORDER BY row, id desc
""", [tuple(self.ids), category_id]
)
batchs = self.env['runbot.batch'].browse([r[0] for r in self.env.cr.fetchall()])
for batch in batchs:
batch.bundle_id.last_done_batch = batch
def _url(self):
self.ensure_one()
return "/runbot/bundle/%s" % self.id
def create(self, values_list):
res = super().create(values_list)
if res.is_base:
model = self.browse()
model._get_base_ids.clear_cache(model)
return res
def write(self, values):
res = super().write(values)
if 'is_base' in values:
model = self.browse()
model._get_base_ids.clear_cache(model)
return res
def _force(self, category_id=None):
self.ensure_one()
if self.last_batch.state == 'preparing':
return
values = {
'last_update': fields.Datetime.now(),
'bundle_id': self.id,
'state': 'preparing',
}
if category_id:
values['category_id'] = category_id
new = self.env['runbot.batch'].create(values)
self.last_batch = new
return new
def _consistency_warning(self):
if self.defined_base_id:
return [('info', 'This bundle has a forced base: %s' % self.defined_base_id.name)]
warnings = []
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):
self.branch_ids.sorted(key=lambda b: (b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr))
branch_groups = {repo: [] for repo in self.branch_ids.mapped('remote_id.repo_id').sorted('sequence')}
for branch in self.branch_ids.sorted(key=lambda b: (b.is_pr)):
branch_groups[branch.remote_id.repo_id].append(branch)
return branch_groups
def _generate_custom_trigger_action(self, context):
return {
'type': 'ir.actions.act_window',
'name': 'Generate custom trigger',
'view_mode': 'form',
'res_model': 'runbot.trigger.custom.wizard',
'target': 'new',
'context': context,
}
def action_generate_custom_trigger_multi_action(self):
context = {
'default_bundle_id': self.id,
'default_config_id': self.env.ref('runbot.runbot_build_config_custom_multi').id,
'default_child_config_id': self.env.ref('runbot.runbot_build_config_restore_and_test').id,
'default_extra_params': False,
'default_child_extra_params': '--test-tags /module.test_method',
'default_number_build': 10,
}
return self._generate_custom_trigger_action(context)
def action_generate_custom_trigger_restore_action(self):
context = {
'default_bundle_id': self.id,
'default_config_id': self.env.ref('runbot.runbot_build_config_restore_and_test').id,
'default_child_config_id': False,
'default_extra_params': '--test-tags /module.test_method',
'default_child_extra_params': False,
'default_number_build': 0,
}
return self._generate_custom_trigger_action(context)