"""Pose une version : aligne les fichiers, commite, tague. Le numéro de version est écrit à quatre endroits qui doivent rester d'accord (paquet Python, module, frontend, installeur Windows). Les tenir à jour à la main, c'est publier tôt ou tard un tag `v0.3.0` sur un code qui se déclare `0.1.0`. Ce script fait la mise à jour d'un bloc et refuse d'avancer au moindre doute plutôt que de produire une version à moitié cohérente. uv run python scripts/release.py 0.2.0 Il s'arrête avant le `git push` : le tag reste local tant qu'il n'est pas poussé, et c'est le push qui déclenche la CI (images Docker + build Windows). """ import argparse import re import subprocess import sys from pathlib import Path RACINE = Path(__file__).resolve().parent.parent # Un fichier, le motif qui y porte la version, et le remplacement. Le motif doit # capturer la version dans le groupe 1 pour que l'on puisse relire l'ancienne # valeur, et matcher **exactement une fois** (vérifié plus bas). PORTEURS_DE_VERSION = [ ("pyproject.toml", re.compile(r'^version = "([^"]+)"$', re.M), 'version = "{v}"'), ( "src/plesna_gerance/__init__.py", re.compile(r'^__version__ = "([^"]+)"$', re.M), '__version__ = "{v}"', ), ( "frontend/package.json", re.compile(r'^ "version": "([^"]+)",$', re.M), ' "version": "{v}",', ), ( "packaging/installer.iss", re.compile(r'^#define AppVersion "([^"]+)"$', re.M), '#define AppVersion "{v}"', ), ] SEMVER = re.compile(r"^\d+\.\d+\.\d+$") class Refus(Exception): """Condition non remplie : on s'arrête sans rien modifier.""" def git(*args: str) -> str: """Lance une commande git dans le dépôt et renvoie sa sortie.""" resultat = subprocess.run( ["git", "-C", str(RACINE), *args], capture_output=True, text=True, ) if resultat.returncode != 0: raise Refus(f"git {' '.join(args)} a échoué :\n{resultat.stderr.strip()}") return resultat.stdout.strip() def verifier_le_depot(version: str, autoriser_hors_main: bool) -> None: """Refuse de poser une version depuis un état de dépôt douteux.""" branche = git("rev-parse", "--abbrev-ref", "HEAD") if branche != "main" and not autoriser_hors_main: raise Refus( f"branche courante « {branche} » : une version se pose sur main.\n" "Fusionner d'abord, ou forcer avec --autoriser-hors-main." ) if git("status", "--porcelain"): raise Refus( "l'arbre de travail n'est pas propre : commiter ou remiser d'abord.\n" "Une version doit correspondre à un état de code identifiable." ) if git("tag", "--list", f"v{version}"): raise Refus( f"le tag v{version} existe déjà. Choisir un numéro supérieur " "(un tag publié ne se réécrit pas)." ) def appliquer_la_version(version: str) -> list[str]: """Écrit la version dans les fichiers porteurs. Renvoie ceux qui ont changé.""" modifies = [] for chemin_relatif, motif, remplacement in PORTEURS_DE_VERSION: chemin = RACINE / chemin_relatif contenu = chemin.read_text(encoding="utf-8") occurrences = motif.findall(contenu) if len(occurrences) != 1: raise Refus( f"{chemin_relatif} : {len(occurrences)} ligne(s) de version trouvée(s), " "une seule attendue. Le fichier a changé de forme : " "corriger le motif dans scripts/release.py." ) if occurrences[0] == version: continue chemin.write_text( motif.sub(remplacement.format(v=version), contenu), encoding="utf-8" ) modifies.append(chemin_relatif) return modifies def main() -> int: parseur = argparse.ArgumentParser(description=__doc__) parseur.add_argument("version", help="numéro de version, sans le v (ex. 0.2.0)") parseur.add_argument( "--autoriser-hors-main", action="store_true", help="pose la version depuis une autre branche (rattrapage)", ) arguments = parseur.parse_args() version = arguments.version.lstrip("v") if not SEMVER.match(version): print( f"Version « {arguments.version} » invalide : attendu MAJEUR.MINEUR.CORRECTIF " "(ex. 0.2.0).", file=sys.stderr, ) return 1 try: verifier_le_depot(version, arguments.autoriser_hors_main) modifies = appliquer_la_version(version) if modifies: git("add", *modifies) git("commit", "-m", f"chore: passe en version {version}") print(f"Version écrite dans : {', '.join(modifies)}") else: print(f"Les fichiers déclarent déjà {version} : aucun commit de version.") git("tag", "-a", f"v{version}", "-m", f"Version {version}") except Refus as refus: print(f"Publication interrompue : {refus}", file=sys.stderr) return 1 print(f"Tag v{version} posé sur {git('rev-parse', '--short', 'HEAD')}.") print("\nRien n'est publié tant que le tag n'est pas poussé :") print(f" git push origin main v{version}") print("\nLe push déclenche la CI : images Docker taguées puis build Windows.") return 0 if __name__ == "__main__": sys.exit(main())