diff --git a/runbot/controllers/frontend.py b/runbot/controllers/frontend.py index 18ca8e70..601f98d8 100644 --- a/runbot/controllers/frontend.py +++ b/runbot/controllers/frontend.py @@ -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/' + ], 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/' ], 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) diff --git a/runbot/documentation/codeowner.md b/runbot/documentation/codeowner.md new file mode 100644 index 00000000..ee035e97 --- /dev/null +++ b/runbot/documentation/codeowner.md @@ -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. diff --git a/runbot/documentation/images/Screenshot from 2020-09-23 12-20-40.png b/runbot/documentation/images/Screenshot from 2020-09-23 12-20-40.png deleted file mode 100644 index ac7ae735..00000000 Binary files a/runbot/documentation/images/Screenshot from 2020-09-23 12-20-40.png and /dev/null differ diff --git a/runbot/documentation/readme.md b/runbot/documentation/readme.md new file mode 100644 index 00000000..eb0ee8bd --- /dev/null +++ b/runbot/documentation/readme.md @@ -0,0 +1,3 @@ +# User documentation + +- [Teams and codeowner](codeowner.md) \ No newline at end of file diff --git a/runbot/models/build_error.py b/runbot/models/build_error.py index 483f72c6..b15e76e3 100644 --- a/runbot/models/build_error.py +++ b/runbot/models/build_error.py @@ -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): diff --git a/runbot/models/codeowner.py b/runbot/models/codeowner.py index 802cfc53..e0bd8b1f 100644 --- a/runbot/models/codeowner.py +++ b/runbot/models/codeowner.py @@ -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)) diff --git a/runbot/models/dockerfile.py b/runbot/models/dockerfile.py index f08d8475..003c0b10 100644 --- a/runbot/models/dockerfile.py +++ b/runbot/models/dockerfile.py @@ -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': ''}) - 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 = '' diff --git a/runbot/models/res_users.py b/runbot/models/res_users.py index a74e2b62..c04aa380 100644 --- a/runbot/models/res_users.py +++ b/runbot/models/res_users.py @@ -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 diff --git a/runbot/models/team.py b/runbot/models/team.py index bebf708c..9a2a5392 100644 --- a/runbot/models/team.py +++ b/runbot/models/team.py @@ -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): diff --git a/runbot/static/src/js/runbot.js b/runbot/static/src/js/runbot.js index fe09e18d..4775c9e7 100644 --- a/runbot/static/src/js/runbot.js +++ b/runbot/static/src/js/runbot.js @@ -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(); }); }); diff --git a/runbot/templates/build_error.xml b/runbot/templates/build_error.xml index a7550261..5d727681 100644 --- a/runbot/templates/build_error.xml +++ b/runbot/templates/build_error.xml @@ -8,9 +8,7 @@
Last seen date
Module
-
Summary
-
Nb Seen
-
Random
+
Summary
Assigned to
 
@@ -22,24 +20,23 @@
-
+
- + + Assign to me
- -
-
-
- - - + + + + +
@@ -59,16 +56,25 @@
-

Your assigned bug on Runbot Builds

- - - user_errors + +

Your assigned build errors

+ + + user_errors + + + +

Team unassigned build errors

+ + + user_errors + -

Current Bugs on Runbot Builds

-
-
- - - -
+ + + + + diff --git a/runbot/templates/utils.xml b/runbot/templates/utils.xml index 2c29ae13..9f449450 100644 --- a/runbot/templates/utils.xml +++ b/runbot/templates/utils.xml @@ -137,13 +137,26 @@