[IMP] runbot: documentation and views imps

Add a small documentation for users, mainly about teams and codeowners.
Improves some views and hide some menu_items to keep interface easier
to navigate.
This commit is contained in:
Xavier-Do 2022-12-20 15:35:47 +01:00 committed by Christophe Monniez
parent ada69222c9
commit c83f89fc23
15 changed files with 177 additions and 59 deletions

View File

@ -32,6 +32,7 @@ def route(routes, **kw):
refresh = kwargs.get('refresh', False)
nb_build_errors = request.env['runbot.build.error'].search_count([('random', '=', True), ('parent_id', '=', False)])
nb_assigned_errors = request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)])
nb_team_errors = request.env['runbot.build.error'].search_count([('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids)])
kwargs['more'] = more
kwargs['projects'] = projects
@ -60,6 +61,7 @@ def route(routes, **kw):
response.qcontext['title'] = 'Runbot %s' % project.name or ''
response.qcontext['nb_build_errors'] = nb_build_errors
response.qcontext['nb_assigned_errors'] = nb_assigned_errors
response.qcontext['nb_team_errors'] = nb_team_errors
return response
return response_wrap
return decorator
@ -390,6 +392,17 @@ class Runbot(Controller):
}
return request.render(view_id if view_id else "runbot.monitoring", qctx)
@route(['/runbot/errors/assign/<int:build_error_id>'
], type='http', auth='user', methods=['POST'], csrf=False, sitemap=False)
def build_errors_assign(self, build_error_id=None, **kwargs):
build_error = request.env['runbot.build.error'].browse(build_error_id)
if build_error.responsible:
return build_error.responsible.name
if request.env.user._is_internal():
build_error.sudo().responsible = request.env.user
return request.env.user.name
return 'Error'
@route(['/runbot/errors',
'/runbot/errors/page/<int:page>'
], type='http', auth='user', website=True, sitemap=False)
@ -409,9 +422,11 @@ class Runbot(Controller):
current_user_errors = request.env['runbot.build.error'].search([
('responsible', '=', request.env.user.id),
('parent_id', '=', False),
], order='last_seen_date desc, build_count desc')
current_team_errors = request.env['runbot.build.error'].search([
('responsible', '=', False),
('team_id', 'in', request.env.user.runbot_team_ids.ids)
], order='last_seen_date desc, build_count desc')
domain = [('parent_id', '=', False), ('responsible', '!=', request.env.user.id), ('build_count', '>', 1)]
build_errors_count = request.env['runbot.build.error'].search_count(domain)
url_args = {}
@ -422,10 +437,12 @@ class Runbot(Controller):
qctx = {
'current_user_errors': current_user_errors,
'current_team_errors': current_team_errors,
'build_errors': build_errors,
'title': 'Build Errors',
'sort_order_choices': sort_order_choices,
'pager': pager
'page': page,
'pager': pager,
}
return request.render('runbot.build_error', qctx)

View File

@ -0,0 +1,51 @@
# Teams and Codeowner
## How
Codeowner is using two way to define which team should be notified when a file is modified:
- Module ownership to link a module to a team. (Editable by team manager)
- Regexes, to target specific files or more specific rules. (Editable by runbot admin)
For each file, the codeowner will check all regexes and all module ownership.
If a module ownersip is a `fallback`, the team won't be added as a reviewer if any previous rule matched for a file.
If no reviewer is found for a file, a fallback github team is added as reviewer.
The codeowner is not applied on draft pull request (and will give a red ci as a reminder)
A pr is considered draft if:
- marked as draft on github
- contains `[DRAFT]` or `[WIP]` in the title
- is linked to any other draft pr (in the same bundle)
The codeowner is not applied on forwardport initial push. Any following push (conflict resolution) will trigger the codeowner again.
## Module ownership
Module ownership links a module to a team with an additionnal `is_fallback` flag to define if the codeowner should only be triggered if no one else was added for a file.
Module ownership is also a way to define which team should be contacted for some question on a module.
Module coverage should idealy reach 100% with module ownership. Having all files covered allows to ensure that at least one reviewer will be added when a pr is open (mainly for external contributors). This can sometimes generate to much github notifications, this is why it is important to configure members, create subteams, and skip pr policy.
## Team management
Team managers can be anyone from the team with a basic knowledge of the guidelines to follow and a good understanding of the system.
Team manager can modify.
- Teams
- Module ownership
- Github account of users
Some basic config can be done on Teams
- `Github team`: the corresponding github team to add as reviewer
- `Github logins`: additional github logins in the github team, mainly for github users no listed in the members of the runbot team. Mainly usefull if `Skip team pr` is checked. This list can be updated automatically using the `Fetch members` action. This field can also be manually modified to avoid being notified by some github login, even if it is adviced to add them as a `Team members` if they have an internal user.
- `Skip team pr`: If checked, don't add the team as reviewer if the pr was oppened by one of the members of the team.
- `Module Ownership`: The list of modules owned by the team. `Fallback` options can be edited from there but it is adviced to use the `Modules` or `Module ownership` menu to add or remove a module, mainly to avoid removing all ownership from a module.
- `Team members`: the members of the team. Those members will see a link to the team dashboard and team errors count will be displayed on main page
## Disable codeowner on demand
In some rare cases, if a pr modifies a lot of files in an almost automated way, it can be useful to disable the codeowner. This can be done on a bundle. Note that forwardport won't be impacted, and this should be done per forwardport in case of conflict.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,3 @@
# User documentation
- [Teams and codeowner](codeowner.md)

View File

@ -120,7 +120,7 @@ class BuildError(models.Model):
@api.depends('content')
def _compute_summary(self):
for build_error in self:
build_error.summary = build_error.content[:50]
build_error.summary = build_error.content[:80]
@api.depends('build_ids', 'child_ids.build_ids')
def _compute_children_build_ids(self):

View File

@ -7,7 +7,7 @@ from odoo.exceptions import ValidationError
class Codeowner(models.Model):
_name = 'runbot.codeowner'
_description = "Notify github teams based on filenames regex"
_description = "Codeowner regex"
_inherit = "mail.thread"
project_id = fields.Many2one('runbot.project', required=True, default=lambda self: self.env.ref('runbot.main_project', raise_if_not_found=False))

View File

@ -14,12 +14,12 @@ class Dockerfile(models.Model):
name = fields.Char('Dockerfile name', required=True, help="Name of Dockerfile")
image_tag = fields.Char(compute='_compute_image_tag', store=True)
template_id = fields.Many2one('ir.ui.view', string='Docker Template', domain=[('type', '=', 'qweb')], context={'default_type': 'qweb', 'default_arch_base': '<t></t>'})
arch_base = fields.Text(related='template_id.arch_base', readonly=False)
arch_base = fields.Text(related='template_id.arch_base', readonly=False, related_sudo=True)
dockerfile = fields.Text(compute='_compute_dockerfile', tracking=True)
to_build = fields.Boolean('To Build', help='Build Dockerfile. Check this when the Dockerfile is ready.', default=False)
version_ids = fields.One2many('runbot.version', 'dockerfile_id', string='Versions')
description = fields.Text('Description')
view_ids = fields.Many2many('ir.ui.view', compute='_compute_view_ids')
view_ids = fields.Many2many('ir.ui.view', compute='_compute_view_ids', groups="runbot.group_runbot_admin")
project_ids = fields.One2many('runbot.project', 'dockerfile_id', string='Default for Projects')
bundle_ids = fields.One2many('runbot.bundle', 'dockerfile_id', string='Used in Bundles')
@ -37,7 +37,7 @@ class Dockerfile(models.Model):
def _compute_dockerfile(self):
for rec in self:
try:
res = rec.template_id._render() if rec.template_id else ''
res = rec.template_id.sudo()._render() if rec.template_id else ''
rec.dockerfile = re.sub(r'^\s*$', '', res, flags=re.M).strip()
except QWebException:
rec.dockerfile = ''

View File

@ -26,3 +26,8 @@ class ResUsers(models.Model):
if list(values.keys()) == ['github_login'] and self.env.user.has_group('runbot.group_runbot_team_manager'):
return super(ResUsers, self.sudo()).write(values)
return super().write(values)
# backport of 16.0 feature TODO remove after migration
def _is_internal(self):
self.ensure_one()
return not self.sudo().share

View File

@ -19,6 +19,7 @@ class RunbotTeam(models.Model):
_name = 'runbot.team'
_description = "Runbot Team"
_order = 'name, id'
_inherit = 'mail.thread'
name = fields.Char('Team', required=True)
project_id = fields.Many2one('runbot.project', 'Project', help='Project to monitor', required=True,
@ -34,10 +35,11 @@ class RunbotTeam(models.Model):
'e.g.: `*website*,-*website_sale*`'
)
module_ownership_ids = fields.One2many('runbot.module.ownership', 'team_id')
codeowner_ids = fields.One2many('runbot.codeowner', 'team_id')
upgrade_exception_ids = fields.One2many('runbot.upgrade.exception', 'team_id', string='Team Upgrade Exceptions')
github_team = fields.Char('Github team')
github_logins = fields.Char('Github logins', help='Additional github logins, prefer adding the login on the member of the team')
skip_team_pr = fields.Boolean('Skip team pr', help="Don't add codeowner if pr was created by a member of the team")
github_team = fields.Char('Github team', tracking=True)
github_logins = fields.Char('Github logins', help='Additional github logins, prefer adding the login on the member of the team', tracking=True)
skip_team_pr = fields.Boolean('Skip team pr', help="Don't add codeowner if pr was created by a member of the team", tracking=True)
@api.model_create_single
def create(self, values):

View File

@ -9,14 +9,21 @@
return;
}
var xhr = new XMLHttpRequest();
var url = e.target.href
if (data.runbotBuild) {
url = '/runbot/build/' + data.runbotBuild + '/' + operation
}
var elem = e.target
xhr.addEventListener('load', function () {
if (operation == 'rebuild' && window.location.href.split('?')[0].endsWith('/build/' + data.runbotBuild)){
window.location.href = window.location.href.replace('/build/' + data.runbotBuild, '/build/' + xhr.responseText);
} else if (operation == 'action') {
elem.parentElement.innerText = this.responseText
} else {
window.location.reload();
}
});
xhr.open('POST', '/runbot/build/' + data.runbotBuild + '/' + operation);
xhr.open('POST', url);
xhr.send();
});
});

