diff --git a/desktop/build-windows.bat b/desktop/build-windows.bat index 644bb3d..a2f4bf2 100644 --- a/desktop/build-windows.bat +++ b/desktop/build-windows.bat @@ -52,11 +52,15 @@ xcopy /E /I /Q /Y "..\frontend" "frontend" echo ✅ Staged REM ── 3. Build Tauri ──────────────────────────────────────────── -echo [3/6] Building Tauri application (NSIS per-user installer)... -cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis +echo [3/6] Building Tauri application (NSIS + MSI installers)... +cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis,msi if errorlevel 1 ( - echo ❌ Build failed! - goto :cleanup + echo ⚠️ Standard bundle build failed, trying fallback to NSIS only... + cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis + if errorlevel 1 ( + echo ❌ Build failed! + goto :cleanup + ) ) echo ✅ Build successful @@ -76,8 +80,9 @@ if exist "frontend" rmdir /s /q "frontend" echo ✅ Cleaned REM ── 6. Résultats ─────────────────────────────────────────────── -echo [6/6] Results: +echo [6/6] Binaries generated: dir /b "%OUTDIR%\bundle\nsis\*.exe" 2>nul +dir /b "%OUTDIR%\bundle\msi\*.msi" 2>nul echo. -echo ✅ Done! Installer: %OUTDIR%\bundle\nsis\ +echo ✅ Done! Installers generated in: %OUTDIR%\bundle\ endlocal diff --git a/docs/DEVELOPMENT_AND_RELEASES.md b/docs/DEVELOPMENT_AND_RELEASES.md new file mode 100644 index 0000000..aa0202f --- /dev/null +++ b/docs/DEVELOPMENT_AND_RELEASES.md @@ -0,0 +1,126 @@ +# Guide de Développement, Build Local & Publication des Releases Gitea — ObsiGate + +Ce guide décrit la procédure pas-à-pas pour **tester**, **compiler localement** l'application Desktop (Windows et Linux), **effectuer les commits & pushs**, puis **publier les binaires** sur la section **Releases / Publications** de Gitea (`https://git.dracodev.net/Projets/ObsiGate/releases`). + +--- + +## 1. Tests et Prévisualisation Locale + +### A. Backend & Frontend (Tests unitaires & Linting) +```powershell +# Tests backend Python (FastAPI) +.\.venv\Scripts\python.exe -m pytest tests/ + +# Linting backend (ruff & mypy) +.\.venv\Scripts\python.exe -m ruff check backend/ +.\.venv\Scripts\python.exe -m mypy backend/ --ignore-missing-imports + +# Validation & tests unitaires frontend +node tests/frontend/validate-imports.mjs +node tests/frontend/unit.test.mjs +``` + +### B. Mode Développement Desktop (Tauri 2 Live Preview) +Pour développer l'application desktop avec rechargement à chaud (Hot Reload) : +```powershell +cd desktop +cargo tauri dev +``` +*Le backend Python et l'interface WebView Tauri démarreront automatiquement sur `http://127.0.0.1:17890`.* + +--- + +## 2. Compilation Locale de l'Application Desktop + +### A. Windows (Installeur `.exe` NSIS et Paquet `.msi`) +1. Ouvrez un terminal PowerShell ou CMD sur votre machine Windows. +2. Déplacez-vous dans le dossier `desktop/` et exécutez le script : + ```powershell + cd desktop + .\build-windows.bat + ``` +3. **Binaires générés dans** `desktop/target/x86_64-pc-windows-msvc/release/bundle/` : + - `ObsiGate_x.y.z_x64-setup.exe` (Installeur utilisateur NSIS) + - `ObsiGate_x.y.z_x64_en-US.msi` (Paquet d'installation MSI) + +### B. Linux (Exécutable `.AppImage` et Paquet `.deb`) +1. Ouvrez un terminal Bash (Machine Linux ou WSL2 / Conteneur Docker). +2. Déplacez-vous dans le dossier `desktop/` et exécutez le script : + ```bash + cd desktop + chmod +x build-linux.sh + ./build-linux.sh + ``` +3. **Binaires générés dans** `desktop/target/x86_64-unknown-linux-gnu/release/bundle/` : + - `ObsiGate_x.y.z_amd64.AppImage` (Exécutable portable Linux) + - `obsigate_x.y.z_amd64.deb` (Paquet Debian/Ubuntu) + +--- + +## 3. Commit, Push & Tagging Git + +Une fois les modifications testées et validées : + +1. **Commit et Push du code source** : + ```bash + git add . + git commit -m "feat: préparation release v2.0.0" + git push origin main + ``` + *Ceci déclenchera la vérification CI standard sur Gitea (lint, tests unitaires, build Docker).* + +2. **Création du Tag de Version** : + ```bash + git tag -a v2.0.0 -m "Release v2.0.0" + git push origin v2.0.0 + ``` + +--- + +## 4. Publication des Binaires sur Gitea Releases + +Un script automatisé (`scripts/publish_release.py`) se charge de téléverser vos binaires compilés localement et de générer l'empreinte cryptographique `checksums.txt` (SHA256). + +### A. Obtenir un Jeton d'Accès Gitea (Une seule fois) +1. Connectez-vous sur votre instance Gitea : `https://git.dracodev.net`. +2. Cliquez sur votre **Profil** $\rightarrow$ **Paramètres** $\rightarrow$ **Applications**. +3. Dans **Gérer les jetons d'accès personnels**, entrez le nom `ObsiGate-Release` et cochez le jeton avec accès en écriture sur les dépôts (`repo`). +4. Copiez le jeton généré. + +### B. Exécution de la Publication + +**Option 1 — Définir la variable d'environnement (Recommandé) :** +- **Windows PowerShell** : + ```powershell + $env:GITEA_TOKEN="votre_jeton_gitea_ici" + .\.venv\Scripts\python.exe scripts/publish_release.py --tag v2.0.0 + ``` +- **Linux / Bash** : + ```bash + export GITEA_TOKEN="votre_jeton_gitea_ici" + python3 scripts/publish_release.py --tag v2.0.0 + ``` + +**Option 2 — Utiliser l'option `--token` :** +```powershell +.\.venv\Scripts\python.exe scripts/publish_release.py --tag v2.0.0 --token "votre_jeton_gitea_ici" +``` + +**Option 3 — Tester à blanc avec `--dry-run` (Sans modifier Gitea) :** +```powershell +.\.venv\Scripts\python.exe scripts/publish_release.py --tag v2.0.0 --dry-run +``` + +--- + +## 5. Résultat Attendu dans Gitea + +Une fois le script exécuté, visitez la page [ObsiGate Releases](https://git.dracodev.net/Projets/ObsiGate/releases). + +Vous y trouverez la section de publication officielle contenant : +- 📦 `ObsiGate_2.0.0_x64-setup.exe` (Installeur Windows) +- 📦 `ObsiGate_2.0.0_x64_en-US.msi` (Package MSI Windows) +- 📦 `ObsiGate_2.0.0_amd64.AppImage` (Executable autonome Linux) +- 📦 `obsigate_2.0.0_amd64.deb` (Paquet Debian/Ubuntu) +- 🔐 `checksums.txt` (Empreintes de sécurité SHA-256) +- 📄 Code source `.zip` / `.tar.gz` (générés automatiquement) diff --git a/scripts/publish_release.py b/scripts/publish_release.py new file mode 100644 index 0000000..5f3672e --- /dev/null +++ b/scripts/publish_release.py @@ -0,0 +1,339 @@ +#!/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())