[FIX] runbot: fix coverage since shared sources

The requirements path and python version where defined from
server in cmd. Since in coverage we add a 'python' before server,
it is difficult to define which element of the cmd is the server.

A solution here is simply to define requirements install and
python version when building cmd since we have access to all
build/source informations. We also add python part in every
cases, and coverage params are now a _cmd python_params.

The _cmd method now returns a Command object instead of a
list, which behave has a list for the cmd part but also contains
a pres and posts list.

pres are requirement install, preparation, ...
cmd is the original cmd list, element can be append or added, this
will allow to keep existing python job without to much changes.
posts are post cmd commands, like coverage result making.

This commit also fix issue with create_job dependencies.
This commit is contained in:
Xavier-Do 2019-07-15 11:27:46 +02:00
parent 785001acac
commit 81fefee137
9 changed files with 150 additions and 96 deletions

View File

@ -99,13 +99,3 @@ def local_pgadmin_cursor():
finally:
if cnx:
cnx.close()
def get_py_version(build):
"""return the python name to use from build instance"""
executables = [ 'odoo-bin', 'openerp-server' ]
for server_path in map(build._path, executables):
if os.path.exists(server_path):
with open(server_path, 'r') as f:
if f.readline().strip().endswith('python3'):
return 'python3'
return 'python'

View File