View File

@ -8,9 +8,7 @@
<div class="row">
<div class="col">Last seen date</div>
<div class="col col-md-3">Module</div>
<div class="col col-md-3">Summary</div>
<div class="col">Nb Seen</div>
<div class="col">Random</div>
<div class="col col-md-5">Summary</div>
<div class="col">Assigned to</div>
<div class="col">&amp;nbsp;</div>
</div>
@ -22,24 +20,23 @@
<div class="row">
<div class="col"><t t-esc="build_error.last_seen_date" t-options='{"widget": "datetime"}'/></div>
<div class="col col-md-3"><t t-esc="build_error.module_name"/></div>
<div class="col col-md-3">
<div class="col col-md-5">
<button class="btn btn-link" type="button" data-toggle="collapse" t-attf-data-target="#collapse{{build_error.id}}" aria-expanded="true" aria-controls="collapseOne">
<i class="fa fa-minus"/>
</button>
<code><t t-esc="build_error.summary"/></code>
</div>
<div class="col">
<t t-esc="build_error.build_count"/>
<t t-if="build_error.responsible" t-esc="build_error.responsible.name"/>
<a t-else="" t-attf-href="/runbot/errors/assign/{{build_error.id}}" data-runbot="action" role="button" class="btn btn-primary btn-sm">Assign to me</a>
</div>
<div class="col">
<i t-if="build_error.random" class="fa fa-random"/>
</div>
<div class="col"><t t-esc="build_error.responsible.name"/></div>
<div class="col">
<a groups="base.group_user" t-attf-href="/web/#id={{build_error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env.ref('runbot.runbot_menu_root').id}}" target="new" title="View in Backend">
<i class="fa fa-list"/>
</a>
<a t-att-href="build_error.last_seen_build_id.build_url" t-attf-title="View last affected build ({{build_error.last_seen_build_id.id}})"><i class="fa fa-external-link"/></a>
<a groups="base.group_user" t-attf-href="/web/#id={{build_error.id}}&amp;view_type=form&amp;model=runbot.build.error&amp;menu_id={{env.ref('runbot.runbot_menu_root').id}}" target="new" title="View in Backend">
<span class="badge badge-info" t-esc="build_error.build_count" t-attf-title="This error was seen {{build_error.build_count}} View in backend"/>
</a>
<span groups="!base.group_user" class="badge badge-info" t-esc="build_error.build_count" t-attf-title="This error was seen {{build_error.build_count}}"/>
<span class="fa fa-random" t-if="build_error.random"/>
</div>
</div>
</div>
@ -59,16 +56,25 @@
<div class="container-fluid">
<div class="row">
<div class='col-md-12'>
<h3>Your assigned bug on Runbot Builds</h3>
<t t-call="runbot.build_error_cards">
<t t-set="build_errors" t-value="current_user_errors"/>
<t t-set="accordion_id">user_errors</t>
<t t-if="page==1 and current_user_errors">
<h4>Your assigned build errors</h4>
<t t-call="runbot.build_error_cards">
<t t-set="build_errors" t-value="current_user_errors"/>
<t t-set="accordion_id">user_errors</t>
</t>
</t>
<t t-if="page==1 and current_team_errors">
<h4>Team unassigned build errors</h4>
<t t-call="runbot.build_error_cards">
<t t-set="build_errors" t-value="current_team_errors"/>
<t t-set="accordion_id">user_errors</t>
</t>
</t>
<h3>Current Bugs on Runbot Builds</h3>
<t t-if="build_errors">
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="dropdown mr-auto">
<nav class="navbar navbar-expand-lg navbar-light">
<h4 class="d-inline"> All builds errors</h4>
<span class="ml-auto">
<span class="dropdown mr-auto">
<a role="button" href="#" class="dropdown-toggle btn btn-secondary" data-toggle="dropdown">
Sort By: <t t-esc="request.params.get('sort', '')"/>
</a>
@ -77,12 +83,12 @@
<a role="menuitem" class="dropdown-item" t-attf-href="/runbot/errors?sort={{sort_choice}}"><t t-esc="sort_order_choices[sort_choice]"/></a>
</t>
</div>
</div>
<span class="ml-auto">
<t t-call="website.pager" />
</span>
</nav>
</div>
</span>
<span class="ml-auto">
<t t-call="website.pager" />
</span>
</nav>
<t t-call="runbot.build_error_cards">
<t t-set="build_errors" t-value="build_errors"/>

