diff --git a/runbot/common.py b/runbot/common.py
index 0740ce89e..f6792dbbb 100644
--- a/runbot/common.py
+++ b/runbot/common.py
@@ -14,7 +14,7 @@
from babel.dates import LC_TIME, TIMEDELTA_UNITS, Locale
from collections import OrderedDict
from datetime import timedelta
-from markupsafe import Markup
+from markupsafe import Markup, escape
from odoo.fields import Domain
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, file_open, html_escape, OrderedSet
@@ -413,3 +413,46 @@ def test_tags_to_search_domain(self, exclude_error_id=None):
search_domains.append(tag_domain)
search_domain = Domain.OR(search_domains)
return search_domain
+
+
+ANSI_ESCAPE_RE = re.compile(r'\x1b\[([0-9;]*)m')
+
+ANSI_COLORS = {
+ 30: '#000000', 31: '#AA0000', 32: '#00AA00', 33: '#AA5500',
+ 34: '#0000AA', 35: '#AA00AA', 36: '#00AAAA', 37: '#AAAAAA',
+ 90: '#555555', 91: '#FF5555', 92: '#55FF55', 93: '#FFFF55',
+ 94: '#5555FF', 95: '#FF55FF', 96: '#55FFFF', 97: '#FFFFFF',
+}
+
+
+def ansi_to_html(text: str):
+ result = []
+ open_span = False
+ pos = 0
+
+ for match in ANSI_ESCAPE_RE.finditer(text):
+ chunk = text[pos:match.start()]
+ if chunk:
+ result.append(escape(chunk))
+
+ codes = match.group(1)
+ codes = [int(c) for c in codes.split(';') if c] if codes else [0]
+
+ for code in codes:
+ if open_span:
+ result.append(Markup(''))
+ open_span = False
+ if code in ANSI_COLORS:
+ result.append(Markup(f''))
+ open_span = True
+
+ pos = match.end()
+
+ remaining = text[pos:]
+ if remaining:
+ result.append(escape(remaining))
+
+ if open_span:
+ result.append(Markup(''))
+
+ return Markup('') + Markup('').join(result) + Markup('')
diff --git a/runbot/models/docker.py b/runbot/models/docker.py
index d7115e3d5..b1e5f1b49 100644
--- a/runbot/models/docker.py
+++ b/runbot/models/docker.py
@@ -10,6 +10,7 @@
from odoo import api, exceptions, fields, models
+from ..common import ansi_to_html
from ..container import docker_build
from ..fields import JsonDictField
@@ -20,6 +21,7 @@
USERGID = os.getgid()
USERNAME = getpass.getuser()
+
class DockerLayer(models.Model):
_name = 'runbot.docker_layer'
_inherit = 'mail.thread'
@@ -385,6 +387,7 @@ def _build(self, host=None):
content = self._get_cached_content(docker_build_path)
with open(self.env['runbot.runbot']._path('docker', tag_dir, 'Dockerfile'), 'w', encoding="utf-8") as Dockerfile:
Dockerfile.write(content)
+ self.env.cr.commit() # avoid to have a running transaction during the build
result = docker_build(docker_build_path, self.image_future_tag, self.pull_on_build, self.nocache)
duration = result['duration']
msg = result['msg']
@@ -434,7 +437,6 @@ def clean_output(output):
self.nocache = False
return image_id
-
class DockerBuildOutput(models.Model):
_name = 'runbot.docker_build_result'
_description = "Result of a docker file build"
@@ -445,9 +447,10 @@ class DockerBuildOutput(models.Model):
duration = fields.Float("Exec time")
dockerfile_id = fields.Many2one('runbot.dockerfile', string="Docker file")
output = fields.Text('Output')
+ formated_output = fields.Html('Colored Output', compute='_compute_formated_output')
content = fields.Text('Content')
identifier = fields.Char('Identifier')
- summary = fields.Char("Summary", compute='_compute_summary', store=True)
+ summary = fields.Html("Summary", compute='_compute_summary', store=True)
metadata = JsonDictField("Metadata", help="Additionnal data about this image generated by nightly builds")
@api.depends('output')
@@ -458,7 +461,11 @@ def _compute_summary(self):
if len(line) > 5:
summary = line
break
- record.summary = summary
+ record.summary = ansi_to_html(summary)
+
+ def _compute_formated_output(self):
+ for record in self:
+ record.formated_output = ansi_to_html(record.output)
def _getdocker_metadata_diff(self, other_build_result_id):
build_result_b = self.env['runbot.docker_build_result'].browse(other_build_result_id)
diff --git a/runbot/models/host.py b/runbot/models/host.py
index 324e6c387..60db1ff3b 100644
--- a/runbot/models/host.py
+++ b/runbot/models/host.py
@@ -198,19 +198,16 @@ def _docker_update_images(self):
docker_tag(dockerfile.image_previous_identifier, dockerfile.image_previous_tag)
docker_tag(dockerfile.image_identifier, dockerfile.image_tag)
docker_tag(dockerfile.image_future_identifier, dockerfile.image_future_tag)
- for tag in [dockerfile.image_tag, dockerfile.image_future_tag]:
- try:
- docker_push(tag) # for now, always push locally
- if is_main_registry:
- docker_registry_url = self.docker_registry_url
- if self.docker_registry_url:
- docker_registry_url = self.docker_registry_url
- else:
- docker_registry_url = icp.get_param('runbot.docker_registry_url', default='').strip('/')
- if docker_registry_url:
+ tags_to_push = [dockerfile.image_tag, dockerfile.image_future_tag]
+ docker_registry_url = self._get_docker_registry_url()
+ if docker_registry_url:
+ self.env.cr.commit()
+ for tag in tags_to_push:
+ try:
+ if is_main_registry or docker_registry_url == self.docker_registry_url:
docker_push(tag, docker_registry_url)
- except ImageNotFound:
- _logger.warning("Image tag `%s` not found. Skipping push", tag)
+ except ImageNotFound:
+ _logger.warning("Image tag `%s` not found. Skipping push", tag)
else:
if future_identifier:
docker_tag(future_identifier, dockerfile.image_tag) # for a setup without registry
diff --git a/runbot/views/dockerfile_views.xml b/runbot/views/dockerfile_views.xml
index 27d45ac5e..60eb5760c 100644
--- a/runbot/views/dockerfile_views.xml
+++ b/runbot/views/dockerfile_views.xml
@@ -207,7 +207,11 @@
-
+
+
+
+
+