mirror of
https://github.com/odoo/runbot.git
synced 2025-03-16 07:55:45 +07:00
[REF] runbot, runbot_cla: upgrade to Odoo 11.0
The previous code of runbot and runbot_cla was made for Odoo API version 8.0. This commit makes it work with Odoo API 11.0 and Python 3. Also, the present refactoring splits the code into multiple files to make it easier to read (I hope). The main change due to Python 3 is the job locking mechanism: Since PEP-446 file descriptors are non-inheritable by default. A new method (os.set_inheritable) was introduced to explicitely make fd inheritable. Also, the close_fds parameter of the subprocess.Popen method is now True by default. Finally, PEP-3151 changed the exception raised by fcntl.flock from IOError to OSError (and IOError became an alias of OSError). As a consequence of all that, the runbot locking mechanism to check if a job is finished was not working in python3.
This commit is contained in:
parent
e33881c9b4
commit
21c31b4c3c
runbot
__init__.py__manifest__.py__openerp__.pycommon.py
controllers
croninterval.pydata
models
res_config.pyres_config_view.xmlrunbot.pyrunbot.xmltemplates
views
runbot_cla
@ -1,2 +1,6 @@
|
||||
import runbot
|
||||
import res_config
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import croninterval
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import common
|
||||
|
25
runbot/__manifest__.py
Normal file
25
runbot/__manifest__.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "runbot",
|
||||
'summary': "Runbot",
|
||||
'description': "Runbot for Odoo 11.0",
|
||||
'author': "Odoo SA",
|
||||
'website': "http://runbot.odoo.com",
|
||||
'category': 'Website',
|
||||
'version': '2.0',
|
||||
'depends': ['website', 'base'],
|
||||
'data': [
|
||||
'security/runbot_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir.rule.csv',
|
||||
'views/repo_views.xml',
|
||||
'views/branch_views.xml',
|
||||
'views/build_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'templates/frontend.xml',
|
||||
'templates/build.xml',
|
||||
'templates/assets.xml',
|
||||
'templates/nginx.xml',
|
||||
'data/runbot_cron.xml'
|
||||
],
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
'name': 'Runbot',
|
||||
'category': 'Website',
|
||||
'summary': 'Runbot',
|
||||
'version': '1.3',
|
||||
'description': "Runbot",
|
||||
'author': 'Odoo SA',
|
||||
'depends': ['website', 'base_setup'],
|
||||
'external_dependencies': {
|
||||
'python': ['matplotlib'],
|
||||
},
|
||||
'data': [
|
||||
'runbot.xml',
|
||||
'res_config_view.xml',
|
||||
'security/runbot_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir.rule.csv',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
101
runbot/common.py
Normal file
101
runbot/common.py
Normal file
@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import contextlib
|
||||
import fcntl
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import psycopg2
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fqdn():
|
||||
return socket.getfqdn()
|
||||
|
||||
|
||||
def time2str(t):
|
||||
time.strftime(DEFAULT_SERVER_DATETIME_FORMAT, t)
|
||||
|
||||
|
||||
def dt2time(datetime):
|
||||
"""Convert datetime to time"""
|
||||
return time.mktime(time.strptime(datetime, DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
|
||||
|
||||
def now():
|
||||
return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
|
||||
def lock(filename):
|
||||
fd = os.open(filename, os.O_CREAT | os.O_RDWR, 0o600)
|
||||
if hasattr(os, 'set_inheritable'):
|
||||
os.set_inheritable(fd, True) # needed since pep-446
|
||||
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
|
||||
def locked(filename):
|
||||
result = False
|
||||
try:
|
||||
fd = os.open(filename, os.O_CREAT | os.O_RDWR, 0o600)
|
||||
except OSError:
|
||||
os.close(fd)
|
||||
return False
|
||||
try:
|
||||
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError: # since pep-3151 fcntl raises OSError and IOError is now an alias of OSError
|
||||
result = True
|
||||
finally:
|
||||
os.close(fd)
|
||||
return result
|
||||
|
||||
|
||||
def grep(filename, string):
|
||||
if os.path.isfile(filename):
|
||||
return open(filename).read().find(string) != -1
|
||||
return False
|
||||
|
||||
|
||||
def uniq_list(l):
|
||||
return OrderedDict.fromkeys(l).keys()
|
||||
|
||||
|
||||
def flatten(list_of_lists):
|
||||
return list(itertools.chain.from_iterable(list_of_lists))
|
||||
|
||||
|
||||
def rfind(filename, pattern):
|
||||
"""Determine in something in filename matches the pattern"""
|
||||
if os.path.isfile(filename):
|
||||
regexp = re.compile(pattern, re.M)
|
||||
with open(filename, 'r') as f:
|
||||
if regexp.findall(f.read()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def s2human(time):
|
||||
"""Convert a time in second into an human readable string"""
|
||||
for delay, desc in [(86400, 'd'),(3600, 'h'),(60, 'm')]:
|
||||
if time >= delay:
|
||||
return str(int(time / delay)) + desc
|
||||
return str(int(time)) + "s"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def local_pgadmin_cursor():
|
||||
cnx = None
|
||||
try:
|
||||
cnx = psycopg2.connect("dbname=postgres")
|
||||
cnx.autocommit = True # required for admin commands
|
||||
yield cnx.cursor()
|
||||
finally:
|
||||
if cnx:
|
||||
cnx.close()
|
4
runbot/controllers/__init__.py
Normal file
4
runbot/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import frontend
|
||||
from . import hook
|
219
runbot/controllers/frontend.py
Normal file
219
runbot/controllers/frontend.py
Normal file
@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.website.controllers.main import QueryURL
|
||||
from odoo.http import request
|
||||
from ..common import uniq_list, flatten, s2human
|
||||
|
||||
|
||||
class Runbot(http.Controller):
|
||||
|
||||
def build_info(self, build):
|
||||
real_build = build.duplicate_id if build.state == 'duplicate' else build
|
||||
return {
|
||||
'id': build.id,
|
||||
'name': build.name,
|
||||
'state': real_build.state,
|
||||
'result': real_build.result,
|
||||
'guess_result': real_build.guess_result,
|
||||
'subject': build.subject,
|
||||
'author': build.author,
|
||||
'committer': build.committer,
|
||||
'dest': build.dest,
|
||||
'real_dest': real_build.dest,
|
||||
'job_age': s2human(real_build.job_age),
|
||||
'job_time': s2human(real_build.job_time),
|
||||
'job': real_build.job,
|
||||
'domain': real_build.domain,
|
||||
'host': real_build.host,
|
||||
'port': real_build.port,
|
||||
'server_match': real_build.server_match,
|
||||
'duplicate_of': build.duplicate_id if build.state == 'duplicate' else False,
|
||||
'coverage': build.branch_id.coverage,
|
||||
}
|
||||
|
||||
@http.route(['/runbot', '/runbot/repo/<model("runbot.repo"):repo>'], website=True, auth='public', type='http')
|
||||
def repo(self, repo=None, search='', limit='100', refresh='', **kwargs):
|
||||
branch_obj = request.env['runbot.branch']
|
||||
build_obj = request.env['runbot.build']
|
||||
repo_obj = request.env['runbot.repo']
|
||||
|
||||
repo_ids = repo_obj.search([])
|
||||
repos = repo_obj.browse(repo_ids)
|
||||
if not repo and repos:
|
||||
repo = repos[0].id
|
||||
|
||||
context = {
|
||||
'repos': repos.ids,
|
||||
'repo': repo,
|
||||
'host_stats': [],
|
||||
'pending_total': build_obj.search_count([('state', '=', 'pending')]),
|
||||
'limit': limit,
|
||||
'search': search,
|
||||
'refresh': refresh,
|
||||
}
|
||||
|
||||
build_ids = []
|
||||
if repo:
|
||||
filters = {key: kwargs.get(key, '1') for key in ['pending', 'testing', 'running', 'done', 'deathrow']}
|
||||
domain = [('repo_id', '=', repo.id)]
|
||||
domain += [('state', '!=', key) for key, value in iter(filters.items()) if value == '0']
|
||||
if search:
|
||||
domain += ['|', '|', ('dest', 'ilike', search), ('subject', 'ilike', search), ('branch_id.branch_name', 'ilike', search)]
|
||||
|
||||
build_ids = build_obj.search(domain, limit=int(limit))
|
||||
branch_ids, build_by_branch_ids = [], {}
|
||||
|
||||
if build_ids:
|
||||
branch_query = """
|
||||
SELECT br.id FROM runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id WHERE bu.id in %s
|
||||
ORDER BY bu.sequence DESC
|
||||
"""
|
||||
sticky_dom = [('repo_id', '=', repo.id), ('sticky', '=', True)]
|
||||
sticky_branch_ids = [] if search else branch_obj.search(sticky_dom).ids
|
||||
request._cr.execute(branch_query, (tuple(build_ids.ids),))
|
||||
branch_ids = uniq_list(sticky_branch_ids + [br[0] for br in request._cr.fetchall()])
|
||||
|
||||
build_query = """
|
||||
SELECT
|
||||
branch_id,
|
||||
max(case when br_bu.row = 1 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 2 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 3 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 4 then br_bu.build_id end)
|
||||
FROM (
|
||||
SELECT
|
||||
br.id AS branch_id,
|
||||
bu.id AS build_id,
|
||||
row_number() OVER (PARTITION BY branch_id) AS row
|
||||
FROM
|
||||
runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id
|
||||
WHERE
|
||||
br.id in %s
|
||||
GROUP BY br.id, bu.id
|
||||
ORDER BY br.id, bu.id DESC
|
||||
) AS br_bu
|
||||
WHERE
|
||||
row <= 4
|
||||
GROUP BY br_bu.branch_id;
|
||||
"""
|
||||
request._cr.execute(build_query, (tuple(branch_ids),))
|
||||
build_by_branch_ids = {
|
||||
rec[0]: [r for r in rec[1:] if r is not None] for rec in request._cr.fetchall()
|
||||
}
|
||||
|
||||
branches = branch_obj.browse(branch_ids)
|
||||
build_ids = flatten(build_by_branch_ids.values())
|
||||
build_dict = {build.id: build for build in build_obj.browse(build_ids)}
|
||||
|
||||
def branch_info(branch):
|
||||
return {
|
||||
'branch': branch,
|
||||
'builds': [self.build_info(build_dict[build_id]) for build_id in build_by_branch_ids[branch.id]]
|
||||
}
|
||||
|
||||
context.update({
|
||||
'branches': [branch_info(b) for b in branches],
|
||||
'testing': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'testing')]),
|
||||
'running': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'running')]),
|
||||
'pending': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'pending')]),
|
||||
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, limit=limit, refresh=refresh, **filters),
|
||||
'filters': filters,
|
||||
})
|
||||
|
||||
# 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([('state', '=', 'testing'), ('host', '=', result['host'])]),
|
||||
'running': build_obj.search_count([('state', '=', 'running'), ('host', '=', result['host'])]),
|
||||
})
|
||||
return http.request.render('runbot.repo', context)
|
||||
|
||||
@http.route(['/runbot/build/<int:build_id>/kill'], type='http', auth="user", methods=['POST'], csrf=False)
|
||||
def build_ask_kill(self, build_id, search=None, **post):
|
||||
build = request.env['runbot.build'].browse(build_id)
|
||||
build._ask_kill()
|
||||
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
|
||||
|
||||
@http.route(['/runbot/build/<int:build_id>/force'], type='http', auth="public", methods=['POST'], csrf=False)
|
||||
def build_force(self, build_id, search=None, **post):
|
||||
build = request.env['runbot.build'].browse(build_id)
|
||||
build._force()
|
||||
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
|
||||
|
||||
@http.route(['/runbot/build/<int:build_id>'], type='http', auth="public", website=True)
|
||||
def build(self, build_id, search=None, **post):
|
||||
"""Events/Logs"""
|
||||
|
||||
Build = request.env['runbot.build']
|
||||
Logging = request.env['ir.logging']
|
||||
|
||||
build = Build.browse([build_id])[0]
|
||||
if not build.exists():
|
||||
return request.not_found()
|
||||
|
||||
real_build = build.duplicate_id if build.state == 'duplicate' else build
|
||||
|
||||
# other builds
|
||||
build_ids = Build.search([('branch_id', '=', build.branch_id.id)])
|
||||
other_builds = Build.browse(build_ids)
|
||||
domain = [('build_id', '=', real_build.id)]
|
||||
log_type = request.params.get('type', '')
|
||||
if log_type:
|
||||
domain.append(('type', '=', log_type))
|
||||
level = request.params.get('level', '')
|
||||
if level:
|
||||
domain.append(('level', '=', level.upper()))
|
||||
if search:
|
||||
domain.append(('message', 'ilike', search))
|
||||
logging_ids = Logging.sudo().search(domain)
|
||||
|
||||
context = {
|
||||
'repo': build.repo_id,
|
||||
'build': self.build_info(build),
|
||||
'br': {'branch': build.branch_id},
|
||||
'logs': Logging.sudo().browse(logging_ids).ids,
|
||||
'other_builds': other_builds.ids
|
||||
}
|
||||
return request.render("runbot.build", context)
|
||||
|
||||
@http.route(['/runbot/b/<branch_name>', '/runbot/<model("runbot.repo"):repo>/<branch_name>'], type='http', auth="public", website=True)
|
||||
def fast_launch(self, branch_name=False, repo=False, **post):
|
||||
"""Connect to the running Odoo instance"""
|
||||
Build = request.env['runbot.build']
|
||||
|
||||
domain = [('branch_id.branch_name', '=', branch_name)]
|
||||
|
||||
if repo:
|
||||
domain.extend([('branch_id.repo_id', '=', repo.id)])
|
||||
order = "sequence desc"
|
||||
else:
|
||||
order = 'repo_id ASC, sequence DESC'
|
||||
|
||||
# Take the 10 lasts builds to find at least 1 running... Else no luck
|
||||
builds = Build.search(domain, order=order, limit=10)
|
||||
|
||||
if builds:
|
||||
last_build = False
|
||||
for build in Build.browse(builds):
|
||||
if build.state == 'running' or (build.state == 'duplicate' and build.duplicate_id.state == 'running'):
|
||||
last_build = build if build.state == 'running' else build.duplicate_id
|
||||
break
|
||||
|
||||
if not last_build:
|
||||
# Find the last build regardless the state to propose a rebuild
|
||||
last_build = Build.browse(builds[0])
|
||||
|
||||
if last_build.state != 'running':
|
||||
url = "/runbot/build/%s?ask_rebuild=1" % last_build.id
|
||||
else:
|
||||
url = build.branch_id._get_branch_quickconnect_url(last_build.domain, last_build.dest)[build.branch_id.id]
|
||||
else:
|
||||
return request.not_found()
|
||||
return werkzeug.utils.redirect(url)
|
16
runbot/controllers/hook.py
Normal file
16
runbot/controllers/hook.py
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import http, tools
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class RunbotHook(http.Controller):
|
||||
|
||||
@http.route(['/runbot/hook/<int:repo_id>'], type='http', auth="public", website=True)
|
||||
def hook(self, repo_id=None, **post):
|
||||
# TODO if repo_id == None parse the json['repository']['ssh_url'] and find the right repo
|
||||
repo = request.env['runbot.repo'].sudo().browse([repo_id])
|
||||
repo.hook_time = datetime.datetime.now().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
return ""
|
9
runbot/croninterval.py
Normal file
9
runbot/croninterval.py
Normal file
@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import odoo
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
# increase cron frequency from 0.016 Hz to 0.1 Hz to reduce starvation and improve throughput with many workers
|
||||
# TODO: find a nicer way than monkey patch to accomplish this
|
||||
odoo.service.server.SLEEP_INTERVAL = 10
|
||||
odoo.addons.base.ir.ir_cron._intervalTypes['minutes'] = lambda interval: relativedelta(seconds=interval * 10)
|
13
runbot/data/runbot_cron.xml
Normal file
13
runbot/data/runbot_cron.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding='UTF-8'?>
|
||||
<odoo>
|
||||
<record model="ir.cron" id="runbot_repo_cron">
|
||||
<field name="name">Runbot Cron</field>
|
||||
<field name="model_id" ref="model_runbot_repo"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
4
runbot/models/__init__.py
Normal file
4
runbot/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import repo, branch, build, event
|
||||
from . import res_config_settings
|
81
runbot/models/branch.py
Normal file
81
runbot/models/branch.py
Normal file
@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import re
|
||||
from subprocess import CalledProcessError
|
||||
from odoo import models, fields, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_re_coverage = re.compile(r'\bcoverage\b')
|
||||
|
||||
|
||||
class runbot_branch(models.Model):
|
||||
|
||||
_name = "runbot.branch"
|
||||
_order = 'name'
|
||||
_sql_constraints = [('branch_repo_uniq', 'unique (name,repo_id)', 'The branch must be unique per repository !')]
|
||||
|
||||
repo_id = fields.Many2one('runbot.repo', 'Repository', required=True, ondelete='cascade')
|
||||
name = fields.Char('Ref Name', required=True)
|
||||
branch_name = fields.Char(compute='_get_branch_name', type='char', string='Branch', readonly=1, store=True)
|
||||
branch_url = fields.Char(compute='_get_branch_url', type='char', string='Branch url', readonly=1)
|
||||
pull_head_name = fields.Char(compute='_get_pull_head_name', type='char', string='PR HEAD name', readonly=1, store=True)
|
||||
sticky = fields.Boolean('Sticky')
|
||||
coverage = fields.Boolean('Coverage')
|
||||
state = fields.Char('Status')
|
||||
modules = fields.Char("Modules to Install", help="Comma-separated list of modules to install and test.")
|
||||
job_timeout = fields.Integer('Job Timeout (minutes)', help='For default timeout: Mark it zero')
|
||||
# test_tags = fields.Char("Test tags", help="Tags for the --test-tags params (same syntax)") # keep for next version
|
||||
|
||||
@api.depends('name')
|
||||
def _get_branch_name(self):
|
||||
"""compute the branch name based on ref name"""
|
||||
for branch in self:
|
||||
if branch.name:
|
||||
branch.branch_name = branch.name.split('/')[-1]
|
||||
|
||||
@api.depends('branch_name')
|
||||
def _get_branch_url(self):
|
||||
"""compute the branch url based on branch_name"""
|
||||
for branch in self:
|
||||
if branch.name:
|
||||
if re.match('^[0-9]+$', branch.branch_name):
|
||||
branch.branch_url = "https://%s/pull/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
else:
|
||||
branch.branch_url = "https://%s/tree/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
|
||||
def _get_pull_info(self):
|
||||
self.ensure_one()
|
||||
repo = self.repo_id
|
||||
if repo.token and self.name.startswith('refs/pull/'):
|
||||
pull_number = self.name[len('refs/pull/'):]
|
||||
return repo._github('/repos/:owner/:repo/pulls/%s' % pull_number, ignore_errors=True) or {}
|
||||
return {}
|
||||
|
||||
def _is_on_remote(self):
|
||||
# check that a branch still exists on remote
|
||||
self.ensure_one()
|
||||
branch = self
|
||||
repo = branch.repo_id
|
||||
try:
|
||||
repo._git(['ls-remote', '-q', '--exit-code', repo.name, branch.name])
|
||||
except CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def create(self, vals):
|
||||
vals.setdefault('coverage', _re_coverage.search(vals.get('name') or '') is not None)
|
||||
return super(runbot_branch, self).create(vals)
|
||||
|
||||
@api.depends('branch_name')
|
||||
def _get_pull_head_name(self):
|
||||
"""compute pull head name"""
|
||||
for branch in self:
|
||||
pi = self._get_pull_info()
|
||||
if pi:
|
||||
self.pull_head_name = pi['head']['ref']
|
||||
|
||||
def _get_branch_quickconnect_url(self, fqdn, dest):
|
||||
self.ensure_one()
|
||||
r = {}
|
||||
r[self.id] = "http://%s/web/login?db=%s-all&login=admin&redirect=/web?debug=1" % (fqdn, dest)
|
||||
return r
|
786
runbot/models/build.py
Normal file
786
runbot/models/build.py
Normal file
@ -0,0 +1,786 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import glob
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
import resource
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from subprocess import CalledProcessError
|
||||
from ..common import dt2time, fqdn, now, locked, grep, time2str, rfind, uniq_list, local_pgadmin_cursor, lock
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import config, appdirs
|
||||
|
||||
_re_error = r'^(?:\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3} \d+ (?:ERROR|CRITICAL) )|(?:Traceback \(most recent call last\):)$'
|
||||
_re_warning = r'^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3} \d+ WARNING '
|
||||
re_job = re.compile('_job_\d')
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class runbot_build(models.Model):
|
||||
|
||||
_name = "runbot.build"
|
||||
_order = 'id desc'
|
||||
|
||||
branch_id = fields.Many2one('runbot.branch', 'Branch', required=True, ondelete='cascade', index=True)
|
||||
repo_id = fields.Many2one(related='branch_id.repo_id')
|
||||
name = fields.Char('Revno', required=True)
|
||||
host = fields.Char('Host')
|
||||
port = fields.Integer('Port')
|
||||
dest = fields.Char(compute='_get_dest', type='char', string='Dest', readonly=1, store=True)
|
||||
domain = fields.Char(compute='_get_domain', type='char', string='URL')
|
||||
date = fields.Datetime('Commit date')
|
||||
author = fields.Char('Author')
|
||||
author_email = fields.Char('Author Email')
|
||||
committer = fields.Char('Committer')
|
||||
committer_email = fields.Char('Committer Email')
|
||||
subject = fields.Text('Subject')
|
||||
sequence = fields.Integer('Sequence')
|
||||
modules = fields.Char("Modules to Install")
|
||||
result = fields.Char('Result', default='') # ok, ko, warn, skipped, killed, manually_killed
|
||||
guess_result = fields.Char(compute='_guess_result')
|
||||
pid = fields.Integer('Pid')
|
||||
state = fields.Char('Status', default='pending') # pending, testing, running, done, duplicate, deathrow
|
||||
job = fields.Char('Job') # job_*
|
||||
job_start = fields.Datetime('Job start')
|
||||
job_end = fields.Datetime('Job end')
|
||||
job_time = fields.Integer(compute='_get_time', string='Job time')
|
||||
job_age = fields.Integer(compute='_get_age', string='Job age')
|
||||
duplicate_id = fields.Many2one('runbot.build', 'Corresponding Build')
|
||||
server_match = fields.Selection([('builtin', 'This branch includes Odoo server'),
|
||||
('exact', 'branch/PR exact name'),
|
||||
('prefix', 'branch whose name is a prefix of current one'),
|
||||
('fuzzy', 'Fuzzy - common ancestor found'),
|
||||
('default', 'No match found - defaults to master')],
|
||||
string='Server branch matching')
|
||||
|
||||
def create(self, vals):
|
||||
build_id = super(runbot_build, self).create(vals)
|
||||
extra_info = {'sequence': self.id}
|
||||
|
||||
# detect duplicate
|
||||
duplicate_id = None
|
||||
domain = [
|
||||
('repo_id', '=', build_id.repo_id.duplicate_id.id),
|
||||
('name', '=', build_id.name),
|
||||
('duplicate_id', '=', False),
|
||||
'|', ('result', '=', False), ('result', '!=', 'skipped')
|
||||
]
|
||||
|
||||
for duplicate in self.search(domain):
|
||||
duplicate_id = duplicate.id
|
||||
# Consider the duplicate if its closest branches are the same than the current build closest branches.
|
||||
for extra_repo in build_id.repo_id.dependency_ids:
|
||||
build_closest_name = build_id._get_closest_branch_name(extra_repo.id)[1]
|
||||
duplicate_closest_name = duplicate._get_closest_branch_name(extra_repo.id)[1]
|
||||
if build_closest_name != duplicate_closest_name:
|
||||
duplicate_id = None
|
||||
if duplicate_id:
|
||||
extra_info.update({'state': 'duplicate', 'duplicate_id': duplicate_id})
|
||||
build_id.write({'duplicate_id': build_id.id})
|
||||
build_id.write(extra_info)
|
||||
return build_id
|
||||
|
||||
def _reset(self):
|
||||
self.write({'state': 'pending'})
|
||||
|
||||
def _branch_exists(self, branch_id):
|
||||
Branch = self.env['runbot.branch']
|
||||
branch = Branch.search([('id', '=', branch_id)])
|
||||
if branch and branch[0]._is_on_remote():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_closest_branch_name(self, target_repo_id):
|
||||
"""Return (repo, branch name) of the closest common branch between build's branch and
|
||||
any branch of target_repo or its duplicated repos.
|
||||
|
||||
Rules priority for choosing the branch from the other repo is:
|
||||
1. Same branch name
|
||||
2. A PR whose head name match
|
||||
3. Match a branch which is the dashed-prefix of current branch name
|
||||
4. Common ancestors (git merge-base)
|
||||
Note that PR numbers are replaced by the branch name of the PR target
|
||||
to prevent the above rules to mistakenly link PR of different repos together.
|
||||
"""
|
||||
self.ensure_one()
|
||||
Branch = self.env['runbot.branch']
|
||||
|
||||
build = self
|
||||
branch, repo = build.branch_id, build.repo_id
|
||||
pi = branch._get_pull_info()
|
||||
name = pi['base']['ref'] if pi else branch.branch_name
|
||||
|
||||
target_repo = self.env['runbot.repo'].browse(target_repo_id)
|
||||
|
||||
target_repo_ids = [target_repo.id]
|
||||
r = target_repo.duplicate_id
|
||||
while r:
|
||||
if r.id in target_repo_ids:
|
||||
break
|
||||
target_repo_ids.append(r.id)
|
||||
r = r.duplicate_id
|
||||
|
||||
_logger.debug('Search closest of %s (%s) in repos %r', name, repo.name, target_repo_ids)
|
||||
|
||||
sort_by_repo = lambda d: (not d['sticky'], # sticky first
|
||||
target_repo_ids.index(d['repo_id'][0]),
|
||||
-1 * len(d.get('branch_name', '')),
|
||||
-1 * d['id'])
|
||||
result_for = lambda d, match='exact': (d['repo_id'][0], d['name'], match)
|
||||
fields = ['name', 'repo_id', 'sticky']
|
||||
|
||||
# 1. same name, not a PR
|
||||
domain = [
|
||||
('repo_id', 'in', target_repo_ids),
|
||||
('branch_name', '=', name),
|
||||
('name', '=like', 'refs/heads/%'),
|
||||
]
|
||||
targets = Branch.search_read(domain, fields, order='id DESC')
|
||||
targets = sorted(targets, key=sort_by_repo)
|
||||
if targets and self._branch_exists(targets[0]['id']):
|
||||
return result_for(targets[0])
|
||||
|
||||
# 2. PR with head name equals
|
||||
domain = [
|
||||
('repo_id', 'in', target_repo_ids),
|
||||
('pull_head_name', '=', name),
|
||||
('name', '=like', 'refs/pull/%'),
|
||||
]
|
||||
pulls = Branch.search_read(domain, fields, order='id DESC')
|
||||
pulls = sorted(pulls, key=sort_by_repo)
|
||||
for pull in pulls:
|
||||
pi = pull._get_pull_info()
|
||||
if pi.get('state') == 'open':
|
||||
return result_for(pull)
|
||||
|
||||
# 3. Match a branch which is the dashed-prefix of current branch name
|
||||
branches = Branch.search_read(
|
||||
[('repo_id', 'in', target_repo_ids), ('name', '=like', 'refs/heads/%')],
|
||||
fields + ['branch_name'], order='id DESC',
|
||||
)
|
||||
branches = sorted(branches, key=sort_by_repo)
|
||||
|
||||
for branch in branches:
|
||||
if name.startswith(branch['branch_name'] + '-') and self._branch_exists(branch['id']):
|
||||
return result_for(branch, 'prefix')
|
||||
|
||||
# 4. Common ancestors (git merge-base)
|
||||
for target_id in target_repo_ids:
|
||||
common_refs = {}
|
||||
self.env.cr.execute("""
|
||||
SELECT b.name
|
||||
FROM runbot_branch b,
|
||||
runbot_branch t
|
||||
WHERE b.repo_id = %s
|
||||
AND t.repo_id = %s
|
||||
AND b.name = t.name
|
||||
AND b.name LIKE 'refs/heads/%%'
|
||||
""", [repo.id, target_id])
|
||||
for common_name, in self.env.cr.fetchall():
|
||||
try:
|
||||
commit = repo._git(['merge-base', branch['name'], common_name]).strip()
|
||||
cmd = ['log', '-1', '--format=%cd', '--date=iso', commit]
|
||||
common_refs[common_name] = repo._git(cmd).strip()
|
||||
except CalledProcessError:
|
||||
# If merge-base doesn't find any common ancestor, the command exits with a
|
||||
# non-zero return code, resulting in subprocess.check_output raising this
|
||||
# exception. We ignore this branch as there is no common ref between us.
|
||||
continue
|
||||
if common_refs:
|
||||
b = sorted(common_refs.iteritems(), key=operator.itemgetter(1), reverse=True)[0][0]
|
||||
return target_id, b, 'fuzzy'
|
||||
|
||||
# 5. last-resort value
|
||||
return target_repo_id, 'master', 'default'
|
||||
|
||||
@api.depends('name', 'branch_id.name')
|
||||
def _get_dest(self):
|
||||
for build in self:
|
||||
nickname = build.branch_id.name.split('/')[2]
|
||||
nickname = re.sub(r'"|\'|~|\:', '', nickname)
|
||||
nickname = re.sub(r'_|/|\.', '-', nickname)
|
||||
build.dest = ("%05d-%s-%s" % (build.id, nickname[:32], build.name[:6])).lower()
|
||||
|
||||
def _get_domain(self):
|
||||
domain = self.env['ir.config_parameter'].sudo().get_param('runbot.runbot_domain', fqdn())
|
||||
for build in self:
|
||||
if build.repo_id.nginx:
|
||||
build.domain = "%s.%s" % (build.dest, build.host)
|
||||
else:
|
||||
build.domain = "%s:%s" % (domain, build.port)
|
||||
|
||||
def _guess_result(self):
|
||||
self.env.cr.execute("""
|
||||
SELECT b.id,
|
||||
CASE WHEN b.state != 'testing' THEN b.result
|
||||
WHEN array_agg(l.level)::text[] && ARRAY['ERROR', 'CRITICAL'] THEN 'ko'
|
||||
WHEN array_agg(l.level)::text[] && ARRAY['WARNING'] THEN 'warn'
|
||||
ELSE 'ok'
|
||||
END
|
||||
FROM runbot_build b
|
||||
LEFT JOIN ir_logging l ON (l.build_id = b.id AND l.level != 'INFO')
|
||||
WHERE b.id IN %s
|
||||
GROUP BY b.id
|
||||
""", [tuple(self.ids)])
|
||||
return dict(self.env.cr.fetchall())
|
||||
|
||||
def _get_time(self):
|
||||
"""Return the time taken by the tests"""
|
||||
for build in self:
|
||||
if build.job_end:
|
||||
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))
|
||||
|
||||
def _get_age(self):
|
||||
"""Return the time between job start and now"""
|
||||
for build in self:
|
||||
if build.job_start:
|
||||
build.job_age = int(time.time() - dt2time(build.job_start))
|
||||
|
||||
def _force(self):
|
||||
"""Force a rebuild"""
|
||||
for build in self:
|
||||
pending_ids = self.search([('state', '=', 'pending')], order='id', limit=1)
|
||||
if pending_ids:
|
||||
sequence = pending_ids[0].id
|
||||
else:
|
||||
sequence = self.search([], order='id desc', limit=1)[0].id
|
||||
# Force it now
|
||||
rebuild = True
|
||||
if build.state == 'done' and build.result == 'skipped':
|
||||
build.write({'state': 'pending', 'sequence': sequence, 'result': ''})
|
||||
# or duplicate it
|
||||
elif build.state in ['running', 'done', 'duplicate', 'deathrow']:
|
||||
new_build = build.copy({'sequence': sequence})
|
||||
build = new_build
|
||||
else:
|
||||
rebuild = False
|
||||
if rebuild:
|
||||
build._log('rebuild', 'Rebuild initiated by %s' % self.env.user.name)
|
||||
|
||||
def _skip(self):
|
||||
"""Mark builds ids as skipped"""
|
||||
self.write({'state': 'done', 'result': 'skipped'})
|
||||
to_unduplicate = self.search([('id', 'in', self.ids), ('duplicate_id', '!=', False)])
|
||||
to_unduplicate._force()
|
||||
|
||||
def _local_cleanup(self):
|
||||
for build in self:
|
||||
# Cleanup the *local* cluster
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute("""
|
||||
SELECT datname
|
||||
FROM pg_database
|
||||
WHERE pg_get_userbyid(datdba) = current_user
|
||||
AND datname LIKE %s
|
||||
""", [build.dest + '%'])
|
||||
to_delete = local_cr.fetchall()
|
||||
for db, in to_delete:
|
||||
self._local_pg_dropdb(db)
|
||||
|
||||
# cleanup: find any build older than 7 days.
|
||||
root = self.env['runbot.repo']._root()
|
||||
build_dir = os.path.join(root, 'build')
|
||||
builds = os.listdir(build_dir)
|
||||
self.env.cr.execute("""
|
||||
SELECT dest
|
||||
FROM runbot_build
|
||||
WHERE dest IN %s
|
||||
AND (state != 'done' OR job_end > (now() - interval '7 days'))
|
||||
""", [tuple(builds)])
|
||||
actives = set(b[0] for b in self.env.cr.fetchall())
|
||||
|
||||
for b in builds:
|
||||
path = os.path.join(build_dir, b)
|
||||
if b not in actives and os.path.isdir(path) and not os.path.isabs(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
# cleanup old unused databases
|
||||
self.env.cr.execute("select id from runbot_build where state in ('testing', 'running')")
|
||||
db_ids = [id[0] for id in self.env.cr.fetchall()]
|
||||
if db_ids:
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute("""
|
||||
SELECT datname
|
||||
FROM pg_database
|
||||
WHERE pg_get_userbyid(datdba) = current_user
|
||||
AND datname ~ '^[0-9]+-.*'
|
||||
AND SUBSTRING(datname, '^([0-9]+)-.*')::int not in %s
|
||||
|
||||
""", [tuple(db_ids)])
|
||||
to_delete = local_cr.fetchall()
|
||||
for db, in to_delete:
|
||||
self._local_pg_dropdb(db)
|
||||
|
||||
def _list_jobs(self):
|
||||
"""List methods that starts with _job_[[:digit:]]"""
|
||||
return sorted(job[1:] for job in dir(self) if re_job.match(job))
|
||||
|
||||
def _find_port(self):
|
||||
# currently used port
|
||||
ids = self.search([('state', 'not in', ['pending', 'done'])])
|
||||
ports = set(i['port'] for i in ids.read(['port']))
|
||||
|
||||
# starting port
|
||||
icp = self.env['ir.config_parameter']
|
||||
port = int(icp.get_param('runbot.starting_port', default=2000))
|
||||
|
||||
# find next free port
|
||||
while port in ports:
|
||||
port += 2
|
||||
return port
|
||||
|
||||
def _logger(self, *l):
|
||||
l = list(l)
|
||||
for build in self:
|
||||
l[0] = "%s %s" % (build.dest, l[0])
|
||||
_logger.debug(*l)
|
||||
|
||||
def _schedule(self):
|
||||
"""schedule the build"""
|
||||
jobs = self._list_jobs()
|
||||
|
||||
icp = self.env['ir.config_parameter']
|
||||
# For retro-compatibility, keep this parameter in seconds
|
||||
default_timeout = int(icp.get_param('runbot.timeout', default=1800)) / 60
|
||||
|
||||
for build in self:
|
||||
if build.state == 'deathrow':
|
||||
build._kill(result='manually_killed')
|
||||
continue
|
||||
elif build.state == 'pending':
|
||||
# allocate port and schedule first job
|
||||
port = self._find_port()
|
||||
values = {
|
||||
'host': fqdn(),
|
||||
'port': port,
|
||||
'state': 'testing',
|
||||
'job': jobs[0],
|
||||
'job_start': now(),
|
||||
'job_end': False,
|
||||
}
|
||||
build.write(values)
|
||||
else:
|
||||
# check if current job is finished
|
||||
lock_path = build._path('logs', '%s.lock' % build.job)
|
||||
if locked(lock_path):
|
||||
# kill if overpassed
|
||||
timeout = (build.branch_id.job_timeout or default_timeout) * 60
|
||||
if build.job != jobs[-1] and build.job_time > timeout:
|
||||
build._logger('%s time exceded (%ss)', build.job, build.job_time)
|
||||
build.write({'job_end': now()})
|
||||
build._kill(result='killed')
|
||||
continue
|
||||
build._logger('%s finished', build.job)
|
||||
# schedule
|
||||
v = {}
|
||||
# testing -> running
|
||||
if build.job == jobs[-2]:
|
||||
v['state'] = 'running'
|
||||
v['job'] = jobs[-1]
|
||||
v['job_end'] = now(),
|
||||
# running -> done
|
||||
elif build.job == jobs[-1]:
|
||||
v['state'] = 'done'
|
||||
v['job'] = ''
|
||||
# testing
|
||||
else:
|
||||
v['job'] = jobs[jobs.index(build.job) + 1]
|
||||
build.write(v)
|
||||
build.refresh()
|
||||
|
||||
# run job
|
||||
pid = None
|
||||
if build.state != 'done':
|
||||
build._logger('running %s', build.job)
|
||||
job_method = getattr(self, '_' + build.job) # compute the job method to run
|
||||
os.makedirs(build._path('logs'), exist_ok=True)
|
||||
lock_path = build._path('logs', '%s.lock' % build.job)
|
||||
log_path = build._path('logs', '%s.txt' % build.job)
|
||||
try:
|
||||
pid = job_method(build, lock_path, log_path)
|
||||
build.write({'pid': pid})
|
||||
except Exception:
|
||||
_logger.exception('%s failed running method %s', build.dest, build.job)
|
||||
build._log(build.job, "failed running job method, see runbot log")
|
||||
build._kill(result='ko')
|
||||
continue
|
||||
# needed to prevent losing pids if multiple jobs are started and one them raise an exception
|
||||
self.env.cr.commit()
|
||||
|
||||
if pid == -2:
|
||||
# no process to wait, directly call next job
|
||||
# FIXME find a better way that this recursive call
|
||||
build._schedule()
|
||||
|
||||
# cleanup only needed if it was not killed
|
||||
if build.state == 'done':
|
||||
build._local_cleanup()
|
||||
|
||||
def _path(self, *l, **kw):
|
||||
"""Return the repo build path"""
|
||||
self.ensure_one()
|
||||
build = self
|
||||
root = self.env['runbot.repo']._root()
|
||||
return os.path.join(root, 'build', build.dest, *l)
|
||||
|
||||
def _server(self, *l, **kw):
|
||||
"""Return the build server path"""
|
||||
self.ensure_one()
|
||||
build = self
|
||||
if os.path.exists(build._path('odoo')):
|
||||
return build._path('odoo', *l)
|
||||
return build._path('openerp', *l)
|
||||
|
||||
def _filter_modules(self, modules, available_modules, explicit_modules):
|
||||
blacklist_modules = set(['auth_ldap', 'document_ftp', 'base_gengo',
|
||||
'website_gengo', 'website_instantclick',
|
||||
'pad', 'pad_project', 'note_pad',
|
||||
'pos_cache', 'pos_blackbox_be'])
|
||||
|
||||
mod_filter = lambda m: (
|
||||
m in available_modules and
|
||||
(m in explicit_modules or (not m.startswith(('hw_', 'theme_', 'l10n_')) and
|
||||
m not in blacklist_modules))
|
||||
)
|
||||
return uniq_list(filter(mod_filter, modules))
|
||||
|
||||
def _checkout(self):
|
||||
for build in self:
|
||||
# starts from scratch
|
||||
if os.path.isdir(build._path()):
|
||||
shutil.rmtree(build._path())
|
||||
|
||||
# runbot log path
|
||||
os.makedirs(build._path("logs"), exist_ok=True)
|
||||
os.makedirs(build._server('addons'), exist_ok=True)
|
||||
|
||||
# checkout branch
|
||||
build.branch_id.repo_id._git_export(build.name, build._path())
|
||||
|
||||
has_server = os.path.isfile(build._server('__init__.py'))
|
||||
server_match = 'builtin'
|
||||
|
||||
# build complete set of modules to install
|
||||
modules_to_move = []
|
||||
modules_to_test = ((build.branch_id.modules or '') + ',' +
|
||||
(build.repo_id.modules or ''))
|
||||
modules_to_test = list(filter(None, modules_to_test.split(','))) # ???
|
||||
explicit_modules = set(modules_to_test)
|
||||
_logger.debug("manual modules_to_test for build %s: %s", build.dest, modules_to_test)
|
||||
|
||||
if not has_server:
|
||||
if build.repo_id.modules_auto == 'repo':
|
||||
modules_to_test += [
|
||||
os.path.basename(os.path.dirname(a))
|
||||
for a in (glob.glob(build._path('*/__openerp__.py')) +
|
||||
glob.glob(build._path('*/__manifest__.py')))
|
||||
]
|
||||
_logger.debug("local modules_to_test for build %s: %s", build.dest, modules_to_test)
|
||||
|
||||
for extra_repo in build.repo_id.dependency_ids:
|
||||
repo_id, closest_name, server_match = build._get_closest_branch_name(extra_repo.id)
|
||||
repo = self.env['runbot.repo'].browse(repo_id)
|
||||
_logger.debug('branch %s of %s: %s match branch %s of %s',
|
||||
build.branch_id.name, build.repo_id.name,
|
||||
server_match, closest_name, repo.name)
|
||||
build._log(
|
||||
'Building environment',
|
||||
'%s match branch %s of %s' % (server_match, closest_name, repo.name)
|
||||
)
|
||||
repo._git_export(closest_name, build._path())
|
||||
|
||||
# Finally mark all addons to move to openerp/addons
|
||||
modules_to_move += [
|
||||
os.path.dirname(module)
|
||||
for module in (glob.glob(build._path('*/__openerp__.py')) +
|
||||
glob.glob(build._path('*/__manifest__.py')))
|
||||
]
|
||||
|
||||
# move all addons to server addons path
|
||||
for module in uniq_list(glob.glob(build._path('addons/*')) + modules_to_move):
|
||||
basename = os.path.basename(module)
|
||||
addon_path = build._server('addons', basename)
|
||||
if os.path.exists(addon_path):
|
||||
build._log(
|
||||
'Building environment',
|
||||
'You have duplicate modules in your branches "%s"' % basename
|
||||
)
|
||||
if os.path.islink(addon_path) or os.path.isfile(addon_path):
|
||||
os.remove(addon_path)
|
||||
else:
|
||||
shutil.rmtree(addon_path)
|
||||
shutil.move(module, build._server('addons'))
|
||||
|
||||
available_modules = [
|
||||
os.path.basename(os.path.dirname(a))
|
||||
for a in (glob.glob(build._server('addons/*/__openerp__.py')) +
|
||||
glob.glob(build._server('addons/*/__manifest__.py')))
|
||||
]
|
||||
if build.repo_id.modules_auto == 'all' or (build.repo_id.modules_auto != 'none' and has_server):
|
||||
modules_to_test += available_modules
|
||||
|
||||
modules_to_test = self._filter_modules(modules_to_test,
|
||||
set(available_modules), explicit_modules)
|
||||
_logger.debug("modules_to_test for build %s: %s", build.dest, modules_to_test)
|
||||
build.write({'server_match': server_match,
|
||||
'modules': ','.join(modules_to_test)})
|
||||
|
||||
def _local_pg_dropdb(self, dbname):
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute('DROP DATABASE IF EXISTS "%s"' % dbname)
|
||||
# cleanup filestore
|
||||
datadir = appdirs.user_data_dir()
|
||||
paths = [os.path.join(datadir, pn, 'filestore', dbname) for pn in 'OpenERP Odoo'.split()]
|
||||
cmd = ['rm', '-rf'] + paths
|
||||
_logger.debug(' '.join(cmd))
|
||||
subprocess.call(cmd)
|
||||
|
||||
def _local_pg_createdb(self, dbname):
|
||||
self._local_pg_dropdb(dbname)
|
||||
_logger.debug("createdb %s", dbname)
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute("""CREATE DATABASE "%s" TEMPLATE template0 LC_COLLATE 'C' ENCODING 'unicode'""" % dbname)
|
||||
|
||||
def _log(self, func, message):
|
||||
self.ensure_one()
|
||||
_logger.debug("Build %s %s %s", self.id, func, message)
|
||||
self.env['ir.logging'].create({
|
||||
'build_id': self.id,
|
||||
'level': 'INFO',
|
||||
'type': 'runbot',
|
||||
'name': 'odoo.runbot',
|
||||
'message': message,
|
||||
'path': 'runbot',
|
||||
'func': func,
|
||||
'line': '0',
|
||||
})
|
||||
|
||||
def reset(self):
|
||||
self.write({'state': 'pending'})
|
||||
|
||||
def _reap(self):
|
||||
while True:
|
||||
try:
|
||||
pid, status, rusage = os.wait3(os.WNOHANG)
|
||||
except OSError:
|
||||
break
|
||||
if pid == 0:
|
||||
break
|
||||
_logger.debug('reaping: pid: %s status: %s', pid, status)
|
||||
|
||||
def _kill(self, result=None):
|
||||
host = fqdn()
|
||||
for build in self:
|
||||
if build.host != host:
|
||||
continue
|
||||
build._log('kill', 'Kill build %s' % build.dest)
|
||||
if build.pid:
|
||||
build._logger('killing %s', build.pid)
|
||||
try:
|
||||
os.killpg(build.pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
v = {'state': 'done', 'job': False}
|
||||
if result:
|
||||
v['result'] = result
|
||||
build.write(v)
|
||||
self.env.cr.commit()
|
||||
build._github_status()
|
||||
build._local_cleanup()
|
||||
|
||||
def _ask_kill(self):
|
||||
self.ensure_one()
|
||||
user = self.env.user
|
||||
uid = self.env.uid
|
||||
if self.state == 'pending':
|
||||
self._skip(ids=self.ids)
|
||||
self._log('_ask_kill', 'Skipping build %s, requested by %s (user #%s)' % (self.dest, user.name, uid))
|
||||
elif self.state in ['testing', 'running']:
|
||||
self.write({'state': 'deathrow'})
|
||||
self._log('_ask_kill', 'Killing build %s, requested by %s (user #%s)' % (self.dest, user.name, uid))
|
||||
|
||||
def _cmd(self):
|
||||
"""Return a tuple describing the command to start the build
|
||||
First part is list with the command and parameters
|
||||
Second part is a list of Odoo modules
|
||||
"""
|
||||
self.ensure_one()
|
||||
build = self
|
||||
bins = [
|
||||
'odoo-bin', # >= 10.0
|
||||
'openerp-server', # 9.0, 8.0
|
||||
'openerp-server.py', # 7.0
|
||||
'bin/openerp-server.py', # < 7.0
|
||||
]
|
||||
for server_path in map(build._path, bins):
|
||||
if os.path.isfile(server_path):
|
||||
break
|
||||
|
||||
# commandline
|
||||
cmd = [
|
||||
build._path(server_path),
|
||||
"--xmlrpc-port=%d" % build.port,
|
||||
]
|
||||
# options
|
||||
if grep(build._server("tools/config.py"), "no-xmlrpcs"):
|
||||
cmd.append("--no-xmlrpcs")
|
||||
if grep(build._server("tools/config.py"), "no-netrpc"):
|
||||
cmd.append("--no-netrpc")
|
||||
if grep(build._server("tools/config.py"), "log-db"):
|
||||
logdb = self.env.cr.dbname
|
||||
if config['db_host'] and grep(build._server('sql_db.py'), 'allow_uri'):
|
||||
logdb = 'postgres://{cfg[db_user]}:{cfg[db_password]}@{cfg[db_host]}/{db}'.format(cfg=config, db=self.env.cr.dbname)
|
||||
cmd += ["--log-db=%s" % logdb]
|
||||
if grep(build._server('tools/config.py'), 'log-db-level'):
|
||||
cmd += ["--log-db-level", '25']
|
||||
|
||||
if grep(build._server("tools/config.py"), "data-dir"):
|
||||
datadir = build._path('datadir')
|
||||
if not os.path.exists(datadir):
|
||||
os.mkdir(datadir)
|
||||
cmd += ["--data-dir", datadir]
|
||||
|
||||
# if build.branch_id.test_tags:
|
||||
# cmd.extend(['--test_tags', "'%s'" % build.branch_id.test_tags]) # keep for next version
|
||||
|
||||
return cmd, build.modules
|
||||
|
||||
def _spawn(self, cmd, lock_path, log_path, cpu_limit=None, shell=False, env=None):
|
||||
def preexec_fn():
|
||||
os.setsid()
|
||||
if cpu_limit:
|
||||
# set soft cpulimit
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
|
||||
r = resource.getrusage(resource.RUSAGE_SELF)
|
||||
cpu_time = r.ru_utime + r.ru_stime
|
||||
resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + cpu_limit, hard))
|
||||
# close parent files
|
||||
os.closerange(3, os.sysconf("SC_OPEN_MAX"))
|
||||
lock(lock_path)
|
||||
out = open(log_path, "w")
|
||||
_logger.debug("spawn: %s stdout: %s", ' '.join(cmd), log_path)
|
||||
p = subprocess.Popen(cmd, stdout=out, stderr=out, preexec_fn=preexec_fn, shell=shell, env=env, close_fds=False)
|
||||
return p.pid
|
||||
|
||||
def _github_status(self):
|
||||
"""Notify github of failed/successful builds"""
|
||||
runbot_domain = self.env['runbot.repo']._domain()
|
||||
for build in self:
|
||||
desc = "runbot build %s" % (build.dest,)
|
||||
if build.state == 'testing':
|
||||
state = 'pending'
|
||||
elif build.state in ('running', 'done'):
|
||||
state = 'error'
|
||||
if build.result == 'ok':
|
||||
state = 'success'
|
||||
if build.result == 'ko':
|
||||
state = 'failure'
|
||||
desc += " (runtime %ss)" % (build.job_time,)
|
||||
else:
|
||||
continue
|
||||
status = {
|
||||
"state": state,
|
||||
"target_url": "http://%s/runbot/build/%s" % (runbot_domain, build.id),
|
||||
"description": desc,
|
||||
"context": "ci/runbot"
|
||||
}
|
||||
_logger.debug("github updating status %s to %s", build.name, state)
|
||||
build.repo_id._github('/repos/:owner/:repo/statuses/%s' % build.name, status, ignore_errors=True)
|
||||
|
||||
# Jobs definitions
|
||||
# They all need "build, lock_pathn log_path" parameters
|
||||
def _job_00_init(self, build, lock_path, log_path):
|
||||
build._log('init', 'Init build environment')
|
||||
# notify pending build - avoid confusing users by saying nothing
|
||||
build._github_status()
|
||||
build._checkout()
|
||||
return -2
|
||||
|
||||
def _job_10_test_base(self, build, lock_path, log_path):
|
||||
build._log('test_base', 'Start test base module')
|
||||
# run base test
|
||||
self._local_pg_createdb("%s-base" % build.dest)
|
||||
cmd, mods = build._cmd()
|
||||
if grep(build._server("tools/config.py"), "test-enable"):
|
||||
cmd.append("--test-enable")
|
||||
cmd += ['-d', '%s-base' % build.dest, '-i', 'base', '--stop-after-init', '--log-level=test', '--max-cron-threads=0']
|
||||
return self._spawn(cmd, lock_path, log_path, cpu_limit=300)
|
||||
|
||||
def _job_20_test_all(self, build, lock_path, log_path):
|
||||
build._log('test_all', 'Start test all modules')
|
||||
self._local_pg_createdb("%s-all" % build.dest)
|
||||
cmd, mods = build._cmd()
|
||||
if grep(build._server("tools/config.py"), "test-enable"):
|
||||
cmd.append("--test-enable")
|
||||
cmd += ['-d', '%s-all' % build.dest, '-i', mods, '--stop-after-init', '--log-level=test', '--max-cron-threads=0']
|
||||
env = None
|
||||
if build.branch_id.coverage:
|
||||
env = self._coverage_env(build)
|
||||
available_modules = [
|
||||
os.path.basename(os.path.dirname(a))
|
||||
for a in (glob.glob(build._server('addons/*/__openerp__.py')) +
|
||||
glob.glob(build._server('addons/*/__manifest__.py')))
|
||||
]
|
||||
bad_modules = set(available_modules) - set((mods or '').split(','))
|
||||
omit = ['--omit', ','.join(build._server('addons', m) for m in bad_modules)] if bad_modules else []
|
||||
cmd = ['coverage', 'run', '--branch', '--source', build._server()] + omit + cmd[:]
|
||||
# reset job_start to an accurate job_20 job_time
|
||||
build.write({'job_start': now()})
|
||||
return self._spawn(cmd, lock_path, log_path, cpu_limit=2100, env=env)
|
||||
|
||||
def _coverage_env(self, build):
|
||||
return dict(os.environ, COVERAGE_FILE=build._path('.coverage'))
|
||||
|
||||
def _job_21_coverage(self, build, lock_path, log_path):
|
||||
if not build.branch_id.coverage:
|
||||
return -2
|
||||
cov_path = build._path('coverage')
|
||||
os.makedirs(cov_path, exist_ok=True)
|
||||
cmd = ["coverage", "html", "-d", cov_path, "--ignore-errors"]
|
||||
return self._spawn(cmd, lock_path, log_path, env=self._coverage_env(build))
|
||||
|
||||
def _job_30_run(self, build, lock_path, log_path):
|
||||
# adjust job_end to record an accurate job_20 job_time
|
||||
build._log('run', 'Start running build %s' % build.dest)
|
||||
log_all = build._path('logs', 'job_20_test_all.txt')
|
||||
log_time = time.localtime(os.path.getmtime(log_all))
|
||||
v = {
|
||||
'job_end': time2str(log_time),
|
||||
}
|
||||
if grep(log_all, ".modules.loading: Modules loaded."):
|
||||
if rfind(log_all, _re_error):
|
||||
v['result'] = "ko"
|
||||
elif rfind(log_all, _re_warning):
|
||||
v['result'] = "warn"
|
||||
elif not grep(build._server("test/common.py"), "post_install") or grep(log_all, "Initiating shutdown."):
|
||||
v['result'] = "ok"
|
||||
else:
|
||||
v['result'] = "ko"
|
||||
build.write(v)
|
||||
build._github_status()
|
||||
|
||||
# run server
|
||||
cmd, mods = build._cmd()
|
||||
if os.path.exists(build._server('addons/im_livechat')):
|
||||
cmd += ["--workers", "2"]
|
||||
cmd += ["--longpolling-port", "%d" % (build.port + 1)]
|
||||
cmd += ["--max-cron-threads", "1"]
|
||||
else:
|
||||
# not sure, to avoid old server to check other dbs
|
||||
cmd += ["--max-cron-threads", "0"]
|
||||
|
||||
cmd += ['-d', "%s-all" % build.dest]
|
||||
|
||||
if grep(build._server("tools/config.py"), "db-filter"):
|
||||
if build.repo_id.nginx:
|
||||
cmd += ['--db-filter', '%d.*$']
|
||||
else:
|
||||
cmd += ['--db-filter', '%s.*$' % build.dest]
|
||||
return self._spawn(cmd, lock_path, log_path, cpu_limit=None)
|
48
runbot/models/event.py
Normal file
48
runbot/models/event.py
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
TYPES = [(t, t.capitalize()) for t in 'client server runbot'.split()]
|
||||
|
||||
|
||||
class runbot_event(models.Model):
|
||||
|
||||
_inherit = "ir.logging"
|
||||
_order = 'id'
|
||||
|
||||
build_id = fields.Many2one('runbot.build', 'Build', index=True, ondelete='cascade')
|
||||
type = fields.Selection(TYPES, string='Type', required=True, index=True)
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
parent_class = super(runbot_event, self)
|
||||
if hasattr(parent_class, 'init'):
|
||||
parent_class.init()
|
||||
|
||||
self._cr.execute("""
|
||||
CREATE OR REPLACE FUNCTION runbot_set_logging_build() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF (new.build_id IS NULL AND new.dbname IS NOT NULL AND new.dbname != current_database()) THEN
|
||||
UPDATE ir_logging l
|
||||
SET build_id = split_part(new.dbname, '-', 1)::integer
|
||||
WHERE l.id = new.id;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TRIGGER runbot_new_logging
|
||||
AFTER INSERT ON ir_logging
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE runbot_set_logging_build();
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN
|
||||
END;
|
||||
$$;
|
||||
""")
|
309
runbot/models/repo.py
Normal file
309
runbot/models/repo.py
Normal file
@ -0,0 +1,309 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import dateutil
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.modules.module import get_module_resource
|
||||
from odoo.tools import config
|
||||
from ..common import fqdn, dt2time
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class runbot_repo(models.Model):
|
||||
|
||||
_name = "runbot.repo"
|
||||
|
||||
name = fields.Char('Repository', required=True)
|
||||
# branch_ids = fields.One2many('runbot.branch', inverse_name='repo_id') # keep for next version
|
||||
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
|
||||
nginx = fields.Boolean('Nginx')
|
||||
mode = fields.Selection([('disabled', 'Disabled'),
|
||||
('poll', 'Poll'),
|
||||
('hook', 'Hook')],
|
||||
default='poll',
|
||||
string="Mode", required=True, help="hook: Wait for webhook on /runbot/hook/<id> i.e. github push event")
|
||||
hook_time = fields.Datetime('Last hook time')
|
||||
duplicate_id = fields.Many2one('runbot.repo', 'Duplicate repo', help='Repository for finding duplicate builds')
|
||||
modules = fields.Char("Modules to install", help="Comma-separated list of modules to install and test.")
|
||||
modules_auto = fields.Selection([('none', 'None (only explicit modules list)'),
|
||||
('repo', 'Repository modules (excluding dependencies)'),
|
||||
('all', 'All modules (including dependencies)')],
|
||||
default='repo',
|
||||
string="Other modules to install automatically")
|
||||
|
||||
dependency_ids = fields.Many2many(
|
||||
'runbot.repo', 'runbot_repo_dep_rel', column1='dependant_id', column2='dependency_id',
|
||||
string='Extra dependencies',
|
||||
help="Community addon repos which need to be present to run tests.")
|
||||
token = fields.Char("Github token", groups="runbot.group_runbot_admin")
|
||||
group_ids = fields.Many2many('res.groups', string='Limited to groups')
|
||||
|
||||
def _root(self):
|
||||
"""Return root directory of repository"""
|
||||
default = os.path.join(os.path.dirname(__file__), '../static')
|
||||
return os.path.abspath(default)
|
||||
|
||||
@api.depends('name')
|
||||
def _get_path(self):
|
||||
"""compute the server path of repo from the name"""
|
||||
root = self._root()
|
||||
for repo in self:
|
||||
name = repo.name
|
||||
for i in '@:/':
|
||||
name = name.replace(i, '_')
|
||||
repo.path = os.path.join(root, 'repo', name)
|
||||
|
||||
@api.depends('name')
|
||||
def _get_base_url(self):
|
||||
for repo in self:
|
||||
name = re.sub('.+@', '', repo.name)
|
||||
name = re.sub('^https://', '', repo.name) # support https repo style
|
||||
name = re.sub('.git$', '', name)
|
||||
name = name.replace(':', '/')
|
||||
repo.base = name
|
||||
|
||||
def _git(self, cmd):
|
||||
"""Execute a git command 'cmd'"""
|
||||
for repo in self:
|
||||
cmd = ['git', '--git-dir=%s' % repo.path] + cmd
|
||||
_logger.info("git command: %s", ' '.join(cmd))
|
||||
return subprocess.check_output(cmd)
|
||||
|
||||
def _git_export(self, treeish, dest):
|
||||
"""Export a git repo to dest"""
|
||||
self.ensure_one()
|
||||
_logger.debug('checkout %s %s %s', self.name, treeish, dest)
|
||||
p1 = subprocess.Popen(['git', '--git-dir=%s' % self.path, 'archive', treeish], stdout=subprocess.PIPE)
|
||||
p2 = subprocess.Popen(['tar', '-xmC', dest], stdin=p1.stdout, stdout=subprocess.PIPE)
|
||||
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
|
||||
p2.communicate()[0]
|
||||
|
||||
def _github(self, url, payload=None, ignore_errors=False):
|
||||
"""Return a http request to be sent to github"""
|
||||
for repo in self:
|
||||
if not repo.token:
|
||||
return
|
||||
try:
|
||||
match_object = re.search('([^/]+)/([^/]+)/([^/.]+(.git)?)', repo.base)
|
||||
if match_object:
|
||||
url = url.replace(':owner', match_object.group(2))
|
||||
url = url.replace(':repo', match_object.group(3))
|
||||
url = 'https://api.%s%s' % (match_object.group(1), url)
|
||||
session = requests.Session()
|
||||
session.auth = (repo.token, 'x-oauth-basic')
|
||||
session.headers.update({'Accept': 'application/vnd.github.she-hulk-preview+json'})
|
||||
if payload:
|
||||
response = session.post(url, data=json.dumps(payload))
|
||||
else:
|
||||
response = session.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception:
|
||||
if ignore_errors:
|
||||
_logger.exception('Ignored github error %s %r', url, payload)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _update_git(self):
|
||||
""" Update the git repo on FS """
|
||||
self.ensure_one()
|
||||
repo = self
|
||||
_logger.debug('repo %s updating branches', repo.name)
|
||||
|
||||
icp = self.env['ir.config_parameter']
|
||||
max_age = int(icp.get_param('runbot.max_age', default=30))
|
||||
|
||||
Build = self.env['runbot.build']
|
||||
Branch = self.env['runbot.branch']
|
||||
|
||||
if not os.path.isdir(os.path.join(repo.path)):
|
||||
os.makedirs(repo.path)
|
||||
if not os.path.isdir(os.path.join(repo.path, 'refs')):
|
||||
_logger.info("Cloning repository '%s' in '%s'" % (repo.name, repo.path))
|
||||
subprocess.call(['git', 'clone', '--bare', repo.name, repo.path])
|
||||
|
||||
# check for mode == hook
|
||||
fname_fetch_head = os.path.join(repo.path, 'FETCH_HEAD')
|
||||
if os.path.isfile(fname_fetch_head):
|
||||
fetch_time = os.path.getmtime(fname_fetch_head)
|
||||
if repo.mode == 'hook' and repo.hook_time and dt2time(repo.hook_time) < fetch_time:
|
||||
t0 = time.time()
|
||||
_logger.debug('repo %s skip hook fetch fetch_time: %ss ago hook_time: %ss ago',
|
||||
repo.name, int(t0 - fetch_time), int(t0 - dt2time(repo.hook_time)))
|
||||
return
|
||||
|
||||
repo._git(['fetch', '-p', 'origin', '+refs/heads/*:refs/heads/*'])
|
||||
repo._git(['fetch', '-p', 'origin', '+refs/pull/*/head:refs/pull/*'])
|
||||
|
||||
fields = ['refname', 'objectname', 'committerdate:iso8601', 'authorname', 'authoremail', 'subject', 'committername', 'committeremail']
|
||||
fmt = "%00".join(["%(" + field + ")" for field in fields])
|
||||
git_refs = repo._git(['for-each-ref', '--format', fmt, '--sort=-committerdate', 'refs/heads', 'refs/pull'])
|
||||
git_refs = git_refs.decode('utf-8').strip()
|
||||
|
||||
refs = [[field for field in line.split('\x00')] for line in git_refs.split('\n')]
|
||||
|
||||
self.env.cr.execute("""
|
||||
WITH t (branch) AS (SELECT unnest(%s))
|
||||
SELECT t.branch, b.id
|
||||
FROM t LEFT JOIN runbot_branch b ON (b.name = t.branch)
|
||||
WHERE b.repo_id = %s;
|
||||
""", ([r[0] for r in refs], repo.id))
|
||||
ref_branches = {r[0]: r[1] for r in self.env.cr.fetchall()}
|
||||
|
||||
for name, sha, date, author, author_email, subject, committer, committer_email in refs:
|
||||
# create or get branch
|
||||
# branch = repo.branch_ids.search([('name', '=', name), ('repo_id', '=', repo.id)])
|
||||
# if not branch:
|
||||
# _logger.debug('repo %s found new branch %s', repo.name, name)
|
||||
# branch = self.branch_ids.create({
|
||||
# 'repo_id': repo.id,
|
||||
# 'name': name})
|
||||
# keep for next version with a branch_ids field
|
||||
|
||||
if ref_branches.get(name):
|
||||
branch_id = ref_branches[name]
|
||||
else:
|
||||
_logger.debug('repo %s found new branch %s', repo.name, name)
|
||||
branch_id = Branch.create({'repo_id': repo.id, 'name': name}).id
|
||||
branch = Branch.browse([branch_id])[0]
|
||||
|
||||
# skip the build for old branches (Could be checked before creating the branch in DB ?)
|
||||
if dateutil.parser.parse(date[:19]) + datetime.timedelta(days=max_age) < datetime.datetime.now():
|
||||
continue
|
||||
|
||||
# create build (and mark previous builds as skipped) if not found
|
||||
build_ids = Build.search([('branch_id', '=', branch.id), ('name', '=', sha)])
|
||||
if not build_ids:
|
||||
_logger.debug('repo %s branch %s new build found revno %s', branch.repo_id.name, branch.name, sha)
|
||||
build_info = {
|
||||
'branch_id': branch.id,
|
||||
'name': sha,
|
||||
'author': author,
|
||||
'author_email': author_email,
|
||||
'committer': committer,
|
||||
'committer_email': committer_email,
|
||||
'subject': subject,
|
||||
'date': dateutil.parser.parse(date[:19]),
|
||||
}
|
||||
if not branch.sticky:
|
||||
# pending builds are skipped as we have a new ref
|
||||
builds_to_skip = Build.search(
|
||||
[('branch_id', '=', branch.id), ('state', '=', 'pending')],
|
||||
order='sequence asc')
|
||||
builds_to_skip._skip()
|
||||
if builds_to_skip:
|
||||
build_info['sequence'] = builds_to_skip[0].sequence
|
||||
Build.create(build_info)
|
||||
|
||||
# skip old builds (if their sequence number is too low, they will not ever be built)
|
||||
skippable_domain = [('repo_id', '=', repo.id), ('state', '=', 'pending')]
|
||||
icp = self.env['ir.config_parameter']
|
||||
running_max = int(icp.get_param('runbot.running_max', default=75))
|
||||
builds_to_be_skipped = Build.search(skippable_domain, order='sequence desc', offset=running_max)
|
||||
builds_to_be_skipped._skip()
|
||||
|
||||
def _update(self, repos):
|
||||
""" Update the physical git reposotories on FS"""
|
||||
for repo in repos:
|
||||
try:
|
||||
repo._update_git()
|
||||
except Exception:
|
||||
_logger.exception('Fail to update repo %s', repo.name)
|
||||
|
||||
def _scheduler(self, ids=None):
|
||||
"""Schedule builds for the repository"""
|
||||
icp = self.env['ir.config_parameter']
|
||||
workers = int(icp.get_param('runbot.workers', default=6))
|
||||
running_max = int(icp.get_param('runbot.running_max', default=75))
|
||||
host = fqdn()
|
||||
|
||||
Build = self.env['runbot.build']
|
||||
domain = [('repo_id', 'in', ids)]
|
||||
domain_host = domain + [('host', '=', host)]
|
||||
|
||||
# schedule jobs (transitions testing -> running, kill jobs, ...)
|
||||
build_ids = Build.search(domain_host + [('state', 'in', ['testing', 'running', 'deathrow'])])
|
||||
build_ids._schedule()
|
||||
|
||||
# launch new tests
|
||||
testing = Build.search_count(domain_host + [('state', '=', 'testing')])
|
||||
pending = Build.search_count(domain + [('state', '=', 'pending')])
|
||||
|
||||
while testing < workers and pending > 0:
|
||||
|
||||
# find sticky pending build if any, otherwise, last pending (by id, not by sequence) will do the job
|
||||
|
||||
pending_ids = Build.search(domain + [('state', '=', 'pending'), ('branch_id.sticky', '=', True)], limit=1)
|
||||
if not pending_ids:
|
||||
pending_ids = Build.search(domain + [('state', '=', 'pending')], order="sequence", limit=1)
|
||||
|
||||
pending_ids._schedule()
|
||||
|
||||
# compute the number of testing and pending jobs again
|
||||
testing = Build.search_count(domain_host + [('state', '=', 'testing')])
|
||||
pending = Build.search_count(domain + [('state', '=', 'pending')])
|
||||
|
||||
# terminate and reap doomed build
|
||||
build_ids = Build.search(domain_host + [('state', '=', 'running')]).ids
|
||||
# sort builds: the last build of each sticky branch then the rest
|
||||
sticky = {}
|
||||
non_sticky = []
|
||||
for build in Build.browse(build_ids):
|
||||
if build.branch_id.sticky and build.branch_id.id not in sticky:
|
||||
sticky[build.branch_id.id] = build.id
|
||||
else:
|
||||
non_sticky.append(build.id)
|
||||
build_ids = list(sticky.values())
|
||||
build_ids += non_sticky
|
||||
# terminate extra running builds
|
||||
Build.browse(build_ids)[running_max:]._kill()
|
||||
Build.browse(build_ids)._reap()
|
||||
|
||||
def _domain(self):
|
||||
return self.env.get('ir.config_parameter').get_param('runbot.runbot_domain', fqdn())
|
||||
|
||||
def _reload_nginx(self):
|
||||
settings = {}
|
||||
settings['port'] = config.get('http_port')
|
||||
settings['runbot_static'] = os.path.join(get_module_resource('runbot', 'static'), '')
|
||||
nginx_dir = os.path.join(self._root(), 'nginx')
|
||||
settings['nginx_dir'] = nginx_dir
|
||||
settings['re_escape'] = re.escape
|
||||
nginx_repos = self.search([('nginx', '=', True)], order='id')
|
||||
if nginx_repos:
|
||||
builds = self.env['runbot.build'].search([('repo_id', 'in', nginx_repos.ids), ('state', '=', 'running')])
|
||||
settings['builds'] = self.env['runbot.build'].browse(builds.ids)
|
||||
|
||||
nginx_config = self.env['ir.ui.view'].render_template("runbot.nginx_config", settings)
|
||||
os.makedirs(nginx_dir, exist_ok=True)
|
||||
open(os.path.join(nginx_dir, 'nginx.conf'), 'wb').write(nginx_config)
|
||||
try:
|
||||
_logger.debug('reload nginx')
|
||||
pid = int(open(os.path.join(nginx_dir, 'nginx.pid')).read().strip(' \n'))
|
||||
os.kill(pid, signal.SIGHUP)
|
||||
except Exception:
|
||||
_logger.debug('start nginx')
|
||||
if subprocess.check_output(['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf']):
|
||||
# obscure nginx bug leaving orphan worker listening on nginx port
|
||||
if not subprocess.check_output(['pkill', '-f', '-P1', 'nginx: worker']):
|
||||
_logger.debug('failed to start nginx - orphan worker killed, retrying')
|
||||
subprocess.call(['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf'])
|
||||
else:
|
||||
_logger.debug('failed to start nginx - failed to kill orphan worker - oh well')
|
||||
|
||||
def _cron(self):
|
||||
repos = self.search([('mode', '!=', 'disabled')])
|
||||
self._update(repos)
|
||||
self._scheduler(repos.ids)
|
||||
self._reload_nginx()
|
38
runbot/models/res_config_settings.py
Normal file
38
runbot/models/res_config_settings.py
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .. import common
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
runbot_workers = fields.Integer('Total number of workers')
|
||||
runbot_running_max = fields.Integer('Maximum number of running builds')
|
||||
runbot_timeout = fields.Integer('Default timeout (in seconds)')
|
||||
runbot_starting_port = fields.Integer('Starting port for running builds')
|
||||
runbot_domain = fields.Char('Runbot domain')
|
||||
runbot_max_age = fields.Integer('Max branch age (in days)')
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
res.update(runbot_workers=int(get_param('runbot.runbot_workers', default=6)),
|
||||
runbot_running_max=int(get_param('runbot.runbot_running_max', default=75)),
|
||||
runbot_timeout=int(get_param('runbot.runbot_timeout', default=1800)),
|
||||
runbot_starting_port=int(get_param('runbot.runbot_starting_port', default=2000)),
|
||||
runbot_domain=get_param('runbot.runbot_domain', default=common.fqdn()),
|
||||
runbot_max_age=int(get_param('runbot.runbot_max_age', default=30)))
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||
set_param("runbot.runbot_workers", self.runbot_workers)
|
||||
set_param("runbot.runbot_running_max", self.runbot_running_max)
|
||||
set_param("runbot.runbot_timeout", self.runbot_timeout)
|
||||
set_param("runbot.runbot_starting_port", self.runbot_starting_port)
|
||||
set_param("runbot.runbot_domain", self.runbot_domain)
|
||||
set_param("runbot.runbot_max_age", self.runbot_max_age)
|
@ -1,60 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
|
||||
class runbot_config_settings(osv.osv_memory):
|
||||
_name = 'runbot.config.settings'
|
||||
_inherit = 'res.config.settings'
|
||||
_columns = {
|
||||
'default_workers': fields.integer('Total Number of Workers'),
|
||||
'default_running_max': fields.integer('Maximum Number of Running Builds'),
|
||||
'default_timeout': fields.integer('Default Timeout (in seconds)'),
|
||||
'default_starting_port': fields.integer('Starting Port for Running Builds'),
|
||||
'default_domain': fields.char('Runbot Domain'),
|
||||
}
|
||||
|
||||
def get_default_parameters(self, cr, uid, fields, context=None):
|
||||
icp = self.pool['ir.config_parameter']
|
||||
workers = icp.get_param(cr, uid, 'runbot.workers', default=6)
|
||||
running_max = icp.get_param(cr, uid, 'runbot.running_max', default=75)
|
||||
timeout = icp.get_param(cr, uid, 'runbot.timeout', default=1800)
|
||||
starting_port = icp.get_param(cr, uid, 'runbot.starting_port', default=2000)
|
||||
runbot_domain = icp.get_param(cr, uid, 'runbot.domain', default='runbot.odoo.com')
|
||||
return {
|
||||
'default_workers': int(workers),
|
||||
'default_running_max': int(running_max),
|
||||
'default_timeout': int(timeout),
|
||||
'default_starting_port': int(starting_port),
|
||||
'default_domain': runbot_domain,
|
||||
}
|
||||
|
||||
def set_default_parameters(self, cr, uid, ids, context=None):
|
||||
config = self.browse(cr, uid, ids[0], context)
|
||||
icp = self.pool['ir.config_parameter']
|
||||
icp.set_param(cr, uid, 'runbot.workers', config.default_workers)
|
||||
icp.set_param(cr, uid, 'runbot.running_max', config.default_running_max)
|
||||
icp.set_param(cr, uid, 'runbot.timeout', config.default_timeout)
|
||||
icp.set_param(cr, uid, 'runbot.starting_port', config.default_starting_port)
|
||||
icp.set_param(cr, uid, 'runbot.domain', config.default_domain)
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_runbot_configuration" model="ir.ui.view">
|
||||
<field name="name">Configure Runbot</field>
|
||||
<field name="model">runbot.config.settings</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configure Runbot" class= "oe_form_configuration" version="7.0">
|
||||
<header>
|
||||
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
|
||||
</header>
|
||||
<separator string="Runbot"/>
|
||||
<group>
|
||||
<label for="id" string="Settings"/>
|
||||
<div>
|
||||
<div>
|
||||
<field name="default_workers" class="oe_inline"/>
|
||||
<label for="default_workers"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="default_running_max" class="oe_inline"/>
|
||||
<label for="default_running_max"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="default_timeout" class="oe_inline"/>
|
||||
<label for="default_timeout"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="default_starting_port" class="oe_inline"/>
|
||||
<label for="default_starting_port"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="default_domain" class="oe_inline"/>
|
||||
<label for="default_domain"/>
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_runbot_configuration" model="ir.actions.act_window">
|
||||
<field name="name">Configure Runbot</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
</record>
|
||||
<menuitem id="menu_runbot_configuration" name="Runbot" parent="base.menu_config"
|
||||
sequence="19" action="action_runbot_configuration"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
1739
runbot/runbot.py
1739
runbot/runbot.py
File diff suppressed because it is too large
Load Diff
@ -1,785 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<menuitem id="menu_runbot_top" name="Runbot"/>
|
||||
<menuitem id="menu_runbot" name="Runbot" parent="menu_runbot_top"/>
|
||||
|
||||
<!-- repos -->
|
||||
<record id="view_repo_form" model="ir.ui.view">
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="repos" version="7.0">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" class="oe_inline"/></h1>
|
||||
<button name="update" type="object" string="Update" groups="runbot.group_runbot_admin"/>
|
||||
<button name="cron" type="object" string="Cron" groups="runbot.group_runbot_admin"/>
|
||||
</div>
|
||||
<group string="Params">
|
||||
<field name="sequence"/>
|
||||
<field name="mode"/>
|
||||
<field name="nginx"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="dependency_ids" widget="many2many_tags"/>
|
||||
<field name="modules"/>
|
||||
<field name="modules_auto"/>
|
||||
<field name="token"/>
|
||||
<field name="group_ids" widget="many2many_tags"/>
|
||||
<field name="hook_time" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_repo_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="repos">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="mode"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_repo_form" model="ir.actions.act_window">
|
||||
<field name="name">Repositories</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.repo</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="menu_repo_form" action="action_repo_form" parent="menu_runbot"/>
|
||||
|
||||
<!-- Branches -->
|
||||
<record id="view_branch_form" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Branch" version="7.0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="branch_url"/>
|
||||
<field name="pull_head_name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="job_timeout"/>
|
||||
<field name="state"/>
|
||||
<field name="modules"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_branch_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Branches">
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_branch_search" model="ir.ui.view">
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<filter string="Sticky" domain="[('sticky','=', True)]"/>
|
||||
<separator />
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Repo" domain="[]" context="{'group_by':'repo_id'}"/>
|
||||
<filter string="Status" domain="[]" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_branch" model="ir.actions.act_window">
|
||||
<field name="name">Branches</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.branch</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="menu_branch" action="action_branch" parent="menu_runbot"/>
|
||||
|
||||
<!-- Builds -->
|
||||
<record id="view_build_form" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build" version="7.0">
|
||||
<header>
|
||||
<button name="reset" type="object" string="Reset"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="author_email"/>
|
||||
<field name="committer"/>
|
||||
<field name="committer_email"/>
|
||||
<field name="subject"/>
|
||||
<field name="port"/>
|
||||
<field name="dest"/>
|
||||
<field name="state"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="host"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_end"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="modules"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Builds">
|
||||
<field name="sequence"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="committer"/>
|
||||
<field name="state"/>
|
||||
<field name="port"/>
|
||||
<field name="job"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="host"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_graph" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Builds" type="pivot">
|
||||
<field name="create_date" interval="week" type="row"/>
|
||||
<field name="state" type="col"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_search" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="branch_id"/>
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="dest"/>
|
||||
<separator/>
|
||||
<filter string="Pending" domain="[('state','=', 'pending')]"/>
|
||||
<filter string="Testing" domain="[('state','=', 'testing')]"/>
|
||||
<filter string="Running" domain="[('state','=', 'running')]"/>
|
||||
<filter string="Done" domain="[('state','=','done')]"/>
|
||||
<filter string="Duplicate" domain="[('state','=', 'duplicate')]"/>
|
||||
<filter string="Deathrow" domain="[('state','=', 'deathrow')]"/>
|
||||
<separator />
|
||||
<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':'state'}"/>
|
||||
<filter string="Result" domain="[]" context="{'group_by':'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'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_build" model="ir.actions.act_window">
|
||||
<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">graph,tree,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_build" action="action_build" parent="menu_runbot"/>
|
||||
|
||||
<!-- Events -->
|
||||
<record id="logging_action" model="ir.actions.act_window">
|
||||
<field name="name">Events</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">ir.logging</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<menuitem id="logging_menu" action="logging_action" parent="menu_runbot"/>
|
||||
|
||||
|
||||
<!-- Website menu -->
|
||||
<record id="website_menu" model="website.menu">
|
||||
<field name="name">Runbot</field>
|
||||
<field name="url">/runbot</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Templates -->
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend" name="runbot.assets.frontend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/octicons/2.0.2/octicons.css"/>
|
||||
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- 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.name.split(':')[1]"/></a></li>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- remove black bar with switcher d'apps -->
|
||||
<template id="inherits_no_black_bar" inherit_id="website.user_navbar" name="Inherits No black user_navbar">
|
||||
<xpath expr="//t[@t-if="website and menu_data"]" position="attributes">
|
||||
<attribute name="groups">base.group_website_publisher</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build_name">
|
||||
<t t-if="bu['state']=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
|
||||
<t t-if="bu['state']=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu['state']=='testing'">
|
||||
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu['guess_result'], 'info')"/>
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> testing</span> <t t-esc="bu['job']"/> <small t-if="not hide_time"><t t-esc="bu['job_time']"/></small>
|
||||
</t>
|
||||
<t t-if="bu['result']=='ok'"><i class="text-success fa fa-thumbs-up"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='ko'"><i class="text-danger fa fa-thumbs-down"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='warn'"><i class="text-warning fa fa-warning"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
|
||||
<t t-if="bu['result']=='killed'"><i class="text-danger fa fa-times"/> killed</t>
|
||||
<t t-if="bu['result']=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
|
||||
|
||||
<t t-if="bu['server_match'] in ('default', 'fuzzy')">
|
||||
<i class="text-warning fa fa-question-circle fa-fw"
|
||||
title="Server branch cannot be determined exactly. Please use naming convention '9.0-my-branch' to build with '9.0' server branch."/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build_button">
|
||||
<div t-attf-class="pull-right">
|
||||
<div t-attf-class="btn-group {{klass}}">
|
||||
<a t-if="bu['state']=='running'" t-attf-href="http://{{bu['domain']}}/?db={{bu['dest']}}-all" class="btn btn-primary"><i class="fa fa-sign-in"/></a>
|
||||
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default"><i class="fa fa-file-text-o"/></a>
|
||||
<a t-attf-href="https://#{repo.base}/commit/#{bu['name']}" class="btn btn-default"><i class="fa fa-github"/></a>
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"><i class="fa fa-cog"/><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-if="bu['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>
|
||||
<t t-if="bu['state']=='running'">
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_dest']}}-all">Connect all <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_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['state'] in ['done','running','deathrow'] and bu_index==0" groups="runbot.group_user">
|
||||
<a href="#" class="runbot-rebuild" t-att-data-runbot-build="bu['id']">Rebuild <i class="fa fa-refresh"/></a>
|
||||
</li>
|
||||
<li t-if="bu['state'] in ['pending','testing','running']" groups="runbot.group_user">
|
||||
<a href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
|
||||
</li>
|
||||
<li t-if="bu['state']!='testing' and bu['state']!='pending'" class="divider"></li>
|
||||
<li><a t-attf-href="/runbot/build/{{bu['id']}}">Logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/logs/job_10_test_base.txt">Full base logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/logs/job_20_test_all.txt">Full all logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['coverage'] and bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/coverage/index.html">Coverage <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['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="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 -->
|
||||
<li class="divider"></li>
|
||||
<li class="disabled"><a href="#">Runtime: <t t-esc="bu['job_time']"/>s</a></li>
|
||||
<li class="disabled"><a href="#">Port: <t t-esc="bu['port']"/></a></li>
|
||||
<li class="disabled"><a href="#">Age: <t t-esc="bu['job_age']"/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="runbot.repo">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</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"><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>
|
||||
<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>
|
||||
<li class="divider"></li>
|
||||
<li t-att-class="'active' if limit=='100' else ''"><a t-att-href="qu(limit=100)">Show last 100</a></li>
|
||||
<li t-att-class="'active' if limit=='1000' else ''"><a t-att-href="qu(limit=1000)">Show last 1000</a></li>
|
||||
<li t-att-class="'active' if limit=='10000' else ''"><a t-att-href="qu(limit=10000)">Show last 10000</a></li>
|
||||
</ul>
|
||||
</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, <t t-esc="hs['running']"/> running
|
||||
</span>&nbsp;
|
||||
</t>
|
||||
<span class="label label-info">Pending: <t t-esc="pending_total"/></span>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
<div t-if="not repo" class="mb32">
|
||||
<h1>No Repository yet.</h1>
|
||||
</div>
|
||||
|
||||
<table t-if="repo" class="table table-condensed table-bordered">
|
||||
<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>
|
||||
|
||||
</tr>
|
||||
<tr t-foreach="branches" t-as="br">
|
||||
<td>
|
||||
<i t-if="br['branch'].sticky" class="fa fa-star" style="color: #f0ad4e" />
|
||||
<b t-esc="br['branch'].branch_name"/>
|
||||
<small><t t-esc="br['builds'][0]['job_age']"/></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>
|
||||
<a t-attf-href="/runbot/#{repo.id}/#{br['branch'].branch_name}" class="btn btn-default btn-xs"><i class="fa fa-fast-forward" title="Quick Connect"/></a>
|
||||
</div>
|
||||
</td>
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu['state']=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state']=='testing'"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu['state']=='deathrow'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'skipped'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t>
|
||||
<td t-attf-class="{{klass}}">
|
||||
<t t-call="runbot.build_button"><t t-set="klass">btn-group-sm</t></t>
|
||||
<t t-if="bu['subject']">
|
||||
<span t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-id="bu['author']">
|
||||
<t t-esc="bu['author']"/>
|
||||
<t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']">
|
||||
(<span class="octicon octicon-arrow-right"></span>&nbsp;<t t-esc="bu['committer']"/>)
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<small><t t-esc="bu['dest']"/> on <t t-esc="bu['host']"/></small><br/>
|
||||
<t t-call="runbot.build_name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="runbot.sticky-dashboard">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.bg-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
h4 {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
.r-mb02 { margin-bottom: 0.2em; }
|
||||
</style>
|
||||
</t>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div class="container-fluid">
|
||||
<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, <t t-esc="hs['running']"/> running
|
||||
</span>&nbsp;
|
||||
</t>
|
||||
<span class="label label-info">Pending: <t t-esc="pending_total"/></span>
|
||||
</p>
|
||||
</div>
|
||||
<t t-foreach="repo_dict.values()" t-as="repo">
|
||||
<h4><span><t t-esc="repo['name']"/></span>
|
||||
<small class="pull-right">
|
||||
<t t-esc="repo['testing']"/> testing,
|
||||
<t t-esc="repo['running']"/> running,
|
||||
<t t-esc="repo['pending']"/> pending.
|
||||
</small></h4>
|
||||
<div t-foreach="repo['branches'].values()" t-as="br">
|
||||
<div class="col-md-1">
|
||||
<b t-esc="br['name']"/><br/>
|
||||
<small><t t-esc="br['builds'][0]['job_age']"/></small>
|
||||
</div>
|
||||
<div class="col-md-11 r-mb02">
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu['state']=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state']=='testing'"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu['state']=='deathrow'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'skipped'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t>
|
||||
<div t-attf-class="bg-{{klass}} col-md-4">
|
||||
<i class="fa fa-at"></i>
|
||||
<t t-esc="bu['author']"/>
|
||||
<t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']">
|
||||
(<i class="fa fa-sign-out"></i>&nbsp;<t t-esc="bu['committer']"/>)
|
||||
</t>
|
||||
<br/>
|
||||
<i class="fa fa-envelope-o"></i>
|
||||
<a t-attf-href="https://#{repo['base']}/commit/#{bu['name']}"><t t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/></a>
|
||||
<br/>
|
||||
<t t-call="runbot.build_name"/> — <small><a t-attf-href="/runbot/build/{{bu['id']}}"><t t-esc="bu['dest']"/></a> on <t t-esc="bu['host']"/> <a t-if="bu['state'] == 'running'" t-attf-href="http://{{bu['domain']}}/?db={{bu['dest']}}-all"><i class="fa fa-sign-in"></i></a></small>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build">
|
||||
<t t-call='website.layout'>
|
||||
<div class="container" style="width: 100%;">
|
||||
<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="bu_index" t-value="-1"/>
|
||||
<t t-set="klass" t-value="''"/>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
<p class="navbar-text">
|
||||
</p>
|
||||
<form class="navbar-form navbar-left form-inline">
|
||||
<div class="btn-group">
|
||||
<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 role="presentation" class="dropdown-header">Level</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='debug')"><i t-if="level == 'debug'" class="fa fa-check"/> Debug</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='info')"><i t-if="level == 'info'" class="fa fa-check"/> Info</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='warning')"><i t-if="level == 'warning'" class="fa fa-check"/> Warning</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='error')"><i t-if="level == 'error'" class="fa fa-check"/> Error</a></li>
|
||||
<li role="presentation" class="dropdown-header">Type</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='runbot')"><i t-if="type == 'runbot'" class="fa fa-check"/> Runbot</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='server')"><i t-if="type == 'server'" class="fa fa-check"/> Server</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='client')"><i t-if="type == 'client'" class="fa fa-check"/> Client</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default">Expand</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Other builds <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<t t-foreach='other_builds' t-as='other_build'>
|
||||
<li><a t-attf-href="/runbot/build/{{other_build.id}}">
|
||||
<t t-esc='other_build.dest'/>
|
||||
<t t-call="runbot.build_name">
|
||||
<t t-set="bu" t-value="other_build"/>
|
||||
<t t-set="hide_time" t-value="True"></t>
|
||||
</t>
|
||||
</a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" t-attf-action="/runbot/build/{{build['id']}}" method="get">
|
||||
<div class="form-group">
|
||||
<input type="search" name="search" class="form-control" placeholder="Search" t-att-value="search or ''"/>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<p>
|
||||
Subject: <t t-esc="build['subject']"/><br/>
|
||||
Author: <t t-esc="build['author']"/><br/>
|
||||
Committer: <t t-esc="build['committer']"/><br/>
|
||||
</p>
|
||||
<p t-if="build['duplicate_of']">Duplicate of <a t-attf-href="/runbot/build/#{build['duplicate_of'].id}"><t t-esc="build['duplicate_of'].dest"/></a></p>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Level</th>
|
||||
<th>Type</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<t t-foreach="logs" t-as="l">
|
||||
<tr t-att-class="dict(ERROR='danger', WARNING='warning').get(l.level)">
|
||||
<td style="white-space: nowrap;"><t t-esc="l.create_date"/></td>
|
||||
<td><b t-esc="l.level"/></td>
|
||||
<td><t t-esc="l.type"/></td>
|
||||
<td>
|
||||
<a t-if="l.type != 'runbot'" t-attf-href="https://{{repo.base}}/blob/{{build['name']}}/{{l.path}}#L{{l.line}}"><t t-esc="l.name"/>:<t t-esc="l.line"/></a> <t t-esc="l.func"/>
|
||||
<t t-if="'\n' not in l.message"><t t-esc="l.message"/></t>
|
||||
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-esc="l.message"/></pre>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.webclient_config">
|
||||
[global]
|
||||
server.environment = "development"
|
||||
server.socket_host = "0.0.0.0"
|
||||
server.socket_port = %d
|
||||
server.thread_pool = 10
|
||||
tools.sessions.on = True
|
||||
log.access_level = "INFO"
|
||||
log.error_level = "INFO"
|
||||
tools.csrf.on = False
|
||||
tools.log_tracebacks.on = False
|
||||
tools.cgitb.on = True
|
||||
openerp.server.host = 'localhost'
|
||||
openerp.server.port = '%d'
|
||||
openerp.server.protocol = 'socket'
|
||||
openerp.server.timeout = 450
|
||||
[openerp-web]
|
||||
dblist.filter = 'BOTH'
|
||||
dbbutton.visible = True
|
||||
company.url = ''
|
||||
openerp.server.host = 'localhost'
|
||||
openerp.server.port = '%d'
|
||||
openerp.server.protocol = 'socket'
|
||||
openerp.server.timeout = 450
|
||||
</template>
|
||||
|
||||
<template id="runbot.nginx_config">
|
||||
pid <t t-esc="nginx_dir"/>/nginx.pid;
|
||||
error_log <t t-esc="nginx_dir"/>/error.log;
|
||||
worker_processes 1;
|
||||
events { worker_connections 1024; }
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
server_names_hash_max_size 512;
|
||||
server_names_hash_bucket_size 256;
|
||||
client_max_body_size 10M;
|
||||
index index.html;
|
||||
log_format full '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" $request_time';
|
||||
access_log <t t-esc="nginx_dir"/>/access.log full;
|
||||
error_log <t t-esc="nginx_dir"/>/error.log;
|
||||
client_body_temp_path <t t-esc="nginx_dir"/>;
|
||||
fastcgi_temp_path <t t-esc="nginx_dir"/>;
|
||||
|
||||
autoindex on;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css text/plain application/xml application/json application/javascript;
|
||||
|
||||
proxy_temp_path <t t-esc="nginx_dir"/>;
|
||||
proxy_read_timeout 600;
|
||||
proxy_connect_timeout 600;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
server {
|
||||
listen 8080 default;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="port"/>; }
|
||||
location /longpolling/im/poll { return 404; }
|
||||
location /longpolling/poll { return 404; }
|
||||
location /runbot/static/ {
|
||||
alias <t t-esc="runbot_static"/>;
|
||||
autoindex off;
|
||||
location ~ /runbot/static/build/[^/]+/logs/ {
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
}
|
||||
<t t-foreach="builds" t-as="build">
|
||||
server {
|
||||
listen 8080;
|
||||
server_name ~^<t t-raw="re_escape(build.dest)"/>[-.].*$;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="build.port"/>; }
|
||||
location /longpolling { proxy_pass http://127.0.0.1:<t t-esc="build.port + 1"/>; }
|
||||
}
|
||||
</t>
|
||||
}
|
||||
</template>
|
||||
|
||||
<template id="runbot.badge_default"><?xml version="1.0"?>
|
||||
<svg t-attf-xmlns="http://www.w3.org/2000/svg" t-attf-width="{{left.width + right.width}}" height="18">
|
||||
<!-- from https://github.com/badges/shields/tree/master/templates -->
|
||||
<linearGradient id="smooth" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
|
||||
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
|
||||
<stop offset=".9" stop-color="#000" stop-opacity=".3"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity=".5"/>
|
||||
</linearGradient>
|
||||
<rect rx="4" t-attf-width="{{ left.width + right.width }}" height="18" t-att-fill="left.color"/>
|
||||
<rect rx="4" t-att-x="left.width" t-att-width="right.width" height="18" t-att-fill="right.color"/>
|
||||
<rect t-att-x="left.width" width="4" height="18" t-att-fill="right.color"/>
|
||||
<rect rx="4" t-attf-width="{{ left.width + right.width }}" height="18" fill="url(#smooth)"/>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text t-attf-x="{{left.width/2+1}}" y="13" fill="#010101" fill-opacity=".3"><t t-esc="left.text"/></text>
|
||||
<text t-attf-x="{{left.width/2+1}}" y="12"><t t-esc="left.text"/></text>
|
||||
<text t-attf-x="{{left.width+right.width/2-1}}" y="13" fill="#010101" fill-opacity=".3"><t t-esc="right.text"/></text>
|
||||
<text t-attf-x="{{left.width+right.width/2-1}}" y="12"><t t-esc="right.text"/></text>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<template id="runbot.badge_flat"><?xml version="1.0"?>
|
||||
<svg t-attf-xmlns="http://www.w3.org/2000/svg" t-attf-width="{{left.width + right.width}}" height="20">
|
||||
<!-- from https://github.com/badges/shields/tree/master/templates -->
|
||||
<linearGradient id="smooth" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".1"/>
|
||||
<stop offset=".1" stop-color="#fff" stop-opacity=".1"/>
|
||||
<stop offset=".9" stop-color="#fff" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<rect rx="3" t-attf-width="{{ left.width + right.width }}" height="20" t-att-fill="left.color"/>
|
||||
<rect rx="3" t-att-x="left.width" t-att-width="right.width" height="20" t-att-fill="right.color"/>
|
||||
<rect t-att-x="left.width" width="4" height="20" t-att-fill="right.color"/>
|
||||
<rect rx="3" t-attf-width="{{ left.width + right.width }}" height="20" fill="url(#smooth)"/>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text t-attf-x="{{left.width/2+1}}" y="15" fill="#010101" fill-opacity=".3"><t t-esc="left.text"/></text>
|
||||
<text t-attf-x="{{left.width/2+1}}" y="14"><t t-esc="left.text"/></text>
|
||||
<text t-attf-x="{{left.width+right.width/2-1}}" y="15" fill="#010101" fill-opacity=".3"><t t-esc="right.text"/></text>
|
||||
<text t-attf-x="{{left.width+right.width/2-1}}" y="14"><t t-esc="right.text"/></text>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Cron -->
|
||||
<record model="ir.cron" id="repo_cron">
|
||||
<field name='name'>Runbot Cron</field>
|
||||
<field name='interval_number'>1</field>
|
||||
<field name='interval_type'>minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="function">_cron</field>
|
||||
<field name="args">()</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
</openerp>
|
10
runbot/templates/assets.xml
Normal file
10
runbot/templates/assets.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend" name="runbot.assets.frontend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/octicons/2.0.2/octicons.css"/>
|
||||
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
181
runbot/templates/build.xml
Normal file
181
runbot/templates/build.xml
Normal file
@ -0,0 +1,181 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.build_name">
|
||||
<t t-if="bu['state']=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
|
||||
<t t-if="bu['state']=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu['state']=='testing'">
|
||||
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu['guess_result'], 'info')"/>
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> testing</span> <t t-esc="bu['job']"/> <small t-if="not hide_time"><t t-esc="bu['job_time']"/></small>
|
||||
</t>
|
||||
<t t-if="bu['result']=='ok'"><i class="text-success fa fa-thumbs-up"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='ko'"><i class="text-danger fa fa-thumbs-down"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='warn'"><i class="text-warning fa fa-warning"/><small t-if="not hide_time"> age <t t-esc="bu['job_age']"/> time <t t-esc="bu['job_time']"/></small></t>
|
||||
<t t-if="bu['result']=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
|
||||
<t t-if="bu['result']=='killed'"><i class="text-danger fa fa-times"/> killed</t>
|
||||
<t t-if="bu['result']=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
|
||||
|
||||
<t t-if="bu['server_match'] in ('default', 'fuzzy')">
|
||||
<i class="text-warning fa fa-question-circle fa-fw"
|
||||
title="Server branch cannot be determined exactly. Please use naming convention '9.0-my-branch' to build with '9.0' server branch."/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="runbot.build_button">
|
||||
<div t-attf-class="pull-right">
|
||||
<div t-attf-class="btn-group {{klass}}">
|
||||
<a t-if="bu['state']=='running'" t-attf-href="http://{{bu['domain']}}/?db={{bu['dest']}}-all" class="btn btn-primary"><i class="fa fa-sign-in"/></a>
|
||||
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default"><i class="fa fa-file-text-o"/></a>
|
||||
<a t-attf-href="https://#{repo.base}/commit/#{bu['name']}" class="btn btn-default"><i class="fa fa-github"/></a>
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"><i class="fa fa-cog"/><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-if="bu['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>
|
||||
<t t-if="bu['state']=='running'">
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_dest']}}-all">Connect all <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_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['state'] in ['done','running','deathrow'] and bu_index==0" groups="runbot.group_user">
|
||||
<a href="#" class="runbot-rebuild" t-att-data-runbot-build="bu['id']">Rebuild <i class="fa fa-refresh"/></a>
|
||||
</li>
|
||||
<li t-if="bu['state'] in ['pending','testing','running']" groups="runbot.group_user">
|
||||
<a href="#" class="runbot-kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
|
||||
</li>
|
||||
<li t-if="bu['state']!='testing' and bu['state']!='pending'" class="divider"></li>
|
||||
<li><a t-attf-href="/runbot/build/{{bu['id']}}">Logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/logs/job_10_test_base.txt">Full base logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/logs/job_20_test_all.txt">Full all logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['coverage'] and bu['host']"><a t-attf-href="http://{{bu['host']}}/runbot/static/build/#{bu['real_dest']}/coverage/index.html">Coverage <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu['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="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 -->
|
||||
<li class="divider"></li>
|
||||
<li class="disabled"><a href="#">Runtime: <t t-esc="bu['job_time']"/>s</a></li>
|
||||
<li class="disabled"><a href="#">Port: <t t-esc="bu['port']"/></a></li>
|
||||
<li class="disabled"><a href="#">Age: <t t-esc="bu['job_age']"/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Event / Logs page -->
|
||||
<template id="runbot.build">
|
||||
<t t-call='website.layout'>
|
||||
<div class="container" style="width: 100%;">
|
||||
<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="bu_index" t-value="-1"/>
|
||||
<t t-set="klass" t-value="''"/>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
<p class="navbar-text">
|
||||
</p>
|
||||
<form class="navbar-form navbar-left form-inline">
|
||||
<div class="btn-group">
|
||||
<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 role="presentation" class="dropdown-header">Level</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='debug')"><i t-if="level == 'debug'" class="fa fa-check"/> Debug</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='info')"><i t-if="level == 'info'" class="fa fa-check"/> Info</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='warning')"><i t-if="level == 'warning'" class="fa fa-check"/> Warning</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'type', level='error')"><i t-if="level == 'error'" class="fa fa-check"/> Error</a></li>
|
||||
<li role="presentation" class="dropdown-header">Type</li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='runbot')"><i t-if="type == 'runbot'" class="fa fa-check"/> Runbot</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='server')"><i t-if="type == 'server'" class="fa fa-check"/> Server</a></li>
|
||||
<li><a t-att-href="'?' + keep_query('search', 'level',type='client')"><i t-if="type == 'client'" class="fa fa-check"/> Client</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default">Expand</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Other builds <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<t t-foreach='other_builds' t-as='other_build'>
|
||||
<li><a t-attf-href="/runbot/build/{{other_build.id}}">
|
||||
<t t-esc='other_build.dest'/>
|
||||
<t t-call="runbot.build_name">
|
||||
<t t-set="bu" t-value="other_build"/>
|
||||
<t t-set="hide_time" t-value="True"></t>
|
||||
</t>
|
||||
</a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" t-attf-action="/runbot/build/{{build['id']}}" method="get">
|
||||
<div class="form-group">
|
||||
<input type="search" name="search" class="form-control" placeholder="Search" t-att-value="search or ''"/>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<p>
|
||||
Subject: <t t-esc="build['subject']"/><br/>
|
||||
Author: <t t-esc="build['author']"/><br/>
|
||||
Committer: <t t-esc="build['committer']"/><br/>
|
||||
</p>
|
||||
<p t-if="build['duplicate_of']">Duplicate of <a t-attf-href="/runbot/build/#{build['duplicate_of'].id}"><t t-esc="build['duplicate_of'].dest"/></a></p>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Level</th>
|
||||
<th>Type</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<t t-foreach="logs" t-as="l">
|
||||
<tr t-att-class="dict(ERROR='danger', WARNING='warning').get(l.level)">
|
||||
<td style="white-space: nowrap;"><t t-esc="l.create_date"/></td>
|
||||
<td><b t-esc="l.level"/></td>
|
||||
<td><t t-esc="l.type"/></td>
|
||||
<td>
|
||||
<a t-if="l.type != 'runbot'" t-attf-href="https://{{repo.base}}/blob/{{build['name']}}/{{l.path}}#L{{l.line}}"><t t-esc="l.name"/>:<t t-esc="l.line"/></a> <t t-esc="l.func"/>
|
||||
<t t-if="'\n' not in l.message"><t t-esc="l.message"/></t>
|
||||
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-esc="l.message"/></pre>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
161
runbot/templates/frontend.xml
Normal file
161
runbot/templates/frontend.xml
Normal file
@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.name.split(':')[1]"/></a></li>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- remove black bar with switcher d'apps -->
|
||||
<template id="inherits_no_black_bar" inherit_id="website.user_navbar" name="Inherits No black user_navbar">
|
||||
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="attributes">
|
||||
<attribute name="groups">base.group_website_publisher</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
<!-- Frontend repository block -->
|
||||
<template id="runbot.repo">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</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"><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>
|
||||
<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>
|
||||
<li class="divider"></li>
|
||||
<li t-att-class="'active' if limit=='100' else ''"><a t-att-href="qu(limit=100)">Show last 100</a></li>
|
||||
<li t-att-class="'active' if limit=='1000' else ''"><a t-att-href="qu(limit=1000)">Show last 1000</a></li>
|
||||
<li t-att-class="'active' if limit=='10000' else ''"><a t-att-href="qu(limit=10000)">Show last 10000</a></li>
|
||||
</ul>
|
||||
</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, <t t-esc="hs['running']"/> running
|
||||
</span>&nbsp;
|
||||
</t>
|
||||
<span class="label label-info">Pending: <t t-esc="pending_total"/></span>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
<div t-if="not repo" class="mb32">
|
||||
<h1>No Repository yet.</h1>
|
||||
</div>
|
||||
|
||||
<table t-if="repo" class="table table-condensed table-bordered">
|
||||
<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>
|
||||
|
||||
</tr>
|
||||
<tr t-foreach="branches" t-as="br">
|
||||
<td>
|
||||
<i t-if="br['branch'].sticky" class="fa fa-star" style="color: #f0ad4e" />
|
||||
<b t-esc="br['branch'].branch_name"/>
|
||||
<small><t t-esc="br['builds'][0]['job_age']"/></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>
|
||||
<a t-attf-href="/runbot/#{repo.id}/#{br['branch'].branch_name}" class="btn btn-default btn-xs"><i class="fa fa-fast-forward" title="Quick Connect"/></a>
|
||||
</div>
|
||||
</td>
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu['state']=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state']=='testing'"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu['state']=='deathrow'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] == 'skipped'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu['state'] in ['running','done'] and bu['result'] in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t>
|
||||
<td t-attf-class="{{klass}}">
|
||||
<t t-call="runbot.build_button"><t t-set="klass">btn-group-sm</t></t>
|
||||
<t t-if="bu['subject']">
|
||||
<span t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-id="bu['author']">
|
||||
<t t-esc="bu['author']"/>
|
||||
<t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']">
|
||||
(<span class="octicon octicon-arrow-right"></span>&nbsp;<t t-esc="bu['committer']"/>)
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<small><t t-esc="bu['dest']"/> on <t t-esc="bu['host']"/></small><br/>
|
||||
<t t-call="runbot.build_name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
58
runbot/templates/nginx.xml
Normal file
58
runbot/templates/nginx.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.nginx_config">
|
||||
pid <t t-esc="nginx_dir"/>/nginx.pid;
|
||||
error_log <t t-esc="nginx_dir"/>/error.log;
|
||||
worker_processes 1;
|
||||
events { worker_connections 1024; }
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
server_names_hash_max_size 512;
|
||||
server_names_hash_bucket_size 256;
|
||||
client_max_body_size 10M;
|
||||
index index.html;
|
||||
log_format full '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" $request_time';
|
||||
access_log <t t-esc="nginx_dir"/>/access.log full;
|
||||
error_log <t t-esc="nginx_dir"/>/error.log;
|
||||
client_body_temp_path <t t-esc="nginx_dir"/>;
|
||||
fastcgi_temp_path <t t-esc="nginx_dir"/>;
|
||||
|
||||
autoindex on;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css text/plain application/xml application/json application/javascript;
|
||||
|
||||
proxy_temp_path <t t-esc="nginx_dir"/>;
|
||||
proxy_read_timeout 600;
|
||||
proxy_connect_timeout 600;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
server {
|
||||
listen 8080 default;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="port"/>; }
|
||||
location /longpolling/im/poll { return 404; }
|
||||
location /longpolling/poll { return 404; }
|
||||
location /runbot/static/ {
|
||||
alias <t t-esc="runbot_static"/>;
|
||||
autoindex off;
|
||||
location ~ /runbot/static/build/[^/]+/logs/ {
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
}
|
||||
<t t-foreach="builds" t-as="build">
|
||||
server {
|
||||
listen 8080;
|
||||
server_name ~^<t t-raw="re_escape(build.dest)"/>[-.].*$;
|
||||
location / { proxy_pass http://127.0.0.1:<t t-esc="build.port"/>; }
|
||||
location /longpolling { proxy_pass http://127.0.0.1:<t t-esc="build.port + 1"/>; }
|
||||
}
|
||||
</t>
|
||||
}
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
60
runbot/views/branch_views.xml
Normal file
60
runbot/views/branch_views.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="branch_form" model="ir.ui.view">
|
||||
<field name="name">runbot.branch.form</field>
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<group name="branch_group">
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="branch_url"/>
|
||||
<field name="pull_head_name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="coverage"/>
|
||||
<field name="state"/>
|
||||
<field name="modules"/>
|
||||
<field name="job_timeout"/>
|
||||
<!-- keep for next version
|
||||
<field name="test_tags"/>
|
||||
-->
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="branch_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.branch.tree</field>
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Branches">
|
||||
<field name="repo_id"/>
|
||||
<field name="name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="coverage"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_branch_tree" model="ir.actions.act_window">
|
||||
<field name="name">Branches</field>
|
||||
<field name="res_model">runbot.branch</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Branches"
|
||||
id="runbot_menu_branch_tree"
|
||||
parent="runbot_menu_root"
|
||||
sequence="20"
|
||||
action="open_view_branch_tree"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
108
runbot/views/build_views.xml
Normal file
108
runbot/views/build_views.xml
Normal file
@ -0,0 +1,108 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="build_form" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build">
|
||||
<header>
|
||||
<button name="reset" type="object" string="Reset"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="author_email"/>
|
||||
<field name="committer"/>
|
||||
<field name="committer_email"/>
|
||||
<field name="subject"/>
|
||||
<field name="port"/>
|
||||
<field name="dest"/>
|
||||
<field name="state"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="host"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_end"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="modules"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Builds">
|
||||
<field name="sequence"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="committer"/>
|
||||
<field name="state"/>
|
||||
<field name="port"/>
|
||||
<field name="job"/>
|
||||
<field name="result"/>
|
||||
<field name="pid"/>
|
||||
<field name="host"/>
|
||||
<field name="job_start"/>
|
||||
<field name="job_time"/>
|
||||
<field name="job_age"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_pivot" model="ir.ui.view">
|
||||
<field name="name">runbot.pivot</field>
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Builds analysis">
|
||||
<field name="create_date" interval="week" type="row"/>
|
||||
<field name="state" type="col"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_search" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="branch_id"/>
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="dest"/>
|
||||
<separator/>
|
||||
<filter string="Pending" domain="[('state','=', 'pending')]"/>
|
||||
<filter string="Testing" domain="[('state','=', 'testing')]"/>
|
||||
<filter string="Running" domain="[('state','=', 'running')]"/>
|
||||
<filter string="Done" domain="[('state','=','done')]"/>
|
||||
<filter string="Duplicate" domain="[('state','=', 'duplicate')]"/>
|
||||
<filter string="Deathrow" domain="[('state','=', 'deathrow')]"/>
|
||||
<separator />
|
||||
<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':'state'}"/>
|
||||
<filter string="Result" domain="[]" context="{'group_by':'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'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_build" model="ir.actions.act_window">
|
||||
<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"/>
|
||||
</data>
|
||||
</odoo>
|
59
runbot/views/repo_views.xml
Normal file
59
runbot/views/repo_views.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem name="Runbot" id="runbot_menu_root"/>
|
||||
|
||||
<record id="repo_form" model="ir.ui.view">
|
||||
<field name="name">runbot.repo.form</field>
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<group name="repo_group">
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="mode"/>
|
||||
<field name="nginx"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="dependency_ids" widget="many2many_tags"/>
|
||||
<field name="modules"/>
|
||||
<field name="modules_auto"/>
|
||||
<field name="token"/>
|
||||
<field name="group_ids" widget="many2many_tags"/>
|
||||
<field name="hook_time"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="repo_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.repo.tree</field>
|
||||
<field name="model">runbot.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Repositories">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="mode"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_repo_tree" model="ir.actions.act_window">
|
||||
<field name="name">Repositories</field>
|
||||
<field name="res_model">runbot.repo</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Repositories"
|
||||
id="runbot_menu_repo_tree"
|
||||
parent="runbot_menu_root"
|
||||
sequence="10"
|
||||
action="open_view_repo_tree"
|
||||
/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
67
runbot/views/res_config_settings_views.xml
Normal file
67
runbot/views/res_config_settings_views.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.runbot</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('settings')]" position="inside">
|
||||
<div class="app_settings_block" data-string="Runbot" string="Runbot" data-key="runbot">
|
||||
<h2>Runbot configuration</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_workers" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_workers" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_running_max" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_running_max" style="width: 30%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="content-group">
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_timeout" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_timeout" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_starting_port" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_starting_port" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_domain" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_domain" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_max_age" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_max_age" style="width: 30%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_runbot_configuration" model="ir.actions.act_window">
|
||||
<field name="name">Settings</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'module' : 'runbot'}</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_runbot_global_settings" name="Settings"
|
||||
parent="runbot_menu_root" sequence="0" action="action_runbot_configuration" groups="base.group_system"/>
|
||||
</data>
|
||||
</odoo>
|
@ -1 +1 @@
|
||||
import runbot
|
||||
from . import runbot
|
||||
|
@ -2,9 +2,9 @@
|
||||
'name': 'Runbot CLA',
|
||||
'category': 'Website',
|
||||
'summary': 'Runbot CLA',
|
||||
'version': '1.1',
|
||||
'version': '2.0',
|
||||
'description': "Runbot CLA",
|
||||
'author': 'Odoo SA',
|
||||
'depends': ['runbot'],
|
||||
'data': [ ],
|
||||
'data': [],
|
||||
}
|
@ -4,32 +4,36 @@ import glob
|
||||
import logging
|
||||
import re
|
||||
|
||||
import openerp
|
||||
from openerp.tools import ustr
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class runbot_build(openerp.models.Model):
|
||||
|
||||
class runbot_build(models.Model):
|
||||
_inherit = "runbot.build"
|
||||
|
||||
def _job_05_check_cla(self, cr, uid, build, lock_path, log_path):
|
||||
def _job_05_check_cla(self, build, lock_path, log_path):
|
||||
cla_glob = glob.glob(build._path("doc/cla/*/*.md"))
|
||||
if cla_glob:
|
||||
cla = ''.join(open(f).read() for f in cla_glob)
|
||||
cla = ustr(cla.lower())
|
||||
description = "%s Odoo CLA signature check" % build.author
|
||||
mo = re.search('[^ <@]+@[^ @>]+', build.author_email or '')
|
||||
state = "failure"
|
||||
if mo:
|
||||
email = mo.group(0).lower()
|
||||
if re.match('.*@(odoo|openerp|tinyerp)\.com$', email):
|
||||
state = "success"
|
||||
if cla.find(email) != -1:
|
||||
state = "success"
|
||||
_logger.info('CLA build:%s email:%s result:%s', build.dest, email, state)
|
||||
else:
|
||||
try:
|
||||
cla = ''.join(open(f).read() for f in cla_glob)
|
||||
if cla.lower().find(email) != -1:
|
||||
state = "success"
|
||||
except UnicodeDecodeError:
|
||||
description = 'Invalid CLA encoding (must be utf-8)'
|
||||
_logger.info('CLA build:%s email:%s result:%s', build.dest, email, state)
|
||||
status = {
|
||||
"state": state,
|
||||
"target_url": "https://www.odoo.com/sign-cla",
|
||||
"description": "%s Odoo CLA signature check" % build.author,
|
||||
"description": description,
|
||||
"context": "legal/cla"
|
||||
}
|
||||
build._log('check_cla', 'CLA %s' % state)
|
||||
|
Loading…
Reference in New Issue
Block a user