View File

@ -137,13 +137,26 @@
</template>
<template id="runbot.build_errors_link">
<t t-if="nb_assigned_errors and nb_assigned_errors > 0">
<t t-if="nb_assigned_errors">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link text-danger" t-attf-title="You have {{nb_assigned_errors}} random bug assigned"><i class="fa fa-bug"/><t t-esc="nb_assigned_errors"/></a>
<a href="/runbot/errors" class="nav-link text-danger" t-attf-title="You have {{nb_assigned_errors}} random bug assigned">
<i class="fa fa-bug"/>
<t t-esc="nb_assigned_errors"/>
<span class="text-warning" t-if="nb_team_errors">+<t t-esc="nb_team_errors"/></span>
</a>
</li>
</t>
<t t-elif="nb_build_errors and nb_build_errors > 0">
<t t-elif="nb_team_errors">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link text-warning" t-attf-title="Your team has {{nb_team_errors}} random bug assigned">
<i class="fa fa-bug"/>
<t t-esc="nb_team_errors"/>
</a>
</li>
</t>
<t t-elif="nb_build_errors">
<li class="nav-item divider"/>
<li class="nav-item">
<a href="/runbot/errors" class="nav-link" title="Random Bugs"><i class="fa fa-bug"/></a>

View File

@ -14,12 +14,6 @@
<field name="skip_team_pr"/>
<field name="dashboard_id"/>
<field name="path_glob"/>
<field name="module_ownership_ids">
<tree>
<field name="module_id" readonly="1"/>
<field name="is_fallback" widget="boolean_toggle"/>
</tree>
</field>
</group>
<notebook>
<page string="Team Members">
@ -33,8 +27,27 @@
<page string="Team Errors">
<field name="build_error_ids" nolabel="1" widget="many2many" options="{'not_delete': True, 'no_create': True}"/>
</page>
<page string="Modules">
<field name="module_ownership_ids">
<tree create="0" delete="0">
<field name="module_id" readonly="1"/>
<field name="is_fallback" widget="boolean_toggle"/>
</tree>
</field>
</page>
<page string="Codeowners">
<field name="codeowner_ids">
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>

