taler-deployment

Deployment scripts and configuration files
Log | Files | Refs | README

commit 099201ce8813f17b8cce839a82202ef3a8d0daf5
parent 1e21f30825baca3cfca4a33325be7b25e9f1b91c
Author: Florian Dold <dold@taler.net>
Date:   Tue,  8 Sep 2026 21:15:37 +0200

buildbot: report advisory job failures as warnings

Treat a job as advisory only when warnings are requested without
halting on failure. Preserve fatal deployment and container build
failures, and propagate advisory warnings to the build result.

Diffstat:
Mbuildbot/master.cfg | 12+++++++++---
Abuildbot/test_container_steps.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 116 insertions(+), 3 deletions(-)

diff --git a/buildbot/master.cfg b/buildbot/master.cfg @@ -250,9 +250,13 @@ def container_add_step(HALT_ON_FAILURE, runCommand += [CONTAINER_NAME, jobCmd] + # Halting jobs (including deployment) remain fatal even if they also + # request warnings. Only non-halting jobs may be advisory. + advisory = WARN_ON_FAILURE and not HALT_ON_FAILURE runArg = util.ShellArg(command=runCommand, logname='run inside container', - warnOnFailure=WARN_ON_FAILURE, + flunkOnFailure=not advisory, + warnOnFailure=advisory, haltOnFailure=HALT_ON_FAILURE) buildArg = util.ShellArg(command=["podman", "build", "-t", CONTAINER_NAME, @@ -265,7 +269,8 @@ def container_add_step(HALT_ON_FAILURE, name=stepName, commands=[runArg], haltOnFailure=HALT_ON_FAILURE, - warnOnFailure=WARN_ON_FAILURE, + flunkOnFailure=True, + warnOnWarnings=True, workdir=WORK_DIR ) else: @@ -273,7 +278,8 @@ def container_add_step(HALT_ON_FAILURE, name=stepName, commands=[buildArg, runArg], haltOnFailure=HALT_ON_FAILURE, - warnOnFailure=WARN_ON_FAILURE, + flunkOnFailure=True, + warnOnWarnings=True, workdir=WORK_DIR ) diff --git a/buildbot/test_container_steps.py b/buildbot/test_container_steps.py @@ -0,0 +1,107 @@ +# This file is part of TALER +# (C) 2026 Taler Systems SA +# +# TALER 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, or (at your option) any later version. +# +# TALER 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with TALER; see the file COPYING. If not, +# see <http://www.gnu.org/licenses/> +# + +"""Run with python3 -m unittest discover -s buildbot -p 'test_*.py'.""" + +import ast +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from buildbot.plugins import steps, util +from buildbot.process import results +from twisted.internet import defer +from twisted.trial import unittest + + +# Load the helper without executing the master's workers, services and jobs. +config_path = Path(__file__).with_name("master.cfg") +config_ast = ast.parse(config_path.read_text(), filename=str(config_path)) +helper_ast = next(node for node in config_ast.body + if isinstance(node, ast.FunctionDef) + and node.name == "container_add_step") +namespace = { + "steps": steps, + "util": util, + "os": SimpleNamespace(path=SimpleNamespace(isdir=lambda _: False)), + "pwd": SimpleNamespace(getpwnam=lambda _: SimpleNamespace(pw_uid=1000)), +} +exec(compile(ast.Module(body=[helper_ast], type_ignores=[]), + str(config_path), "exec"), namespace) +container_add_step = namespace["container_add_step"] + + +class ContainerStepTests(unittest.TestCase): + def make_step(self, halt, warn, build=False): + with patch("builtins.print"): + template = container_add_step( + halt, warn, build, "localhost/test", None, + "/workdir", "test", "test-job") + step = template.get_step_factory().buildStep() + # Rendering the environment values is unrelated to result propagation. + for arg in step.commands: + arg.command = [str(value) for value in arg.command] + return step + + def run_sequence(self, step, command_results): + step.makeRemoteShellCommand = Mock(side_effect=[ + SimpleNamespace(results=lambda result=result: result) + for result in command_results + ]) + step.runCommand = Mock(return_value=defer.succeed(None)) + result = self.successResultOf(step.runShellSequence(step.commands)) + build_result, halt = results.computeResultAndTermination( + step, result, results.SUCCESS) + return result, build_result, halt + + def test_advisory_failure_warns_build_and_continues(self): + for build in (False, True): + with self.subTest(build=build): + step = self.make_step(False, True, build) + outcomes = ([results.SUCCESS] if build else []) + [results.FAILURE] + self.assertEqual(self.run_sequence(step, outcomes), + (results.WARNINGS, results.WARNINGS, False)) + + def test_halting_failure_is_fatal_even_when_warnings_requested(self): + for warn in (False, True): + with self.subTest(warn=warn): + step = self.make_step(True, warn) + self.assertEqual(self.run_sequence(step, [results.FAILURE]), + (results.FAILURE, results.FAILURE, True)) + + def test_non_halting_failure_without_warnings_still_fails_build(self): + step = self.make_step(False, False) + self.assertEqual(self.run_sequence(step, [results.FAILURE]), + (results.FAILURE, results.FAILURE, False)) + + def test_container_failure_is_fatal_and_skips_run_command(self): + for halt in (False, True): + with self.subTest(halt=halt): + step = self.make_step(halt, True, build=True) + self.assertEqual(self.run_sequence(step, [results.FAILURE]), + (results.FAILURE, results.FAILURE, halt)) + self.assertEqual(step.runCommand.call_count, 1) + + def test_success_stays_success(self): + for halt in (False, True): + for warn in (False, True): + with self.subTest(halt=halt, warn=warn): + step = self.make_step(halt, warn, build=True) + self.assertEqual(self.run_sequence( + step, [results.SUCCESS, results.SUCCESS]), + (results.SUCCESS, results.SUCCESS, False))