diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index b3495f0f..a93976f9 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -14,6 +14,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Resolve git metadata + id: gitmeta + run: | + echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + echo "tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -30,6 +38,9 @@ jobs: with: context: . push: true + build-args: | + GIT_COMMIT=${{ steps.gitmeta.outputs.sha }} + GIT_TAG=${{ steps.gitmeta.outputs.tag }} tags: | ghcr.io/routstr/proxy:latest ghcr.io/routstr/core:latest diff --git a/Dockerfile b/Dockerfile index adf5a7df..ffb405a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,10 @@ WORKDIR /app COPY . . +ARG GIT_COMMIT="" +ARG GIT_TAG="" +ENV GIT_COMMIT=${GIT_COMMIT} +ENV GIT_TAG=${GIT_TAG} ENV PORT=8000 ENV PYTHONUNBUFFERED=1 diff --git a/Dockerfile.full b/Dockerfile.full index 923b6e8f..12db34fc 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -41,6 +41,10 @@ RUN uv sync --no-dev # Copy the built UI from the ui-builder stage COPY --from=ui-builder /app/ui/out ./ui_out +ARG GIT_COMMIT="" +ARG GIT_TAG="" +ENV GIT_COMMIT=${GIT_COMMIT} +ENV GIT_TAG=${GIT_TAG} ENV PORT=8000 ENV PYTHONUNBUFFERED=1 diff --git a/routstr/core/main.py b/routstr/core/main.py index 7771dd5a..58cbaf43 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -1,5 +1,4 @@ import asyncio -import os from contextlib import asynccontextmanager from pathlib import Path from typing import AsyncGenerator @@ -32,16 +31,12 @@ from .logging import get_logger, setup_logging from .middleware import LoggingMiddleware from .settings import SettingsService from .settings import settings as global_settings +from .version import __version__ # Initialize logging first setup_logging() logger = get_logger(__name__) -if os.getenv("VERSION_SUFFIX") is not None: - __version__ = f"0.4.3-{os.getenv('VERSION_SUFFIX')}" -else: - __version__ = "0.4.3" - @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: diff --git a/routstr/core/version.py b/routstr/core/version.py new file mode 100644 index 00000000..bd03d185 --- /dev/null +++ b/routstr/core/version.py @@ -0,0 +1,81 @@ +"""Application version resolution. + +Priority order: + +1. ``VERSION_SUFFIX`` env var (manual override; preserves prior behaviour). +2. Bare base version when HEAD is on the matching release tag (detected via + ``GIT_TAG`` env or ``git describe --tags --exact-match HEAD``). +3. ``GIT_COMMIT`` env var (build-time injection) -> ``+g``. +4. Local ``.git`` lookup (source checkouts) -> ``+g``. +5. Fallback: bare base version. + +The ``+g`` form is PEP 440 local-version syntax so the result remains a +valid package version. +""" + +from __future__ import annotations + +import os +import subprocess +from functools import lru_cache +from pathlib import Path + +BASE_VERSION = "0.4.3" + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_GIT_TIMEOUT_SECONDS = 2.0 + + +def _run_git(*args: str) -> str | None: + try: + result = subprocess.run( # noqa: S603 - fixed argv, no shell + ["git", *args], + cwd=_REPO_ROOT, + check=False, + capture_output=True, + text=True, + timeout=_GIT_TIMEOUT_SECONDS, + ) + except (FileNotFoundError, subprocess.SubprocessError, OSError): + return None + if result.returncode != 0: + return None + return result.stdout.strip() or None + + +def _git_short_sha() -> str | None: + sha = os.getenv("GIT_COMMIT", "").strip() + if sha: + return sha[:7] + return _run_git("rev-parse", "--short=7", "HEAD") + + +def _on_tagged_release() -> bool: + tag = os.getenv("GIT_TAG", "").strip() + if tag: + return tag.lstrip("v") == BASE_VERSION + described = _run_git("describe", "--tags", "--exact-match", "HEAD") + if not described: + return False + return described.lstrip("v") == BASE_VERSION + + +@lru_cache(maxsize=1) +def get_version() -> str: + suffix = os.getenv("VERSION_SUFFIX") + if suffix is not None: + return f"{BASE_VERSION}-{suffix}" + + if _on_tagged_release(): + return BASE_VERSION + + sha = _git_short_sha() + if not sha: + return BASE_VERSION + + return f"{BASE_VERSION}+g{sha}" + + +__version__ = get_version() + +__all__ = ["BASE_VERSION", "__version__", "get_version"] diff --git a/routstr/nostr/listing.py b/routstr/nostr/listing.py index a512add7..7c60e9b1 100644 --- a/routstr/nostr/listing.py +++ b/routstr/nostr/listing.py @@ -26,7 +26,7 @@ logger = get_logger(__name__) def get_app_version() -> str | None: try: - from ..core.main import __version__ as imported_version + from ..core.version import __version__ as imported_version return imported_version except Exception: