Skip to content

Commit 5332fe1

Browse files
committed
[IMP] runbot: allow bundle tagging
The purppose of this commit is to facilitate the Odoo freeze process. A new BundleTag model is added that allows to tag bundles. A frontend controller is also added to list bundles with a particular tag. A "description" compute stored field is added on the bundle. By default the PR titles are used to fill the description. A "team_id" compute stored field is added to help classifying the bundles. The computed attribution is based on the Odoo convention that a branch name should end with the employee n-gram.
1 parent 8f89379 commit 5332fe1

File tree

12 files changed

+200
-14
lines changed

12 files changed

+200
-14
lines changed

runbot/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
'templates/build_error.xml',
4040
'templates/batches_by_date.xml',
4141
'templates/commit_link_details.xml',
42+
'templates/bundles_by_tag.xml',
4243

4344
'views/branch_views.xml',
4445
'views/build_error_link_views.xml',

runbot/controllers/frontend.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
import functools
33
import logging
4-
from collections import OrderedDict
4+
from collections import OrderedDict, defaultdict
55
from subprocess import CalledProcessError
66
from urllib.parse import urlsplit
77

@@ -79,7 +79,7 @@ def _pending(self):
7979
'/runbot',
8080
'/runbot/<model("runbot.project"):project>',
8181
'/runbot/<model("runbot.project"):project>/search/<search>'], website=True, auth='public', type='http')
82-
def bundles(self, project=None, search='', refresh=False, for_next_freeze=False, limit=40, has_pr=None, **kwargs):
82+
def bundles(self, project=None, search='', refresh=False, limit=40, has_pr=None, **kwargs):
8383
search = search if len(search) < 60 else search[:60]
8484
env = request.env
8585
categories = env['runbot.category'].search([])
@@ -114,9 +114,6 @@ def bundles(self, project=None, search='', refresh=False, for_next_freeze=False,
114114
elif filter_mode == 'default' and not search:
115115
domain.append(('sticky', '=', True))
116116

117-
if for_next_freeze:
118-
domain.append(('for_next_freeze', '=', True))
119-
120117
if search:
121118
search_domains = []
122119
pr_numbers = []
@@ -811,3 +808,28 @@ def commit_links_diffs(self, commit_link_ids=None, versions_filter_ids=None, rep
811808
'commit_links': selected_commit_links,
812809
'diff_by_commit_link_ids': diff_by_commit_link_ids,
813810
})
811+
812+
@route([
813+
'/runbot/bundle/tag/<model("runbot.bundle.tag"):bundle_tag_id>',
814+
'/runbot/<model("runbot.project"):project>/bundle/tag/<model("runbot.bundle.tag"):bundle_tag_id>',
815+
], type="http", auth="user", website=True, sitemap=False)
816+
def bundles_by_tag(self, bundle_tag_id=None, project=None, **kwargs):
817+
projects = self.env['runbot.project'].search([('hidden', '=', False)])
818+
if not project and projects:
819+
project = projects[0]
820+
bundles_by_team = defaultdict(list)
821+
nb_bundles = 0
822+
nb_bundles_done = 0
823+
for bundle in self.env['runbot.bundle'].search([('tag_ids', 'in', bundle_tag_id.id)]):
824+
bundles_by_team[bundle.team_id.name or 'No Team Defined'].append(bundle)
825+
nb_bundles += 1
826+
if any(bundle.branch_ids.filtered(lambda rec: rec.is_pr)) and not any(bundle.branch_ids.filtered(lambda rec: rec.is_pr).mapped('alive')):
827+
nb_bundles_done += 1
828+
829+
qctx = {
830+
'tag': bundle_tag_id,
831+
'bundles_by_team': bundles_by_team,
832+
'nb_bundles': nb_bundles,
833+
'nb_bundles_done': nb_bundles_done,
834+
}
835+
return request.render('runbot.bundles_by_tag', qctx)

runbot/models/branch.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,6 @@ def _recompute_infos(self, payload=None):
283283

284284
if was_alive and not self.alive:
285285
self.close_date = self.env.cr.now()
286-
if self.bundle_id.for_next_freeze:
287-
if not any(branch.alive and branch.is_pr for branch in self.bundle_id.branch_ids):
288-
self.bundle_id.for_next_freeze = False
289286

290287
if (not self.draft and was_draft) or (self.alive and not was_alive) or (self.target_branch_name != init_target_branch_name and self.alive):
291288
self.bundle_id._force()

