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 @@ - + + + + +