diff --git a/bot_bottle/cli/__init__.py b/bot_bottle/cli/__init__.py index 879d067..2c81e6b 100644 --- a/bot_bottle/cli/__init__.py +++ b/bot_bottle/cli/__init__.py @@ -7,6 +7,7 @@ from __future__ import annotations import sys +from ..errors import MissingEnvVarError from ..log import Die, die, error from ..manifest import ManifestError from ._common import PROG @@ -76,9 +77,10 @@ def main(argv: list[str] | None = None) -> int: die(f"unknown command: {command}") try: return handler(rest) or 0 + except MissingEnvVarError as e: + error(str(e)) + return 1 except ManifestError as e: - # Manifest/config problems surface as a catchable exception; - # print the reason and exit non-zero (same UX die() used to give). error(str(e)) return 1 except Die as e: diff --git a/bot_bottle/egress.py b/bot_bottle/egress.py index 8d29a00..98ea7bf 100644 --- a/bot_bottle/egress.py +++ b/bot_bottle/egress.py @@ -23,6 +23,7 @@ from .egress_addon_core import ( PathMatch as CorePathMatch, Route, ) +from .errors import MissingEnvVarError from .log import die if TYPE_CHECKING: @@ -354,16 +355,18 @@ def egress_resolve_token_values( for token_env, token_ref in token_env_map.items(): value = host_env.get(token_ref) if value is None: - die( + raise MissingEnvVarError( + token_ref, f"egress: host env var '{token_ref}' is unset. Set it " f"before launching, or remove the corresponding auth block " - f"from bottle.egress.routes." + f"from bottle.egress.routes.", ) if not value: - die( + raise MissingEnvVarError( + token_ref, f"egress: host env var '{token_ref}' is empty. The " f"egress will not inject an empty token; set it to " - f"the real value or remove the route's auth block." + f"the real value or remove the route's auth block.", ) out[token_env] = value return out diff --git a/bot_bottle/env.py b/bot_bottle/env.py index 767ad04..63eaebb 100644 --- a/bot_bottle/env.py +++ b/bot_bottle/env.py @@ -35,6 +35,7 @@ import re import sys from dataclasses import dataclass, field +from .errors import MissingEnvVarError from .log import die from .manifest import Manifest @@ -136,9 +137,10 @@ def resolve_env(manifest: Manifest) -> ResolvedEnv: host_var = env_entry_interpolated_from(raw) host_value = os.environ.get(host_var, "") if not host_value: - die( + raise MissingEnvVarError( + host_var, f"env entry {name} is interpolated from ${host_var}, " - f"but ${host_var} is unset or empty in the host environment." + f"but ${host_var} is unset or empty in the host environment.", ) forwarded[name] = host_value else: # literal diff --git a/bot_bottle/errors.py b/bot_bottle/errors.py new file mode 100644 index 0000000..8866878 --- /dev/null +++ b/bot_bottle/errors.py @@ -0,0 +1,18 @@ +"""bot-bottle application-level exception hierarchy. + +Exceptions here are caught by the CLI dispatcher (``cli/__init__.py``) +and rendered as ``bot-bottle: error: …`` lines — same UX as ``die()``, +but without coupling library code to stderr output.""" + +from __future__ import annotations + + +class MissingEnvVarError(Exception): + """Raised when a required host environment variable is unset or empty. + + ``var_name`` is the exact name of the missing variable so callers + can display it without re-parsing the message string.""" + + def __init__(self, var_name: str, message: str) -> None: + self.var_name = var_name + super().__init__(message) diff --git a/bot_bottle/git_gate.py b/bot_bottle/git_gate.py index dff530a..734c84d 100644 --- a/bot_bottle/git_gate.py +++ b/bot_bottle/git_gate.py @@ -36,7 +36,8 @@ from abc import ABC from dataclasses import dataclass from pathlib import Path -from .log import die, info +from .errors import MissingEnvVarError +from .log import info from .manifest import ManifestBottle, ManifestGitEntry @@ -564,9 +565,10 @@ def _provision_dynamic_key( pk = entry.Key token = os.environ.get(pk.forge_token_env) if token is None: - die( + raise MissingEnvVarError( + pk.forge_token_env, f"git-gate.repos[{entry.Name!r}] key.forge_token_env" - f" = {pk.forge_token_env!r}: env var is not set" + f" = {pk.forge_token_env!r}: env var is not set", ) api_url = pk.api_url or f"https://{entry.UpstreamHost}" provisioner = get_provisioner(pk.provider, token, api_url) @@ -608,10 +610,11 @@ def revoke_git_gate_provisioned_keys(bottle: ManifestBottle, stage_dir: Path) -> key_id = id_file.read_text().strip() token = os.environ.get(pk.forge_token_env) if token is None: - raise RuntimeError( + raise MissingEnvVarError( + pk.forge_token_env, f"git-gate.repos[{entry.Name!r}] key.forge_token_env" f" = {pk.forge_token_env!r}: env var is not set;" - f" cannot revoke deploy key {key_id}" + f" cannot revoke deploy key {key_id}", ) api_url = pk.api_url or f"https://{entry.UpstreamHost}" provisioner = get_provisioner(pk.provider, token, api_url)