runbot/models/bundle.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import time
22
import logging
33
import datetime
4+
import re
45
import subprocess
56

67
from collections import defaultdict
78
from odoo import models, fields, api, tools
9+
from odoo.exceptions import ValidationError
810
from ..common import dt2time, s2human_long
911

1012

@@ -58,7 +60,9 @@ class Bundle(models.Model):
5860
frontend_url = fields.Char("Frontend URL", compute="_compute_frontend_url")
5961

6062
# extra_info
61-
for_next_freeze = fields.Boolean('Should be in next freeze')
63+
description = fields.Char('Description', compute='_compute_description', store=True, readonly=False)
64+
tag_ids = fields.Many2many('runbot.bundle.tag', string='Tags')
65+
team_id = fields.Many2one('runbot.team', compute='_compute_team_id', store=True, readonly=False)
6266

6367
def _compute_frontend_url(self):
6468
for bundle in self:
@@ -212,6 +216,23 @@ def _compute_all_trigger_custom_ids(self):
212216
parent_bundle = self.env['runbot.bundle'].search([('name', '=', targets.pop())])
213217
bundle.all_trigger_custom_ids = parent_bundle.all_trigger_custom_ids
214218

219+
@api.depends('name')
220+
def _compute_team_id(self):
221+
ngram_re = re.compile(r'.+\((?P<ngram>[a-z]{2,4})\)$')
222+
team_by_ngram_project = dict()
223+
for team in self.env['runbot.team'].search([('module_ownership_ids', '!=', False)]):
224+
for user in team.user_ids:
225+
if m := ngram_re.match(user.name.lower()):
226+
team_by_ngram_project[m.group('ngram'), team.project_id] = team
227+
for bundle in self.filtered_domain([('is_base', '=', False)]):
228+
bundle_ngram = bundle.name.split('-')[-1].lower()
229+
bundle.team_id = team_by_ngram_project.get((bundle_ngram, bundle.project_id))
230+
231+
@api.depends('branch_ids')
232+
def _compute_description(self):
233+
for bundle in self:
234+
bundle.description = ' / '.join(set(bundle.branch_ids.filtered(lambda rec: rec.is_pr and rec.pr_title).mapped('pr_title')))
235+
215236
def _url(self):
216237
self.ensure_one()
217238
return "/runbot/bundle/%s" % self.id
@@ -310,3 +331,12 @@ def action_generate_custom_trigger_restore_action(self):
310331
'default_number_build': 0,
311332
}
312333
return self._generate_custom_trigger_action(context)
334+
335+
336+
class BundleTag(models.Model):
337+
338+
_name = "runbot.bundle.tag"
339+
_description = "Bundle tag"
340+
341+
name = fields.Char(string='Bundle Tag')
342+
bundle_ids = fields.Many2many('runbot.bundle', string='Bundles')

runbot/models/team.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class RunbotTeam(models.Model):
2222
_inherit = 'mail.thread'
2323

2424
name = fields.Char('Team', required=True)
25+
active = fields.Boolean('Active', default=True)
2526
project_id = fields.Many2one('runbot.project', 'Project', help='Project to monitor', required=True,
2627
default=lambda self: self.env.ref('runbot.main_project'))
2728
organisation = fields.Char('organisation', related="project_id.organisation")

runbot/security/ir.model.access.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,5 @@ access_runbot_build_error_merge_filters_admin,access_runbot_build_error_merge_fi
167167
access_runbot_build_error_merge_user,access_runbot_build_error_merge,runbot.model_runbot_build_error_merge,runbot.group_user,1,0,0,0
168168
access_runbot_build_error_merge_filters_user,access_runbot_build_error_merge_filters,runbot.model_runbot_build_error_merge_filters,runbot.group_user,1,0,0,0
169169

