diff --git a/.gitignore b/.gitignore index a194936..1da31a5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ backend/VERSION # Tauri updater signing keys (private key — never commit) desktop/*.key desktop/*.key.pub +desktop/key/ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 8748a5a..a949f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,20 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Le workflow `.gitea/workflows/desktop-build.yml` expose les secrets `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` aux builds Windows et Linux, avec repli automatique en build **non signé** si le secret est - absent (CI toujours verte). Procédure complète : + absent (CI toujours verte). Procédure complète : [docs/DEVELOPMENT_AND_RELEASES.md](./docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). - Reste à produire le manifeste `latest.json`. + +- **#77 Desktop — Manifeste de mise à jour `latest.json`** — nouveau + `scripts/updater_manifest.py` qui construit le document consommé par l'updater + Tauri (version, notes, `pub_date`, `platforms` Windows/Linux avec signature + `.sig` et URLs des assets). Il est généré automatiquement par + `scripts/publish_release.py` (écrit `desktop/latest.json`, l'ajoute aux assets + de la release, upload des `.sig`) et peut être lancé seul. L'endpoint de + l'updater (`desktop/tauri.conf.json`) pointe désormais sur le manifeste + versionné `.../raw/branch/main/desktop/latest.json`. Les builds locaux + (`build-windows.bat`, `build-linux.sh`) détectent automatiquement + `desktop/obsigate-updater.key` pour signer, ou désactivent les artefacts de + mise à jour s'il est absent. Tests : `tests/test_updater_manifest.py` (8). - **#77 Desktop — Protocole de tests E2E manuels** — nouveau [`docs/DESKTOP_E2E_CHECKLIST.md`](./docs/DESKTOP_E2E_CHECKLIST.md) : prérequis, diff --git a/desktop/README.md b/desktop/README.md index 4ecfce7..e75526c 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -303,18 +303,26 @@ Indépendante de la signature Windows, la signature des mises à jour Tauri repo sur une paire de clés que vous générez vous-même : ```bash -cargo tauri signer generate -w ~/.tauri/obsigate.key +cargo tauri signer generate -w obsigate-updater.key ``` - La **clé publique** est déjà renseignée dans `plugins.updater.pubkey` (`tauri.conf.json`). -- La **clé privée** doit être exposée au build via `TAURI_SIGNING_PRIVATE_KEY` - (jamais commitée) ; dans le CI, via les secrets Gitea - `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. -- Le CLI produit des `.sig` par artefact (`*.msi.sig`, `*.AppImage.sig`, …). +- La **clé privée** (`desktop/obsigate-updater.key`, gitignorée) est lue + automatiquement par `build-windows.bat` / `build-linux.sh` ; en CI, via les + secrets Gitea `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. +- Le CLI produit des `.sig` par artefact (`*.exe.sig`, `*.AppImage.sig`, …). -> Le manifeste `latest.json` consommé par l'updater n'est pas généré par le CLI : -> voir [DEVELOPMENT_AND_RELEASES §2bis](../docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). +**Manifeste `latest.json`** (détection des mises à jour) : + +```powershell +python scripts\updater_manifest.py --tag vX.Y.Z # ou via publish_release.py +``` + +`publish_release.py` le génère et l'ajoute aux assets ; il reste à **committer +`desktop/latest.json` sur `main`**. L'updater lit ce fichier versionné : +`https://git.dracodev.net/Projets/ObsiGate/raw/branch/main/desktop/latest.json`. +Détail : [DEVELOPMENT_AND_RELEASES §2bis](../docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). --- diff --git a/desktop/build-linux.sh b/desktop/build-linux.sh index 08eb1d2..23c5b4c 100755 --- a/desktop/build-linux.sh +++ b/desktop/build-linux.sh @@ -38,9 +38,19 @@ cp -r ../backend backend cp -r ../frontend frontend echo "✅ Staged" +# ── 2c. Clé de signature des mises à jour (updater Tauri) ────── +if [ -f "obsigate-updater.key" ]; then + export TAURI_SIGNING_PRIVATE_KEY="$(cat obsigate-updater.key)" + SIGN_CONFIG="" + echo "[2c/5] Clé updater trouvée — artefacts .sig activés" +else + SIGN_CONFIG='--config {"bundle":{"createUpdaterArtifacts":false}}' + echo "[2c/5] Clé updater absente — build sans .sig" +fi + # ── 3. Build Tauri ──────────────────────────────────────────── echo "[3/5] Building Tauri application..." -cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage +cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage $SIGN_CONFIG echo "✅ Build successful" # ── 4. Copier le runtime à côté de l'exécutable ─────────────── diff --git a/desktop/build-windows.bat b/desktop/build-windows.bat index a2f4bf2..ced7936 100644 --- a/desktop/build-windows.bat +++ b/desktop/build-windows.bat @@ -51,12 +51,23 @@ xcopy /E /I /Q /Y "..\backend" "backend" xcopy /E /I /Q /Y "..\frontend" "frontend" echo ✅ Staged +REM ── 2c. Clé de signature des mises à jour (updater Tauri) ────── +set "SIGN_CONFIG=" +if exist "obsigate-updater.key" ( + set /p TAURI_SIGNING_PRIVATE_KEY= updater-off.json echo {"bundle":{"createUpdaterArtifacts":false}} + set "SIGN_CONFIG=--config updater-off.json" + echo [2c/6] Cle updater absente - build sans .sig +) + REM ── 3. Build Tauri ──────────────────────────────────────────── echo [3/6] Building Tauri application (NSIS + MSI installers)... -cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis,msi +cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis,msi %SIGN_CONFIG% if errorlevel 1 ( echo ⚠️ Standard bundle build failed, trying fallback to NSIS only... - cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis + cargo tauri build --target x86_64-pc-windows-msvc --bundles nsis %SIGN_CONFIG% if errorlevel 1 ( echo ❌ Build failed! goto :cleanup @@ -77,6 +88,7 @@ REM ── 5. Nettoyer les dossiers stagés ──────────── echo [5/6] Cleaning up staged dirs... if exist "backend" rmdir /s /q "backend" if exist "frontend" rmdir /s /q "frontend" +if exist "updater-off.json" del /q "updater-off.json" echo ✅ Cleaned REM ── 6. Résultats ─────────────────────────────────────────────── diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 4c1c9ef..e409b45 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -82,7 +82,7 @@ "store": null, "updater": { "endpoints": [ - "https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest" + "https://git.dracodev.net/Projets/ObsiGate/raw/branch/main/desktop/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcwQjU2MDM4QUVEREY3NApSV1IwMysyS0ExWUxCK1RQMUgrYnplQWlpYWU2SHVYajVIUHhzNEFKLzZ2Z2puZW9pSUQ2RE8rUAo=", "windows": { diff --git a/docs/DESKTOP_E2E_CHECKLIST.md b/docs/DESKTOP_E2E_CHECKLIST.md index 08a2294..fdc0c8a 100644 --- a/docs/DESKTOP_E2E_CHECKLIST.md +++ b/docs/DESKTOP_E2E_CHECKLIST.md @@ -128,10 +128,9 @@ fenêtre existante** (single-instance) au lieu de lancer un doublon. **Résultat attendu :** détection de la version N+1, téléchargement, installation sans intervention manuelle, version mise à jour après redémarrage. -**Prérequis bloquant :** la `pubkey` de l'updater dans `desktop/tauri.conf.json` -ne doit **pas** être le placeholder `OBSIGATE_UPDATE_PUBKEY_PLACEHOLDER`, et la -clé privée correspondante doit être fournie au build -(`TAURI_SIGNING_PRIVATE_KEY`). Voir §4. +**Prérequis bloquant :** la release N+1 doit être publiée (binaires **signés** +`.sig` + `latest.json`) et le fichier `desktop/latest.json` **commité sur `main`** +(le manifeste est généré par `scripts/publish_release.py`, cf. §4). --- @@ -172,14 +171,11 @@ aucun raccourci cassé. `OBSIGATE_SIGN_CERT_PFX` est défini ; sinon il est un no-op explicite. Alternatives détaillées dans le [README desktop](../desktop/README.md). - **Signature de l'updater Tauri (gratuite, distincte de la signature Windows) :** - générer une paire de clés, renseigner la `pubkey` dans - `desktop/tauri.conf.json` et exposer la clé privée au build via - `TAURI_SIGNING_PRIVATE_KEY`. La clé privée **ne doit jamais être commitée**. - -```bash -cargo tauri signer generate -w ~/.tauri/obsigate.key -# → copier la clé publique affichée dans plugins.updater.pubkey -``` + déjà configurée — `pubkey` dans `desktop/tauri.conf.json`, clé privée lue depuis + `desktop/obsigate-updater.key` (gitignorée) ou `TAURI_SIGNING_PRIVATE_KEY`. + Le manifeste `latest.json` est généré par `scripts/publish_release.py` puis + commité sur `main`. Procédure : + [DEVELOPMENT_AND_RELEASES §2bis](./DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). ## 5. Clôture diff --git a/docs/DEVELOPMENT_AND_RELEASES.md b/docs/DEVELOPMENT_AND_RELEASES.md index 687a8e5..d346bab 100644 --- a/docs/DEVELOPMENT_AND_RELEASES.md +++ b/docs/DEVELOPMENT_AND_RELEASES.md @@ -121,8 +121,30 @@ les fichiers `.sig` sont uploadés comme artefacts. ### D. Manifeste de mise à jour (`latest.json`) Le CLI Tauri génère les `.sig` mais **pas** le manifeste JSON consommé par -l'updater. L'endpoint configuré -(`.../releases/latest`) doit servir un document de la forme : +l'updater. Celui-ci est produit par `scripts/updater_manifest.py` : + +```powershell +# Windows — après build-windows.bat +.\.venv\Scripts\python.exe scripts\updater_manifest.py --tag v2.3.0 +``` + +`publish_release.py` l'appelle automatiquement : il écrit `desktop/latest.json`, +l'ajoute aux assets de la release, et rappelle la dernière étape manuelle. + +**Flux complet de release :** + +1. `desktop\build-windows.bat` (build + signature `.sig`) ; +2. `python scripts\publish_release.py --tag vX.Y.Z` (checksums + `latest.json` + upload) ; +3. **commit** de `desktop/latest.json` sur `main` puis `git push`. + +L'endpoint de l'updater (`desktop/tauri.conf.json`) pointe vers ce fichier +versionné : + +``` +https://git.dracodev.net/Projets/ObsiGate/raw/branch/main/desktop/latest.json +``` + +Document produit (URLs construites vers les assets de la release) : ```json { @@ -130,14 +152,15 @@ l'updater. L'endpoint configuré "notes": "…", "pub_date": "2026-09-12T00:00:00Z", "platforms": { - "windows-x86_64": { "signature": "", "url": "" }, + "windows-x86_64": { "signature": "", "url": "" }, "linux-x86_64": { "signature": "", "url": "" } } } ``` -Sans ce manifeste, l'updater ne détecte aucune mise à jour (la signature, elle, -est déjà opérationnelle). +> Seules les plateformes dont l'artefact **signé** est présent sont incluses. +> Sans `desktop/latest.json` à jour sur `main`, l'updater ne détecte aucune +> mise à jour (la signature, elle, reste opérationnelle). --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a74ea29..d428b9a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -36,7 +36,7 @@ - **Statut :** livré (A→F) — projet Tauri, backend Python embarqué, fonctionnalités natives, jumplist vaults, bannière 1er lancement, build CI, UX, 24 tests Rust. Détail complet : [features/desktop-tauri.md](./features/desktop-tauri.md) - **Reste à faire :** - [x] **Signature de l'updater Tauri** (gratuit) : paire de clés générée, `pubkey` renseignée, `createUpdaterArtifacts` activé, secrets CI câblés - - [ ] Générer le manifeste `latest.json` pour que l'updater détecte les mises à jour + - [x] **Manifeste `latest.json`** généré par `scripts/updater_manifest.py` (intégré à `publish_release.py`), endpoint updater pointé sur `main` - [ ] **Signature de code Windows** : non retenue (pas de certificat) — alternatives : livrer non signé, SignPath.io (OSS gratuit), Certum OSS, Azure Trusted Signing, certificat EV - [ ] Exécuter les 6 tests E2E **manuels** — protocole documenté : [DESKTOP_E2E_CHECKLIST.md](./DESKTOP_E2E_CHECKLIST.md) @@ -105,7 +105,7 @@ | Priorité | Items | Effort total estimé | |---|---|---| | ✅ Complété | #1 → #59, #61–72, #74–76, #78–80 | ~103 jours réalisés | -| 🔵 P2 restant | #77 Desktop : manifeste `latest.json`, signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour | +| 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour | | ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours | | **Total restant** | **2 items + finitions** | **~7-10 jours** | diff --git a/docs/features/desktop-tauri.md b/docs/features/desktop-tauri.md index 20e71c1..afb126a 100644 --- a/docs/features/desktop-tauri.md +++ b/docs/features/desktop-tauri.md @@ -57,7 +57,8 @@ - [x] **Build Windows local** : `cargo build --release` vérifié (rustc 1.94.1) — `cargo tauri build --bundles msi` prêt - [x] **Build Linux local** : workflow CI couvre `.deb`, `.rpm`, `.AppImage` - [x] **Auto-update** : `tauri-plugin-updater` configuré → vérifie `https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest` - - [x] **Signature de l'updater Tauri** (gratuite, ≠ signature Windows) : paire de clés `minisign` générée, clé publique dans `plugins.updater.pubkey`, `bundle.createUpdaterArtifacts: true`, secrets Gitea `TAURI_SIGNING_PRIVATE_KEY` / `_PASSWORD` exposés au CI (build non signé en repli si le secret est absent). Reste : générer le manifeste `latest.json` pour que l'updater détecte les mises à jour ([guide](../DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri)) + - [x] **Signature de l'updater Tauri** (gratuite, ≠ signature Windows) : paire de clés `minisign` générée, clé publique dans `plugins.updater.pubkey`, `bundle.createUpdaterArtifacts: true`, secrets Gitea `TAURI_SIGNING_PRIVATE_KEY` / `_PASSWORD` exposés au CI (build non signé en repli si le secret est absent). + - [x] **Manifeste `latest.json`** : généré par `scripts/updater_manifest.py` (et automatiquement par `publish_release.py`), endpoint de l'updater pointé sur `raw/branch/main/desktop/latest.json`. Builds locaux signés via `obsigate-updater.key` ([guide](../DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri)) - [ ] **Signature de code Windows** : non retenue (pas de certificat) — alternatives : livrer non signé, SignPath.io (OSS gratuit), Certum Open Source, Azure Trusted Signing, certificat EV - [x] **Page de release** : README desktop existe (`desktop/README.md`) diff --git a/scripts/publish_release.py b/scripts/publish_release.py index 8db5285..276171e 100644 --- a/scripts/publish_release.py +++ b/scripts/publish_release.py @@ -26,6 +26,9 @@ import urllib.error import urllib.parse import urllib.request +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) +from updater_manifest import build_updater_manifest + DEFAULT_GITEA_URL = "https://git.dracodev.net" DEFAULT_REPO = "Projets/ObsiGate" @@ -66,9 +69,13 @@ 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/nsis/*.exe.sig", "target/**/release/bundle/msi/*.msi", + "target/**/release/bundle/msi/*.msi.sig", "target/**/release/bundle/appimage/*.AppImage", + "target/**/release/bundle/appimage/*.AppImage.sig", "target/**/release/bundle/deb/*.deb", + "target/**/release/bundle/deb/*.deb.sig", "release_assets/*", ] @@ -173,6 +180,10 @@ def upload_asset_to_gitea( content_type = "application/vnd.debian.binary-package" elif filename.endswith(".txt"): content_type = "text/plain" + elif filename.endswith(".json"): + content_type = "application/json" + elif filename.endswith(".sig"): + content_type = "text/plain" body = [] body.append(f"--{boundary}".encode("utf-8")) @@ -258,6 +269,22 @@ def main(): all_files_to_upload.append(checksums_path) print(" ✅ checksums.txt généré avec succès") + # 2b. Génération du manifeste de mise à jour Tauri (latest.json) + latest_path = root_dir / "desktop" / "latest.json" + print(f"\n🔄 Génération du manifeste de mise à jour ({latest_path.name})...") + manifest = build_updater_manifest( + root_dir / "desktop", tag, gitea_url, repo, notes=args.notes or "" + ) + if manifest: + latest_path.write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + all_files_to_upload.append(latest_path) + print(f" ✅ latest.json généré ({', '.join(manifest['platforms'])})") + else: + print(" ⚠️ Aucun artefact signé (.sig) — latest.json non généré.") + if args.dry_run: print("\n🧪 Mode Dry-Run terminé. Aucune action distante effectuée.") return 0 @@ -338,6 +365,11 @@ def main(): print("\n==========================================================") print(f" 🎉 Publication terminée avec succès sur Gitea !") print(f" 🔗 {gitea_url}/{repo}/releases/tag/{tag}") + if latest_path.exists(): + print("\n⚠️ Dernière étape : publier le manifeste de mise à jour") + print(f" git add desktop/latest.json && git commit -m \"chore: latest.json {tag}\"") + print(" git push origin main") + print(" (l'updater lit desktop/latest.json sur la branche main)") print("==========================================================") return 0 diff --git a/scripts/updater_manifest.py b/scripts/updater_manifest.py new file mode 100644 index 0000000..f61dc69 --- /dev/null +++ b/scripts/updater_manifest.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +ObsiGate — Génération du manifeste de mise à jour Tauri (`latest.json`). + +L'updater Tauri attend un document JSON de la forme : + + { + "version": "2.3.0", + "notes": "…", + "pub_date": "2026-09-12T10:00:00Z", + "platforms": { + "windows-x86_64": {"signature": "", "url": ""}, + "linux-x86_64": {"signature": "", "url": ""} + } + } + +Ce module construit ce manifeste à partir des artefacts signés produits par +`cargo tauri build` (avec `bundle.createUpdaterArtifacts: true`) et de leurs +fichiers `.sig` (signature minisign). + +Usage en ligne de commande : + + python scripts/updater_manifest.py --tag v2.3.0 + python scripts/updater_manifest.py --tag v2.3.0 --dry-run +""" + +import argparse +import datetime +import json +import pathlib +import sys + +DEFAULT_GITEA_URL = "https://git.dracodev.net" +DEFAULT_REPO = "Projets/ObsiGate" + +# Plateforme Tauri -> liste de motifs de bundles candidats (par ordre de priorité). +# Le premier bundle disposant d'un fichier `.sig` est retenu. +PLATFORM_BUNDLES: dict[str, list[str]] = { + "windows-x86_64": [ + "target/**/release/bundle/nsis/*-setup.exe", + "target/**/release/bundle/nsis/*.exe", + "target/**/release/bundle/msi/*.msi", + ], + "linux-x86_64": [ + "target/**/release/bundle/appimage/*.AppImage", + "target/**/release/bundle/deb/*.deb", + ], +} + + +def read_version(tauri_conf: pathlib.Path) -> str: + """Lit la version depuis `desktop/tauri.conf.json`.""" + with open(tauri_conf, "r", encoding="utf-8") as f: + data = json.load(f) + return str(data.get("version", "0.0.0")) + + +def _find_bundle_with_signature(desktop_dir: pathlib.Path, patterns: list[str]): + """Retourne le premier bundle (le plus récent) disposant d'un `.sig`.""" + for pattern in patterns: + matches = [p for p in desktop_dir.glob(pattern) if p.is_file()] + if not matches: + continue + bundle = max(matches, key=lambda p: p.stat().st_mtime) + sig = bundle.with_name(bundle.name + ".sig") + if sig.is_file(): + return bundle, sig + return None, None + + +def build_updater_manifest( + desktop_dir, + tag: str, + gitea_url: str = DEFAULT_GITEA_URL, + repo: str = DEFAULT_REPO, + notes: str = "", + pub_date: str | None = None, +) -> dict | None: + """Construit le manifeste `latest.json` à partir des artefacts signés. + + Retourne `None` si aucun artefact signé n'est trouvé. + """ + desktop_dir = pathlib.Path(desktop_dir) + tauri_conf = desktop_dir / "tauri.conf.json" + version = read_version(tauri_conf) if tauri_conf.exists() else "0.0.0" + + base_url = f"{gitea_url.rstrip('/')}/{repo.strip('/')}/releases/download/{tag}" + platforms: dict[str, dict[str, str]] = {} + + for platform, patterns in PLATFORM_BUNDLES.items(): + bundle, sig = _find_bundle_with_signature(desktop_dir, patterns) + if bundle is None or sig is None: + continue + platforms[platform] = { + "signature": sig.read_text(encoding="utf-8").strip(), + "url": f"{base_url}/{bundle.name}", + } + + if not platforms: + return None + + if pub_date is None: + pub_date = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + + return { + "version": version, + "notes": notes, + "pub_date": pub_date, + "platforms": platforms, + } + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + + parser = argparse.ArgumentParser( + description="Générer le manifeste de mise à jour Tauri (latest.json)." + ) + parser.add_argument("--tag", "-t", help="Tag de version (ex: v2.3.0)") + parser.add_argument("--gitea-url", default=DEFAULT_GITEA_URL) + parser.add_argument("--repo", default=DEFAULT_REPO) + parser.add_argument("--notes", default="", help="Notes de version") + parser.add_argument( + "--desktop-dir", + default=None, + help="Dossier desktop/ (défaut: /desktop)", + ) + parser.add_argument( + "--output", + default=None, + help="Fichier de sortie (défaut: /latest.json)", + ) + parser.add_argument("--dry-run", action="store_true", help="Afficher sans écrire") + + args = parser.parse_args() + + root_dir = pathlib.Path(__file__).resolve().parent.parent + desktop_dir = pathlib.Path(args.desktop_dir) if args.desktop_dir else root_dir / "desktop" + output = pathlib.Path(args.output) if args.output else desktop_dir / "latest.json" + + tag = args.tag + if not tag: + version = read_version(desktop_dir / "tauri.conf.json") + tag = f"v{version}" if not version.startswith("v") else version + + manifest = build_updater_manifest( + desktop_dir, tag, args.gitea_url, args.repo, notes=args.notes + ) + + if manifest is None: + print("⚠️ Aucun artefact signé (.sig) trouvé — manifeste non généré.") + print("💡 Build d'abord avec la clé updater définie :") + print(" - Windows : .\\desktop\\build-windows.bat") + print(" - Linux : ./desktop/build-linux.sh") + return 1 + + content = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n" + + if args.dry_run: + print(content) + print(f"🧪 Dry-run : rien écrit ({output})") + return 0 + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(content, encoding="utf-8") + print(f"✅ Manifeste écrit : {output}") + print(f" Version : {manifest['version']}") + print(f" Plateformes: {', '.join(manifest['platforms'])}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_updater_manifest.py b/tests/test_updater_manifest.py new file mode 100644 index 0000000..aaa6db5 --- /dev/null +++ b/tests/test_updater_manifest.py @@ -0,0 +1,125 @@ +# tests/test_updater_manifest.py +# Unit tests for scripts/updater_manifest.py — Tauri updater `latest.json` builder. +import json +import sys +from pathlib import Path + +SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts" +sys.path.insert(0, str(SCRIPTS_DIR)) + +from updater_manifest import build_updater_manifest # noqa: E402 + + +def _write(path: Path, content: str = "") -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _make_desktop(tmp_path: Path, version: str = "2.3.0") -> Path: + desktop = tmp_path / "desktop" + _write(desktop / "tauri.conf.json", json.dumps({"version": version})) + return desktop + + +def _add_signed_bundle(desktop: Path, rel: str, signature: str) -> None: + bundle = desktop / rel + _write(bundle, "binary") + _write(bundle.with_name(bundle.name + ".sig"), signature) + + +def test_manifest_both_platforms(tmp_path): + desktop = _make_desktop(tmp_path, "2.3.0") + _add_signed_bundle( + desktop, "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "sig-win" + ) + _add_signed_bundle( + desktop, "target/release/bundle/appimage/ObsiGate_2.3.0_amd64.AppImage", "sig-lin" + ) + + manifest = build_updater_manifest( + desktop, + "v2.3.0", + "https://git.example.com", + "Org/Repo", + notes="Hello", + pub_date="2026-09-12T00:00:00Z", + ) + + assert manifest["version"] == "2.3.0" + assert manifest["notes"] == "Hello" + assert manifest["pub_date"] == "2026-09-12T00:00:00Z" + assert set(manifest["platforms"]) == {"windows-x86_64", "linux-x86_64"} + assert manifest["platforms"]["windows-x86_64"] == { + "signature": "sig-win", + "url": ( + "https://git.example.com/Org/Repo/releases/download/v2.3.0/" + "ObsiGate_2.3.0_x64-setup.exe" + ), + } + assert manifest["platforms"]["linux-x86_64"]["url"].endswith( + "ObsiGate_2.3.0_amd64.AppImage" + ) + + +def test_manifest_prefers_nsis_over_msi(tmp_path): + desktop = _make_desktop(tmp_path) + _add_signed_bundle( + desktop, "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "sig-nsis" + ) + _add_signed_bundle( + desktop, "target/release/bundle/msi/ObsiGate_2.3.0_x64_en-US.msi", "sig-msi" + ) + + manifest = build_updater_manifest(desktop, "v2.3.0") + assert manifest["platforms"]["windows-x86_64"]["signature"] == "sig-nsis" + + +def test_manifest_falls_back_to_msi_when_nsis_unsigned(tmp_path): + desktop = _make_desktop(tmp_path) + _write(desktop / "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "binary") + _add_signed_bundle( + desktop, "target/release/bundle/msi/ObsiGate_2.3.0_x64_en-US.msi", "sig-msi" + ) + + manifest = build_updater_manifest(desktop, "v2.3.0") + assert manifest["platforms"]["windows-x86_64"]["signature"] == "sig-msi" + + +def test_manifest_skips_unsigned_platform(tmp_path): + desktop = _make_desktop(tmp_path) + _add_signed_bundle( + desktop, "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "sig-win" + ) + + manifest = build_updater_manifest(desktop, "v2.3.0") + assert set(manifest["platforms"]) == {"windows-x86_64"} + + +def test_manifest_none_when_no_artifacts(tmp_path): + desktop = _make_desktop(tmp_path) + assert build_updater_manifest(desktop, "v2.3.0") is None + + +def test_manifest_none_when_unsigned(tmp_path): + desktop = _make_desktop(tmp_path) + _write(desktop / "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "binary") + assert build_updater_manifest(desktop, "v2.3.0") is None + + +def test_manifest_generates_iso_pub_date(tmp_path): + desktop = _make_desktop(tmp_path) + _add_signed_bundle( + desktop, "target/release/bundle/nsis/ObsiGate_2.3.0_x64-setup.exe", "sig" + ) + manifest = build_updater_manifest(desktop, "v2.3.0") + assert manifest["pub_date"].endswith("Z") + assert "T" in manifest["pub_date"] + + +def test_manifest_reads_version_from_tauri_conf(tmp_path): + desktop = _make_desktop(tmp_path, "9.9.9") + _add_signed_bundle( + desktop, "target/release/bundle/nsis/ObsiGate_9.9.9_x64-setup.exe", "sig" + ) + manifest = build_updater_manifest(desktop, "v9.9.9") + assert manifest["version"] == "9.9.9"