View File

@ -25,7 +25,7 @@
<page string="Dockerfile">
<field name="dockerfile"/>
</page>
<page string="Views">
<page string="Views" groups="runbot.group_runbot_admin">
<field name="view_ids" widget="one2many">
<tree>
<field name="id"/>
@ -58,7 +58,8 @@
<tree string="Dockerfile" decoration-danger="dockerfile == ''" decoration-warning="to_build == False">
<field name="name"/>
<field name="image_tag"/>
<field name="to_build" widget="boolean_toggle"/>
<field name="to_build" groups="!runbot.group_runbot_admin"/>
<field name="to_build" widget="boolean_toggle" groups="runbot.group_runbot_admin"/>
<field name="version_ids" widget="many2many_tags"/>
<field name="project_ids" widget="many2many_tags"/>
<field name="bundle_ids"/>

View File

@ -11,16 +11,13 @@
<menuitem name="Commits" id="runbot_menu_commit_tree" parent="menu_objects" sequence="50" action="open_view_commit_tree"/>
<menuitem id="runbot_menu_bundle_custom_trigger" parent="menu_objects" sequence="70" action="action_bundle_custom_trigger"/>
<menuitem name="Hosts" id="runbot_menu_host_tree" parent="runbot_menu_root" sequence="300" action="open_view_host_tree"/>
<menuitem name="Hosts" id="runbot_menu_host_tree" parent="runbot_menu_root" sequence="300" action="open_view_host_tree" groups="runbot.group_runbot_admin"/>
<menuitem id="runbot_menu_trigger" parent="runbot_menu_root" sequence="500" action="runbot_triggers_action"/>
<menuitem id="runbot_menu_trigger" parent="runbot_menu_root" sequence="500" action="runbot_triggers_action" groups="runbot.group_runbot_admin"/>
<menuitem name="Configs" id="runbot_menu_configs" parent="runbot_menu_root" sequence="600"/>
<menuitem name="Configs" id="runbot_menu_configs" parent="runbot_menu_root" sequence="600" groups="runbot.group_build_config_user"/>
<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 name="CodeOwners" id="runbot_menu_codeowner_tree" parent="runbot_menu_configs" sequence="30" action="open_view_codeowner_tree"/>
<menuitem name="Modules" id="runbot_menu_module_tree" parent="runbot_menu_configs" sequence="40" action="open_view_runbot_module"/>
<menuitem name="Modules ownership" id="runbot_menu_module_ownership_tree" parent="runbot_menu_configs" sequence="50" action="open_view_runbot_module_ownership"/>
<menuitem id="runbot_menu_upgrade_exceptions_tree" parent="runbot_menu_root" sequence="700" action="open_view_upgrade_exception_tree"/>
@ -32,12 +29,15 @@
<menuitem name="Teams" id="runbot_menu_teams" parent="runbot_menu_root" sequence="1000"/>
<menuitem name="Teams" id="runbot_menu_team_tree" parent="runbot_menu_teams" sequence="30" action="open_view_runbot_team"/>
<menuitem name="Dashboards" id="runbot_menu_runbot_dashboard_tree" parent="runbot_menu_teams" sequence="40" action="open_view_runbot_dashboard"/>
<menuitem name="Dashboard Tiles" id="runbot_menu_runbot_dashboard_tile_tree" parent="runbot_menu_teams" sequence="50" action="open_view_runbot_dashboard_tile"/>
<menuitem name="Dashboards" id="runbot_menu_runbot_dashboard_tree" parent="runbot_menu_teams" sequence="40" action="open_view_runbot_dashboard" groups="runbot.group_runbot_admin"/>
<menuitem name="Dashboard Tiles" id="runbot_menu_runbot_dashboard_tile_tree" parent="runbot_menu_teams" sequence="50" action="open_view_runbot_dashboard_tile" groups="runbot.group_runbot_admin"/>
<menuitem name="Modules" id="runbot_menu_module_tree" parent="runbot_menu_teams" sequence="70" action="open_view_runbot_module"/>
<menuitem name="Modules ownership" id="runbot_menu_module_ownership_tree" parent="runbot_menu_teams" sequence="80" action="open_view_runbot_module_ownership"/>
<menuitem name="CodeOwners" id="runbot_menu_codeowner_tree" parent="runbot_menu_teams" sequence="90" action="open_view_codeowner_tree"/>
<menuitem name="Warnings" id="runbot_menu_warning_root" parent="runbot_menu_root" sequence="1200" action="open_view_warning_tree"/>
<menuitem name="Warnings" id="runbot_menu_warning_root" parent="runbot_menu_root" sequence="1200" action="open_view_warning_tree" groups="runbot.group_runbot_admin"/>
<menuitem name="Settings" id="menu_runbot_settings" parent="runbot_menu_root" sequence="9000" />
<menuitem name="Settings" id="menu_runbot_settings" parent="runbot_menu_root" sequence="9000" groups="runbot.group_runbot_admin"/>
<menuitem id="menu_runbot_global_settings" parent="menu_runbot_settings" action="action_runbot_configuration" groups="base.group_system"/>
<menuitem id="menu_bundle_project" action="action_bundle_project" sequence="10" parent="menu_runbot_settings"/>
<menuitem id="menu_bundle_version" action="action_bundle_version" sequence="20" parent="menu_runbot_settings"/>
@ -49,7 +49,7 @@
<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"/>
<menuitem name="Technical" id="runbot_menu_technical" parent="menu_runbot_settings" sequence="10000"/>
<menuitem name="Technical" id="runbot_menu_technical" parent="menu_runbot_settings" sequence="10000" groups="base.group_system"/>
<menuitem id="runbot_menu_ir_cron_act" action="base.ir_cron_act" parent="runbot_menu_technical"/>
<menuitem id="runbot_menu_base_automation_act" action="base_automation.base_automation_act" parent="runbot_menu_technical"/>
<menuitem id="runbot_menu_action_ui_view" action="base.action_ui_view" parent="runbot_menu_technical"/>