[IMP] runbot: introduce matrix for upgrade

The initial way to configure upgrades was working fine and automatically
but does not alway match the reality of what should be supported.

Saying, upgrade from the last main version and last intermediate
(for 18, from 17.0 and 17.4) we may also want to test from 17.2 because
for some reason there are many people in 17.2 taht will need to upgrade
in 18.0. But the transition 17.2->17.3 doesn't make sense anymore
after a while since people will go in 18 imediatly at some point.

This matrix can be generated automatically from the parameters and new
versions should appear automatically, but it is possible to tweak the
values on demand.
This commit is contained in:
Xavier-Do 2025-02-21 09:56:34 +01:00
parent 1ed9278d6e
commit fb7b8480b8
10 changed files with 322 additions and 5 deletions

View File

@ -52,6 +52,7 @@
'views/res_config_settings_views.xml',
'views/stat_views.xml',
'views/upgrade.xml',
'views/upgrade_matrix_views.xml',
'views/warning_views.xml',
'views/custom_trigger_wizard_views.xml',
'wizards/stat_regex_wizard_views.xml',

View File

@ -189,6 +189,9 @@ class ConfigStep(models.Model):
upgrade_from_all_intermediate_version = fields.Boolean() # 13.2 # 13.1
upgrade_from_version_ids = fields.Many2many('runbot.version', relation='runbot_upgrade_from_version_ids', string='Forced version to use as source (cartesian with target)')
# wip replace previous field by matrix
upgrade_matrix_id = fields.Many2one('runbot.upgrade.matrix', 'Upgrade matrix', tracking=True)
upgrade_flat = fields.Boolean("Flat", help="Take all decisions in on build")
upgrade_config_id = fields.Many2one('runbot.build.config',string='Upgrade Config', tracking=True, index=True)

View File

@ -204,6 +204,9 @@ class Bundle(models.Model):
if records.is_base:
model = self.browse()
model.env.registry.clear_cache()
matrix = self.env['runbot.upgrade.matrix'].search([('project_id', '=', record.project_id.id)], limit=1)
if matrix:
matrix._update_matrix_entries()
elif record.project_id.tmp_prefix and record.name.startswith(record.project_id.tmp_prefix):
record['no_build'] = True
elif record.project_id.staging_prefix and record.name.startswith(record.project_id.staging_prefix):

View File

