"""Single source of truth for the application version string. Resolution order mirrors the Dockerfile build chain so the dev checkout and the runtime container agree on the value: REMNAWAVE_MINISHOP_VERSION env > .build-version file > live ``git describe`` > ``dev+unknown`` The same value powers the admin sidebar (web process) and the anonymous telemetry beacon (worker process), so "active installs" and version breakdowns line up across both. """ from __future__ import annotations import os import re import subprocess from pathlib import Path from typing import Optional # ``/app`` in the container (parent of ``/app/backend``); repo root in dev. # Matches where the Dockerfile drops .build-version / .build-tag / .build-commit. APP_ROOT = Path(__file__).resolve().parents[3] _APP_VERSION_CACHE: Optional[str] = None def _run_git_command(*args: str) -> str: try: result = subprocess.run( ["git", *args], cwd=APP_ROOT, check=True, capture_output=True, text=True, timeout=1.5, ) except (OSError, subprocess.SubprocessError): return "" return result.stdout.strip() def _normalize_version_branch(raw_branch: str) -> str: branch = str(raw_branch or "").strip() for prefix in ("refs/heads/", "refs/remotes/origin/", "origin/"): if branch.startswith(prefix): branch = branch[len(prefix) :] break if branch in ("", "HEAD"): return "" return re.sub(r"[^A-Za-z0-9._-]+", "-", branch).strip("-")[:48] def _resolve_version_branch() -> str: for env_name in ( "REMNAWAVE_MINISHOP_BRANCH", "GIT_BRANCH", "BRANCH_NAME", "GITHUB_REF_NAME", "CI_COMMIT_REF_NAME", ): branch = _normalize_version_branch(os.getenv(env_name, "")) if branch: return branch return _normalize_version_branch( _run_git_command("branch", "--show-current") or _run_git_command("symbolic-ref", "--quiet", "--short", "HEAD") ) def _format_app_version(tag: str, sha: str, branch: str) -> str: branch_suffix = "" if not branch or branch == "main" else f"-{branch}" if tag and sha: return f"{tag}{branch_suffix}+g{sha}" if sha: return f"dev{branch_suffix}+g{sha}" if tag: return f"{tag}{branch_suffix}" return f"dev{branch_suffix}+unknown" def _read_build_file(name: str) -> str: try: return (APP_ROOT / name).read_text(encoding="utf-8").strip() except OSError: return "" def resolve_app_version() -> str: """Full version string (cached), e.g. ``v3.4.6+gabc1234``.""" global _APP_VERSION_CACHE if _APP_VERSION_CACHE: return _APP_VERSION_CACHE env_version = os.getenv("REMNAWAVE_MINISHOP_VERSION", "").strip() if env_version: _APP_VERSION_CACHE = env_version return env_version build_version = _read_build_file(".build-version") if build_version: _APP_VERSION_CACHE = build_version return build_version tag = _run_git_command("describe", "--tags", "--abbrev=0") sha = _run_git_command("rev-parse", "--short", "HEAD") branch = _resolve_version_branch() version = _format_app_version(tag, sha, branch) _APP_VERSION_CACHE = version return version def resolve_app_version_tag() -> str: """Clean release tag for low-cardinality breakdowns, e.g. ``v3.4.6``. Prefers the build-time ``.build-tag`` artifact, then a live ``git describe``; falls back to the full version string when no tag is known. """ build_tag = _read_build_file(".build-tag") if build_tag and build_tag != "unknown": return build_tag tag = _run_git_command("describe", "--tags", "--abbrev=0") if tag: return tag return resolve_app_version()