Stamp official Docker builds with a low-cardinality provenance marker and report build_provenance/image_modified in anonymous telemetry. Local and fork builds default to custom, while official GitHub/GitLab release paths mark images as official.
188 lines
5.8 KiB
Python
188 lines
5.8 KiB
Python
"""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``
|
|
|
|
Build provenance is intentionally separate from the version: official release
|
|
automation stamps official images, while local/fork builds default to custom.
|
|
|
|
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
|
|
_APP_BUILD_PROVENANCE_CACHE: Optional[str] = None
|
|
|
|
BUILD_PROVENANCE_OFFICIAL = "official"
|
|
BUILD_PROVENANCE_CUSTOM = "custom"
|
|
BUILD_PROVENANCE_UNKNOWN = "unknown"
|
|
_BUILD_PROVENANCE_VALUES = {
|
|
BUILD_PROVENANCE_OFFICIAL,
|
|
BUILD_PROVENANCE_CUSTOM,
|
|
BUILD_PROVENANCE_UNKNOWN,
|
|
}
|
|
|
|
|
|
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 _normalize_build_provenance(raw: str) -> str:
|
|
value = str(raw or "").strip().lower()
|
|
if not value:
|
|
return ""
|
|
aliases = {
|
|
"true": BUILD_PROVENANCE_OFFICIAL,
|
|
"1": BUILD_PROVENANCE_OFFICIAL,
|
|
"yes": BUILD_PROVENANCE_OFFICIAL,
|
|
"upstream": BUILD_PROVENANCE_OFFICIAL,
|
|
"release": BUILD_PROVENANCE_OFFICIAL,
|
|
"false": BUILD_PROVENANCE_CUSTOM,
|
|
"0": BUILD_PROVENANCE_CUSTOM,
|
|
"no": BUILD_PROVENANCE_CUSTOM,
|
|
"fork": BUILD_PROVENANCE_CUSTOM,
|
|
"modified": BUILD_PROVENANCE_CUSTOM,
|
|
"local": BUILD_PROVENANCE_CUSTOM,
|
|
}
|
|
value = aliases.get(value, value)
|
|
if value in _BUILD_PROVENANCE_VALUES:
|
|
return value
|
|
return BUILD_PROVENANCE_CUSTOM
|
|
|
|
|
|
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()
|
|
|
|
|
|
def resolve_build_provenance() -> str:
|
|
"""Low-cardinality image provenance: ``official``, ``custom`` or ``unknown``."""
|
|
global _APP_BUILD_PROVENANCE_CACHE
|
|
if _APP_BUILD_PROVENANCE_CACHE:
|
|
return _APP_BUILD_PROVENANCE_CACHE
|
|
|
|
env_value = _normalize_build_provenance(os.getenv("REMNAWAVE_MINISHOP_BUILD_PROVENANCE", ""))
|
|
if env_value:
|
|
_APP_BUILD_PROVENANCE_CACHE = env_value
|
|
return env_value
|
|
|
|
build_value = _normalize_build_provenance(_read_build_file(".build-provenance"))
|
|
if build_value:
|
|
_APP_BUILD_PROVENANCE_CACHE = build_value
|
|
return build_value
|
|
|
|
_APP_BUILD_PROVENANCE_CACHE = BUILD_PROVENANCE_CUSTOM
|
|
return _APP_BUILD_PROVENANCE_CACHE
|
|
|
|
|
|
def resolve_image_modified() -> bool:
|
|
"""True for non-official builds, including forks and local rebuilds."""
|
|
return resolve_build_provenance() != BUILD_PROVENANCE_OFFICIAL
|