#!/usr/bin/env python3 """ ObsiGate — Script de publication des binaires Desktop vers Gitea Releases. Ce script recherche les binaires compilés localement (Windows .exe/.msi, Linux .AppImage/.deb), génère le fichier checksums.txt (empreintes SHA256) et crée/met à jour la Release sur Gitea via son API REST (https://git.dracodev.net/api/v1). Usage: python scripts/publish_release.py --tag v2.0.0 python scripts/publish_release.py --tag v2.0.0 --dry-run python scripts/publish_release.py --tag v2.0.0 --token VOTRE_TOKEN_GITEA Prérequis: Variable d'environnement GITEA_TOKEN ou jeton défini dans .env ou via --token. """ import argparse import hashlib import json import os import pathlib import re import sys import urllib.error import urllib.parse import urllib.request DEFAULT_GITEA_URL = "https://git.dracodev.net" DEFAULT_REPO = "Projets/ObsiGate" def load_env_token() -> str: """Recherche GITEA_TOKEN dans les variables d'environnement ou dans le fichier .env.""" token = os.environ.get("GITEA_TOKEN", "").strip() if token: return token env_path = pathlib.Path(".env") if env_path.exists(): with open(env_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("GITEA_TOKEN="): return line.split("=", 1)[1].strip().strip('"').strip("'") return "" def get_current_git_tag() -> str: """Tente de lire la version depuis tauri.conf.json ou pyproject.toml si pas de tag fourni.""" tauri_conf = pathlib.Path("desktop/tauri.conf.json") if tauri_conf.exists(): try: with open(tauri_conf, "r", encoding="utf-8") as f: data = json.load(f) ver = data.get("version") if ver: return f"v{ver}" if not ver.startswith("v") else ver except Exception: pass return "v2.0.0" def find_desktop_binaries(base_dir: pathlib.Path) -> list[pathlib.Path]: """Recherche les binaires Desktop générés par Tauri dans le répertoire desktop/.""" patterns = [ "target/**/release/bundle/nsis/*.exe", "target/**/release/bundle/msi/*.msi", "target/**/release/bundle/appimage/*.AppImage", "target/**/release/bundle/deb/*.deb", "release_assets/*", ] found = [] desktop_path = base_dir / "desktop" if (base_dir / "desktop").exists() else base_dir for pattern in patterns: for p in desktop_path.glob(pattern): if p.is_file() and not p.name.endswith(".pdb"): found.append(p) # Dédoublonner par nom de fichier (garder le plus récent si doublon) unique_files: dict[str, pathlib.Path] = {} for path in found: name = path.name if name not in unique_files or path.stat().st_mtime > unique_files[name].stat().st_mtime: unique_files[name] = path return sorted(list(unique_files.values()), key=lambda x: x.name) def calculate_sha256(filepath: pathlib.Path) -> str: """Calcule l'empreinte SHA-256 d'un fichier.""" sha = hashlib.sha256() with open(filepath, "rb") as f: while chunk := f.read(65536): sha.update(chunk) return sha.hexdigest() def generate_checksums_file(files: list[pathlib.Path], output_path: pathlib.Path) -> dict[str, str]: """Génère le fichier checksums.txt contenant SHA256 et noms de fichiers.""" checksums = {} lines = [] for f in files: digest = calculate_sha256(f) checksums[f.name] = digest lines.append(f"{digest} {f.name}") with open(output_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") return checksums def make_gitea_request( url: str, method: str = "GET", token: str = "", data: bytes | None = None, content_type: str | None = None, ) -> tuple[int, dict | str]: """Effectue une requête HTTP vers l'API REST Gitea.""" req = urllib.request.Request(url, method=method) if token: req.add_header("Authorization", f"token {token}") if content_type: req.add_header("Content-Type", content_type) req.add_header("Accept", "application/json") try: with urllib.request.urlopen(req, data=data) as resp: body = resp.read().decode("utf-8") status = resp.status try: return status, json.loads(body) except Exception: return status, body except urllib.error.HTTPError as e: body = e.read().decode("utf-8") try: return e.code, json.loads(body) except Exception: return e.code, body def upload_asset_to_gitea( upload_url: str, file_path: pathlib.Path, token: str, ) -> bool: """Téléverse un fichier binaire vers l'API Gitea Release en Multipart Form.""" boundary = "----WebKitFormBoundary" + hashlib.md5(file_path.name.encode()).hexdigest()[:16] with open(file_path, "rb") as f: file_content = f.read() filename = file_path.name content_type = "application/octet-stream" if filename.endswith(".exe"): content_type = "application/x-msdownload" elif filename.endswith(".msi"): content_type = "application/x-ole-storage" elif filename.endswith(".deb"): content_type = "application/vnd.debian.binary-package" elif filename.endswith(".txt"): content_type = "text/plain" body = [] body.append(f"--{boundary}".encode("utf-8")) body.append(f'Content-Disposition: form-data; name="attachment"; filename="{filename}"'.encode("utf-8")) body.append(f"Content-Type: {content_type}".encode("utf-8")) body.append(b"") body.append(file_content) body.append(f"--{boundary}--".encode("utf-8")) body.append(b"") payload = b"\r\n".join(body) status, resp = make_gitea_request( url=upload_url, method="POST", token=token, data=payload, content_type=f"multipart/form-data; boundary={boundary}", ) if status in (200, 201): print(f" ✅ Uploadé : {filename} ({len(file_content) / (1024*1024):.2f} MB)") return True else: print(f" ❌ Erreur upload pour {filename} (HTTP {status}) : {resp}") return False def main(): if hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass parser = argparse.ArgumentParser(description="Publier des binaires Desktop sur Gitea Releases.") parser.add_argument("--tag", "-t", help="Tag de version (ex: v2.0.0)") parser.add_argument("--token", help="Jeton d'accès API Gitea (GITEA_TOKEN)") parser.add_argument("--gitea-url", default=DEFAULT_GITEA_URL, help=f"URL Gitea (défaut: {DEFAULT_GITEA_URL})") parser.add_argument("--repo", default=DEFAULT_REPO, help=f"Dépôt Gitea (défaut: {DEFAULT_REPO})") parser.add_argument("--notes", help="Notes de version (description textuelle)") parser.add_argument("--dry-run", action="store_true", help="Simuler sans envoyer à Gitea") parser.add_argument("--draft", action="store_true", help="Marquer la release comme brouillon") parser.add_argument("--prerelease", action="store_true", help="Marquer comme pré-release") args = parser.parse_args() tag = args.tag or get_current_git_tag() token = args.token or load_env_token() gitea_url = args.gitea_url.rstrip("/") repo = args.repo.strip("/") print("==========================================================") print(f" ObsiGate — Publication Gitea Release ({tag})") print("==========================================================") print(f"📍 Serveur Gitea : {gitea_url}") print(f"📦 Dépôt : {repo}") print(f"🏷️ Tag : {tag}") print(f"🧪 Mode Dry-Run : {'OUI' if args.dry_run else 'NON'}") # 1. Recherche des binaires root_dir = pathlib.Path(__file__).resolve().parent.parent binaries = find_desktop_binaries(root_dir) if not binaries: print("\n⚠️ Aucun binaire Desktop trouvé dans desktop/target/.") print("💡 Pensez à exécuter au préalable :") print(" - Windows : .\\desktop\\build-windows.bat") print(" - Linux : ./desktop/build-linux.sh") else: print(f"\n🔍 {len(binaries)} binaire(s) trouvé(s) :") for b in binaries: size_mb = b.stat().st_size / (1024 * 1024) print(f" • {b.name} ({size_mb:.2f} MB) -> {b}") # 2. Génération de checksums.txt checksums_path = root_dir / "desktop" / "checksums.txt" all_files_to_upload = list(binaries) if binaries: print(f"\n🔐 Génération des empreintes SHA-256 ({checksums_path.name})...") generate_checksums_file(binaries, checksums_path) all_files_to_upload.append(checksums_path) print(" ✅ checksums.txt généré avec succès") if args.dry_run: print("\n🧪 Mode Dry-Run terminé. Aucune action distante effectuée.") return 0 if not token: print("\n❌ ERREUR : Aucun jeton Gitea fourni.") print("Veuillez définir la variable GITEA_TOKEN ou passer --token VOTRE_TOKEN.") print("Pour générer un jeton sur Gitea : Mon Profil -> Paramètres -> Applications -> Générer un jeton") return 1 if not all_files_to_upload: print("\n❌ Aucun fichier à téléverser. Abandon.") return 1 # 3. Récupérer ou Créer la Release sur Gitea api_release_url = f"{gitea_url}/api/v1/repos/{repo}/releases" print(f"\n📡 Vérification de la release '{tag}' sur Gitea...") status, releases = make_gitea_request(api_release_url, token=token) target_release = None if status == 200 and isinstance(releases, list): for rel in releases: if rel.get("tag_name") == tag: target_release = rel break release_body = args.notes or f"### ObsiGate {tag}\n\nPublication des binaires Desktop officiels (Windows & Linux)." if target_release: release_id = target_release["id"] print(f"ℹ️ Release existante trouvée (ID: {release_id})") else: print(f"🆕 Création d'une nouvelle release pour le tag '{tag}'...") payload = json.dumps({ "tag_name": tag, "target_commitish": "main", "name": f"ObsiGate {tag}", "body": release_body, "draft": args.draft, "prerelease": args.prerelease, }).encode("utf-8") status, new_rel = make_gitea_request( api_release_url, method="POST", token=token, data=payload, content_type="application/json", ) if status in (200, 201) and isinstance(new_rel, dict): target_release = new_rel release_id = target_release["id"] print(f"✅ Release créée avec succès (ID: {release_id})") else: print(f"❌ Impossible de créer la release (HTTP {status}) : {new_rel}") return 1 # 4. Nettoyage des anciens assets portant le même nom si présents existing_assets = target_release.get("assets", []) upload_url = f"{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets" print(f"\n📤 Téléversement de {len(all_files_to_upload)} fichier(s) vers la release Gitea...") for file_path in all_files_to_upload: # Supprimer si asset déjà existant for asset in existing_assets: if asset.get("name") == file_path.name: asset_id = asset["id"] del_url = f"{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}" make_gitea_request(del_url, method="DELETE", token=token) print(f" 🗑️ Ancien fichier {file_path.name} remplacé") break upload_asset_to_gitea(upload_url, file_path, token) print("\n==========================================================") print(f" 🎉 Publication terminée avec succès sur Gitea !") print(f" 🔗 {gitea_url}/{repo}/releases/tag/{tag}") print("==========================================================") return 0 if __name__ == "__main__": sys.exit(main())