@ -1,5 +1,5 @@
import re
from odoo import models, fields
from odoo import models, fields, api
from odoo.exceptions import UserError
@ -69,3 +69,138 @@ class BuildResult(models.Model):
return res
else:
raise UserError('Nothing found here')
class UpgradeMatrix(models.Model):
_name = 'runbot.upgrade.matrix'
_description = 'Upgrade matrix'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char('Name', required=True)
project_id = fields.Many2one('runbot.project', 'Project', required=True)
entry_ids = fields.One2many('runbot.upgrade.matrix.entry', 'matrix_id', 'Entries')
auto_update = fields.Boolean('Auto update', default=True, help="Automatically update the matrix entries enabled state when new versions are created")
# fields defining default behaviour to generate the matix,
upgrade_to_major_versions = fields.Boolean()
upgrade_to_all_versions = fields.Boolean()
upgrade_from_previous_major_version = fields.Boolean()
upgrade_from_last_intermediate_version = fields.Boolean()
upgrade_from_all_intermediate_version = fields.Boolean()
matrix_summary = fields.Text('Matrix summary', compute='_compute_matrix_summary', store=True, tracking=True)
@api.depends('entry_ids.from_version_id', 'entry_ids.to_version_id', 'entry_ids.enabled')
def _compute_matrix_summary(self):
for matrix in self:
matrix.matrix_summary = ''
versions = {}
lines = []
for entry in self.entry_ids.sorted(lambda e: (e.to_version_id.number, e.from_version_id.number)):
if entry.enabled:
versions.setdefault(entry.to_version_id, []).append(entry.from_version_id.number)
else:
versions.setdefault(entry.to_version_id, []).append('-')
for to_version, from_versions in versions.items():
from_versions_string = ', '.join(sorted(from_versions))
lines.append(f'{to_version.number} - ({from_versions_string})')
matrix.matrix_summary = '\n'.join(lines)
print('recompute')
print(matrix.matrix_summary)
def update_matrix_entries(self):
for metric in self:
metric._update_matrix_entries()
def _update_matrix_entries(self):
self.ensure_one()
existing_entries = self.with_context(active_test=False).entry_ids
entries_per_versions = {(e.from_version_id.id, e.to_version_id.id): e for e in existing_entries}
# get all versions
versions = self.env['runbot.bundle'].search([('project_id', '=', self.project_id.id), ('is_base', '=', True)]).mapped('version_id')
for target_version in versions:
compatible_versions = target_version.intermediate_version_ids | target_version.previous_major_version_id
for source_version in versions:
if (source_version.id, target_version.id) not in entries_per_versions:
if target_version == source_version:
continue
if source_version not in compatible_versions:
continue
self.env['runbot.upgrade.matrix.entry'].create({
'matrix_id': self.id,
'from_version_id': source_version.id,
'to_version_id': target_version.id
})
if self.auto_update:
existing_entries._update_enabled()
def reset_matrix_enabled(self):
for matrix in self:
matrix.entry_ids._update_enabled(force=True)
class UpgradeMatrixEntry(models.Model):
_name = 'runbot.upgrade.matrix.entry'
_description = 'Upgrade matrix entry'
_order = 'to_version_number desc, from_version_number desc, id desc'
matrix_id = fields.Many2one('runbot.upgrade.matrix', 'Matrix', required=True, ondelete='cascade')
from_version_id = fields.Many2one('runbot.version', 'From version', required=True, ondelete='cascade')
to_version_id = fields.Many2one('runbot.version', 'To version', required=True, ondelete='cascade')
from_version_number = fields.Char(related='from_version_id.number', string="To version number", store=True)
to_version_number = fields.Char(related='to_version_id.number', string="From version number", store=True)
target_bundle_id = fields.Many2one('runbot.bundle', compute='_compute_target_bundle_id', store=True)
enabled = fields.Boolean('Enabled', default=True)
active = fields.Boolean('Active', compute='_compute_active', store=True)
manually_edited = fields.Boolean('Manually edited', default=False)
_sql_constraints = [
('unique_matrix_entry', 'unique(matrix_id, from_version_id, to_version_id)', 'Matrix entry already exists')
]
@api.onchange('enabled')
def _onchange_enabled(self):
self.manually_edited = True
def create(self, vals):
entry = super().create(vals)
entry._update_enabled()
@api.depends('to_version_id', 'matrix_id.project_id')
def _compute_target_bundle_id(self):
for entry in self:
entry.target_bundle_id = entry.to_version_id.with_context(project_id=entry.matrix_id.project_id.id).base_bundle_id
@api.depends('target_bundle_id.sticky')
def _compute_active(self):
for entry in self:
entry.active = entry.target_bundle_id.sticky
def _update_enabled(self, force=False):
for entry in self:
if entry.manually_edited and not force:
continue
entry.manually_edited = False
matrix = entry.matrix_id
to_enabled = False
from_enabled = False
if matrix.upgrade_to_all_versions:
to_enabled = True
elif matrix.upgrade_to_major_versions and entry.to_version_id.is_major:
to_enabled = True
if not to_enabled:
entry.enabled = False
continue
if matrix.upgrade_from_all_intermediate_version:
from_enabled = True
elif matrix.upgrade_from_last_intermediate_version and entry.to_version_id.intermediate_version_ids and entry.from_version_id == entry.to_version_id.intermediate_version_ids[-1]:
from_enabled = True
elif matrix.upgrade_from_previous_major_version and entry.from_version_id == entry.to_version_id.previous_major_version_id:
from_enabled = True
entry.enabled = to_enabled and from_enabled

View File

@ -129,8 +129,18 @@ access_runbot_upgrade_regex_user,access_runbot_upgrade_regex_user,runbot.model_r
access_runbot_upgrade_regex_admin,access_runbot_upgrade_regex_admin,runbot.model_runbot_upgrade_regex,runbot.group_runbot_admin,1,1,1,1
access_runbot_upgrade_exception_user,access_runbot_upgrade_exception_user,runbot.model_runbot_upgrade_exception,runbot.group_user,1,0,0,0
access_runbot_upgrade_exception_manager,access_runbot_upgrade_exception_manager,runbot.model_runbot_upgrade_exception,runbot.group_runbot_upgrade_manager,1,1,1,1
access_runbot_upgrade_exception_admin,access_runbot_upgrade_exception_admin,runbot.model_runbot_upgrade_exception,runbot.group_runbot_admin,1,1,1,1
access_runbot_upgrade_matrix_user,access_runbot_upgrade_matrix_user,runbot.model_runbot_upgrade_matrix,runbot.group_user,1,0,0,0
access_runbot_upgrade_matrix_manager,access_runbot_upgrade_matrix_manager,runbot.model_runbot_upgrade_matrix,runbot.group_runbot_upgrade_manager,1,1,1,1
access_runbot_upgrade_matrix_admin,access_runbot_upgrade_matrix_admin,runbot.model_runbot_upgrade_matrix,runbot.group_runbot_admin,1,1,1,1
access_runbot_upgrade_matrix_entry_user,access_runbot_upgrade_matrix_entry_user,runbot.model_runbot_upgrade_matrix_entry,runbot.group_user,1,0,0,0
access_runbot_upgrade_matrix_entry_manager,access_runbot_upgrade_matrix_entry_manager,runbot.model_runbot_upgrade_matrix_entry,runbot.group_runbot_upgrade_manager,1,1,1,1
access_runbot_upgrade_matrix_entry_admin,access_runbot_upgrade_matrix_entry_admin,runbot.model_runbot_upgrade_matrix_entry,runbot.group_runbot_admin,1,1,1,1
access_runbot_dockerfile_user,access_runbot_dockerfile_user,runbot.model_runbot_dockerfile,runbot.group_user,1,0,0,0
access_runbot_dockerfile_admin,access_runbot_dockerfile_admin,runbot.model_runbot_dockerfile,runbot.group_runbot_admin,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

