Skip to content

check_bumpversion_config

rhiza_hooks.check_bumpversion_config

Check that bump-my-version can actually discover this project's version config.

bump-my-version reads its configuration from a fixed set of filenames. When it finds none it does not fail — it falls back to git describe and reports the last reachable tag as the current version. Release tooling then computes bump candidates from that number instead of the project's own, which can offer a version that has already been published.

That failure is silent by construction, so this hook makes it loud: if the project declares a version, a bumpversion section must live somewhere the tool will look.

The specific trap this was written for: rhiza syncs a fully-formed [tool.bumpversion] block into .rhiza/.cfg.toml, which is not one of the searched filenames, so it never takes effect.

The same hook also checks the config's targets — the [[tool.bumpversion.files]] entries a release rewrites — because they fail equally late and just as quietly:

  • An entry pointing at a template-owned file loses its pattern at the next sync, which restores that file. The release after that aborts, long after the commit that caused it.
  • A pattern that no longer occurs in its file (or occurs twice) breaks the bump itself. bump-my-version reports that loudly, but only while cutting a version.

Locating and parsing the configuration lives in :mod:rhiza_hooks._bumpversion_config, which owns the two on-disk formats and normalises them into a :class:~rhiza_hooks._bumpversion_config.BumpversionConfig. This module is the CLI/orchestration layer: it judges that result and reports. That module is private, so the package's public surface is unchanged by the split; within it a leading underscore marks a helper with no caller outside its own file, which is why this module imports only unprefixed names from it.

Exit codes

0 - Validation passed 1 - Validation failed

check_bumpversion_config(repo_root)

Check the bumpversion config: discoverable, agreeing with pyproject, and rewritable.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository.

required

Returns:

Type Description
list[str]

List of error messages (empty when the configuration is sound).

Source code in rhiza_hooks/check_bumpversion_config.py
def check_bumpversion_config(repo_root: Path) -> list[str]:
    """Check the bumpversion config: discoverable, agreeing with pyproject, and rewritable.

    Args:
        repo_root: Root directory of the repository.

    Returns:
        List of error messages (empty when the configuration is sound).
    """
    project_version = read_project_version(repo_root)
    if project_version is None:
        # No statically declared version: nothing for bump-my-version to own.
        return []

    config = find_config(repo_root)
    if config is None:
        return [_no_config_message(repo_root, project_version)]

    errors = _version_mismatch(config.filename, config.current_version, project_version)

    managed = managed_paths(repo_root)
    for target in config.targets:
        errors.extend(_check_target(repo_root, project_version, target, managed))

    return errors

find_discoverable_config(repo_root)

Locate the first bumpversion section bump-my-version would actually read.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository.

required

Returns:

Type Description
tuple[str, str | None] | None

A (filename, current_version) pair for the winning config, where

tuple[str, str | None] | None

current_version is None when the section omits that key. Returns None

tuple[str, str | None] | None

when no searched file carries a bumpversion section.

Source code in rhiza_hooks/check_bumpversion_config.py
def find_discoverable_config(repo_root: Path) -> tuple[str, str | None] | None:
    """Locate the first bumpversion section bump-my-version would actually read.

    Args:
        repo_root: Root directory of the repository.

    Returns:
        A ``(filename, current_version)`` pair for the winning config, where
        ``current_version`` is None when the section omits that key. Returns None
        when no searched file carries a bumpversion section.
    """
    config = find_config(repo_root)
    if config is None:
        return None
    return config.filename, config.current_version

has_undiscovered_config(repo_root)

Report whether a bumpversion section sits in a file that is never searched.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository.

required

Returns:

Type Description
bool

True if .rhiza/.cfg.toml carries a [tool.bumpversion] section.

Source code in rhiza_hooks/check_bumpversion_config.py
def has_undiscovered_config(repo_root: Path) -> bool:
    """Report whether a bumpversion section sits in a file that is never searched.

    Args:
        repo_root: Root directory of the repository.

    Returns:
        True if ``.rhiza/.cfg.toml`` carries a ``[tool.bumpversion]`` section.
    """
    data = load_toml(repo_root / _UNDISCOVERED)
    if data is None:
        return False
    tool = data.get("tool")
    return isinstance(tool, dict) and isinstance(tool.get("bumpversion"), dict)

main(argv=None)

Run the hook and return a process exit code.

Source code in rhiza_hooks/check_bumpversion_config.py
def main(argv: list[str] | None = None) -> int:
    """Run the hook and return a process exit code."""
    parser = argparse.ArgumentParser(description="Check bump-my-version configuration is discoverable")
    parser.add_argument(
        "filenames",
        nargs="*",
        help="Filenames (ignored, checks repo root)",
    )
    parser.parse_args(argv)  # validate/consume pre-commit's filename args; result unused

    errors = check_bumpversion_config(find_repo_root())

    for error in errors:
        print(f"ERROR: {error}")

    return 1 if errors else 0

read_project_version(repo_root)

Read [project].version from pyproject.toml.

Parameters:

Name Type Description Default
repo_root Path

Root directory of the repository.

required

Returns:

Type Description
str | None

The declared version, or None when there is no pyproject.toml, no

str | None

[project] table, or no static version key. A project using

str | None

dynamic = ["version"] therefore reads as None and is not checked —

str | None

its version does not live in a file bump-my-version would rewrite.

Source code in rhiza_hooks/check_bumpversion_config.py
def read_project_version(repo_root: Path) -> str | None:
    """Read ``[project].version`` from pyproject.toml.

    Args:
        repo_root: Root directory of the repository.

    Returns:
        The declared version, or None when there is no pyproject.toml, no
        ``[project]`` table, or no static ``version`` key. A project using
        ``dynamic = ["version"]`` therefore reads as None and is not checked —
        its version does not live in a file bump-my-version would rewrite.
    """
    data = load_toml(repo_root / "pyproject.toml")
    if data is None:
        return None
    project = data.get("project")
    if not isinstance(project, dict):
        return None
    version = project.get("version")
    return version if isinstance(version, str) else None