170+
access_runbot_bundle_tag_admin,access_runbot_bundle_tag_admin,runbot.model_runbot_bundle_tag,runbot.group_runbot_admin,1,1,1,1
171+
access_runbot_bundle_tag_user,access_runbot_bundle_tag_user,runbot.model_runbot_bundle_tag,group_user,1,0,0,0
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<data>
4+
<template id="runbot.bundle_tag_slot">
5+
<t t-set="link_class" t-value="'link-primary'"/>
6+
<t t-set="link_title" t-value="bundle.name"/>
7+
<t t-if="not any(bundle.branch_ids.filtered(lambda rec: rec.is_pr))">
8+
<t t-set="link_class" t-value="'link-danger'"/>
9+
<t t-set="link_title" t-value="'No PR yet'"/>
10+
</t>
11+
<t t-elif="not any(bundle.branch_ids.filtered(lambda rec: rec.is_pr).mapped('alive'))">
12+
<t t-set="link_class" t-value="'link-success text-decoration-line-through'"/>
13+
<t t-set="link_title" t-value="'All PR are closed'"/>
14+
<t t-set="done" t-value="True"/>
15+
</t>
16+
<t t-call="runbot.branch_github_menu">
17+
<t t-set="btn_classes" t-value="'btn btn-ssm'"/>
18+
</t>
19+
<a t-out="bundle.description or bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}" t-att-class="link_class" t-att-title="link_title" target="_blank"/>
20+
<t t-set="other_tags" t-value="bundle.tag_ids.filtered(lambda rec: rec != tag)"/>
21+
<t t-foreach="other_tags" t-as="other_tag">
22+
<span class="badge rounded-pill text-bg-success"><t t-out="other_tag.name"/></span>
23+
</t>
24+
<t t-if="not done">
25+
<span t-attf-class="badge bg-info" data-toggle="tooltip" title="Last batch age">
26+
<t t-out="bundle.last_batch._get_formated_age()"/>
27+
</span>
28+
</t>
29+
</template>
30+
<template id="runbot.bundles_by_tag">
31+
<t t-call='runbot.layout'>
32+
<div class="container-fluid">
33+
<h1 class="text-capitalize">
34+
<t t-out="tag.name"/>
35+
<button type="button" class="btn btn-primary btn-ssm disabled"><t t-out="nb_bundles_done"/> / <t t-out="nb_bundles"/></button>
36+
</h1>
37+
<t t-foreach="sorted(bundles_by_team.keys())" t-as="team">
38+
<t t-set="team_bundles" t-value="bundles_by_team.get(team)"/>
39+
<details t-att-open="bool(team_bundles) and ''"><summary t-out="team"/>
40+
<ul>
41+
<t t-foreach="team_bundles" t-as="bundle">
42+
<li><t t-call="runbot.bundle_tag_slot"/></li>
43+
</t>
44+
</ul>
45+
</details>
46+
</t>
47+
</div>
48+
</t>
49+
</template>
50+
</data>
51+
</odoo>

runbot/templates/utils.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@
413413
</template>
414414

415415
<template id="runbot.branch_github_menu">
416-
<button t-attf-class="btn btn-default btn-ssm dropdown-toggle" data-bs-toggle="dropdown" title="Github links" aria-label="Github links" aria-expanded="false">
416+
<button t-attf-class="{{btn_classes or 'btn btn-default btn-ssm'}} dropdown-toggle" data-bs-toggle="dropdown" title="Github links" aria-label="Github links" aria-expanded="false">
417417
<i t-attf-class="fa fa-github {{'text-primary' if any(branch_id.is_pr and branch_id.alive for branch_id in bundle.branch_ids) else 'text-secondary' if all(not branch_id.alive for branch_id in bundle.branch_ids) else ''}}"/>
418418
<span class="caret"/>
419419
</button>

runbot/tests/test_branch.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
# -*- coding: utf-8 -*-
1+
from unittest.mock import patch, mock_open
2+
3+
from odoo.tests.common import new_test_user
24
from odoo.tools import mute_logger
5+
36
from .common import RunbotCase, RunbotCaseMinimalSetup
47

58

