diff --git a/runbot/__manifest__.py b/runbot/__manifest__.py index 1583afd0..f29a7c28 100644 --- a/runbot/__manifest__.py +++ b/runbot/__manifest__.py @@ -6,7 +6,7 @@ 'author': "Odoo SA", 'website': "http://runbot.odoo.com", 'category': 'Website', - 'version': '4.6', + 'version': '4.8', 'depends': ['website', 'base'], 'data': [ 'security/runbot_security.xml', @@ -33,5 +33,6 @@ 'data/build_parse.xml', 'data/runbot_error_regex_data.xml', 'data/error_link.xml', + 'data/website_data.xml', ], } diff --git a/runbot/common.py b/runbot/common.py index 6c5e6812..4ad24b10 100644 --- a/runbot/common.py +++ b/runbot/common.py @@ -54,7 +54,7 @@ def time2str(t): def dt2time(datetime): """Convert datetime to time""" - return time.mktime(time.strptime(datetime, DEFAULT_SERVER_DATETIME_FORMAT)) + return time.mktime(datetime.timetuple()) def now(): diff --git a/runbot/controllers/badge.py b/runbot/controllers/badge.py index 5903ac34..e6890e78 100644 --- a/runbot/controllers/badge.py +++ b/runbot/controllers/badge.py @@ -35,7 +35,7 @@ class RunbotBadge(Controller): build = builds[0] etag = request.httprequest.headers.get('If-None-Match') - retag = hashlib.md5(build[last_update].encode()).hexdigest() + retag = hashlib.md5(str(build[last_update]).encode()).hexdigest() if etag == retag: return werkzeug.wrappers.Response(status=304) diff --git a/runbot/controllers/frontend.py b/runbot/controllers/frontend.py index 4ad29ddf..12b6442c 100644 --- a/runbot/controllers/frontend.py +++ b/runbot/controllers/frontend.py @@ -39,6 +39,7 @@ class Runbot(Controller): 'host_stats': [], 'pending_total': pending[0], 'pending_level': pending[1], + 'hosts_data': request.env['runbot.host'].search([]), 'search': search, 'refresh': refresh, } @@ -101,15 +102,11 @@ class Runbot(Controller): def branch_info(branch): return { 'branch': branch, - 'fqdn': fqdn(), 'builds': [build_dict[build_id] for build_id in build_by_branch_ids.get(branch.id) or []] } context.update({ 'branches': [branch_info(b) for b in branches], - 'testing': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'testing')]), - 'running': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'running')]), - 'pending': build_obj.search_count([('repo_id', '=', repo.id), ('local_state', '=', 'pending')]), 'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, refresh=refresh), 'fqdn': fqdn(), }) @@ -117,14 +114,6 @@ class Runbot(Controller): # consider host gone if no build in last 100 build_threshold = max(build_ids or [0]) - 100 - for result in build_obj.read_group([('id', '>', build_threshold)], ['host'], ['host']): - if result['host']: - context['host_stats'].append({ - 'host': result['host'], - 'testing': build_obj.search_count([('local_state', '=', 'testing'), ('host', '=', result['host'])]), - 'running': build_obj.search_count([('local_state', '=', 'running'), ('host', '=', result['host'])]), - }) - context.update({'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message')}) return request.render('runbot.repo', context) @@ -302,21 +291,27 @@ class Runbot(Controller): return request.render("runbot.glances", qctx) @route('/runbot/monitoring', type='http', auth='user', website=True) - def monitoring(self, refresh=None): + @route('/runbot/monitoring/<int:config_id>', type='http', auth='user', website=True) + @route('/runbot/monitoring/<int:config_id>/<int:view_id>', type='http', auth='user', website=True) + def monitoring(self, config_id=None, view_id=None, refresh=None): glances_ctx = self._glances_ctx() pending = self._pending() hosts_data = request.env['runbot.host'].search([]) - monitored_config_id = int(request.env['ir.config_parameter'].sudo().get_param('runbot.monitored_config_id', 1)) - request.env.cr.execute("""SELECT DISTINCT ON (branch_id) branch_id, id FROM runbot_build - WHERE config_id = %s - AND global_state in ('running', 'done') - AND branch_id in (SELECT id FROM runbot_branch where sticky='t') - AND local_state != 'duplicate' - ORDER BY branch_id ASC, id DESC""", [int(monitored_config_id)]) - last_monitored = request.env['runbot.build'].browse([r[1] for r in request.env.cr.fetchall()]) + last_monitored = None + if config_id or config_id is None: + monitored_config_id = config_id or int(request.env['ir.config_parameter'].sudo().get_param('runbot.monitored_config_id', 1)) + request.env.cr.execute("""SELECT DISTINCT ON (branch_id) branch_id, id FROM runbot_build + WHERE config_id = %s + AND global_state in ('running', 'done') + AND branch_id in (SELECT id FROM runbot_branch where sticky='t') + AND local_state != 'duplicate' + ORDER BY branch_id ASC, id DESC""", [int(monitored_config_id)]) + last_monitored = request.env['runbot.build'].browse([r[1] for r in request.env.cr.fetchall()]) + config = request.env['runbot.build.config'].browse(monitored_config_id) qctx = { + 'config': config, 'refresh': refresh, 'pending_total': pending[0], 'pending_level': pending[1], @@ -326,7 +321,7 @@ class Runbot(Controller): 'auto_tags': request.env['runbot.build.error'].disabling_tags(), 'build_errors': request.env['runbot.build.error'].search([('random', '=', True)]) } - return request.render("runbot.monitoring", qctx) + return request.render(request.env['ir.ui.view'].browse('view_id') if view_id else config.monitoring_view_id.id or "runbot.monitoring", qctx) @route(['/runbot/branch/<int:branch_id>', '/runbot/branch/<int:branch_id>/page/<int:page>'], website=True, auth='public', type='http') def branch_builds(self, branch_id=None, search='', page=1, limit=50, refresh='', **kwargs): @@ -337,9 +332,9 @@ class Runbot(Controller): url='/runbot/branch/%s' % branch_id, total=builds_count, page=page, - step=50 + step=50, ) builds = request.env['runbot.build'].search(domain, limit=limit, offset=pager.get('offset',0)) - context = {'pager': pager, 'builds': builds} + context = {'pager': pager, 'builds': builds, 'repo': request.env['runbot.branch'].browse(branch_id).repo_id} return request.render("runbot.branch", context) diff --git a/runbot/controllers/hook.py b/runbot/controllers/hook.py index b6eedd27..aeb70bb4 100644 --- a/runbot/controllers/hook.py +++ b/runbot/controllers/hook.py @@ -31,7 +31,7 @@ class RunbotHook(http.Controller): # force update of dependencies to in case a hook is lost if not payload or event == 'push' or (event == 'pull_request' and payload.get('action') in ('synchronize', 'opened', 'reopened')): - (repo | repo.dependency_ids).write({'hook_time': time.time()}) + (repo | repo.dependency_ids).set_hook_time(time.time()) else: _logger.debug('Ignoring unsupported hook %s %s', event, payload.get('action', '')) return "" diff --git a/runbot/data/website_data.xml b/runbot/data/website_data.xml new file mode 100644 index 00000000..104c9acf --- /dev/null +++ b/runbot/data/website_data.xml @@ -0,0 +1,5 @@ +<odoo> + <record id="website.homepage_page" model="website.page"> + <field name="url">/home</field> + </record> +</odoo> diff --git a/runbot/migrations/1.3/post-logging-build_id.py b/runbot/migrations/11.0.1.3/post-logging-build_id.py similarity index 100% rename from runbot/migrations/1.3/post-logging-build_id.py rename to runbot/migrations/11.0.1.3/post-logging-build_id.py diff --git a/runbot/migrations/4.0/post-migration.py b/runbot/migrations/11.0.4.0/post-migration.py similarity index 100% rename from runbot/migrations/4.0/post-migration.py rename to runbot/migrations/11.0.4.0/post-migration.py diff --git a/runbot/migrations/4.0/pre-migration.py b/runbot/migrations/11.0.4.0/pre-migration.py similarity index 100% rename from runbot/migrations/4.0/pre-migration.py rename to runbot/migrations/11.0.4.0/pre-migration.py diff --git a/runbot/migrations/4.4/post-migration.py b/runbot/migrations/11.0.4.4/post-migration.py similarity index 100% rename from runbot/migrations/4.4/post-migration.py rename to runbot/migrations/11.0.4.4/post-migration.py diff --git a/runbot/migrations/4.5/post-migration.py b/runbot/migrations/11.0.4.5/post-migration.py similarity index 100% rename from runbot/migrations/4.5/post-migration.py rename to runbot/migrations/11.0.4.5/post-migration.py diff --git a/runbot/models/branch.py b/runbot/models/branch.py index 16aa14a8..b7129a99 100644 --- a/runbot/models/branch.py +++ b/runbot/models/branch.py @@ -12,6 +12,7 @@ _re_patch = re.compile(r'.*patch-\d+$') class runbot_branch(models.Model): _name = "runbot.branch" + _description = "Branch" _order = 'name' _sql_constraints = [('branch_repo_uniq', 'unique (name,repo_id)', 'The branch must be unique per repository !')] @@ -35,7 +36,7 @@ class runbot_branch(models.Model): no_auto_build = fields.Boolean("Don't automatically build commit on this branch", default=False) rebuild_requested = fields.Boolean("Request a rebuild", help="Rebuild the latest commit even when no_auto_build is set.", default=False) - branch_config_id = fields.Many2one('runbot.build.config', 'Run Config') + branch_config_id = fields.Many2one('runbot.build.config', 'Branch Config') config_id = fields.Many2one('runbot.build.config', 'Run Config', compute='_compute_config_id', inverse='_inverse_config_id') @api.depends('sticky', 'defined_sticky', 'target_branch_name', 'name') @@ -47,16 +48,24 @@ class runbot_branch(models.Model): elif branch.defined_sticky: branch.closest_sticky = branch.defined_sticky # be carefull with loop elif branch.target_branch_name: - corresping_branch = self.search([('branch_name', '=', branch.target_branch_name), ('repo_id', '=', branch.repo_id.id)]) - branch.closest_sticky = corresping_branch.closest_sticky + corresponding_branch = self.search([('branch_name', '=', branch.target_branch_name), ('repo_id', '=', branch.repo_id.id)]) + branch.closest_sticky = corresponding_branch.closest_sticky else: repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids self.env.cr.execute("select id from runbot_branch where sticky = 't' and repo_id = any(%s) and %s like name||'%%'", (repo_ids, branch.name or '')) branch.closest_sticky = self.browse(self.env.cr.fetchone()) - @api.depends('closest_sticky.previous_version') + @api.depends('closest_sticky') #, 'closest_sticky.previous_version') def _compute_previous_version(self): - for branch in self: + for branch in self.sorted(key='sticky', reverse=True): + # orm does not support non_searchable.non_stored dependency. + # thus, the closest_sticky.previous_version dependency will log an error + # when previous_version is written. + # this dependency is usefull to make the compute recursive, avoiding to have + # both record and record.closest_sticky in self, in that order, making the record.previous_version + # empty in all cases. + # Sorting self on sticky will mitigate the problem. but it is still posible to + # have computation errors if defined_sticky is not sticky. (which is not a normal use case) if branch.closest_sticky == branch: repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids domain = [('branch_name', 'like', '%.0'), ('sticky', '=', True), ('branch_name', '!=', 'master'), ('repo_id', 'in', repo_ids)] @@ -66,11 +75,12 @@ class runbot_branch(models.Model): else: branch.previous_version = branch.closest_sticky.previous_version - @api.depends('previous_version', 'closest_sticky.intermediate_stickies') + @api.depends('previous_version', 'closest_sticky') def _compute_intermediate_stickies(self): - for branch in self: + for branch in self.sorted(key='sticky', reverse=True): if branch.closest_sticky == branch: if not branch.previous_version: + branch.intermediate_stickies = [(5, 0, 0)] continue repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids domain = [('id', '>', branch.previous_version.id), ('sticky', '=', True), ('branch_name', '!=', 'master'), ('repo_id', 'in', repo_ids)] @@ -133,7 +143,7 @@ class runbot_branch(models.Model): return False return True - @api.model + @api.model_create_single def create(self, vals): if not vals.get('config_id') and ('use-coverage' in (vals.get('name') or '')): coverage_config = self.env.ref('runbot.runbot_build_config_test_coverage', raise_if_not_found=False) @@ -287,6 +297,6 @@ class runbot_branch(models.Model): for branch in self: if not branch.rebuild_requested: branch.rebuild_requested = True - branch.repo_id.write({'hook_time': time.time()}) + branch.repo_id.set_hook_time(time.time()) else: branch.rebuild_requested = False diff --git a/runbot/models/build.py b/runbot/models/build.py index 3c08a4b4..5d8fdeb3 100644 --- a/runbot/models/build.py +++ b/runbot/models/build.py @@ -32,6 +32,8 @@ def make_selection(array): class runbot_build(models.Model): _name = "runbot.build" + _description = "Build" + _order = 'id desc' _rec_name = 'id' @@ -55,16 +57,16 @@ class runbot_build(models.Model): # state machine global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True) - local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, oldname='state', index=True) + local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True) global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True) - local_result = fields.Selection(make_selection(result_order), string='Build Result', oldname='result') + local_result = fields.Selection(make_selection(result_order), string='Build Result') triggered_result = fields.Selection(make_selection(result_order), string='Triggered Result') # triggered by db only requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True) nb_pending = fields.Integer("Number of pending in queue", default=0) nb_testing = fields.Integer("Number of test slot use", default=0) - nb_running = fields.Integer("Number of test slot use", default=0) + nb_running = fields.Integer("Number of run slot use", default=0) # should we add a stored field for children results? active_step = fields.Many2one('runbot.build.config.step', 'Active step') @@ -74,7 +76,7 @@ class runbot_build(models.Model): build_start = fields.Datetime('Build start') build_end = fields.Datetime('Build end') job_time = fields.Integer(compute='_compute_job_time', string='Job time') - build_time = fields.Integer(compute='_compute_build_time', string='Job time') + build_time = fields.Integer(compute='_compute_build_time', string='Build time') build_age = fields.Integer(compute='_compute_build_age', string='Build age') duplicate_id = fields.Many2one('runbot.build', 'Corresponding Build', index=True) revdep_build_ids = fields.Many2many('runbot.build', 'runbot_rev_dep_builds', @@ -116,18 +118,21 @@ class runbot_build(models.Model): for build in self: build.log_list = ','.join({step.name for step in build.config_id.step_ids() if step._has_log()}) - @api.depends('nb_testing', 'nb_pending', 'local_state', 'duplicate_id.global_state') + @api.depends('children_ids.global_state', 'local_state', 'duplicate_id.global_state') def _compute_global_state(self): - # could we use nb_pending / nb_testing ? not in a compute, but in a update state method for record in self: if record.duplicate_id: record.global_state = record.duplicate_id.global_state else: waiting_score = record._get_state_score('waiting') - if record._get_state_score(record.local_state) < waiting_score or record.nb_pending + record.nb_testing == 0: - record.global_state = record.local_state + if record._get_state_score(record.local_state) > waiting_score and record.children_ids: # if finish, check children + children_state = record._get_youngest_state([child.global_state for child in record.children_ids]) + if record._get_state_score(children_state) > waiting_score: + record.global_state = record.local_state + else: + record.global_state = 'waiting' else: - record.global_state = 'waiting' + record.global_state = record.local_state def _get_youngest_state(self, states): index = min([self._get_state_score(state) for state in states]) @@ -166,24 +171,7 @@ class runbot_build(models.Model): def _get_result_score(self, result): return result_order.index(result) - def _update_nb_children(self, new_state, old_state=None): - # could be interresting to update state in batches. - tracked_count_list = ['pending', 'testing', 'running'] - if (new_state not in tracked_count_list and old_state not in tracked_count_list) or new_state == old_state: - return - - for record in self: - values = {} - if old_state in tracked_count_list: - values['nb_%s' % old_state] = record['nb_%s' % old_state] - 1 - if new_state in tracked_count_list: - values['nb_%s' % new_state] = record['nb_%s' % new_state] + 1 - - record.write(values) - if record.parent_id: - record.parent_id._update_nb_children(new_state, old_state) - - @api.depends('real_build.active_step') + @api.depends('active_step', 'duplicate_id.active_step') def _compute_job(self): for build in self: build.job = build.real_build.active_step.name @@ -196,13 +184,13 @@ class runbot_build(models.Model): def copy(self, values=None): raise UserError("Cannot duplicate build!") + @api.model_create_single def create(self, vals): branch = self.env['runbot.branch'].search([('id', '=', vals.get('branch_id', False))]) # branche 10174? if branch.no_build: return self.env['runbot.build'] vals['config_id'] = vals['config_id'] if 'config_id' in vals else branch.config_id.id build_id = super(runbot_build, self).create(vals) - build_id._update_nb_children(build_id.local_state) extra_info = {'sequence': build_id.id if not build_id.sequence else build_id.sequence} context = self.env.context @@ -309,8 +297,6 @@ class runbot_build(models.Model): build_by_old_values = defaultdict(lambda: self.env['runbot.build']) for record in self: build_by_old_values[record.local_state] += record - for local_state, builds in build_by_old_values.items(): - builds._update_nb_children(values.get('local_state'), local_state) assert 'state' not in values local_result = values.get('local_result') for build in self: @@ -318,6 +304,8 @@ class runbot_build(models.Model): res = super(runbot_build, self).write(values) for build in self: assert bool(not build.duplicate_id) ^ (build.local_state == 'duplicate') # don't change duplicate state without removing duplicate id. + if 'log_counter' in values: # not 100% usefull but more correct ( see test_ir_logging) + self.flush() return res def update_build_end(self): @@ -360,6 +348,8 @@ class runbot_build(models.Model): build.job_time = int(dt2time(build.job_end) - dt2time(build.job_start)) elif build.job_start: build.job_time = int(time.time() - dt2time(build.job_start)) + else: + build.job_time = 0 @api.depends('build_start', 'build_end', 'duplicate_id.build_time') def _compute_build_time(self): @@ -370,6 +360,8 @@ class runbot_build(models.Model): build.build_time = int(dt2time(build.build_end) - dt2time(build.build_start)) elif build.build_start: build.build_time = int(time.time() - dt2time(build.build_start)) + else: + build.build_time = 0 @api.depends('job_start', 'duplicate_id.build_age') def _compute_build_age(self): @@ -379,6 +371,8 @@ class runbot_build(models.Model): build.build_age = build.duplicate_id.build_age elif build.job_start: build.build_age = int(time.time() - dt2time(build.build_start)) + else: + build.build_age = 0 def _get_params(self): try: diff --git a/runbot/models/build_config.py b/runbot/models/build_config.py index 29e2fbde..8518844c 100644 --- a/runbot/models/build_config.py +++ b/runbot/models/build_config.py @@ -21,6 +21,7 @@ PYTHON_DEFAULT = "# type python code here\n\n\n\n\n\n" class Config(models.Model): _name = "runbot.build.config" + _description = "Build config" _inherit = "mail.thread" name = fields.Char('Config name', required=True, unique=True, track_visibility='onchange', help="Unique name for config please use trigram as postfix for custom configs") @@ -29,9 +30,10 @@ class Config(models.Model): update_github_state = fields.Boolean('Notify build state to github', default=False, track_visibility='onchange') protected = fields.Boolean('Protected', default=False, track_visibility='onchange') group = fields.Many2one('runbot.build.config', 'Configuration group', help="Group of config's and config steps") - group_name = fields.Char(related='group.name') + group_name = fields.Char('Group name', related='group.name') + monitoring_view_id = fields.Many2one('ir.ui.view', 'Monitoring view') - @api.model + @api.model_create_single def create(self, values): res = super(Config, self).create(values) res._check_step_ids_order() @@ -84,6 +86,7 @@ class Config(models.Model): class ConfigStep(models.Model): _name = 'runbot.build.config.step' + _description = "Config step" _inherit = 'mail.thread' # general info @@ -161,7 +164,7 @@ class ConfigStep(models.Model): copy._write({'protected': False}) return copy - @api.model + @api.model_create_single def create(self, values): self._check(values) return super(ConfigStep, self).create(values) @@ -560,6 +563,7 @@ class ConfigStep(models.Model): class ConfigStepOrder(models.Model): _name = 'runbot.build.config.step.order' + _description = "Config step order" _order = 'sequence, id' # a kind of many2many rel with sequence @@ -571,7 +575,7 @@ class ConfigStepOrder(models.Model): def _onchange_step_id(self): self.sequence = self.step_id.default_sequence - @api.model + @api.model_create_single def create(self, values): if 'sequence' not in values and values.get('step_id'): values['sequence'] = self.env['runbot.build.config.step'].browse(values.get('step_id')).default_sequence diff --git a/runbot/models/build_dependency.py b/runbot/models/build_dependency.py index dc246341..0c8090dd 100644 --- a/runbot/models/build_dependency.py +++ b/runbot/models/build_dependency.py @@ -3,6 +3,7 @@ from odoo import models, fields class RunbotBuildDependency(models.Model): _name = "runbot.build.dependency" + _description = "Build dependency" build_id = fields.Many2one('runbot.build', 'Build', required=True, ondelete='cascade', index=True) dependecy_repo_id = fields.Many2one('runbot.repo', 'Dependency repo', required=True, ondelete='cascade') diff --git a/runbot/models/build_error.py b/runbot/models/build_error.py index 723d2052..57b8b28a 100644 --- a/runbot/models/build_error.py +++ b/runbot/models/build_error.py @@ -13,6 +13,8 @@ _logger = logging.getLogger(__name__) class RunbotBuildError(models.Model): _name = "runbot.build.error" + _description = "Build error" + _inherit = "mail.thread" _rec_name = "id" @@ -34,7 +36,7 @@ class RunbotBuildError(models.Model): parent_id = fields.Many2one('runbot.build.error', 'Linked to') child_ids = fields.One2many('runbot.build.error', 'parent_id', string='Child Errors', context={'active_test': False}) children_build_ids = fields.Many2many('runbot.build', compute='_compute_children_build_ids', string='Children builds') - error_history_ids = fields.One2many('runbot.build.error', compute='_compute_error_history_ids', string='Old errors') + error_history_ids = fields.Many2many('runbot.build.error', compute='_compute_error_history_ids', string='Old errors', context={'active_test': False}) first_seen_build_id = fields.Many2one('runbot.build', compute='_compute_first_seen_build_id', string='First Seen build') first_seen_date = fields.Datetime(string='First Seen Date', related='first_seen_build_id.create_date') last_seen_build_id = fields.Many2one('runbot.build', compute='_compute_last_seen_build_id', string='Last Seen build') @@ -47,7 +49,7 @@ class RunbotBuildError(models.Model): if build_error.test_tags and '-' in build_error.test_tags: raise ValidationError('Build error test_tags should not be negated') - @api.model + @api.model_create_single def create(self, vals): cleaners = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')]) content = vals.get('content') @@ -57,7 +59,6 @@ class RunbotBuildError(models.Model): }) return super().create(vals) - @api.multi def write(self, vals): if 'active' in vals: for build_error in self: @@ -100,7 +101,7 @@ class RunbotBuildError(models.Model): for build_error in self: build_error.first_seen_build_id = build_error.children_build_ids and build_error.children_build_ids[-1] or False - @api.depends('fingerprint') + @api.depends('fingerprint', 'child_ids.fingerprint') def _compute_error_history_ids(self): for error in self: fingerprints = [error.fingerprint] + [rec.fingerprint for rec in error.child_ids] @@ -160,9 +161,9 @@ class RunbotBuildError(models.Model): @api.model def test_tags_list(self): - active_errors = self.search([('test_tags', '!=', 'False'), ('random', '=', True)]) + active_errors = self.search([('test_tags', '!=', False), ('random', '=', True)]) test_tag_list = active_errors.mapped('test_tags') - return [test_tag for error_tags in test_tag_list for test_tag in error_tags.split(',')] + return [test_tag for error_tags in test_tag_list for test_tag in (error_tags).split(',')] @api.model def disabling_tags(self): @@ -172,6 +173,7 @@ class RunbotBuildError(models.Model): class RunbotBuildErrorTag(models.Model): _name = "runbot.build.error.tag" + _description = "Build error tag" name = fields.Char('Tag') error_ids = fields.Many2many('runbot.build.error', string='Errors') @@ -180,6 +182,7 @@ class RunbotBuildErrorTag(models.Model): class RunbotErrorRegex(models.Model): _name = "runbot.error.regex" + _description = "Build error regex" _inherit = "mail.thread" _rec_name = 'id' _order = 'sequence, id' diff --git a/runbot/models/event.py b/runbot/models/event.py index 5a888a7e..e4f7fff9 100644 --- a/runbot/models/event.py +++ b/runbot/models/event.py @@ -16,9 +16,8 @@ class runbot_event(models.Model): build_id = fields.Many2one('runbot.build', 'Build', index=True, ondelete='cascade') active_step_id = fields.Many2one('runbot.build.config.step', 'Active step', index=True) - type = fields.Selection(TYPES, string='Type', required=True, index=True) + type = fields.Selection(selection_add=TYPES, string='Type', required=True, index=True) - @api.model_cr def init(self): parent_class = super(runbot_event, self) if hasattr(parent_class, 'init'): @@ -75,6 +74,7 @@ FOR EACH ROW EXECUTE PROCEDURE runbot_set_logging_build(); class RunbotErrorLog(models.Model): _name = "runbot.error.log" + _description = "Error log" _auto = False _order = 'id desc' @@ -130,7 +130,6 @@ class RunbotErrorLog(models.Model): BuildError._parse_logs(self) - @api.model_cr def init(self): """ Create an SQL view for ir.logging """ tools.drop_view_if_exists(self._cr, 'runbot_error_log') diff --git a/runbot/models/host.py b/runbot/models/host.py index b5bdb031..6ac52778 100644 --- a/runbot/models/host.py +++ b/runbot/models/host.py @@ -6,6 +6,7 @@ _logger = logging.getLogger(__name__) class RunboHost(models.Model): _name = "runbot.host" + _description = "Host" _order = 'id' _inherit = 'mail.thread' @@ -37,7 +38,7 @@ class RunboHost(models.Model): host.nb_testing = count_by_host_state[host.name].get('testing', 0) host.nb_running = count_by_host_state[host.name].get('running', 0) - @api.model + @api.model_create_single def create(self, values): if not 'disp_name' in values: values['disp_name'] = values['name'] @@ -57,10 +58,15 @@ class RunboHost(models.Model): return int(icp.get_param('runbot.runbot_running_max', default=75)) def set_psql_conn_count(self): - _logger.debug('Updating psql connection count...') self.ensure_one() with local_pgadmin_cursor() as local_cr: local_cr.execute("SELECT sum(numbackends) FROM pg_stat_database;") res = local_cr.fetchone() self.psql_conn_count = res and res[0] or 0 + + def _total_testing(self): + return sum(host.nb_testing for host in self) + + def _total_workers(self): + return sum(host.get_nb_worker() for host in self) diff --git a/runbot/models/ir_cron.py b/runbot/models/ir_cron.py index a1fbd529..527afa26 100644 --- a/runbot/models/ir_cron.py +++ b/runbot/models/ir_cron.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta from odoo import models, fields odoo.service.server.SLEEP_INTERVAL = 5 -odoo.addons.base.ir.ir_cron._intervalTypes['seconds'] = lambda interval: relativedelta(seconds=interval) +odoo.addons.base.models.ir_cron._intervalTypes['seconds'] = lambda interval: relativedelta(seconds=interval) class ir_cron(models.Model): _inherit = "ir.cron" diff --git a/runbot/models/repo.py b/runbot/models/repo.py index d74c92a9..36af0b3f 100644 --- a/runbot/models/repo.py +++ b/runbot/models/repo.py @@ -30,10 +30,11 @@ class RunbotException(Exception): class runbot_repo(models.Model): _name = "runbot.repo" + _description = "Repo" _order = 'sequence, id' name = fields.Char('Repository', required=True) - short_name = fields.Char('Repository', compute='_compute_short_name', store=False, readonly=True) + short_name = fields.Char('Short name', compute='_compute_short_name', store=False, readonly=True) sequence = fields.Integer('Sequence') path = fields.Char(compute='_get_path', string='Directory', readonly=True) base = fields.Char(compute='_get_base_url', string='Base URL', readonly=True) # Could be renamed to a more explicit name like base_url @@ -60,7 +61,7 @@ class runbot_repo(models.Model): token = fields.Char("Github token", groups="runbot.group_runbot_admin") group_ids = fields.Many2many('res.groups', string='Limited to groups') - repo_config_id = fields.Many2one('runbot.build.config', 'Run Config') + repo_config_id = fields.Many2one('runbot.build.config', 'Repo Config') config_id = fields.Many2one('runbot.build.config', 'Run Config', compute='_compute_config_id', inverse='_inverse_config_id') server_files = fields.Char('Server files', help='Comma separated list of possible server files') # odoo-bin,openerp-server,openerp-server.py @@ -104,20 +105,15 @@ class runbot_repo(models.Model): for repo in self: repo.hook_time = times.get(repo.id, 0) - def write(self, values): - # hooktime and reftime table are here to avoid sql update on repo. - # using inverse will still trigger write_date and write_uid update. - # this hack allows to avoid that - - hook_time = values.pop('hook_time', None) - get_ref_time = values.pop('get_ref_time', None) + def set_hook_time(self, value): for repo in self: - if hook_time: - self.env['runbot.repo.hooktime'].create({'time': hook_time, 'repo_id': repo.id}) - if get_ref_time: - self.env['runbot.repo.reftime'].create({'time': get_ref_time, 'repo_id': repo.id}) - if values: - super().write(values) + self.env['runbot.repo.hooktime'].create({'time': value, 'repo_id': repo.id}) + self.invalidate_cache() + + def set_ref_time(self, value): + for repo in self: + self.env['runbot.repo.reftime'].create({'time': value, 'repo_id': repo.id}) + self.invalidate_cache() def _gc_times(self): self.env.cr.execute(""" @@ -283,7 +279,7 @@ class runbot_repo(models.Model): get_ref_time = round(self._get_fetch_head_time(), 4) if not self.get_ref_time or get_ref_time > self.get_ref_time: - self.get_ref_time = get_ref_time + self.set_ref_time(get_ref_time) fields = ['refname', 'objectname', 'committerdate:iso8601', 'authorname', 'authoremail', 'subject', 'committername', 'committeremail'] fmt = "%00".join(["%(" + field + ")" for field in fields]) git_refs = self._git(['for-each-ref', '--format', fmt, '--sort=-committerdate', 'refs/heads', 'refs/pull']) @@ -392,7 +388,7 @@ class runbot_repo(models.Model): indirect.build_type = 'indirect' new_build.revdep_build_ids += indirect - @api.multi + def _create_pending_builds(self): """ Find new commits in physical repos""" refs = {} @@ -451,7 +447,6 @@ class runbot_repo(models.Model): repo = self repo._git(['fetch', '-p', 'origin', '+refs/heads/*:refs/heads/*', '+refs/pull/*/head:refs/pull/*']) - @api.multi def _update(self, force=True): """ Update the physical git reposotories on FS""" for repo in reversed(self): @@ -462,10 +457,9 @@ class runbot_repo(models.Model): def _commit(self): self.env.cr.commit() - self.invalidate_cache() - self.env.reset() + self.env.cache.invalidate() + self.env.clear() - @api.multi def _scheduler(self, host): nb_workers = host.get_nb_worker() @@ -693,7 +687,7 @@ class runbot_repo(models.Model): self._commit() except Exception as e: self.env.cr.rollback() - self.env.reset() + self.env.clear() _logger.exception(e) message = str(e) if host.last_exception == message: @@ -765,6 +759,7 @@ class runbot_repo(models.Model): class RefTime(models.Model): _name = "runbot.repo.reftime" + _description = "Repo reftime" _log_access = False time = fields.Float('Time', index=True, required=True) @@ -773,7 +768,8 @@ class RefTime(models.Model): class HookTime(models.Model): _name = "runbot.repo.hooktime" + _description = "Repo hooktime" _log_access = False time = fields.Float('Time') - repo_id = fields.Many2one('runbot.repo', 'Repository', required=True, ondelete='cascade') \ No newline at end of file + repo_id = fields.Many2one('runbot.repo', 'Repository', required=True, ondelete='cascade') diff --git a/runbot/models/res_config_settings.py b/runbot/models/res_config_settings.py index 202b87e6..3afc2356 100644 --- a/runbot/models/res_config_settings.py +++ b/runbot/models/res_config_settings.py @@ -33,7 +33,6 @@ class ResConfigSettings(models.TransientModel): ) return res - @api.multi def set_values(self): super(ResConfigSettings, self).set_values() set_param = self.env['ir.config_parameter'].sudo().set_param diff --git a/runbot/security/runbot_security.xml b/runbot/security/runbot_security.xml index a4efeb10..9a77fdec 100644 --- a/runbot/security/runbot_security.xml +++ b/runbot/security/runbot_security.xml @@ -17,6 +17,10 @@ <field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/> </record> + <record id="base.group_portal" model="res.groups"> + <field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/> + </record> + <record id="group_runbot_admin" model="res.groups"> <field name="name">Manager</field> <field name="category_id" ref="module_category"/> diff --git a/runbot/static/src/css/runbot.css b/runbot/static/src/css/runbot.css new file mode 100644 index 00000000..97439884 --- /dev/null +++ b/runbot/static/src/css/runbot.css @@ -0,0 +1,80 @@ +.separator { + border-top: 2px solid #666; + font-weight: bold; +} + +[data-toggle="collapse"] .fa:before { + content: "\f139"; +} + +[data-toggle="collapse"].collapsed .fa:before { + content: "\f13a"; +} + +body, .table{ + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color:#444; +} + +.btn-default { + background-color: #fff; + color: #444; + border-color: #ccc; +} + +.btn-default:hover { + background-color: #ccc; + color: #444; + border-color: #ccc; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.89rem; + line-height: 1.5; + border-radius: 0.2rem; +} +.btn-ssm, .btn-group-ssm > .btn { + padding: 0.22rem 0.4rem; + font-size: 0.82rem; + line-height: 1; + border-radius: 0.2rem; +} + +.killed, .bg-killed, .bg-killed-light { + background-color: #aaa; +} + +.dropdown-toggle:after { content: none } + +.branch_name { + max-width: 250px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.branch_time { + float:right; + margin-left:10px; +} + +.bg-success-light { + background-color: #dff0d8; +} +.bg-danger-light { + background-color: #f2dede; +} +.bg-info-light { + background-color: #d9edf7; +} + +.text-info{ + color: #096b72 !important; +} +.build_subject_buttons { + display: flex; +} +.build_buttons { + margin-left: auto +} diff --git a/runbot/static/src/js/runbot.js b/runbot/static/src/js/runbot.js index c8061be6..ca775d23 100644 --- a/runbot/static/src/js/runbot.js +++ b/runbot/static/src/js/runbot.js @@ -41,7 +41,7 @@ return false; }); }); - $(function() { - new Clipboard('.clipbtn'); - }); + //$(function() { + // new Clipboard('.clipbtn'); + //}); })(jQuery); diff --git a/runbot/static/src/less/runbot.less b/runbot/static/src/less/runbot.less deleted file mode 100644 index dbad1694..00000000 --- a/runbot/static/src/less/runbot.less +++ /dev/null @@ -1,12 +0,0 @@ -.separator { - border-top: 2px solid #666; - font-weight: bold; -} - -[data-toggle="collapse"] .fa:before { - content: "\f139"; -} - -[data-toggle="collapse"].collapsed .fa:before { - content: "\f13a"; -} diff --git a/runbot/templates/branch.xml b/runbot/templates/branch.xml index 9e5f5f79..d92f8eb8 100644 --- a/runbot/templates/branch.xml +++ b/runbot/templates/branch.xml @@ -33,7 +33,7 @@ <t t-if="build.global_result == 'skipped'"><t t-set="rowclass">default</t></t> <t t-if="build.global_result in ['killed', 'manually_killed']"><t t-set="rowclass">killed</t></t> </t> - <tr t-attf-class="{{rowclass}}"> + <tr t-attf-class="bg-{{rowclass}}-light"> <td><t t-esc="build.create_date" /></td> <td><a t-attf-href="/runbot/build/{{build['id']}}" title="Build details" aria-label="Build details"><t t-esc="build.dest" /></a></td> <td> diff --git a/runbot/templates/build.xml b/runbot/templates/build.xml index 1aa8b790..e7a8a2f8 100644 --- a/runbot/templates/build.xml +++ b/runbot/templates/build.xml @@ -40,13 +40,6 @@ </t> </small> </t> - <t t-set="nb_sum" t-value="bu.nb_pending+bu.nb_testing+bu.nb_running"/> - <t t-if="nb_sum > 1"><!-- maybe only display this info if > 3 --> - <span t-attf-title="{{bu.nb_pending}} pending, {{bu.nb_testing}} testing, {{bu.nb_running}} running"> - <t t-esc="nb_sum"/> - <i class="fa fa-cogs"/> - </span> - </t> </template> <template id="runbot.build_button"> <div t-attf-class="pull-right"> @@ -54,9 +47,9 @@ <a t-if="bu.real_build.local_state=='running'" t-attf-href="http://{{bu['domain']}}/?db={{bu.real_build.dest}}-all" class="btn btn-primary" title="Sign in on this build" aria-label="Sign in on this build"><i class="fa fa-sign-in"/></a> <a t-if="bu.real_build.local_state=='done' and bu.real_build.requested_action != 'wake_up'" href="#" t-att-data-runbot-build="bu.real_build.id" class="btn btn-default runbot-wakeup" title="Wake up this build" aria-label="Wake up this build"><i class="fa fa-coffee"/></a> <a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default" title="Build details" aria-label="Build details"><i class="fa fa-file-text-o"/></a> - <a t-attf-href="https://#{repo.base}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a> + <a t-if="show_commit_button" t-attf-href="https://#{repo.base}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build options" aria-label="Build options" aria-expanded="false"><i class="fa fa-cog"/><span class="caret"></span></button> - <ul class="dropdown-menu" role="menu"> + <ul class="dropdown-menu dropdown-menu-right" role="menu"> <li t-if="bu.global_result=='skipped'" groups="runbot.group_runbot_admin"> <a href="#" class="runbot-rebuild" t-att-data-runbot-build="bu['id']">Force Build <i class="fa fa-level-up"></i></a> </li> @@ -89,12 +82,12 @@ <li t-if="bu.global_state not in ('testing', 'waiting', 'pending')" class="divider"></li> <li><a t-attf-href="/runbot/build/{{bu['id']}}">Logs <i class="fa fa-file-text-o"/></a></li> <t t-set="log_url" t-value="'http://%s' % bu.real_build.host if bu.real_build.host != fqdn else ''"/> - <t t-if="bu.real_build.host" t-foreach="bu.log_list.split(',')" t-as="log_name" > + <t t-if="bu.real_build.host" t-foreach="(bu.log_list or '').split(',')" t-as="log_name" > <li><a t-attf-href="{{log_url}}/runbot/static/build/#{bu['real_build'].dest}/logs/#{log_name}.txt">Full <t t-esc="log_name"/> logs <i class="fa fa-file-text-o"/></a></li> </t> <li t-if="bu.coverage and bu.real_build.host"><a t-attf-href="http://{{bu.real_build.host}}/runbot/static/build/#{bu['real_build'].dest}/coverage/index.html">Coverage <i class="fa fa-file-text-o"/></a></li> <li t-if="bu.global_state!='pending'" class="divider"></li> - <li><a t-attf-href="{{br['branch'].branch_url}}">Branch or pull <i class="fa fa-github"/></a></li> + <li><a t-attf-href="{{br['branch'].branch_url}}"><t t-esc="'Branch ' if not br['branch'].pull_head_name else 'Pull '"/><i class="fa fa-github"/></a></li> <li><a t-attf-href="https://{{repo.base}}/commit/{{bu['name']}}">Commit <i class="fa fa-github"/></a></li> <li><a t-attf-href="https://{{repo.base}}/compare/{{br['branch'].branch_name}}">Compare <i class="fa fa-github"/></a></li> <!-- TODO branch.pull from --> @@ -118,54 +111,32 @@ </template> <template id="runbot.build"> <t t-call='website.layout'> - <div class="container" style="width: 100%;"> + + <t t-set="nav_form"> + <form class="form-inline"> + <div class="btn-group"> + <t t-call="runbot.build_button"> + <t t-set="bu" t-value="build"/> + <t t-set="klass" t-value="''"/> + <t t-set="show_commit_button" t-value="True"/> + </t> + </div> + </form> + <form class="form-inline" t-attf-action="/runbot/build/#{build['id']}/force" method='POST' t-if="request.params.get('ask_rebuild')" groups="runbot.group_user"> + <a href='#' class="btn btn-danger runbot-rebuild" t-attf-data-runbot-build="#{build['id']}" > <i class='fa fa-refresh'/> Force Rebuild</a> + </form> + </t> <div class="row" > <div class='col-md-12'> - <nav class="navbar navbar-default" role="navigation"> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a class="navbar-brand" t-attf-href="/runbot/repo/#{ slug(repo) }"><b><t t-esc="repo.base"/></b></a> - <a class="navbar-brand" t-attf-href="/runbot/build/{{build['id']}}"> - <t t-esc="build['dest']"/> - <t t-call="runbot.build_name"> - <t t-set="bu" t-value="build"/> - </t> - </a> - </div> - <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> - <form class="navbar-form navbar-left form-inline"> - <div class="btn-group"> - <t t-call="runbot.build_button"> - <t t-set="bu" t-value="build"/> - <t t-set="klass" t-value="''"/> - </t> - </div> - </form> - <p class="navbar-text"> - </p> - - <form class="navbar-form navbar-left form-inline" t-attf-action="/runbot/build/#{build['id']}/force" method='POST' t-if="request.params.get('ask_rebuild')" groups="runbot.group_user"> - <a href='#' class="btn btn-danger runbot-rebuild" t-attf-data-runbot-build="#{build['id']}" > <i class='fa fa-refresh'/> Force Rebuild</a> - </form> - </div> - </div> - </nav> - <table class="table table-condensed tabel-bordered"> <tr> <t t-set="rowclass"><t t-call="runbot.build_class"><t t-set="build" t-value="build"/></t></t> - <td t-attf-class="{{rowclass}}"> + <td t-attf-class="bg-{{rowclass.strip()}}-light"> Subject: <t t-esc="build['subject']"/><br/> Author: <t t-esc="build['author']"/><br/> Committer: <t t-esc="build['committer']"/><br/> Commit: <a t-attf-href="https://{{build.repo_id.base}}/commit/{{build.name}}"><t t-esc="build.name"/></a><br/> - <t t-foreach="build.dependency_ids" t-as="dep"> + <t t-foreach="build.sudo().dependency_ids" t-as="dep"> Dep: <t t-esc="dep.dependecy_repo_id.short_name"/>:<a t-attf-href="https://{{dep.dependecy_repo_id.base}}/commit/{{dep.dependency_hash}}"><t t-esc="dep.dependency_hash"/></a> <t t-if='dep.closest_branch_id'> from branch <t t-esc="dep.closest_branch_id.name"/></t> <br/> @@ -173,26 +144,27 @@ Branch: <span id="branchclp"><t t-esc="build.branch_id.branch_name"/></span> <a href="#" class="clipbtn octicon octicon-clippy" data-clipboard-target="#branchclp" title="Copy branch name to clipboard"/><br/> Build host: <t t-esc="build.real_build.host"/><br/> + Build dest: <t t-esc="build['dest']"/><br/> </td> <td t-if="build.real_build.children_ids"> Children: - <t t-if="build.real_build.nb_pending > 0"><t t-esc="build.real_build.nb_pending"/> pending </t> - <t t-if="build.real_build.nb_testing > 0"><t t-esc="build.real_build.nb_testing"/> testing </t> - <t t-if="build.real_build.nb_running > 0"><t t-esc="build.real_build.nb_running"/> running </t> <table class="table table-condensed"> <t t-foreach="build.real_build.children_ids.sorted('id')" t-as="child"> <t t-set="rowclass"><t t-call="runbot.build_class"><t t-set="build" t-value="child"/></t></t> - <tr><td t-attf-class="{{rowclass}}"> + <tr><td t-attf-class="bg-{{rowclass.strip()}}-light"> <a t-attf-href="/runbot/build/{{child.id}}" >Build <t t-esc="child.id"/></a> - with config <a t-attf-href="/web#id={{child.config_id.id}}&view_type=form&model=runbot.build.config"><t t-esc="child.config_id.name"/></a> + with config <t t-esc="child.config_id.name"/> + <a groups="runbot.group_build_config_user" t-attf-href="/web#id={{child.config_id.id}}&view_type=form&model=runbot.build.config">...</a> <t t-if="child.orphan_result"><i class="fa fa-chain-broken" title="Build result ignored for parent" /></t> <t t-if="child.job"> Running step: <t t-esc="child.job"/></t> <t t-if="child.global_state in ['testing', 'waiting']"> <i class="fa fa-spinner fa-spin"/> <t t-esc="child.global_state"/> </t> - <a t-if="child.real_build.local_state=='running'" t-attf-href="http://{{child.domain}}/?db={{child.real_build.dest}}-all" title="Sign in on this build" aria-label="Sign in on this build"><i class="fa fa-sign-in"/></a> - <a t-if="child.real_build.local_state=='done' and child.real_build.requested_action != 'wake_up'" href="#" t-att-data-runbot-build="child.real_build.id" class="runbot-wakeup" title="Wake up this build" aria-label="Wake up this build"><i class="fa fa-coffee"/></a> + <t t-call="runbot.build_button"> + <t t-set="bu" t-value="child"/> + <t t-set="klass" t-value="'btn-group-ssm'"/> + </t> </td></tr> </t> @@ -203,7 +175,7 @@ <p t-if="build.parent_id">Child of <a t-attf-href="/runbot/build/#{build.parent_id.id}"><t t-esc="build.parent_id.dest"/></a> <t t-if="build.orphan_result">&nbsp;<i class="fa fa-chain-broken" title="Build result ignored for parent" />&nbsp;Orphaned build, the result does not affect parent build result</t></p> <p t-if="build.duplicate_id">Duplicate of <a t-attf-href="/runbot/build/#{build.duplicate_id.id}"><t t-esc="build.duplicate_id.dest"/></a></p> - <table class="table table-condensed table-striped"> + <table class="table table-condensed"> <tr> <th>Date</th> <th>Level</th> @@ -213,13 +185,13 @@ <t t-foreach="build.real_build.sudo().log_ids" t-as="l"> <t t-set="subbuild" t-value="(([child for child in build.real_build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or [build.browse()])[0]"/> <t t-set="logclass" t-value="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(l.level)"/> - <tr t-att-class="logclass"> + <tr t-attf-class="bg-{{logclass}}-light"> <td style="white-space: nowrap; width:1%;"><t t-esc="l.create_date"/></td> <td style="white-space: nowrap; width:1%;"><b t-if="l.level != 'SEPARATOR' and l.type != 'link'" t-esc="l.level"/></td> <td style="white-space: nowrap; width:1%;"><t t-if="l.level != 'SEPARATOR' and l.type != 'link'" t-esc="l.type"/></td> <t t-set="message_class" t-value="''"/> <t t-if="subbuild" t-set="message_class"><t t-call="runbot.build_class"><t t-set="build" t-value="subbuild"/></t></t> - <td t-att-class="message_class"> + <td t-attf-class="bg-{{message_class.strip() or logclass}}-light"> <t t-if="l.type not in ('runbot', 'link')"> <t t-if="l.type == 'subbuild'"> <a t-attf-href="/runbot/build/{{l.path}}">Build #<t t-esc="l.path"/></a> @@ -241,7 +213,7 @@ <t t-if="l.type == 'subbuild' and subbuild.sudo().error_log_ids"> <a data-toggle="collapse" t-attf-data-target="#subbuild-{{subbuild.id}}"><i class="fa"></i></a> <div t-attf-id="subbuild-{{subbuild.id}}" class="collapse in"> - <table class="table table-condensed table-striped" style="margin-bottom:0;"> + <table class="table table-condensed" style="margin-bottom:0;"> <t t-foreach="subbuild.sudo().error_log_ids" t-as="sl"> <tr> <td t-att-class="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(sl.level)"> @@ -262,7 +234,6 @@ </t> </table> </div> - </div> </div> </t> </template> diff --git a/runbot/templates/dashboard.xml b/runbot/templates/dashboard.xml index da5630b3..07394522 100644 --- a/runbot/templates/dashboard.xml +++ b/runbot/templates/dashboard.xml @@ -141,21 +141,7 @@ <div class="row"> <div class="col-md-12"> <div> - <span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span> - <t t-set="testing">0</t> - <t t-set="workers">0</t> - - <t t-foreach="hosts_data.sorted(key=lambda h:h.name)" t-as="host"> - <t t-set="testing" t-value="int(testing) + host.nb_testing"/> - <t t-set="workers" t-value="int(workers) + host.sudo().get_nb_worker()"/> - </t> - <t t-set="klass">success</t> - - <t t-if="int(testing)/workers > 0"><t t-set="klass">info</t></t> - <t t-if="int(testing)/workers > 0.75"><t t-set="klass">warning</t></t> - <t t-if="int(testing)/workers >= 1"><t t-set="klass">danger</t></t> - - <span t-attf-class="label label-{{klass}}">Testing: <t t-esc="testing"/>/<t t-esc="workers"/></span> + <t t-call="slots_infos"/> </div> <t t-foreach="glances_data.keys()" t-as="repo"> <div> @@ -178,9 +164,9 @@ <t t-if="host.nb_testing > host.sudo().get_nb_worker()"><t t-set="klass">danger</t></t> <span t-attf-class="label label-{{klass}}"><span t-esc="host.nb_testing"/>/<span t-esc="host.sudo().get_nb_worker()"/></span> <t t-esc="host.nb_running"/> - <t t-set="succes_time" t-value="int(datetime.datetime.now().timestamp() - datetime.datetime.strptime(host.last_success, '%Y-%m-%d %H:%M:%S').timestamp())"/> - <t t-set="start_time" t-value="int(datetime.datetime.now().timestamp() - datetime.datetime.strptime(host.last_start_loop, '%Y-%m-%d %H:%M:%S').timestamp())"/> - <t t-set="end_time" t-value="int(datetime.datetime.now().timestamp() - datetime.datetime.strptime(host.last_end_loop, '%Y-%m-%d %H:%M:%S').timestamp())"/> + <t t-set="succes_time" t-value="int(datetime.datetime.now().timestamp() - host.last_success.timestamp())"/> + <t t-set="start_time" t-value="int(datetime.datetime.now().timestamp() - host.last_start_loop.timestamp())"/> + <t t-set="end_time" t-value="int(datetime.datetime.now().timestamp() - host.last_end_loop.timestamp())"/> <t t-set="klass">success</t> <t t-if="succes_time > 30"><t t-set="klass">info</t></t> diff --git a/runbot/templates/frontend.xml b/runbot/templates/frontend.xml index 943a4650..c7a29692 100644 --- a/runbot/templates/frontend.xml +++ b/runbot/templates/frontend.xml @@ -2,13 +2,48 @@ <odoo> <data> <!-- Replace default menu ( Home / Contactus and co...) with 5 first repos) --> - <template id="inherits_branch_in_menu" inherit_id="website.layout" name="Inherits Show top 5 branches in menu"> - <xpath expr="//t[@t-foreach="website.menu_id.child_id"][@t-as="submenu"]" position="replace"> - <t t-if="repos" > - <t t-foreach="repos[:5]" t-as="re"> - <li><a t-attf-href="/runbot/repo/{{slug(re)}}?search={{request.params.get('search', '')}}"><i class='fa fa-github' /> <t t-esc="re.short_name"/></a></li> - </t> - </t> + <template id="inherits_branch_in_menu" inherit_id="website.layout" name="Inherits Show top 6 repo in menu and dropdown"> + <xpath expr="//footer" position="replace"> + </xpath> + <xpath expr="//nav" position="replace"> + <nav class="navbar navbar-expand-md navbar-light bg-light"> + <a t-if="repo" t-attf-href="/runbot/repo/{{slug(repo)}}?search={{request.params.get('search', '')}}"> + <b style="color:#777;"><t t-esc="repo.short_name"/></b> + </a> + <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#top_menu_collapse"> + <span class="navbar-toggler-icon"/> + </button> + <div class="collapse navbar-collapse" id="top_menu_collapse"> + <ul class="nav navbar-nav ml-auto text-right" id="top_menu"> + <t t-if="repos" > + <t t-foreach="repos[:6]" t-as="re"> + <li ><a t-attf-href="/runbot/repo/{{slug(re)}}?search={{request.params.get('search', '')}}"><i class='fa fa-github' /> <t t-esc="re.short_name"/></a></li> + </t> + <li t-if="len(repos)>6" class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-plus"/></a> + <ul class="dropdown-menu"> + <t t-foreach='repos[6:]' t-as='re'> + <li><a t-attf-href="/runbot/repo/{{slug(re)}}"><t t-esc="re.short_name"/></a></li> + </t> + </ul> + </li> + </t> + <li class="nav-item divider" t-ignore="true" t-if="not user_id._is_public()"/> + <li class="nav-item dropdown" t-ignore="true" t-if="not user_id._is_public()"> + <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"> + <b> + <span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) > 25 else user_id.name"/> + </b> + </a> + <div class="dropdown-menu js_usermenu" role="menu"> + <a id="o_logout" class="dropdown-item" t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a> + </div> + </li> + </ul> + <t t-raw="nav_form or ''"> + </t> + </div> + </nav> </xpath> </template> @@ -21,6 +56,18 @@ <attribute name="t-value">'o_connected_user' if env['ir.ui.view'].user_has_groups('base.group_website_publisher') else None</attribute> </xpath> </template> + + <template id="runbot.slots_infos" name="Hosts slot nb pending/testing/slots"> + <span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span> + <t t-set="testing" t-value="hosts_data._total_testing()"/> + <t t-set="workers" t-value="hosts_data._total_workers()"/> + <t t-set="klass">success</t> + <t t-if="int(testing)/workers > 0"><t t-set="klass">info</t></t> + <t t-if="int(testing)/workers > 0.75"><t t-set="klass">warning</t></t> + <t t-if="int(testing)/workers >= 1"><t t-set="klass">danger</t></t> + <span t-attf-class="label label-{{klass}}">Testing: <t t-esc="testing"/>/<t t-esc="workers"/></span> + </template> + <!-- Frontend repository block --> <template id="runbot.repo"> <t t-call='website.layout'> @@ -28,58 +75,20 @@ <t t-if="refresh"> <meta http-equiv="refresh" t-att-content="refresh"/> </t> - <style> - .killed { - background-color: #aaa; - } - </style> + </t> + <t t-set="nav_form"> + <form class="form-inline my-2 my-lg-0" role="search" t-att-action="qu(search='')" method="get"> + <div class="input-group md-form form-sm form-2 pl-0"> + <input class="form-control my-0 py-1 red-border" type="text" placeholder="Search" aria-label="Search" name="search" t-att-value="search"/> + <div class="input-group-append"> + <button type='submit' class="input-group-text red lighten-3" id="basic-text1"><i class="fa fa-search text-grey"></i></button> + </div> + </div> + </form> </t> <div class="container-fluid"> <div class="row"> <div class='col-md-12'> - <nav class="navbar navbar-default" role="navigation"> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <t t-if="repo"> - <ul class="nav navbar-nav"> - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><b style="font-size: 18px;"><t t-esc="repo.base"/></b><b class="caret"></b></a> - <ul class="dropdown-menu"> - <t t-foreach='repos' t-as='re'> - <li><a t-attf-href="/runbot/repo/{{slug(re)}}"><t t-esc="re.base"/></a></li> - </t> - </ul> - </li> - </ul> - - </t> - </div> - <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> - <t t-if="repo"> - <form class="navbar-form navbar-right" role="search" t-att-action="qu(search='')" method="get"> - <div class="form-group"> - <input type="search" name="search" class="form-control" placeholder="Search" t-att-value="search"/> - <button type="submit" class="btn btn-default">Search</button> - </div> - </form> - </t> - </div> - <p class="text-center"> - <t t-foreach="host_stats" t-as="hs"> - <span class="label label-default"> - <t t-esc="hs['host']"/>: <t t-esc="hs['testing']"/> testing - </span>&nbsp; - </t> - <span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span> - </p> - </div> - </nav> <div t-if="message" class="alert alert-warning" role="alert"> <t t-esc="message" /> </div> @@ -90,21 +99,16 @@ <table t-if="repo" class="table table-condensed table-bordered" style="table-layout: initial;"> <tr> <th>Branch</th> - <td colspan="4" class="text-right"> - <t t-esc="repo.base"/>: - <t t-esc="testing"/> testing, - <t t-esc="running"/> running, - <t t-esc="pending"/> pending. + <td colspan="4"> + <span class="pull-right" t-call="runbot.slots_infos"/> </td> - </tr> <tr t-foreach="branches" t-as="br"> - <td> - <i t-if="br['branch'].sticky" class="fa fa-star" style="color: #f0ad4e" /> - <a t-attf-href="/runbot/branch/#{br['branch'].id}"><b t-esc="br['branch'].branch_name"/></a> - <small><t t-esc="br['builds'] and br['builds'][0].get_formated_build_time()"/></small><br/> - <div class="btn-group btn-group-xs"> - <a t-attf-href="{{br['branch'].branch_url}}" class="btn btn-default btn-xs">Branch or pull <i class="fa fa-github"/></a> + <td style="width:12%"> + <small class="branch_time" ><t t-esc="br['builds'] and br['builds'][0].get_formated_build_time()"/></small> + <div class="branch_name"><i t-if="br['branch'].sticky" class="fa fa-star" style="color: #f0ad4e" /><a t-attf-href="/runbot/branch/#{br['branch'].id}"><b t-esc="br['branch'].branch_name"/></a></div> + <div class="btn-group btn-group-xs"> + <a t-attf-href="{{br['branch'].branch_url}}" class="btn btn-default btn-xs"><t t-esc="'Branch ' if not br['branch'].pull_head_name else 'Pull '"/><i class="fa fa-github"/></a> <a t-attf-href="/runbot/quick_connect/#{br['branch'].id}" class="btn btn-default btn-xs" aria-label="Quick Connect"><i class="fa fa-fast-forward" title="Quick Connect"/></a> </div> <t t-if="br['branch'].sticky"> @@ -134,20 +138,21 @@ <t t-if="bu.global_result == 'skipped'"><t t-set="klass">default</t></t> <t t-if="bu.global_result in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t> </t> - <td t-attf-class="{{klass}}"> + <td t-attf-class="bg-{{klass}}-light" style="width:22%"> <t t-call="runbot.build_button"> <t t-set="klass">btn-group-sm</t> <t t-set="show_rebuild_button" t-value="bu==br['builds'][0]"></t> + <t t-set="show_commit_button" t-value="True"/> </t> <t t-if="bu['build_type']=='scheduled'"><i class="fa fa-moon-o" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t> <t t-if="bu['build_type'] in ('rebuild', 'indirect')"><i class="fa fa-recycle" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t> - <t t-if="bu['subject']"> + <span t-if="bu['subject']" class="build_subject"> <t t-if="bu.config_id != bu.branch_id.config_id"> <b t-esc="bu.config_id.name"/> </t> <span t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/> <br/> - </t> + </span> <t t-id="bu['author']"> <t t-esc="bu['author']"/> <t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']"> diff --git a/runbot/tests/common.py b/runbot/tests/common.py index fbc34973..24571649 100644 --- a/runbot/tests/common.py +++ b/runbot/tests/common.py @@ -39,6 +39,8 @@ class RunbotCase(TransactionCase): self.start_patcher('docker_build', 'odoo.addons.runbot.models.build.docker_build') self.start_patcher('docker_ps', 'odoo.addons.runbot.models.repo.docker_ps', []) self.start_patcher('docker_stop', 'odoo.addons.runbot.models.repo.docker_stop') + self.start_patcher('cr_commit', 'odoo.sql_db.Cursor.commit', None) + self.start_patcher('repo_commit', 'odoo.addons.runbot.models.repo.runbot_repo._commit', None) def start_patcher(self, patcher_name, patcher_path, return_value=Dummy, side_effect=Dummy): patcher = patch(patcher_path) diff --git a/runbot/tests/test_branch.py b/runbot/tests/test_branch.py index 5bd531d3..6c5ac680 100644 --- a/runbot/tests/test_branch.py +++ b/runbot/tests/test_branch.py @@ -120,7 +120,7 @@ class TestBranchRelations(RunbotCase): self.assertEqual(b.previous_version.branch_name, '13.0') self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1', 'saas-13.2']) - b.closest_sticky = self.last + b.defined_sticky = self.last self.assertEqual(b.closest_sticky.branch_name, 'saas-13.2') self.assertEqual(b.previous_version.branch_name, '13.0') diff --git a/runbot/tests/test_build.py b/runbot/tests/test_build.py index 5a7ae37a..1d6974a8 100644 --- a/runbot/tests/test_build.py +++ b/runbot/tests/test_build.py @@ -327,68 +327,66 @@ class Test_Build(RunbotCase): 'extra_params': '5', }) - def assert_state(nb_pending, nb_testing, nb_running, global_state, build): - self.assertEqual(build.nb_pending, nb_pending) - self.assertEqual(build.nb_testing, nb_testing) - self.assertEqual(build.nb_running, nb_running) + def assert_state(global_state, build): self.assertEqual(build.global_state, global_state) - assert_state(5, 0, 0, 'pending', build1) - assert_state(3, 0, 0, 'pending', build1_1) - assert_state(1, 0, 0, 'pending', build1_2) - assert_state(1, 0, 0, 'pending', build1_1_1) - assert_state(1, 0, 0, 'pending', build1_1_2) + assert_state('pending', build1) + assert_state('pending', build1_1) + assert_state('pending', build1_2) + assert_state('pending', build1_1_1) + assert_state('pending', build1_1_2) build1.local_state = 'testing' build1_1.local_state = 'testing' build1.local_state = 'done' build1_1.local_state = 'done' - assert_state(3, 0, 0, 'waiting', build1) - assert_state(2, 0, 0, 'waiting', build1_1) - assert_state(1, 0, 0, 'pending', build1_2) - assert_state(1, 0, 0, 'pending', build1_1_1) - assert_state(1, 0, 0, 'pending', build1_1_2) + assert_state('waiting', build1) + assert_state('waiting', build1_1) + assert_state('pending', build1_2) + assert_state('pending', build1_1_1) + assert_state('pending', build1_1_2) build1_1_1.local_state = 'testing' - assert_state(2, 1, 0, 'waiting', build1) - assert_state(1, 1, 0, 'waiting', build1_1) - assert_state(1, 0, 0, 'pending', build1_2) - assert_state(0, 1, 0, 'testing', build1_1_1) - assert_state(1, 0, 0, 'pending', build1_1_2) + assert_state('waiting', build1) + assert_state('waiting', build1_1) + assert_state('pending', build1_2) + assert_state('testing', build1_1_1) + assert_state('pending', build1_1_2) build1_2.local_state = 'testing' - assert_state(1, 2, 0, 'waiting', build1) - assert_state(1, 1, 0, 'waiting', build1_1) - assert_state(0, 1, 0, 'testing', build1_2) - assert_state(0, 1, 0, 'testing', build1_1_1) - assert_state(1, 0, 0, 'pending', build1_1_2) + assert_state('waiting', build1) + assert_state('waiting', build1_1) + assert_state('testing', build1_2) + assert_state('testing', build1_1_1) + assert_state('pending', build1_1_2) build1_2.local_state = 'testing' # writing same state a second time - assert_state(1, 2, 0, 'waiting', build1) - assert_state(1, 1, 0, 'waiting', build1_1) - assert_state(0, 1, 0, 'testing', build1_2) - assert_state(0, 1, 0, 'testing', build1_1_1) - assert_state(1, 0, 0, 'pending', build1_1_2) + assert_state('waiting', build1) + assert_state('waiting', build1_1) + assert_state('testing', build1_2) + assert_state('testing', build1_1_1) + assert_state('pending', build1_1_2) build1_1_2.local_state = 'done' build1_1_1.local_state = 'done' build1_2.local_state = 'done' - assert_state(0, 0, 0, 'done', build1) - assert_state(0, 0, 0, 'done', build1_1) - assert_state(0, 0, 0, 'done', build1_2) - assert_state(0, 0, 0, 'done', build1_1_1) - assert_state(0, 0, 0, 'done', build1_1_2) + assert_state('done', build1) + assert_state('done', build1_1) + assert_state('done', build1_2) + assert_state('done', build1_1_1) + assert_state('done', build1_1_2) def test_duplicate_childrens(self): build_old = self.create_build({ 'branch_id': self.branch_10.id, 'name': 'd0d0caca0000ffffffffffffffffffffffffffff', 'extra_params': '0', + 'local_state': 'done' }) build_parent = self.create_build({ 'branch_id': self.branch_10.id, @@ -404,8 +402,7 @@ class Test_Build(RunbotCase): build_parent.local_state = 'done' self.assertEqual(build_child.local_state, 'duplicate') self.assertEqual(build_child.duplicate_id, build_old) - self.assertEqual(build_parent.nb_pending, 0) - self.assertEqual(build_parent.nb_testing, 0) + self.assertEqual(build_child.global_state, 'done') self.assertEqual(build_parent.global_state, 'done') @@ -734,6 +731,10 @@ class TestClosestBranch(RunbotCase): 'repo_id': self.community_repo.id, 'name': 'refs/pull/123456' }) + + # trigger compute and ensure that mock_github is used. (using correct side effect would work too) + self.assertEqual(server_pr.pull_head_name, 'foo-dev:bar_branch') + mock_github.return_value = { 'head': {'label': 'foo-dev:foobar_branch'}, 'base': {'ref': '10.0'}, @@ -743,6 +744,8 @@ class TestClosestBranch(RunbotCase): 'repo_id': self.enterprise_repo.id, 'name': 'refs/pull/789101' }) + self.assertEqual(addons_pr.pull_head_name, 'foo-dev:foobar_branch') + closest = addons_pr._get_closest_branch(self.community_repo.id) self.assertEqual((self.branch_odoo_10, 'pr_target'), addons_pr._get_closest_branch(self.community_repo.id)) def test_closest_branch_05_master(self): diff --git a/runbot/tests/test_build_error.py b/runbot/tests/test_build_error.py index bb68513b..0d94245f 100644 --- a/runbot/tests/test_build_error.py +++ b/runbot/tests/test_build_error.py @@ -92,6 +92,7 @@ class TestBuildError(RunbotCase): self.assertIn(ko_build_new, new_build_error.build_ids, 'The parsed build with a re-apearing error should generate a new runbot.build.error') self.assertIn(build_error, new_build_error.error_history_ids, 'The old error should appear in history') + def test_build_error_links(self): build_a = self.create_test_build({'local_result': 'ko'}) build_b = self.create_test_build({'local_result': 'ko'}) @@ -111,8 +112,7 @@ class TestBuildError(RunbotCase): # test that the random bug is parent when linking errors all_errors = error_a | error_b all_errors.link_errors() - - self.assertIn(error_b.child_ids, error_a, 'Random error should be the parent') + self.assertEqual(error_b.child_ids, error_a, 'Random error should be the parent') # Test that changing bug resolution is propagated to children error_b.active = True diff --git a/runbot/tests/test_frontend.py b/runbot/tests/test_frontend.py index 5c81f706..a89a599b 100644 --- a/runbot/tests/test_frontend.py +++ b/runbot/tests/test_frontend.py @@ -53,9 +53,6 @@ class Test_Frontend(RunbotCase): self.assertEqual(self.sticky_branch, context['branches'][0]['branch'], "The sticky branch should be in first place") self.assertEqual(self.branch, context['branches'][1]['branch'], "The non sticky branch should be in second place") self.assertEqual(len(context['branches'][0]['builds']), 4, "Only the 4 last builds should appear in the context") - self.assertEqual(context['pending'], 2, "There should be 2 pending builds") - self.assertEqual(context['running'], 2, "There should be 2 running builds") - self.assertEqual(context['testing'], 2, "There should be 2 testing builds") self.assertEqual(context['pending_total'], 2, "There should be 2 pending builds") self.assertEqual(context['pending_level'], 'info', "The pending level should be info") return Response() diff --git a/runbot/tests/test_repo.py b/runbot/tests/test_repo.py index 92690100..5111de2f 100644 --- a/runbot/tests/test_repo.py +++ b/runbot/tests/test_repo.py @@ -176,20 +176,22 @@ class Test_Repo(RunbotCase): _logger.info('Create pending builds took: %ssec', (time.time() - inserted_time)) + + @common.warmup def test_times(self): - def _test_times(model, field_name): + def _test_times(model, setter, field_name): repo1 = self.Repo.create({'name': 'bla@example.com:foo/bar'}) repo2 = self.Repo.create({'name': 'bla@example.com:foo2/bar2'}) count = self.cr.sql_log_count - repo1[field_name] = 1.1 - self.assertEqual(self.cr.sql_log_count - count, 1, "Only one insert should have been triggered") - repo2[field_name] = 1.2 + with self.assertQueryCount(1): + getattr(repo1, setter)(1.1) + getattr(repo2, setter)(1.2) self.assertEqual(len(self.env[model].search([])), 2) self.assertEqual(repo1[field_name], 1.1) self.assertEqual(repo2[field_name], 1.2) - repo1[field_name] = 1.3 - repo2[field_name] = 1.4 + getattr(repo1, setter)(1.3) + getattr(repo2, setter)(1.4) self.assertEqual(len(self.env[model].search([])), 4) self.assertEqual(repo1[field_name], 1.3) @@ -205,8 +207,8 @@ class Test_Repo(RunbotCase): self.assertEqual(repo1[field_name], 1.3) self.assertEqual(repo2[field_name], 1.4) - _test_times('runbot.repo.hooktime', 'hook_time') - _test_times('runbot.repo.reftime', 'get_ref_time') + _test_times('runbot.repo.hooktime', 'set_hook_time', 'hook_time') + _test_times('runbot.repo.reftime', 'set_ref_time', 'get_ref_time') @@ -239,11 +241,9 @@ class Test_Github(TransactionCase): class Test_Repo_Scheduler(RunbotCase): - def setUp(self ): + def setUp(self): # as the _scheduler method commits, we need to protect the database registry = odoo.registry() - registry.enter_test_mode() - self.addCleanup(registry.leave_test_mode) super(Test_Repo_Scheduler, self).setUp() self.fqdn_patcher = patch('odoo.addons.runbot.models.host.fqdn') @@ -261,6 +261,7 @@ class Test_Repo_Scheduler(RunbotCase): @patch('odoo.addons.runbot.models.build.runbot_build._schedule') @patch('odoo.addons.runbot.models.build.runbot_build._init_pendings') def test_repo_scheduler(self, mock_init_pendings, mock_schedule, mock_kill): + self.env['ir.config_parameter'].set_param('runbot.runbot_workers', 6) builds = [] # create 6 builds that are testing on the host to verify that @@ -305,7 +306,3 @@ class Test_Repo_Scheduler(RunbotCase): self.Build.search([('name', '=', 'a')]).write({'local_state': 'done'}) self.foo_repo._scheduler(host) - build.invalidate_cache() - scheduled_build.invalidate_cache() - self.assertEqual(build.host, 'host.runbot.com') - self.assertFalse(scheduled_build.host) diff --git a/runbot/tests/test_schedule.py b/runbot/tests/test_schedule.py index 878e4479..59c79397 100644 --- a/runbot/tests/test_schedule.py +++ b/runbot/tests/test_schedule.py @@ -11,8 +11,6 @@ class TestSchedule(RunbotCase): def setUp(self): # entering test mode to avoid that the _schedule method commits records registry = odoo.registry() - registry.enter_test_mode() - self.addCleanup(registry.leave_test_mode) super(TestSchedule, self).setUp() self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar'}) diff --git a/runbot/views/assets.xml b/runbot/views/assets.xml index 15a22832..323f6f70 100644 --- a/runbot/views/assets.xml +++ b/runbot/views/assets.xml @@ -3,7 +3,7 @@ <data> <template id="assets_front_end" inherit_id="web.assets_frontend" name="runbot assets"> <xpath expr="." position="inside"> - <link rel="stylesheet" href="/runbot/static/src/less/runbot.less"/> + <link rel="stylesheet" href="/runbot/static/src/css/runbot.css"/> </xpath> </template> </data> diff --git a/runbot/views/build_error_views.xml b/runbot/views/build_error_views.xml index 67ac339a..e8bea2ea 100644 --- a/runbot/views/build_error_views.xml +++ b/runbot/views/build_error_views.xml @@ -121,7 +121,7 @@ <filter string="Fixed" name="fixed_errors" domain="[('active', '=', False)]"/> <filter string="Not Fixed" name="not_fixed_errors" domain="[('active', '=', True)]"/> <separator/> - <filter string="Not Asigned" name="not_assigned_errors" domain="[('responsible', '=', False)]"/> + <filter string="Not Assigned" name="not_assigned_errors" domain="[('responsible', '=', False)]"/> </search> </field> </record> diff --git a/runbot/views/build_views.xml b/runbot/views/build_views.xml index 757f9072..6bf97f02 100644 --- a/runbot/views/build_views.xml +++ b/runbot/views/build_views.xml @@ -82,21 +82,19 @@ <field name="name"/> <field name="global_state"/> <field name="dest"/> - <separator/> - <filter string="Pending" domain="[('global_state','=', 'pending')]"/> - <filter string="Testing" domain="[('global_state','in', ('testing', 'waiting'))]"/> - <filter string="Running" domain="[('global_state','=', 'running')]"/> - <filter string="Done" domain="[('global_state','=','done')]"/> - <filter string="Duplicate" domain="[('local_state','=', 'duplicate')]"/> - <separator /> + <filter string="Pending" name='pending' domain="[('global_state','=', 'pending')]"/> + <filter string="Testing" name='testing' domain="[('global_state','in', ('testing', 'waiting'))]"/> + <filter string="Running" name='running' domain="[('global_state','=', 'running')]"/> + <filter string="Done" name='done' domain="[('global_state','=','done')]"/> + <filter string="Duplicate" name='duplicate' domain="[('local_state','=', 'duplicate')]"/> <group expand="0" string="Group By..."> - <filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/> - <filter string="Branch" domain="[]" context="{'group_by':'branch_id'}"/> - <filter string="Status" domain="[]" context="{'group_by':'global_state'}"/> - <filter string="Result" domain="[]" context="{'group_by':'global_result'}"/> - <filter string="Start" domain="[]" context="{'group_by':'job_start'}"/> - <filter string="Host" domain="[]" context="{'group_by':'host'}"/> - <filter string="Create Date" domain="[]" context="{'group_by':'create_date'}"/> + <filter string="Repo" name='repo' domain="[]" context="{'group_by':'repo_id'}"/> + <filter string="Branch" name='branch' domain="[]" context="{'group_by':'branch_id'}"/> + <filter string="Status" name='status' domain="[]" context="{'group_by':'global_state'}"/> + <filter string="Result" name='result' domain="[]" context="{'group_by':'global_result'}"/> + <filter string="Start" name='start' domain="[]" context="{'group_by':'job_start'}"/> + <filter string="Host" name='host' domain="[]" context="{'group_by':'host'}"/> + <filter string="Create Date" name='create_date' domain="[]" context="{'group_by':'create_date'}"/> </group> </search> </field> @@ -105,7 +103,6 @@ <field name="name">Builds</field> <field name="type">ir.actions.act_window</field> <field name="res_model">runbot.build</field> - <field name="view_type">form</field> <field name="view_mode">tree,form,graph,pivot</field> </record> <menuitem id="menu_build" action="action_build" parent="runbot_menu_root"/> diff --git a/runbot/views/config_views.xml b/runbot/views/config_views.xml index 6f5fe185..75b39b92 100644 --- a/runbot/views/config_views.xml +++ b/runbot/views/config_views.xml @@ -23,6 +23,7 @@ <field name="update_github_state" groups="base.group_no_one"/> <field name="protected" groups="base.group_no_one"/> <field name="group" groups="base.group_no_one"/> + <field name="monitoring_view_id" groups="base.group_no_one"/> </group> </sheet> <div class="oe_chatter"> @@ -113,7 +114,7 @@ <search string="Search config"> <field name="name"/> <field name="group_name"/> - <filter string="Is in a group" domain="[(['group', '!=', False])]"/> + <filter string="Is in a group" name='is_in_group' domain="[(['group', '!=', False])]"/> <filter string="No step's defined" name="no_step" domain="[(['step_order_ids', '=', False])]"/> </search> </field> @@ -126,12 +127,12 @@ <search string="Search config step"> <field name="name"/> <field name="group_name"/> - <filter string="Install job" domain="[(['job_type', '=', 'install_odoo'])]"/> - <filter string="Run job" domain="[(['job_type', '=', 'run_odoo'])]"/> - <filter string="Python job" domain="[(['job_type', '=', 'python'])]"/> - <filter string="Create job" domain="[(['job_type', '=', 'create_build'])]"/> + <filter string="Install job" name='install_job' domain="[(['job_type', '=', 'install_odoo'])]"/> + <filter string="Run job" name='run_job' domain="[(['job_type', '=', 'run_odoo'])]"/> + <filter string="Python job" name='python_job' domain="[(['job_type', '=', 'python'])]"/> + <filter string="Create job" name='create_job' domain="[(['job_type', '=', 'create_build'])]"/> <separator/> - <filter string="Is in a group" domain="[(['group', '!=', False])]"/> + <filter string="Is in a group" name='is_in_group' domain="[(['group', '!=', False])]"/> <separator/> <filter string="No config defined" name="no_step" domain="[(['step_order_ids', '=', False])]"/> </search> diff --git a/runbot/views/host_views.xml b/runbot/views/host_views.xml index bfbd09b5..ae784994 100644 --- a/runbot/views/host_views.xml +++ b/runbot/views/host_views.xml @@ -16,8 +16,6 @@ <field name="last_success" readonly='1'/> <field name="assigned_only"/> <field name="nb_worker"/> - <field name="nb_testing"/> - <field name="nb_running"/> <field name="last_exception" readonly='1'/> <field name="exception_count" readonly='1'/> </group> diff --git a/runbot/wizards/multi_build_wizard.py b/runbot/wizards/multi_build_wizard.py index 34fe13d1..707180f7 100644 --- a/runbot/wizards/multi_build_wizard.py +++ b/runbot/wizards/multi_build_wizard.py @@ -6,6 +6,7 @@ from odoo import fields, models, api class MultiBuildWizard(models.TransientModel): _name = 'runbot.build.config.multi.wizard' + _description = "Multi wizard" base_name = fields.Char('Generic name', required=True) prefix = fields.Char('Prefix', help="Leave blank to use login.") diff --git a/runbot/wizards/mutli_build_wizard_views.xml b/runbot/wizards/mutli_build_wizard_views.xml index e7f81da5..9741b962 100644 --- a/runbot/wizards/mutli_build_wizard_views.xml +++ b/runbot/wizards/mutli_build_wizard_views.xml @@ -30,7 +30,6 @@ <field name="name">Generate Multi Build Config</field> <field name="type">ir.actions.act_window</field> <field name="res_model">runbot.build.config.multi.wizard</field> - <field name="view_type">form</field> <field name="view_mode">form</field> <field name="view_id" ref="runbot_multi_build_wizard_form"/> <field name="target">new</field> diff --git a/runbot_builder/builder.py b/runbot_builder/builder.py index 5c397347..a4ce66c2 100755 --- a/runbot_builder/builder.py +++ b/runbot_builder/builder.py @@ -43,12 +43,12 @@ class RunbotClient(): sleep_time = self.env['runbot.repo']._scheduler_loop_turn(host) host.last_end_loop = fields.Datetime.now() self.env.cr.commit() - self.env.reset() + self.env.clear() self.sleep(sleep_time) except Exception as e: _logger.exception('Builder main loop failed with: %s', e) self.env.cr.rollback() - self.env.reset() + self.env.clear() self.sleep(10) if self.ask_interrupt.is_set(): diff --git a/runbot_cla/__manifest__.py b/runbot_cla/__manifest__.py index d6054d5e..a7cbd624 100644 --- a/runbot_cla/__manifest__.py +++ b/runbot_cla/__manifest__.py @@ -2,7 +2,7 @@ 'name': 'Runbot CLA', 'category': 'Website', 'summary': 'Runbot CLA', - 'version': '2.0', + 'version': '2.1', 'description': "Runbot CLA", 'author': 'Odoo SA', 'depends': ['runbot'], diff --git a/runbot_cla/data/runbot_build_config_data.xml b/runbot_cla/data/runbot_build_config_data.xml index 560c895b..89092a02 100644 --- a/runbot_cla/data/runbot_build_config_data.xml +++ b/runbot_cla/data/runbot_build_config_data.xml @@ -5,11 +5,4 @@ <field name="job_type">cla_check</field> <field name="protected" eval="True"/> </record> - - <record id="runbot.runbot_build_config_default" model="runbot.build.config"> - <field name="step_order_ids" eval="[(0, 0, {'step_id': ref('runbot_build_config_step_check_cla')})]"/> - </record> - <record id="runbot.runbot_build_config_default_no_run" model="runbot.build.config"> - <field name="step_order_ids" eval="[(0, 0, {'step_id': ref('runbot_build_config_step_check_cla')})]"/> - </record> </odoo>