From 39a78a15dafb9aba29778b8186f576a69a4b3bac Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 22 May 2026 22:04:43 +0000 Subject: [PATCH 1/7] fix(errors): render user-config and base-alias failures as structured errors Unhandled pydantic ValidationErrors and host base-alias lookup failures fell through to the generic handler and were reported as ' internal error: ...' with exit code 70, leaking a raw Python/ pydantic traceback (including the errors.pydantic.dev link) for what are plainly user configuration problems. handle_runtime_error now converts pydantic.ValidationError into a structured CraftValidationError with a 'Recommended resolution' (exit EX_DATAERR), and surfaces 'not a valid BuilddBaseAlias' ValueErrors as a structured config error (exit EX_CONFIG). pydantic.ValidationError is a ValueError subclass, so it is matched first. A TODO marks the deeper fix for the base-alias lookup site. --- craft_application/util/logging.py | 40 ++++++++++++++++++++ tests/unit/util/test_logging.py | 61 ++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/craft_application/util/logging.py b/craft_application/util/logging.py index a02e5d7ed..0adf3b8c2 100644 --- a/craft_application/util/logging.py +++ b/craft_application/util/logging.py @@ -28,6 +28,7 @@ import craft_parts import craft_platforms import craft_providers +import pydantic from craft_application import errors @@ -91,6 +92,45 @@ def handle_runtime_error( retcode=error.retcode, ) transformed.__cause__ = error + case pydantic.ValidationError(): + # An unhandled pydantic ValidationError almost always means the + # user's project configuration contains an invalid value (for + # example an unsupported ``base``). Without this case it falls + # through to the generic handler below and is reported as an + # "internal error" (exit 70) along with a raw pydantic traceback + # and an errors.pydantic.dev link, even though it is a plain user + # configuration error. Render it as a structured config error. + # NB: pydantic.ValidationError is a subclass of ValueError, so this + # case must come before any bare ValueError handling. + return_code = os.EX_DATAERR + transformed = errors.CraftValidationError.from_pydantic( + error, + file_name=f"{app.name}.yaml", + resolution=( + "Check the reported field(s) against the supported " + "values in the documented project schema." + ), + ) + transformed.__cause__ = error + case ValueError() if "BuilddBaseAlias" in str(error): + # Host base-alias lookup failures (e.g. + # ``ValueError("'26.04' is not a valid BuilddBaseAlias")``) + # currently bubble up as a bare ValueError and are reported as an + # "internal error". Surface them as a structured error instead. + # TODO: This is a shallow guard keyed off the exception # noqa: FIX002 + # message. The proper fix is to raise a typed CraftError at the + # base-alias lookup site (in craft_providers/craft_platforms) so + # this string match can be removed. + return_code = os.EX_CONFIG + transformed = craft_cli.CraftError( + f"Unsupported base for this host: {error}", + resolution=( + "Build on a host whose Ubuntu release matches a supported " + "base, or set a supported 'base'/'build-base' in your " + "project configuration." + ), + ) + transformed.__cause__ = error case _: unrecognized_error = True if isinstance(error, craft_platforms.CraftError): diff --git a/tests/unit/util/test_logging.py b/tests/unit/util/test_logging.py index 4e63990fd..a1419fc40 100644 --- a/tests/unit/util/test_logging.py +++ b/tests/unit/util/test_logging.py @@ -1,7 +1,12 @@ import logging +import os +import pydantic +import pytest import pytest_check -from craft_application import util +from craft_application import errors, util +from craft_application.application import AppMetadata +from craft_application.util.logging import handle_runtime_error from hypothesis import given, strategies @@ -18,3 +23,57 @@ def test_setup_loggers_resulting_level(names): for name in names: logger = logging.getLogger(name) pytest_check.equal(logger.level, logging.DEBUG) + + +@pytest.fixture +def app_metadata(): + return AppMetadata(name="testcraft", summary="A summary") + + +def test_handle_runtime_error_pydantic_is_user_config_error(app_metadata): + """A pydantic ValidationError is rendered as a structured config error. + + It must not fall through to the generic 'internal error' (exit 70) path. + """ + + class _Model(pydantic.BaseModel): + base: int + + try: + _Model(base="ubuntu@26.04") + except pydantic.ValidationError as exc: + error: pydantic.ValidationError = exc + + captured = [] + return_code = handle_runtime_error(app_metadata, error, print_error=captured.append) + + pytest_check.equal(return_code, os.EX_DATAERR) + pytest_check.is_instance(captured[0], errors.CraftValidationError) + pytest_check.is_in("testcraft.yaml", captured[0].args[0]) + pytest_check.is_not_none(captured[0].resolution) + pytest_check.is_true("internal error" not in captured[0].args[0]) + pytest_check.equal(captured[0].__cause__, error) + + +def test_handle_runtime_error_base_alias_value_error(app_metadata): + """A host base-alias lookup ValueError becomes a structured error.""" + error = ValueError("'26.04' is not a valid BuilddBaseAlias") + + captured = [] + return_code = handle_runtime_error(app_metadata, error, print_error=captured.append) + + pytest_check.equal(return_code, os.EX_CONFIG) + pytest_check.is_in("Unsupported base", captured[0].args[0]) + pytest_check.is_not_none(captured[0].resolution) + pytest_check.is_true("internal error" not in captured[0].args[0]) + + +def test_handle_runtime_error_generic_value_error_is_internal(app_metadata): + """An unrelated ValueError still reports as an internal error.""" + error = ValueError("something unexpected") + + captured = [] + return_code = handle_runtime_error(app_metadata, error, print_error=captured.append) + + pytest_check.equal(return_code, os.EX_SOFTWARE) + pytest_check.is_in("internal error", captured[0].args[0]) From d865e1d4ae9ab99f7e0b9aeabec90a2b0c935a3f Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 26 May 2026 20:10:39 +1200 Subject: [PATCH 2/7] docs(changelog): note structured user-config and base-alias errors --- docs/reference/changelog.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 4633cb3b5..7e14bf70c 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -58,6 +58,18 @@ Utilities - :py:class:`~craft_application.util.pro_services.ProServices` now exposes ``pro_client_exists()`` and ``get_pro_services()`` as public class methods. +Fixes +===== + +- Unhandled ``pydantic.ValidationError`` is now reported as a structured + project configuration error (exit ``EX_DATAERR``) with a recommended + resolution, rather than as an "internal error" with a raw pydantic + traceback. +- ``ValueError`` failures from a host base-alias lookup + (``'' is not a valid BuilddBaseAlias``) are now reported as a + structured configuration error (exit ``EX_CONFIG``) instead of as an + "internal error". + For a complete list of commits, check out the `7.0.0`_ release on GitHub. 6.4.0 (2026-04-23) From 473519eb1d3ec973cc5ce52d34549f21efb1e195 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 26 May 2026 20:10:44 +1200 Subject: [PATCH 3/7] test(logging): use pytest.raises to bind ValidationError for pyright --- tests/unit/util/test_logging.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/unit/util/test_logging.py b/tests/unit/util/test_logging.py index a1419fc40..7f6fff7d8 100644 --- a/tests/unit/util/test_logging.py +++ b/tests/unit/util/test_logging.py @@ -39,10 +39,9 @@ def test_handle_runtime_error_pydantic_is_user_config_error(app_metadata): class _Model(pydantic.BaseModel): base: int - try: - _Model(base="ubuntu@26.04") - except pydantic.ValidationError as exc: - error: pydantic.ValidationError = exc + with pytest.raises(pydantic.ValidationError) as exc_info: + _Model(base="ubuntu@26.04") # pyright: ignore[reportArgumentType] + error = exc_info.value captured = [] return_code = handle_runtime_error(app_metadata, error, print_error=captured.append) From b52c3c3eb633449bdfcf886695b8a24485aca2aa Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 2 Jun 2026 22:59:26 +1200 Subject: [PATCH 4/7] test(logging): use model_validate per review suggestion Signed-off-by: Tony Meyer --- tests/unit/util/test_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/util/test_logging.py b/tests/unit/util/test_logging.py index 7f6fff7d8..95d9903f1 100644 --- a/tests/unit/util/test_logging.py +++ b/tests/unit/util/test_logging.py @@ -40,7 +40,7 @@ class _Model(pydantic.BaseModel): base: int with pytest.raises(pydantic.ValidationError) as exc_info: - _Model(base="ubuntu@26.04") # pyright: ignore[reportArgumentType] + _Model.model_validate({"base": "ubuntu@26.04"}) error = exc_info.value captured = [] From a0b62d0044d59ce0b764fa0bf6f883b08b51f61f Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 2 Jun 2026 23:03:48 +1200 Subject: [PATCH 5/7] fix(errors): link to tracking issues for base-alias guard Per review on #1082: replace the TODO/lint-silencer with references to canonical/craft-application#1086 (remove this guard) and canonical/craft-providers#969 (raise typed error at the lookup site). Signed-off-by: Tony Meyer --- craft_application/util/logging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/craft_application/util/logging.py b/craft_application/util/logging.py index 0adf3b8c2..8ffda5f54 100644 --- a/craft_application/util/logging.py +++ b/craft_application/util/logging.py @@ -117,10 +117,10 @@ def handle_runtime_error( # ``ValueError("'26.04' is not a valid BuilddBaseAlias")``) # currently bubble up as a bare ValueError and are reported as an # "internal error". Surface them as a structured error instead. - # TODO: This is a shallow guard keyed off the exception # noqa: FIX002 - # message. The proper fix is to raise a typed CraftError at the - # base-alias lookup site (in craft_providers/craft_platforms) so - # this string match can be removed. + # This is a shallow guard keyed off the exception message; the + # proper fix is to raise a typed error at the base-alias lookup + # site in craft-providers. Tracked in #1086 (blocked by + # canonical/craft-providers#969). return_code = os.EX_CONFIG transformed = craft_cli.CraftError( f"Unsupported base for this host: {error}", From e6c9b064b04fd89b173dc96a91397276161be139 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 5 Jun 2026 21:40:07 +1200 Subject: [PATCH 6/7] docs(changelog): move structured-error fixes to 7.1.0 7.0.0 has already been released without these entries, so move them to a new 7.1.0 (unreleased) section. Co-Authored-By: Claude Opus 4.7 --- docs/reference/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index cc113e261..739b43afb 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -66,6 +66,10 @@ Services - ``ServiceFactory.set_kwargs()`` is removed. Use ``ServiceFactory.update_kwargs()`` instead. +For a complete list of commits, check out the `7.0.0`_ release on GitHub. + +7.1.0 (unreleased) +------------------ Fixes ===== @@ -79,7 +83,7 @@ Fixes structured configuration error (exit ``EX_CONFIG``) instead of as an "internal error". -For a complete list of commits, check out the `7.0.0`_ release on GitHub. +For a complete list of commits, check out the `7.1.0`_ release on GitHub. 6.4.0 (2026-04-23) ------------------ @@ -1370,3 +1374,4 @@ For a complete list of commits, check out the `2.7.0`_ release on GitHub. .. _6.3.1: https://github.com/canonical/craft-application/releases/tag/6.3.1 .. _6.4.0: https://github.com/canonical/craft-application/releases/tag/6.4.0 .. _7.0.0: https://github.com/canonical/craft-application/releases/tag/7.0.0 +.. _7.1.0: https://github.com/canonical/craft-application/releases/tag/7.1.0 From 475a0379a921bc860b94e50c03861ff93a567efb Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sat, 6 Jun 2026 08:52:04 +1200 Subject: [PATCH 7/7] fix: correct ordering --- docs/reference/changelog.rst | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 739b43afb..77459b1d2 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -16,6 +16,24 @@ Changelog For a complete list of commits, check out the `1.2.3`_ release on GitHub. +7.1.0 (unreleased) +------------------ + +Fixes +===== + +- Unhandled ``pydantic.ValidationError`` is now reported as a structured + project configuration error (exit ``EX_DATAERR``) with a recommended + resolution, rather than as an "internal error" with a raw pydantic + traceback. +- ``ValueError`` failures from a host base-alias lookup + (``'' is not a valid BuilddBaseAlias``) are now reported as a + structured configuration error (exit ``EX_CONFIG``) instead of as an + "internal error". + +For a complete list of commits, check out the `7.1.0`_ release on GitHub. + + 7.0.0 (2026-06-02) ------------------ @@ -68,23 +86,6 @@ Services For a complete list of commits, check out the `7.0.0`_ release on GitHub. -7.1.0 (unreleased) ------------------- - -Fixes -===== - -- Unhandled ``pydantic.ValidationError`` is now reported as a structured - project configuration error (exit ``EX_DATAERR``) with a recommended - resolution, rather than as an "internal error" with a raw pydantic - traceback. -- ``ValueError`` failures from a host base-alias lookup - (``'' is not a valid BuilddBaseAlias``) are now reported as a - structured configuration error (exit ``EX_CONFIG``) instead of as an - "internal error". - -For a complete list of commits, check out the `7.1.0`_ release on GitHub. - 6.4.0 (2026-04-23) ------------------