@ -31,19 +31,27 @@ ENV COVERAGE_FILE /data/build/.coverage
""" % {'group_id': os.getgid(), 'user_id': os.getuid()}
def build_odoo_cmd(odoo_cmd):
""" returns the chain of commands necessary to run odoo inside the container
: param odoo_cmd: odoo command as a list
: returns: a string with the command chain to execute in the docker container
"""
# build cmd
cmd_chain = []
cmd_chain.append('cd /data/build')
server_path = odoo_cmd[0]
requirement_path = os.path.join(os.path.dirname(server_path), 'requirements.txt')
cmd_chain.append('head -1 %s | grep -q python3 && sudo pip3 install -r %s || sudo pip install -r %s' % (server_path, requirement_path, requirement_path))
cmd_chain.append(' '.join(odoo_cmd))
return ' && '.join(cmd_chain)
class Command():
def __init__(self, pres, cmd, posts):
self.pres = pres or []
self.cmd = cmd
self.posts = posts or []
def __getattr__(self, name):
return getattr(self.cmd, name)
def __getitem__(self, key):
return self.cmd[key]
def __add__(self, l):
return Command(self.pres, self.cmd + l, self.posts)
def build(self):
cmd_chain = []
cmd_chain += [' '.join(pre) for pre in self.pres if pre]
cmd_chain.append(' '.join(self))
cmd_chain += [' '.join(post) for post in self.posts if post]
return ' && '.join(cmd_chain)
def docker_build(log_path, build_dir):
@ -62,6 +70,7 @@ def docker_build(log_path, build_dir):
dbuild = subprocess.Popen(['docker', 'build', '--tag', 'odoo:runbot_tests', '.'], stdout=logs, stderr=logs, cwd=docker_dir)
dbuild.wait()
def docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None, cpu_limit=None, preexec_fn=None, ro_volumes=None):
"""Run tests in a docker container
:param run_cmd: command string to run in container
@ -74,7 +83,8 @@ def docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None,
"""
_logger.debug('Docker run command: %s', run_cmd)
logs = open(log_path, 'w')
logs.write("Docker command:\n%s\n=================================================\n" % run_cmd.replace('&& ', '&&\n').replace('|| ','||\n\t'))
run_cmd = 'cd /data/build && %s' % run_cmd
logs.write("Docker command:\n%s\n=================================================\n" % run_cmd.replace('&& ', '&&\n').replace('|| ', '||\n\t'))
# create start script
docker_command = [
'docker', 'run', '--rm',
@ -154,7 +164,8 @@ def tests(args):
if args.kill:
logfile = os.path.join(args.build_dir, 'logs', 'logs-partial.txt')
container_name = 'odoo-container-test-%s' % datetime.datetime.now().microsecond
docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name)
# FIXME
# docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name)
# Test stopping the container
_logger.info('Waiting 30 sec before killing the build')
time.sleep(30)
@ -169,7 +180,8 @@ def tests(args):
with open(os.path.join(args.build_dir, 'odoo-bin'), 'r') as exfile:
pyversion = 'python3' if 'python3' in exfile.readline() else 'python'
odoo_cmd = [ pyversion, '-m', 'coverage', 'run', '--branch', '--source', '/data/build'] + omit + odoo_cmd
docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name)
# FIXME
# docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name)
time.sleep(1) # give time for the container to start
while docker_is_running(container_name):
@ -200,7 +212,9 @@ def tests(args):
if smtp_host:
odoo_cmd.extend(['--smtp', smtp_host])
container_name = 'odoo-container-test-%s' % datetime.datetime.now().microsecond
docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name, exposed_ports=[args.odoo_port, args.odoo_port + 1], cpu_limit=300)
# FIXME
# docker_run(build_odoo_cmd(odoo_cmd), logfile, args.build_dir, container_name, exposed_ports=[args.odoo_port, args.odoo_port + 1], cpu_limit=300)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s')

View File

@ -9,7 +9,7 @@ import subprocess
import time
import datetime
from ..common import dt2time, fqdn, now, grep, uniq_list, local_pgadmin_cursor, s2human, Commit
from ..container import docker_build, docker_stop, docker_is_running
from ..container import docker_build, docker_stop, docker_is_running, Command
from odoo.addons.runbot.models.repo import HashMissingException
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
@ -283,7 +283,7 @@ class runbot_build(models.Model):
# maybe update duplicate priority if needed
docker_source_folders = set()
for commit in build_id.get_all_commit():
for commit in build_id._get_all_commit():
docker_source_folder = build_id._docker_source_folder(commit)
if docker_source_folder in docker_source_folders:
extra_info['commit_path_mode'] = 'rep_sha'
@ -384,6 +384,14 @@ class runbot_build(models.Model):
params['dep'][result[0]] = result[1]
return params
def _copy_dependency_ids(self):
return [(0, 0, {
'match_type': dep.match_type,
'closest_branch_id': dep.closest_branch_id.id,
'dependency_hash': dep.dependency_hash,
'dependecy_repo_id': dep.dependecy_repo_id.id,
}) for dep in self.dependency_ids]
def _force(self, message=None, exact=False):
"""Force a rebuild and return a recordset of forced builds"""
forced_builds = self.env['runbot.build']
@ -408,19 +416,13 @@ class runbot_build(models.Model):
'build_type': 'rebuild',
}
if exact:
if build.dependency_ids:
values['dependency_ids'] = [(0, 0, {
'match_type': dep.match_type,
'closest_branch_id': dep.closest_branch_id.id,
'dependency_hash': dep.dependency_hash,
'dependecy_repo_id': dep.dependecy_repo_id.id,
}) for dep in build.dependency_ids]
values.update({
'config_id': build.config_id.id,
'extra_params': build.extra_params,
'orphan_result': build.orphan_result,
'dependency_ids': build._copy_dependency_ids(),
})
#if replace: ?
# if replace: ?
if build.parent_id:
values.update({
'parent_id': build.parent_id.id, # attach it to parent
@ -643,7 +645,7 @@ class runbot_build(models.Model):
def _server(self, *path):
"""Return the absolute path to the direcory containing the server file, adding optional *path"""
self.ensure_one()
commit = self.get_server_commit()
commit = self._get_server_commit()
if os.path.exists(commit._source_path('odoo')):
return commit._source_path('odoo', *path)
return commit._source_path('openerp', *path)
@ -688,7 +690,7 @@ class runbot_build(models.Model):
self.ensure_one() # will raise exception if hash not found, we don't want to fail for all build.
# checkout branch
exports = {}
for commit in commits or self.get_all_commit():
for commit in commits or self._get_all_commit():
build_export_path = self._docker_source_folder(commit)
if build_export_path in exports:
self._log('_checkout', 'Multiple repo have same export path in build, some source may be missing for %s' % build_export_path, level='ERROR')
@ -705,7 +707,7 @@ class runbot_build(models.Model):
# checkout branch
repo_modules = []
available_modules = []
for commit in commits or self.get_all_commit():
for commit in commits or self._get_all_commit():
for (addons_path, module, manifest_file_name) in self._get_available_modules(commit):
if commit.repo == self.repo_id:
repo_modules.append(module)
@ -819,54 +821,58 @@ class runbot_build(models.Model):
if not child.duplicate_id:
child._ask_kill()
def get_all_commit(self):
return [Commit(self.repo_id, self.name)] + [Commit(dep.get_repo(), dep.dependency_hash) for dep in self.dependency_ids]
def _get_all_commit(self):
return [Commit(self.repo_id, self.name)] + [Commit(dep._get_repo(), dep.dependency_hash) for dep in self.dependency_ids]
def get_server_commit(self, commits=None):
def _get_server_commit(self, commits=None):
"""
returns a Commit() of the first repo containing server files found in commits or in build commits
the commits param is not used in code base but could be usefull for jobs and crons
"""
for commit in (commits or self.get_all_commit()):
for commit in (commits or self._get_all_commit()):
if commit.repo.server_files:
return commit
raise ValidationError('No repo found with defined server_files')
def get_addons_path(self, commits=None):
for commit in (commits or self.get_all_commit()):
def _get_addons_path(self, commits=None):
for commit in (commits or self._get_all_commit()):
source_path = self._docker_source_folder(commit)
for addons_path in commit.repo.addons_paths.split(','):
if os.path.isdir(commit._source_path(addons_path)):
yield os.path.join(source_path, addons_path).strip(os.sep)
def get_server_info(self, commit=None):
def _get_server_info(self, commit=None):
server_dir = False
server = False
commit = commit or self.get_server_commit()
commit = commit or self._get_server_commit()
for server_file in commit.repo.server_files.split(','):
if os.path.isfile(commit._source_path(server_file)):
return (self._docker_source_folder(commit), server_file)
return (commit, server_file)
self._log('server_info', 'No server found in %s' % commit, level='ERROR')
raise ValidationError('No server found in %s' % commit)
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
def _cmd(self, python_params=None, py_version=None):
"""Return a list describing the command to start the build
"""
self.ensure_one()
build = self
python_params = python_params or []
py_version = py_version if py_version is not None else build._get_py_version()
(server_commit, server_file) = self._get_server_info()
server_dir = self._docker_source_folder(server_commit)
addons_paths = self._get_addons_path()
requirement_path = os.path.join(server_dir, 'requirements.txt')
pres = [['sudo', 'pip%s' % py_version, 'install', '-r', '%s' % requirement_path]]
(server_dir, server_file) = self.get_server_info()
addons_paths = self.get_addons_path()
# commandline
cmd = [os.path.join('/data/build', server_dir, server_file), '--addons-path', ",".join(addons_paths)]
cmd = ['python%s' % py_version] + python_params + [os.path.join(server_dir, server_file), '--addons-path', ",".join(addons_paths)]
# options
if grep(build._server("tools/config.py"), "no-xmlrpcs"): # move that to configs ?
config_path = build._server("tools/config.py")
if grep(config_path, "no-xmlrpcs"): # move that to configs ?
cmd.append("--no-xmlrpcs")
if grep(build._server("tools/config.py"), "no-netrpc"):
if grep(config_path, "no-netrpc"):
cmd.append("--no-netrpc")
if grep(build._server("tools/config.py"), "log-db"):
if grep(config_path, "log-db"):
logdb_uri = self.env['ir.config_parameter'].get_param('runbot.runbot_logdb_uri')
logdb = self.env.cr.dbname
if logdb_uri and grep(build._server('sql_db.py'), 'allow_uri'):
@ -875,7 +881,7 @@ class runbot_build(models.Model):
if grep(build._server('tools/config.py'), 'log-db-level'):
cmd += ["--log-db-level", '25']
if grep(build._server("tools/config.py"), "data-dir"):
if grep(config_path, "data-dir"):
datadir = build._path('datadir')
if not os.path.exists(datadir):
os.mkdir(datadir)
@ -883,8 +889,7 @@ class runbot_build(models.Model):
# use the username of the runbot host to connect to the databases
cmd += ['-r %s' % pwd.getpwuid(os.getuid()).pw_name]
return cmd
return Command(pres, cmd, [])
def _github_status_notify_all(self, status):
"""Notify each repo with a status"""
@ -936,6 +941,15 @@ class runbot_build(models.Model):
new_step = step_ids[next_index] # job to do, state is job_state (testing or running)
return {'active_step': new_step.id, 'local_state': new_step._step_state()}
def _get_py_version(self):
"""return the python name to use from build instance"""
(server_commit, server_file) = self._get_server_info()
server_path = server_commit._source_path(server_file)
with open(server_path, 'r') as f:
if f.readline().strip().endswith('python3'):
return '3'
return ''
def read_file(self, file, mode='r'):
file_path = self._path(file)
try:

View File

@ -5,8 +5,8 @@ import os
import re
import shlex
import time
from ..common import now, grep, get_py_version, time2str, rfind, Commit
from ..container import docker_run, docker_get_gateway_ip, build_odoo_cmd
from ..common import now, grep, time2str, rfind, Commit
from ..container import docker_run, docker_get_gateway_ip, Command
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval, test_python_expr
@ -201,7 +201,7 @@ class ConfigStep(models.Model):
build._logger('Too much build created')
break
children = Build.create({
'dependency_ids': [(4, did.id) for did in build.dependency_ids],
'dependency_ids': build._copy_dependency_ids(),
'config_id': create_config.id,
'parent_id': build.id,
'branch_id': build.branch_id.id,
@ -228,12 +228,11 @@ class ConfigStep(models.Model):
'_logger': _logger,
'log_path': log_path,
'glob': glob.glob,
'build_odoo_cmd': build_odoo_cmd,
'Command': Command,
'base64': base64,
're': re,
'time': time,
'grep': grep,
'get_py_version': get_py_version,
'rfind': rfind,
}
return safe_eval(self.sudo().python_code.strip(), eval_ctx, mode="exec", nocopy=True)
@ -244,8 +243,7 @@ class ConfigStep(models.Model):
build._log('run', 'Start running build %s' % build.dest)
# run server
cmd = build._cmd()
server = build._server()
if os.path.exists(os.path.join(server, 'addons/im_livechat')):
if os.path.exists(build._get_server_commit()._source_path('addons/im_livechat')):
cmd += ["--workers", "2"]
cmd += ["--longpolling-port", "8070"]
cmd += ["--max-cron-threads", "1"]
@ -257,7 +255,7 @@ class ConfigStep(models.Model):
# we need to have at least one job of type install_odoo to run odoo, take the last one for db_name.
cmd += ['-d', '%s-%s' % (build.dest, db_name)]
if grep(os.path.join(server, "tools/config.py"), "db-filter"):
if grep(build._server("tools/config.py"), "db-filter"):
if build.repo_id.nginx:
cmd += ['--db-filter', '%d.*$']
else:
@ -271,19 +269,26 @@ class ConfigStep(models.Model):
build_port = build.port
self.env.cr.commit() # commit before docker run to be 100% sure that db state is consistent with dockers
self.invalidate_cache()
return docker_run(build_odoo_cmd(cmd), log_path, build_path, docker_name, exposed_ports=[build_port, build_port + 1], ro_volumes=exports)
return docker_run(cmd.build(), log_path, build_path, docker_name, exposed_ports=[build_port, build_port + 1], ro_volumes=exports)
def _run_odoo_install(self, build, log_path):
exports = build._checkout()
cmd = build._cmd()
modules_to_install = self._modules_to_install(build)
mods = ",".join(modules_to_install)
python_params = []
py_version = build._get_py_version()
if self.coverage:
build.coverage = True
coverage_extra_params = self._coverage_params(build, modules_to_install)
python_params = ['-m', 'coverage', 'run', '--branch', '--source', '/data/build'] + coverage_extra_params
cmd = build._cmd(python_params, py_version)
# create db if needed
db_name = "%s-%s" % (build.dest, self.db_name)
if self.create_db:
build._local_pg_createdb(db_name)
cmd += ['-d', db_name]
# list module to install
modules_to_install = self._modules_to_install(build)
mods = ",".join(modules_to_install)
if mods:
cmd += ['-i', mods]
if self.test_enable:
@ -301,17 +306,11 @@ class ConfigStep(models.Model):
if self.extra_params:
cmd.extend(shlex.split(self.extra_params))
if self.coverage:
build.coverage = True
coverage_extra_params = self._coverage_params(build, modules_to_install)
py_version = get_py_version(build)
cmd = [py_version, '-m', 'coverage', 'run', '--branch', '--source', '/data/build'] + coverage_extra_params + cmd
cmd += self._post_install_command(build, modules_to_install) # coverage post, extra-checks, ...
cmd.posts.append(self._post_install_command(build, modules_to_install, py_version)) # coverage post, extra-checks, ...
max_timeout = int(self.env['ir.config_parameter'].get_param('runbot.runbot_timeout', default=10000))
timeout = min(self.cpu_limit, max_timeout)
return docker_run(build_odoo_cmd(cmd), log_path, build._path(), build._get_docker_name(), cpu_limit=timeout, ro_volumes=exports)
return docker_run(cmd.build(), log_path, build._path(), build._get_docker_name(), cpu_limit=timeout, ro_volumes=exports)
def _modules_to_install(self, build):
modules_to_install = set([mod.strip() for mod in self.install_modules.split(',')])
@ -322,18 +321,18 @@ class ConfigStep(models.Model):
# todo add without support
return modules_to_install
def _post_install_command(self, build, modules_to_install):
def _post_install_command(self, build, modules_to_install, py_version=None):
if self.coverage:
py_version = get_py_version(build)
py_version = py_version if py_version is not None else build._get_py_version()
# prepare coverage result
cov_path = build._path('coverage')
os.makedirs(cov_path, exist_ok=True)
return ['&&', py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"]
return ['python%s' % py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"]
return []
def _coverage_params(self, build, modules_to_install):
pattern_to_omit = set()
for commit in build.get_all_commit():
for commit in build._get_all_commit():
docker_source_folder = build._docker_source_folder(commit)
for manifest_file in commit.repo.manifest_files.split(','):
pattern_to_omit.add('*%s' % manifest_file)

View File

@ -10,6 +10,6 @@ class RunbotBuildDependency(models.Model):
closest_branch_id = fields.Many2one('runbot.branch', 'Branch', required=True, ondelete='cascade')
match_type = fields.Char('Match Type')
def get_repo(self):
def _get_repo(self):
return self.closest_branch_id.repo_id or self.dependecy_repo_id

View File

@ -59,7 +59,7 @@ class runbot_repo(models.Model):
config_id = fields.Many2one('runbot.build.config', 'Run Config', compute='_compute_config_id', inverse='_inverse_config_id')
server_files = fields.Char('Server files', help='Comma separated list of possible server files') # odoo-bin,openerp-server,openerp-server.py
manifest_files = fields.Char('Addons files', help='Comma separated list of possible addons files', default='__manifest__.py,__openerp__.py')
manifest_files = fields.Char('Addons files', help='Comma separated list of possible addons files', default='__manifest__.py')
addons_paths = fields.Char('Addons files', help='Comma separated list of possible addons path', default='')
def _compute_config_id(self):

View File

@ -84,7 +84,7 @@ class Test_Build(common.TransactionCase):
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
'port': '1234',
})
cmd = build._cmd()
cmd = build._cmd(py_version=3)
self.assertIn('--log-db=%s' % uri, cmd)
@patch('odoo.addons.runbot.models.build.os.path.isdir')
@ -100,8 +100,9 @@ class Test_Build(common.TransactionCase):
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
'port': '1234',
})
cmd = build._cmd()
self.assertEqual('/data/build/bar/server.py', cmd[0])
cmd = build._cmd(py_version=3)
self.assertEqual('python3', cmd[0])
self.assertEqual('bar/server.py', cmd[1])
self.assertIn('--addons-path', cmd)
addons_path_pos = cmd.index('--addons-path') + 1
self.assertEqual(cmd[addons_path_pos], 'bar/addons,bar/core/addons')
@ -151,11 +152,12 @@ class Test_Build(common.TransactionCase):
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
'port': '1234',
})
cmd = build._cmd()
cmd = build._cmd(py_version=3)
self.assertIn('--addons-path', cmd)
addons_path_pos = cmd.index('--addons-path') + 1
self.assertEqual(cmd[addons_path_pos], 'bar-ent,bar/addons,bar/core/addons')
self.assertEqual('/data/build/bar/server.py', cmd[0])
self.assertEqual('bar/server.py', cmd[1])
self.assertEqual('python3', cmd[0])
@patch('odoo.addons.runbot.models.branch.runbot_branch._is_on_remote')
@patch('odoo.addons.runbot.models.build.os.path.isdir')
@ -193,11 +195,12 @@ class Test_Build(common.TransactionCase):
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
'port': '1234',
})
cmd = build._cmd()
cmd = build._cmd(py_version=3)
self.assertIn('--addons-path', cmd)
addons_path_pos = cmd.index('--addons-path') + 1
self.assertEqual(cmd[addons_path_pos], 'bar-d0d0caca,bar-dfdfcfcf/addons,bar-dfdfcfcf/core/addons')
self.assertEqual('/data/build/bar-dfdfcfcf/server.py', cmd[0])
self.assertEqual('bar-dfdfcfcf/server.py', cmd[1])
self.assertEqual('python3', cmd[0])
def test_build_config_from_branch_default(self):
"""test build config_id is computed from branch default config_id"""

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from unittest.mock import patch
from odoo.tests import common
@ -79,3 +80,36 @@ class TestBuildConfigStep(common.TransactionCase):
child_build.local_result = 'ko'
self.assertFalse(self.parent_build.global_result)
@patch('odoo.addons.runbot.models.build.runbot_build._local_pg_createdb')
@patch('odoo.addons.runbot.models.build.runbot_build._get_server_info')
@patch('odoo.addons.runbot.models.build.runbot_build._get_addons_path')
@patch('odoo.addons.runbot.models.build.runbot_build._get_py_version')
@patch('odoo.addons.runbot.models.build.runbot_build._server')
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
@patch('odoo.addons.runbot.models.build_config.docker_run')
def test_coverage(self, mock_docker_run, mock_checkout, mock_server, mock_get_py_version, mock_get_addons_path, mock_get_server_info, mock_local_pg_createdb):
config_step = self.ConfigStep.create({
'name': 'coverage',
'job_type': 'install_odoo',
'coverage': True
})
mock_checkout.return_value = {}
mock_server.return_value = 'bar'
mock_get_py_version.return_value = '3'
mock_get_addons_path.return_value = ['bar/addons']
mock_get_server_info.return_value = (self.parent_build._get_all_commit()[0], 'server.py')
mock_local_pg_createdb.return_value = True
def docker_run(cmd, log_path, build_dir, *args, **kwargs):
cmds = cmd.split(' && ')
self.assertEqual(cmds[0], 'sudo pip3 install -r bar/requirements.txt')
self.assertEqual(cmds[1].split(' bar/server.py')[0], 'python3 -m coverage run --branch --source /data/build --omit *__manifest__.py')
self.assertEqual(cmds[2], 'python3 -m coverage html -d /data/build/coverage --ignore-errors')
self.assertEqual(log_path, 'dev/null/logpath')
mock_docker_run.side_effect = docker_run
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')

View File

@ -22,7 +22,7 @@ class Step(models.Model):
def _runbot_cla_check(self, build, log_path):
build._checkout()
cla_glob = glob.glob(build.get_server_commit()._source_path("doc/cla/*/*.md"))
cla_glob = glob.glob(build._get_server_commit()._source_path("doc/cla/*/*.md"))
if cla_glob:
description = "%s Odoo CLA signature check" % build.author
mo = re.search('[^ <@]+@[^ @>]+', build.author_email or '')