mirror of
https://github.com/odoo/runbot.git
synced 2025-03-16 07:55:45 +07:00
[IMP] runbot: add wake up option on builds
This commit is contained in:
parent
dbe44e2a76
commit
87c794a9aa
@ -6,7 +6,7 @@
|
||||
'author': "Odoo SA",
|
||||
'website': "http://runbot.odoo.com",
|
||||
'category': 'Website',
|
||||
'version': '4.3',
|
||||
'version': '4.4',
|
||||
'depends': ['website', 'base'],
|
||||
'data': [
|
||||
'security/runbot_security.xml',
|
||||
|
@ -45,10 +45,7 @@ class Runbot(Controller):
|
||||
|
||||
build_ids = []
|
||||
if repo:
|
||||
# FIXME or removeme (filters are broken)
|
||||
filters = {key: kwargs.get(key, '1') for key in ['waiting', 'pending', 'testing', 'running', 'done', 'deathrow']}
|
||||
domain = [('repo_id', '=', repo.id)]
|
||||
domain += [('global_state', '!=', key) for key, value in iter(filters.items()) if value == '0']
|
||||
if search:
|
||||
search_domain = []
|
||||
for to_search in search.split("|"):
|
||||
@ -113,8 +110,7 @@ class Runbot(Controller):
|
||||
'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, **filters),
|
||||
'filters': filters,
|
||||
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, refresh=refresh),
|
||||
'fqdn': fqdn(),
|
||||
})
|
||||
|
||||
@ -138,6 +134,12 @@ class Runbot(Controller):
|
||||
build._ask_kill()
|
||||
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
|
||||
|
||||
@route(['/runbot/build/<int:build_id>/wakeup'], type='http', auth="user", methods=['POST'], csrf=False)
|
||||
def build_wake_up(self, build_id, search=None, **post):
|
||||
build = request.env['runbot.build'].sudo().browse(build_id)
|
||||
build._wake_up()
|
||||
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
|
||||
|
||||
@route([
|
||||
'/runbot/build/<int:build_id>/force',
|
||||
'/runbot/build/<int:build_id>/force/<int:exact>',
|
||||
|
5
runbot/migrations/4.4/post-migration.py
Normal file
5
runbot/migrations/4.4/post-migration.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
cr.execute("UPDATE runbot_build SET requested_action='deathrow', local_state='testing' WHERE local_state = 'deathrow'")
|
@ -21,7 +21,7 @@ from subprocess import CalledProcessError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
result_order = ['ok', 'warn', 'ko', 'skipped', 'killed', 'manually_killed']
|
||||
state_order = ['pending', 'testing', 'waiting', 'running', 'deathrow', 'duplicate', 'done']
|
||||
state_order = ['pending', 'testing', 'waiting', 'running', 'duplicate', 'done']
|
||||
|
||||
|
||||
def make_selection(array):
|
||||
@ -57,6 +57,8 @@ class runbot_build(models.Model):
|
||||
local_result = fields.Selection(make_selection(result_order), string='Build Result', oldname='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)
|
||||
@ -402,7 +404,7 @@ class runbot_build(models.Model):
|
||||
else:
|
||||
sequence = self.search([], order='id desc', limit=1)[0].id
|
||||
# Force it now
|
||||
if build.local_state in ['running', 'done', 'duplicate', 'deathrow']:
|
||||
if build.local_state in ['running', 'done', 'duplicate']:
|
||||
values = {
|
||||
'sequence': sequence,
|
||||
'branch_id': build.branch_id.id,
|
||||
@ -533,10 +535,21 @@ class runbot_build(models.Model):
|
||||
for build in self:
|
||||
self.env.cr.commit() # commit between each build to minimise transactionnal errors due to state computations
|
||||
self.invalidate_cache()
|
||||
if build.local_state == 'deathrow':
|
||||
if build.requested_action == 'deathrow':
|
||||
build._kill(result='manually_killed')
|
||||
continue
|
||||
|
||||
if build.requested_action == 'wake_up':
|
||||
if docker_is_running(build._get_docker_name()):
|
||||
build.write({'requested_action': False, 'local_state': 'running'})
|
||||
build._log('wake_up', 'Waking up failed, docker is already running', level='SEPARATOR')
|
||||
else:
|
||||
log_path = build._path('logs', 'wake_up.txt')
|
||||
build.write({'job_start': now(), 'job_end': False, 'active_step': False, 'requested_action': False, 'local_state': 'running'})
|
||||
build._log('wake_up', 'Waking up build', level='SEPARATOR')
|
||||
self.env['runbot.build.config.step']._run_odoo_run(build, log_path)
|
||||
continue
|
||||
|
||||
if build.local_state == 'pending':
|
||||
# allocate port and schedule first job
|
||||
port = self._find_port()
|
||||
@ -789,7 +802,7 @@ class runbot_build(models.Model):
|
||||
continue
|
||||
build._log('kill', 'Kill build %s' % build.dest)
|
||||
docker_stop(build._get_docker_name())
|
||||
v = {'local_state': 'done', 'active_step': False, 'duplicate_id': False, 'build_end': now()} # what if duplicate? state done?
|
||||
v = {'local_state': 'done', 'requested_action': False, 'active_step': False, 'duplicate_id': False, 'build_end': now()} # what if duplicate? state done?
|
||||
if not build.job_end:
|
||||
v['job_end'] = now()
|
||||
if result:
|
||||
@ -815,12 +828,19 @@ class runbot_build(models.Model):
|
||||
build._skip()
|
||||
build._log('_ask_kill', 'Skipping build %s, requested by %s (user #%s)' % (build.dest, user.name, uid))
|
||||
elif build.local_state in ['testing', 'running']:
|
||||
build.write({'local_state': 'deathrow'})
|
||||
build.requested_action = 'deathrow'
|
||||
build._log('_ask_kill', 'Killing build %s, requested by %s (user #%s)' % (build.dest, user.name, uid))
|
||||
for child in build.children_ids: # should we filter build that are target of a duplicate_id?
|
||||
if not child.duplicate_id:
|
||||
child._ask_kill()
|
||||
|
||||
def _wake_up(self):
|
||||
build = self.real_build
|
||||
if build.local_state != 'done':
|
||||
build._log('wake_up', 'Impossibe to wake up, state is not done')
|
||||
else:
|
||||
build.requested_action = 'wake_up'
|
||||
|
||||
def _get_all_commit(self):
|
||||
return [Commit(self.repo_id, self.name)] + [Commit(dep._get_repo(), dep.dependency_hash) for dep in self.dependency_ids]
|
||||
|
||||
@ -939,6 +959,10 @@ class runbot_build(models.Model):
|
||||
if not step_ids: # no job to do, build is done
|
||||
return {'active_step': False, 'local_state': 'done'}
|
||||
|
||||
if not self.active_step and self.local_state != 'pending':
|
||||
# means that a step has been run manually without using config
|
||||
return {'active_step': False, 'local_state': 'done'}
|
||||
|
||||
next_index = step_ids.index(self.active_step) + 1 if self.active_step else 0
|
||||
if next_index >= len(step_ids): # final job, build is done
|
||||
return {'active_step': False, 'local_state': 'done'}
|
||||
|
@ -303,7 +303,7 @@ class runbot_repo(models.Model):
|
||||
])
|
||||
for btk in builds_to_kill:
|
||||
btk._log('repo._update_git', 'Build automatically killed, newer build found.', level='WARNING')
|
||||
builds_to_kill.write({'local_state': 'deathrow'})
|
||||
builds_to_kill.write({'requested_action': 'deathrow'})
|
||||
|
||||
new_build = Build.create(build_info)
|
||||
# create a reverse dependency build if needed
|
||||
@ -409,7 +409,7 @@ class runbot_repo(models.Model):
|
||||
domain_host = domain + [('host', '=', host)]
|
||||
|
||||
# schedule jobs (transitions testing -> running, kill jobs, ...)
|
||||
build_ids = Build.search(domain_host + [('local_state', 'in', ['testing', 'running', 'deathrow'])])
|
||||
build_ids = Build.search(domain_host + ['|', ('local_state', 'in', ['testing', 'running']), ('requested_action', 'in', ['wake_up', 'deathrow'])])
|
||||
build_ids._schedule()
|
||||
self.env.cr.commit()
|
||||
self.invalidate_cache()
|
||||
@ -467,7 +467,7 @@ class runbot_repo(models.Model):
|
||||
pending_build._schedule()
|
||||
|
||||
# terminate and reap doomed build
|
||||
build_ids = Build.search(domain_host + [('local_state', '=', 'running')]).ids
|
||||
build_ids = Build.search(domain_host + [('local_state', '=', 'running')], order='job_start desc').ids
|
||||
# sort builds: the last build of each sticky branch then the rest
|
||||
sticky = {}
|
||||
non_sticky = []
|
||||
|
@ -31,5 +31,15 @@
|
||||
return false;
|
||||
});
|
||||
});
|
||||
$(function() {
|
||||
$('a.runbot-wakeup').click(function() {
|
||||
var $f = $('<form method="POST">'),
|
||||
url = _.str.sprintf('/runbot/build/%s/wakeup', $(this).data('runbot-build')) + window.location.search;
|
||||
$f.attr('action', url);
|
||||
$f.appendTo($('body'));
|
||||
$f.submit();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
@ -2,26 +2,28 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.build_name">
|
||||
<t t-if="bu.global_state=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
|
||||
<t t-if="bu.global_state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')">
|
||||
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu.global_result, 'info')"/>
|
||||
<t t-if="bu.global_state == 'waiting'">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small t-if="not hide_time">time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
<t t-if="bu.real_build.requested_action=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
|
||||
<t t-if="bu.real_build.requested_action=='wake_up'"><i class="text-info fa fa-coffee"/> waking up</t>
|
||||
<t t-if="not bu.requested_action">
|
||||
<t t-if="bu.global_state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')">
|
||||
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu.global_result, 'info')"/>
|
||||
<t t-if="bu.global_state == 'waiting'">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small t-if="not hide_time">time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small> step <t t-esc="bu['job']"/>: </small><small t-if="not hide_time"><t t-esc="bu.get_formated_job_time()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small> step <t t-esc="bu['job']"/>: </small><small t-if="not hide_time"><t t-esc="bu.get_formated_job_time()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
<t t-if="bu.global_state in ('running', 'done')">
|
||||
<t t-if="bu.global_result=='ok'"><i class="text-success fa fa-thumbs-up" title="Success" aria-label="Success"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='ko'"><i class="text-danger fa fa-thumbs-down" title="Failed" aria-label="Failed"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='warn'"><i class="text-warning fa fa-warning" title="Some warnings" aria-label="Some warnings"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
|
||||
<t t-if="bu.global_result=='killed'"><i class="text-danger fa fa-times"/> killed</t>
|
||||
<t t-if="bu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="bu.global_state in ('running', 'done')">
|
||||
<t t-if="bu.global_result=='ok'"><i class="text-success fa fa-thumbs-up" title="Success" aria-label="Success"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='ko'"><i class="text-danger fa fa-thumbs-down" title="Failed" aria-label="Failed"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='warn'"><i class="text-warning fa fa-warning" title="Some warnings" aria-label="Some warnings"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
|
||||
<t t-if="bu.global_result=='killed'"><i class="text-danger fa fa-times"/> killed</t>
|
||||
<t t-if="bu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
|
||||
</t>
|
||||
|
||||
<t t-if="bu.revdep_build_ids">
|
||||
<small class="pull-right">Dep builds:
|
||||
<t t-foreach="bu.sorted_revdep_build_ids()" t-as="rbu">
|
||||
@ -32,7 +34,6 @@
|
||||
<t t-if="rbu.global_result=='skipped'"><i class="text-danger fa fa-ban"/></t>
|
||||
<t t-if="rbu.global_result=='killed'"><i class="text-danger fa fa-times"/>💀</t>
|
||||
<t t-if="rbu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/>🔫</t>
|
||||
<t t-if="rbu.global_state=='deathrow'"><i class="fa fa-crosshairs" style="color: #666666;"/></t>
|
||||
<t t-if="rbu.global_state=='pending'"><i class="fa fa-pause" style="color: #666666;"/></t>
|
||||
<t t-if="rbu.global_state in ('testing', 'waiting')"><i class="fa fa-spinner fa-spin" style="color: #666666;"/></t>
|
||||
</a>
|
||||
@ -63,11 +64,11 @@
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_build'].dest}}-base">Connect base <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/">Connect <i class="fa fa-sign-in"></i></a></li>
|
||||
</t>
|
||||
<li t-if="bu.global_state in ['done','running','deathrow']" groups="base.group_user">
|
||||
<li t-if="bu.global_state in ['done','running'] or requested_action == 'deathrow'" groups="base.group_user">
|
||||
<t t-if="bu_index==0">
|
||||
<a href="#" class="runbot-rebuild" t-att-data-runbot-build="bu['id']"
|
||||
title="Create a new build keeping build commit head, but will recompute all other info (config, dependencies, extra_params)">
|
||||
Default Rebuild <i class="fa fa-refresh"/>
|
||||
Rebuild <i class="fa fa-refresh"/>
|
||||
</a>
|
||||
</t>
|
||||
<a href="#" class="runbot-rebuild-exact" t-att-data-runbot-build="bu['id']"
|
||||
@ -75,9 +76,15 @@
|
||||
Exact Rebuild <i class="fa fa-refresh"/>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="bu.global_state in ['pending','testing', 'waiting', 'running']" groups="base.group_user">
|
||||
<a href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
|
||||
<li t-if="bu.global_state != 'done'" groups="base.group_user">
|
||||
<a t-if="bu.real_build.requested_action != 'deathrow'" href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
|
||||
<span t-else="" class="runbot-kill" > Killing <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
|
||||
</li>
|
||||
<li t-if="bu.global_state == 'done'" groups="base.group_user">
|
||||
<a t-if="bu.real_build.requested_action != 'wake_up'" href="#" class="runbot-wakeup" t-att-data-runbot-build="bu['id']">Wake up <i class="fa fa-coffee"/></a>
|
||||
<span t-else="" class="runbot-wakeup" > Waking up <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
|
||||
</li>
|
||||
|
||||
<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 ''"/>
|
||||
|
@ -47,7 +47,6 @@
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu.global_state=='deathrow'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ok'"><t t-set="klass">success</t></t>
|
||||
|
@ -68,23 +68,6 @@
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="navbar-form navbar-right form-inline">
|
||||
<div class="btn-group" t-if="repo">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
Filter <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-if="filters['pending']=='0'"><a t-att-href="qu(pending=1)">Pending</a></li>
|
||||
<li t-if="filters['pending']=='1'"><a t-att-href="qu(pending='0')"><i class="fa fa-check"/> Pending</a></li>
|
||||
<li t-if="filters['testing']=='0'"><a t-att-href="qu(testing=1)">Testing</a></li>
|
||||
<li t-if="filters['testing']=='1'"><a t-att-href="qu(testing='0')"><i class="fa fa-check"/> Testing</a></li>
|
||||
<li t-if="filters['running']=='0'"><a t-att-href="qu(running=1)">Running</a></li>
|
||||
<li t-if="filters['running']=='1'"><a t-att-href="qu(running='0')"><i class="fa fa-check"/> Running</a></li>
|
||||
<li t-if="filters['done']=='0'"><a t-att-href="qu(done=1)">Done</a></li>
|
||||
<li t-if="filters['done']=='1'"><a t-att-href="qu(done='0')"><i class="fa fa-check"/> Done</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
</div>
|
||||
<p class="text-center">
|
||||
@ -144,7 +127,6 @@
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu.global_state=='deathrow'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ['running','done']">
|
||||
<t t-if="bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
|
@ -32,11 +32,11 @@ class Test_Frontend(common.HttpCase):
|
||||
mock_request._cr = self.cr
|
||||
controller = frontend.Runbot()
|
||||
|
||||
states = ['done', 'pending', 'testing', 'running', 'deathrow']
|
||||
states = ['done', 'pending', 'testing', 'running']
|
||||
branches = [self.branch, self.sticky_branch]
|
||||
names = ['deadbeef', 'd0d0caca', 'deadface', 'cacafeed']
|
||||
# create 5 builds in each branch
|
||||
for i, state, branch, name in zip(range(10), cycle(states), cycle(branches), cycle(names)):
|
||||
for i, state, branch, name in zip(range(8), cycle(states), cycle(branches), cycle(names)):
|
||||
name = '%s%s' % (name, i)
|
||||
build = self.Build.create({
|
||||
'branch_id': branch.id,
|
||||
|
@ -42,7 +42,7 @@ class TestSchedule(common.TransactionCase):
|
||||
})
|
||||
domain = [('repo_id', 'in', (self.repo.id, ))]
|
||||
domain_host = domain + [('host', '=', 'runbotxx')]
|
||||
build_ids = self.Build.search(domain_host + [('local_state', 'in', ['testing', 'running', 'deathrow'])])
|
||||
build_ids = self.Build.search(domain_host + [('local_state', 'in', ['testing', 'running'])])
|
||||
mock_running.return_value = False
|
||||
self.assertEqual(build.local_state, 'testing')
|
||||
build_ids._schedule() # too fast, docker not started
|
||||
|
@ -87,7 +87,6 @@
|
||||
<filter string="Running" domain="[('global_state','=', 'running')]"/>
|
||||
<filter string="Done" domain="[('global_state','=','done')]"/>
|
||||
<filter string="Duplicate" domain="[('global_state','=', 'duplicate')]"/>
|
||||
<filter string="Deathrow" domain="[('global_state','=', 'deathrow')]"/>
|
||||
<separator />
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/>
|
||||
|
Loading…
Reference in New Issue
Block a user