diff --git a/docker/image/Dockerfile b/docker/image/Dockerfile index 182b3db..994e833 100644 --- a/docker/image/Dockerfile +++ b/docker/image/Dockerfile @@ -52,6 +52,7 @@ FROM python:3.14-slim AS build-image ARG DEBIAN_FRONTEND=noninteractive ARG LC_ALL=C +ARG GIT_COMMIT_HASH SHELL ["/bin/bash", "-euo", "pipefail", "-c"] diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py new file mode 100644 index 0000000..fcb8c0b --- /dev/null +++ b/tests/unit/test_version.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: © Jens Bergmann and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ +import importlib +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest + +import version + + +class TestVersion: + def test_get_version_prefers_git_commit_hash_env_var(self, monkeypatch:pytest.MonkeyPatch) -> None: + """Use explicit build metadata when present.""" + monkeypatch.setenv("GIT_COMMIT_HASH", "abc1234") + + with patch("version.shutil.which") as which_mock, patch("version.subprocess.run") as run_mock: + assert version.get_version() == f"{datetime.now(timezone.utc).year}+abc1234" + + which_mock.assert_not_called() + run_mock.assert_not_called() + + def test_get_version_falls_back_to_git(self, monkeypatch:pytest.MonkeyPatch) -> None: + """Resolve the version from git metadata when no override is provided.""" + monkeypatch.delenv("GIT_COMMIT_HASH", raising = False) + result = MagicMock(stdout = "deadbee\n") + + with patch("version.shutil.which", return_value = "/usr/bin/git") as which_mock, patch("version.subprocess.run", return_value = result) as run_mock: + assert version.get_version() == f"{datetime.now(timezone.utc).year}+deadbee" + + which_mock.assert_called_once_with("git") + run_mock.assert_called_once_with( + ["/usr/bin/git", "rev-parse", "--short", "HEAD"], + check = True, + capture_output = True, + text = True, + ) + + def test_get_version_raises_when_git_is_missing(self, monkeypatch:pytest.MonkeyPatch) -> None: + """Fail clearly when no explicit hash and no git executable are available.""" + monkeypatch.delenv("GIT_COMMIT_HASH", raising = False) + + with patch("version.shutil.which", return_value = None), pytest.raises(RuntimeError, match = "set GIT_COMMIT_HASH or build from a valid git checkout"): + version.get_version() + + def test_get_version_raises_when_git_head_is_unavailable(self, monkeypatch:pytest.MonkeyPatch) -> None: + """Fail clearly when git exists but repository metadata is unavailable.""" + monkeypatch.delenv("GIT_COMMIT_HASH", raising = False) + called_process_error = getattr(importlib.import_module("subprocess"), "CalledProcessError") + + with patch("version.shutil.which", return_value = "/usr/bin/git"), patch( + "version.subprocess.run", + side_effect = called_process_error(128, ["/usr/bin/git", "rev-parse", "--short", "HEAD"]), + ), pytest.raises(RuntimeError, match = "set GIT_COMMIT_HASH or build from a valid git checkout"): + version.get_version() diff --git a/version.py b/version.py index 3092b3a..d82af92 100644 --- a/version.py +++ b/version.py @@ -3,6 +3,7 @@ SPDX-FileCopyrightText: © Sebastian Thomschke and contributors SPDX-License-Identifier: AGPL-3.0-or-later SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ """ +import os import shutil import subprocess from datetime import datetime, timezone @@ -10,14 +11,21 @@ from datetime import datetime, timezone # used in pyproject.toml [tool.pdm.version] def get_version() -> str: + commit_hash = os.environ.get("GIT_COMMIT_HASH", "").strip() + if commit_hash: + return f"{datetime.now(timezone.utc).year}+{commit_hash}" + git = shutil.which("git") if git is None: - raise RuntimeError("git executable not found, unable to compute version") - result = subprocess.run( # noqa: S603 running git is safe here - [git, "rev-parse", "--short", "HEAD"], - check=True, - capture_output=True, - text=True, - ) + raise RuntimeError("unable to compute version: set GIT_COMMIT_HASH or build from a valid git checkout") + try: + result = subprocess.run( # noqa: S603 running git is safe here + [git, "rev-parse", "--short", "HEAD"], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as ex: + raise RuntimeError("unable to compute version: set GIT_COMMIT_HASH or build from a valid git checkout") from ex commit_hash = result.stdout.strip() return f"{datetime.now(timezone.utc).year}+{commit_hash}"