Files
ObsiGate/backend/version.py
T
bruno d98c0c2f33
CI / lint (push) Successful in 1m27s
CI / security (push) Successful in 57s
CI / test (push) Successful in 3m11s
CI / build (push) Successful in 54s
CI / e2e (push) Successful in 10m59s
fix(version): BUG-047 la version suit chaque livraison (source unique VERSION + bump SemVer automatique)
VERSION (racine) devient la source unique de verite MAJEUR.MINEUR.CORRECTIF, incrementee a chaque commit par le hook prepare-commit-msg (BREAKING -> majeur, feat -> mineur, sinon correctif) ; post-commit rattache les fichiers derives au commit et cree le tag vX.Y.Z, publie au push (push.followTags). Tous les derives sont resynchronises : package.json, desktop Tauri, README.md/README.fr.md, docs/ROADMAP.md, CHANGELOG.md. Backend, image Docker et desktop lisent le meme fichier (plus de numero code en dur). Outils : scripts/bump_version.py, scripts/install-hooks.sh. Tests : tests/test_version.py (46, garde-fou de coherence globale).
2026-09-16 11:02:47 -04:00

131 lines
4.1 KiB
Python

"""
Version management for ObsiGate.
Source unique de vérité : le fichier `VERSION` à la racine du dépôt, au format
`MAJEUR.MINEUR.CORRECTIF` (incrémenté à chaque livraison par
`scripts/bump_version.py`, hook git `commit-msg`). get_version() retourne
toujours une chaîne propre "x.y.z" — jamais "-dev", "0.0.0-dev" ou "-N-gHASH" :
le même numéro s'affiche dans le badge d'en-tête, la boîte À propos et /api/health.
Les sources sont consultées dans cet ordre :
1. variable d'environnement OBSIGATE_VERSION (surcharge explicite / tests) ;
2. `VERSION` à la racine du dépôt (source unique, copiée dans l'image Docker) ;
3. `backend/VERSION` (ancien emplacement, encore produit par certains builds) ;
4. dernier tag git (`git describe --tags --abbrev=0`) ;
5. "0.0.0".
Exemples :
VERSION = 2.3.0 -> "2.3.0"
tag v2.3.0, 4 commits -> "2.3.0"
aucun VERSION / git -> "0.0.0"
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
_ROOT = Path(__file__).resolve().parent.parent # racine du dépôt ObsiGate
VERSION_FILE = _ROOT / "VERSION" # source unique de vérité
LEGACY_VERSION_FILE = Path(__file__).resolve().parent / "VERSION" # backend/VERSION
_ENV_VAR = "OBSIGATE_VERSION"
def _run_git(args: list[str]) -> str:
"""Run a git command in the repo root; return stdout (stripped) or ''."""
try:
result = subprocess.run(
["git", *args],
cwd=str(_ROOT),
capture_output=True,
text=True,
timeout=5,
check=False,
)
if result.returncode == 0:
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
pass
return ""
def _clean_base(raw: str) -> str:
"""Reduce a version string to its numeric MAJOR.MINOR.PATCH base.
Handles "v2.0.0", "2.0.0-31-gabc1234", "2.0.0-dev", "0.0.0-dev".
Returns "" if no numeric triplet can be parsed.
"""
s = (raw or "").strip().lstrip("vV")
base = s.split("-")[0] # strip -N-gHASH / -dev / -dirty suffixes
parts = base.split(".")
nums = []
for p in parts[:3]:
if not p.isdigit():
break
nums.append(str(int(p)))
if len(nums) != 3:
return ""
return ".".join(nums)
def _read_version_file(path: Path) -> str:
"""Clean base of a VERSION file, or '' when absent/unreadable/invalid."""
try:
if not path.exists():
return ""
return _clean_base(path.read_text(encoding="utf-8", errors="replace"))
except OSError:
return ""
def get_git_describe() -> str:
"""Full `git describe` string (e.g. "2.0.0-31-gabc1234") or '' if no git."""
return _run_git(["describe", "--tags", "--dirty=-dirty"]).lstrip("v")
def get_git_commit() -> str:
"""Short HEAD commit hash (e.g. "abc1234") or '' if unavailable."""
return _run_git(["rev-parse", "--short", "HEAD"])
def get_version() -> str:
"""Return the clean release version x.y.z — never a -suffix.
Priority: OBSIGATE_VERSION -> ./VERSION -> backend/VERSION -> latest git tag
-> "0.0.0".
"""
# 1) Explicit override (docker-compose, tests, builds hors dépôt)
override = _clean_base(os.environ.get(_ENV_VAR, ""))
if override:
return override
# 2) Source unique de vérité : VERSION à la racine du dépôt
base = _read_version_file(VERSION_FILE)
if base:
return base
# 3) Ancien emplacement (backend/VERSION, baké par certains builds)
base = _read_version_file(LEGACY_VERSION_FILE)
if base:
return base
# 4) Dernier tag git
base = _clean_base(_run_git(["describe", "--tags", "--abbrev=0"]))
if base:
return base
# 5) Rien d'exploitable
return "0.0.0"
def get_version_tuple() -> tuple[int, int, int]:
"""Return (major, minor, patch) tuple for programmatic use."""
raw = get_version()
base = _clean_base(raw) or "0.0.0"
major, minor, patch = (int(p) for p in base.split("."))
return (major, minor, patch)
if __name__ == "__main__":
print(get_version())