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).
131 lines
4.1 KiB
Python
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())
|