diff --git a/runbot/container.py b/runbot/container.py
index 1a38358a..3de78ca9 100644
--- a/runbot/container.py
+++ b/runbot/container.py
@@ -270,7 +270,7 @@ def tests(args):
     if args.coverage:
         omit = ['--omit', '*__manifest__.py']
         python_params = [ '-m', 'coverage', 'run', '--branch', '--source', '/data/build'] + omit
-        posts = [['python%s' % py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"]]
+        posts = [['python%s' % py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"], ['python%s' % py_version, "-m", "coverage", "xml", "--ignore-errors"]]
         os.makedirs(os.path.join(args.build_dir, 'coverage'), exist_ok=True)
     elif args.flamegraph:
         flame_log = '/data/build/logs/flame.log'
diff --git a/runbot/models/build_config.py b/runbot/models/build_config.py
index 5e6aed36..02f84005 100644
--- a/runbot/models/build_config.py
+++ b/runbot/models/build_config.py
@@ -388,7 +388,7 @@ class ConfigStep(models.Model):
         if extra_params:
             cmd.extend(shlex.split(extra_params))
 
-        cmd.posts.append(self._post_install_command(build, modules_to_install, py_version))  # coverage post, extra-checks, ...
+        cmd.posts.extend(self._post_install_commands(build, modules_to_install, py_version))  # coverage post, extra-checks, ...
         dump_dir = '/data/build/logs/%s/' % db_name
         sql_dest = '%s/dump.sql' % dump_dir
         filestore_path = '/data/build/datadir/filestore/%s' % db_name
@@ -421,6 +421,10 @@ class ConfigStep(models.Model):
             kwargs['log_type'] = 'link'
         build._log('', **kwargs)
 
+        if self.coverage:
+            message = 'Coverage xml: $$fa-download'
+            build._log('end_job', message, log_type='link', path='/data/build/logs/coverage.xml')
+
         if self.flamegraph:
             link = self._perf_data_url(build, 'log.gz')
             message = 'Flamegraph data: $$fa-download$$'
@@ -433,14 +437,16 @@ class ConfigStep(models.Model):
     def _modules_to_install(self, build):
         return set(build._get_modules_to_test(modules_patterns=self.install_modules))
 
-    def _post_install_command(self, build, modules_to_install, py_version=None):
+    def _post_install_commands(self, build, modules_to_install, py_version=None):
+        cmds = []
         if self.coverage:
             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 ['python%s' % py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"]
-        return []
+            cmds.append(['python%s' % py_version, "-m", "coverage", "html", "-d", "/data/build/coverage", "--ignore-errors"])
+            cmds.append(['python%s' % py_version, "-m", "coverage", "xml", "-o", "/data/build/logs/coverage.xml", "--ignore-errors"])
+        return cmds
 
     def _perfs_data_path(self, ext='log'):
         return '/data/build/logs/flame_%s.%s' % (self.name, ext)
diff --git a/runbot/tests/test_build_config_step.py b/runbot/tests/test_build_config_step.py
index 29dba737..0e8c1a42 100644
--- a/runbot/tests/test_build_config_step.py
+++ b/runbot/tests/test_build_config_step.py
@@ -153,12 +153,10 @@ class TestBuildConfigStep(RunbotCase):
         })
 
         def docker_run(cmd, log_path, *args, **kwargs):
-            cmds = cmd.build().split(' && ')
-            dest = self.parent_build.dest
             self.assertEqual(cmd.pres, [['sudo', 'pip3', 'install', '-r', 'bar/requirements.txt']])
             self.assertEqual(cmd.cmd[:10], ['python3', '-m', 'coverage', 'run', '--branch', '--source', '/data/build', '--omit', '*__manifest__.py', 'bar/server.py'])
-            #['bar/server.py', '--addons-path', 'bar', '--no-xmlrpcs', '--no-netrpc', '-d', '08732-master-d0d0ca-coverage', '--test-enable', '--stop-after-init', '--log-level=test', '--max-cron-threads=0']
-            self.assertEqual(cmd.posts, [['python3', '-m', 'coverage', 'html', '-d', '/data/build/coverage', '--ignore-errors']])
+            self.assertIn(['python3', '-m', 'coverage', 'html', '-d', '/data/build/coverage', '--ignore-errors'], cmd.posts)
+            self.assertIn(['python3', '-m', 'coverage', 'xml', '-o', '/data/build/logs/coverage.xml', '--ignore-errors'], cmd.posts)
             self.assertEqual(log_path, 'dev/null/logpath')
 
         self.patchers['docker_run'].side_effect = docker_run