View File

@ -62,11 +62,16 @@
<field name="category_id" ref="module_project"/>
</record>
<record id="group_runbot_upgrade_manager" model="res.groups">
<field name="name">Upgrade manager</field>
<field name="category_id" ref="module_project"/>
</record>
<record id="group_runbot_admin" model="res.groups">
<field name="name">Runbot administrator</field>
<field name="category_id" ref="module_project"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
<field name="implied_ids" eval="[(4, ref('runbot.group_runbot_advanced_user')), (4, ref('runbot.group_user')), (4, ref('runbot.group_build_config_administrator'))]"/>
<field name="implied_ids" eval="[(4, ref('runbot.group_runbot_advanced_user')), (4, ref('runbot.group_user')), (4, ref('runbot.group_build_config_administrator')), (4, ref('runbot.group_runbot_upgrade_manager'))]"/>
</record>

View File

@ -10,12 +10,18 @@ import { useDynamicPlaceholder } from "@web/views/fields/dynamic_placeholder_hoo
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { useInputField } from "@web/views/fields/input_field_hook";
import { useRef, xml, Component } from "@odoo/owl";
import { useRef, xml, Component, onWillRender } from "@odoo/owl";
import { useAutoresize } from "@web/core/utils/autoresize";
import { getFormattedValue } from "@web/views/utils";
import { UrlField } from "@web/views/fields/url/url_field";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
import { formatX2many } from "@web/views/fields/formatters";
import { BooleanToggleField } from "@web/views/fields/boolean_toggle/boolean_toggle_field";
function stringify(obj) {
return JSON.stringify(obj, null, '\t')
}
@ -170,3 +176,54 @@ registry.category("fields").add("pull_request_url", {
//}
//
//registry.category("fields").add("github_team", GithubTeamWidget);
export class Matrixx2ManyField extends X2ManyField {
static template = 'runbot.Matrixx2ManyField';
static props = { ...standardFieldProps };
static components = { BooleanToggleField };
setup() {
onWillRender(() => {
this.initValues();
})
}
getEntry(from, to) {
const entry = this.entry_per_version[[from, to]];
return entry
}
initValues() {
this.data = this.props.record.data[this.props.name]
this.to_versions = [];
this.from_versions = [];
this.entry_per_version = {};
this.data.records.forEach((record) => {
if (!this.to_versions.includes(record.data.to_version_number)) {
this.to_versions.push(record.data.to_version_number)
}
if (!this.from_versions.includes(record.data.from_version_number)) {
this.from_versions.push(record.data.from_version_number)
}
this.entry_per_version[[record.data.from_version_number, record.data.to_version_number]] = record;
});
console.log(this.to_versions)
this.to_versions.sort()
//this.to_versions.reverse()
this.from_versions.sort()
this.from_versions.reverse()
}
}
export const matrixx2ManyField = {
component: Matrixx2ManyField,
useSubView: false,
};
registry.category("fields").add("version_matrix", matrixx2ManyField);

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="runbot.Matrixx2ManyField">
<table class="table table-sm">
<thead>
<tr>
<th></th>
<th class="thead-dark" t-foreach="to_versions" t-as="to_version" t-esc="to_version" t-key="to_version"/>
</tr>
</thead>
<tbody>
<tr t-foreach="from_versions" t-as="from_version" t-key="from_version">
<th class="thead-dark" t-esc="from_version"/>
<t t-foreach="to_versions" t-as="to_version" t-key="to_version">
<t t-set="entry" t-value="getEntry(from_version, to_version)"/>
<td t-att-style="entry and entry.data.manually_edited ? 'background-color: #CCC' : ''">
<span t-if="entry" >
<BooleanToggleField name="'enabled'" record="entry"/>
</span>
</td>
</t>
</tr>
</tbody>
</table>
</t>
</templates>

View File

@ -26,7 +26,10 @@
<menuitem id="runbot_menu_job_config_tree" parent="runbot_menu_configs" sequence="10" action="open_view_job_config_tree"/>
<menuitem id="runbot_menu_job_tree" parent="runbot_menu_configs" sequence="20" action="open_view_job_tree"/>
<menuitem id="runbot_menu_upgrade_exceptions_tree" parent="runbot_menu_root" sequence="700" action="open_view_upgrade_exception_tree"/>
<menuitem name="Upgrades" id="runbot_menu_upgrade" parent="runbot_menu_root" sequence="700"/>
<menuitem id="runbot_menu_upgrade_exceptions_tree" parent="runbot_menu_upgrade" sequence="10" action="open_view_upgrade_exception_tree"/>
<menuitem id="runbot_menu_upgrade_regex_tree" parent="runbot_menu_upgrade" sequence="20" action="open_view_upgrade_regex_tree"/>
<menuitem name="Upgrade Matrix" id="runbot_menu_upgrade_matrix_tree" parent="runbot_menu_upgrade" sequence="30" action="open_view_runbot_upgrade_matrix_tree"/>
<menuitem name="Docker" id="menu_dockerfile" parent="runbot_menu_root" sequence="800"/>
<menuitem name="Docker files" id="menu_dockerfiles" parent="menu_dockerfile" action="open_view_dockerfile_tree" sequence="801"/>
@ -56,7 +59,6 @@
<menuitem id="runbot_menu_repos" parent="menu_runbot_settings" sequence="30" action="runbot_repos_action"/>
<menuitem id="runbot_menu_remotes" parent="menu_runbot_settings" sequence="40" action="runbot_remotes_action"/>
<menuitem id="runbot_menu_trigger_category" parent="menu_runbot_settings" sequence="50" action="runbot_triggers_category_action"/>
<menuitem id="runbot_menu_upgrade_regex_tree" parent="menu_runbot_settings" sequence="60" action="open_view_upgrade_regex_tree"/>
<menuitem name="Stats Regexes" id="runbot_menu_stat" parent="menu_runbot_settings" sequence="70" action="open_view_stat_regex_tree"/>
<menuitem name="Stat Regex Wizard" id="runbot_menu_stat_wizard" parent="menu_runbot_settings" sequence="80" action="runbot_stat_regex_wizard_action"/>
<menuitem name="Error regex" id="runbot_menu_error_regex_tree" parent="menu_runbot_settings" sequence="20" action="open_view_error_regex"/>

View File

@ -0,0 +1,73 @@
<odoo>
<data>
<record id="runbot_upgrade_matrix_form" model="ir.ui.view">
<field name="name">runbot.upgrade.matrix.form</field>
<field name="model">runbot.upgrade.matrix</field>
<field name="arch" type="xml">
<form>
<sheet>
<header>
<button name="update_matrix_entries" string="Create missing entries" type="object" class="oe_highlight"/>
<button name="reset_matrix_enabled" string="Reset matrix entries default enabled state" type="object" class="oe_highlight"/>
</header>
<group >
<group >
<field name="name"/>
<field name="project_id"/>
<field name="auto_update"/>
</group >
<group >
<field name="upgrade_to_major_versions"/>
<field name="upgrade_to_all_versions"/>
<field name="upgrade_from_previous_major_version"/>
<field name="upgrade_from_last_intermediate_version"/>
<field name="upgrade_from_all_intermediate_version"/>
</group >
</group >
<group >
<field name="entry_ids" widget="version_matrix">
<list>
<field name="from_version_id"/>
<field name="to_version_id"/>
<field name="to_version_number"/>
<field name="from_version_number"/>
<field name="enabled" widget="boolean_toggle"/>
<field name="manually_edited"/>
</list>
</field>
<field name="entry_ids">
<list>
<field name="from_version_id"/>
<field name="to_version_id"/>
<field name="enabled" widget="boolean_toggle"/>
</list>
</field>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="runbot_upgrade_matrix_tree" model="ir.ui.view">
<field name="name">runbot.upgrade.matrix.tree</field>
<field name="model">runbot.upgrade.matrix</field>
<field name="arch" type="xml">
<list string="Upgrade matrix">
<field name="name" optional="show" readonly="1"/>
<field name="project_id" optional="hide" readonly="1"/>
</list>
</field>
</record>
<record id="open_view_runbot_upgrade_matrix_tree" model="ir.actions.act_window">
<field name="name">Upgrade matrix</field>
<field name="res_model">runbot.upgrade.matrix</field>
<field name="view_mode">list,form</field>
</record>
</data>
</odoo>