@@ -300,3 +303,43 @@ def test_is_base_regex_on_dev_remote(self):
300303
'head': mistaken_commit.id
301304
})
302305
self.assertEqual(branch_mistake_dev.bundle_id, dummy_bundle, "A branch matching the is_base_regex should on a secondary repo should goes in dummy bundle")
306+
307+
308+
class TestBundleTeam(RunbotCase):
309+
310+
def test_bundle_team_attribution(self):
311+
self.stop_patcher('isfile')
312+
self.stop_patcher('isdir') # needed to create the user avatar
313+
create_context = {'no_reset_password': True, 'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True}
314+
test_user = new_test_user(self.env, login='testrunbot', name='testrunbot (tru)', context=create_context)
315+
316+
team = self.env['runbot.team'].create({
317+
'name': 'Test Team',
318+
'project_id': self.project.id,
319+
})
320+
321+
team.user_ids += test_user
322+
323+
branch = self.Branch.create({
324+
'remote_id': self.remote_server_dev.id,
325+
'name': 'saas-19.1-test-tru',
326+
'is_pr': False,
327+
})
328+
329+
module = self.env['runbot.module'].create({'name': 'test_module'})
330+
self.env['runbot.module.ownership'].create({
331+
'module_id': module.id,
332+
'team_id': team.id,
333+
})
334+
335+
bundle = self.env['runbot.bundle'].search([('name', '=', branch.name)])
336+
self.assertEqual(bundle.team_id, team)
337+
338+
# now test that a team can be manually set on a bundle
339+
other_team = self.env['runbot.team'].create({
340+
'name': 'Another Test Team',
341+
'project_id': self.project.id,
342+
})
343+
344+
bundle.team_id = other_team
345+
self.assertEqual(bundle.team_id, other_team)

runbot/views/bundle_views.xml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,15 @@
4545
<button name="action_generate_custom_trigger_restore_action" string="New custom restore" type="object" class="oe_highlight"/>
4646
</header>
4747
<sheet>
48+
<group>
49+
<field name="name" widget="char_frontend_url"/>
50+
<field name="description"/>
51+
<field name="tag_ids" widget="many2many_tags" options="{'not_delete': True, 'no_create': True}"/>
52+
</group>
4853
<group>
4954
<group string="Base options">
50-
<field name="name" widget="char_frontend_url"/>
5155
<field name="project_id"/>
56+
<field name="team_id"/>
5257
<field name="sticky" readonly="0"/>
5358
<field name="to_upgrade" readonly="0"/>
5459
<field name="to_upgrade_from" readonly="0"/>
@@ -57,7 +62,6 @@
5762
<field name="base_id"/>
5863
<field name="defined_base_id" readonly="is_base"/>
5964
<field name="version_id"/>
60-
<field name="for_next_freeze"/>
6165
<field name="has_pr"/>
6266
</group>
6367
<group string="Testing options">
@@ -155,6 +159,7 @@
155159
<field name="no_build"/>
156160
<field name="branch_ids"/>
157161
<field name="version_id"/>
162+
<field name="team_id" optional="hide"/>
158163
</list>
159164
</field>
160165
</record>
@@ -167,7 +172,6 @@
167172
<field name="name"/>
168173
<field name="version_id"/>
169174
<separator/>
170-
<filter string="For next freeze" name="for_next_freeze" domain="[('for_next_freeze', '=', True)]"/>
171175
<filter string="Has open pr" name="has_pr" domain="[('has_pr', '=', True)]"/>
172176
<filter string="No pr" name="no_pr" domain="[('branch_ids', '!=', []), '!', ('branch_ids', 'any', [('is_pr', '=', True)])]"/>
173177
<filter string="Is base" name="is_base" domain="[('is_base', '=', True)]"/>
@@ -227,6 +231,30 @@
227231
</list>
228232
</field>
229233
</record>
234+
235+
<record id="view_runbot_bundle_tag_tree" model="ir.ui.view">
236+
<field name="model">runbot.bundle.tag</field>
237+
<field name="arch" type="xml">
238+
<list string="Bundle Tag">
239+
<field name="name"/>
240+
</list>
241+
</field>
242+
</record>
243+
244+
<record id="view_runbot_bundle_tag" model="ir.ui.view">
245+
<field name="model">runbot.bundle.tag</field>
246+
<field name="arch" type="xml">
247+
<form string="Bundle Tags">
248+
<group>
249+
<field name="name"/>
250+
</group>
251+
<list create="0" delete="0" edit="0">
252+
<field name="bundle_ids"/>
253+
</list>
254+
</form>
255+
</field>
256+
</record>
257+
230258
<record id="action_bundle_custom_trigger" model="ir.actions.act_window">
231259
<field name="name">Custom triggers</field>
232260
<field name="type">ir.actions.act_window</field>
@@ -257,5 +285,11 @@
257285
<field name="res_model">runbot.batch</field>
258286
<field name="view_mode">list,form</field>
259287
</record>
288+
<record id="action_bundle_tag" model="ir.actions.act_window">
289+
<field name="name">Bundle Tags</field>
290+
<field name="type">ir.actions.act_window</field>
291+
<field name="res_model">runbot.bundle.tag</field>
292+
<field name="view_mode">list,form</field>
293+
</record>
260294
</data>
261295
</odoo>

0 commit comments

Comments
 (0)