[IMP] runbot: add kanban view and stages to build error page

Introduce a kanban view to the runbot build error records.
Kanban cards show the most valuable field with relevant icons.

The concept of "stage" was also introduced for the build error
model.
Currently there is 4 stages:
 1. New: default stage for any new error build
 2. Solved: when the issue should be solved
 3. Ignored: build error currently ignored (test-tags)
 4. Done: When the issue stop happening
Certain stages change automatically based as handled in a cron
Generally, any not-ignored on-going task end up in done after some
period of time if it is not seen for some time
The records also get eventually archived (field "Open (not fixed)")
after some time.
( see full details in function `_update_stage`).
The other stage are expected to be used by the user. For instance
after fixing an underteministic error the user can change the
stage from new -> solved so that it is moved to done if the build
error is not seen for 7 days (instead of 15 if it was in new).
This commit is contained in:
lse-odoo 2025-02-28 18:12:02 +01:00
parent 3cf9dd6fa2
commit 9108aa8d88
7 changed files with 155 additions and 3 deletions

View File

@ -6,18 +6,20 @@
'author': "Odoo SA", 'author': "Odoo SA",
'website': "http://runbot.odoo.com", 'website': "http://runbot.odoo.com",
'category': 'Website', 'category': 'Website',
'version': '5.9', 'version': '5.10',
'application': True, 'application': True,
'depends': ['base', 'base_automation', 'website'], 'depends': ['base', 'base_automation', 'website'],
'data': [ 'data': [
'templates/dockerfile.xml', 'templates/dockerfile.xml',
'data/dockerfile_data.xml', 'data/dockerfile_data.xml',
'data/build_error_stage.xml',
'data/build_parse.xml', 'data/build_parse.xml',
'data/error_link.xml', 'data/error_link.xml',
'data/runbot_build_config_data.xml', 'data/runbot_build_config_data.xml',
'data/runbot_data.xml', 'data/runbot_data.xml',
'data/runbot_error_regex_data.xml', 'data/runbot_error_regex_data.xml',
'data/website_data.xml', 'data/website_data.xml',
'data/ir_cron_data.xml',
'security/runbot_security.xml', 'security/runbot_security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',

View File

@ -0,0 +1,24 @@
<odoo>
<record model="runbot.build.error.stage" id="build_error_stage_new">
<field name="name">New/Unsolved</field>
<field name="sequence">5</field>
<field name="description">New build error detected by the runbot platform</field>
</record>
<record model="runbot.build.error.stage" id="build_error_stage_solved">
<field name="name">Solved</field>
<field name="sequence">10</field>
<field name="description">Issue should be solved. Will automatically move to the Done state after some time</field>
</record>
<record model="runbot.build.error.stage" id="build_error_stage_ignored">
<field name="name">Ignored</field>
<field name="sequence">15</field>
<field name="description">Issue can be ignored, but should eventually be solved (for instance, test-tags)</field>
<field name="fold">True</field>
</record>
<record model="runbot.build.error.stage" id="build_error_stage_done">
<field name="name">Done</field>
<field name="sequence">20</field>
<field name="description">Issue is solved or dissapeared</field>
<field name="fold">True</field>
</record>
</odoo>

View File

@ -0,0 +1,10 @@
<odoo>
<record id="runbot_update_build_errors_stage" model="ir.cron">
<field name="name">Runbot: Update Build Errors Stage</field>
<field name="model_id" ref="model_runbot_build_error"/>
<field name="state">code</field>
<field name="code">model._update_stage()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
</record>
</odoo>

View File

@ -0,0 +1,14 @@
from odoo import api, SUPERUSER_ID
def migrate(cr, version):
env = api.Environment(cr, SUPERUSER_ID, {})
# By default, the build_error_stage is set to the new stage
build_error_stage_ignored = env.ref('runbot.build_error_stage_ignored').id
build_error_stage_done = env.ref('runbot.build_error_stage_done').id
# Archived build errors are set are considered Done
env['runbot.build.error'].search([['active', "=", False]]).write({'stage_id': build_error_stage_done})
# Build errors with test_tags are set to Ignored
env['runbot.build.error'].search([['test_tags', '!=', False]]).write({'stage_id': build_error_stage_ignored})

View File

@ -90,6 +90,18 @@ def _search_related_error_content_ids(field_name):
return [(f'error_content_ids.{field_name}', operator, value)] return [(f'error_content_ids.{field_name}', operator, value)]
return _search return _search
class BuildErrorStage(models.Model):
_name = 'runbot.build.error.stage'
_description = 'Build Error Stage'
_order = 'sequence'
name = fields.Char(string='Stage Name', required=True)
description = fields.Text(string='Stage description')
sequence = fields.Integer('Sequence', default=1)
fold = fields.Boolean(string='Folded in Kanban', default=False)
class BuildError(models.Model): class BuildError(models.Model):
_name = "runbot.build.error" _name = "runbot.build.error"
_description = "Build error" _description = "Build error"
@ -105,6 +117,7 @@ class BuildError(models.Model):
error_count = fields.Integer("Error count", store=True, compute='_compute_count') error_count = fields.Integer("Error count", store=True, compute='_compute_count')
previous_error_id = fields.Many2one('runbot.build.error', string="Already seen error") previous_error_id = fields.Many2one('runbot.build.error', string="Already seen error")
stage_id = fields.Many2one('runbot.build.error.stage', required=True, tracking=True, group_expand='_read_group_expand_full', default=lambda self: self.env['runbot.build.error.stage'].search([], limit=1))
responsible = fields.Many2one('res.users', 'Assigned fixer', tracking=True) responsible = fields.Many2one('res.users', 'Assigned fixer', tracking=True)
customer = fields.Many2one('res.users', 'Customer', tracking=True) customer = fields.Many2one('res.users', 'Customer', tracking=True)
team_id = fields.Many2one('runbot.team', 'Assigned team', tracking=True) team_id = fields.Many2one('runbot.team', 'Assigned team', tracking=True)
@ -347,6 +360,7 @@ class BuildError(models.Model):
@api.constrains('test_tags') @api.constrains('test_tags')
def _check_test_tags(self): def _check_test_tags(self):
self.stage_id = self.env.ref('runbot.build_error_stage_ignored').id
for build_error in self: for build_error in self:
if build_error.test_tags and '-' in build_error.test_tags: if build_error.test_tags and '-' in build_error.test_tags:
raise ValidationError('Build error test_tags should not be negated') raise ValidationError('Build error test_tags should not be negated')
@ -586,6 +600,37 @@ class BuildError(models.Model):
base_error = self_sorted[0] base_error = self_sorted[0]
base_error._merge(self_sorted - base_error) base_error._merge(self_sorted - base_error)
def _update_stage(self, nbr_day_solved_to_done=7, nbr_day_new_to_done=15, nbr_day_done_to_archive=30):
"""Called automatically by scheduled action to update the stage of the error if necessary"""
now = fields.Datetime.now()
build_error_stage_new = self.env.ref('runbot.build_error_stage_new').id
build_error_stage_solved = self.env.ref('runbot.build_error_stage_solved').id
build_error_stage_done = self.env.ref('runbot.build_error_stage_done').id
# Very old done error eventually get archived
self.search([
('stage_id', '=', build_error_stage_done),
('last_seen_date', '<', now - relativedelta(days=nbr_day_done_to_archive)),
]).write({'active': False})
# Done errors that did happen again recently are moved back to new
self.search([
('stage_id', '=', build_error_stage_done),
('last_seen_date', '>=', now - relativedelta(days=nbr_day_new_to_done)),
]).write({'stage_id': build_error_stage_new})
# New error that did not appear after a long time are marked as done
# Solved error that did not appear after a short time are marked as done
self.search([
'|',
'&',
('stage_id', '=', build_error_stage_new),
('last_seen_date', '<', now - relativedelta(days=nbr_day_new_to_done)),
'&',
('stage_id', '=', build_error_stage_solved),
('last_seen_date', '<', now - relativedelta(days=nbr_day_solved_to_done)),
]).write({'stage_id': build_error_stage_done})
class BuildErrorContent(models.Model): class BuildErrorContent(models.Model):

View File

@ -151,4 +151,5 @@ access_runbot_build_stat_regex_wizard,access_runbot_build_stat_regex_wizard,mode
access_runbot_host_message,access_runbot_host_message,runbot.model_runbot_host_message,runbot.group_runbot_admin,1,0,0,0 access_runbot_host_message,access_runbot_host_message,runbot.model_runbot_host_message,runbot.group_runbot_admin,1,0,0,0
access_runbot_build_error_stage_user,access_runbot_build_error_stage_user,runbot.model_runbot_build_error_stage,base.group_user,1,0,0,0
access_runbot_build_error_stage_admin,access_runbot_build_error_stage_admin,runbot.model_runbot_build_error_stage,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
151
152
153
154
155

View File

@ -5,6 +5,9 @@
<field name="model">runbot.build.error</field> <field name="model">runbot.build.error</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header>
<field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
</header>
<sheet> <sheet>
<div name="button_box"> <div name="button_box">
<button class="oe_stat_button" type="object" icon="fa-exclamation-circle" name="action_get_build_link_record"> <button class="oe_stat_button" type="object" icon="fa-exclamation-circle" name="action_get_build_link_record">
@ -329,6 +332,59 @@
<field name="binding_view_types">list</field> <field name="binding_view_types">list</field>
</record> </record>
<record id="build_error_view_kanban" model="ir.ui.view">
<field name="name">runbot.build.error.kanban</field>
<field name="model">runbot.build.error</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" quick_create="false" default_order="last_seen_date desc">
<templates>
<t t-name="card">
<widget name="web_ribbon" title="Test-tags" bg_color="bg-danger" invisible="not test_tags"/>
<field name="name" class="fw-bold fs-5"/>
<group>
<div style="display: flex; align-items: center;">
<i class="fa fa-clock-o me-2" title="Date interval from first seen to last seen"/>
<field name="first_seen_date" widget="remaining_days"/>
<i class="fa fa-long-arrow-right mx-2 oe_read_only" title="to"/>
<field name="last_seen_date" widget="remaining_days"/>
</div>
<div class="d-flex align-items-center gap-1">
<i class="fa fa-repeat" title="Number of occurence"/>
<field name="error_count"/>
</div>
<div class="d-flex align-items-center gap-1" style="display: flex; align-items: center;">
<i class="fa fa-code-fork" title="Concerned Odoo versions"/>
<field name="version_ids" widget="many2many_tags"/>
</div>
<div class="d-flex align-items-center gap-1" style="display: flex; align-items: center;">
<i class="fa fa-bullseye" title="Triggers"/>
<field name="trigger_ids" widget="many2many_tags"/>
</div>
</group>
<footer>
<div class="d-flex align-items-center gap-1">
<field name="activity_ids" widget="kanban_activity"/>
</div>
<div class="d-flex align-items-center gap-1 ms-auto">
<i class="fa fa-random text-danger" title="inconsistant" invisible="not random"/>
<i class="fa fa-users" title="Responsible team"/>
<field name="team_id"/> <i t-if="!record.team_id.raw_value">no team</i>
<i class="fa fa-address-card" title="Investigator"/>
<field name="customer" widget="many2one_avatar_user"/>
<i class="fa fa-wrench" title="Solver"/>
<field name="responsible" widget="many2one_avatar_user"/>
<field name="fixing_pr_url" widget="url" text="PR" invisible="not fixing_pr_url"/>
</div>
</footer>
</t>
</templates>
</kanban>
</field>
</record>
<record id="build_error_view_tree" model="ir.ui.view"> <record id="build_error_view_tree" model="ir.ui.view">
<field name="name">runbot.build.error.list</field> <field name="name">runbot.build.error.list</field>
<field name="model">runbot.build.error</field> <field name="model">runbot.build.error</field>
@ -478,7 +534,7 @@
<field name="name">Errors</field> <field name="name">Errors</field>
<field name="res_model">runbot.build.error</field> <field name="res_model">runbot.build.error</field>
<field name="path">error</field> <field name="path">error</field>
<field name="view_mode">list,form</field> <field name="view_mode">kanban,list,form</field>
<field name="context">{'search_default_not_fixed_errors': True, 'active_test': False}</field> <field name="context">{'search_default_not_fixed_errors': True, 'active_test': False}</field>
</record> </record>