feat(desktop): manifeste de mise a jour latest.json + builds locaux signes (#77)
CI / lint (push) Successful in 1m7s
CI / security (push) Successful in 43s
CI / test (push) Successful in 2m12s
CI / build (push) Successful in 42s
CI / e2e (push) Successful in 10m43s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 1m7s
CI / security (push) Successful in 43s
CI / test (push) Successful in 2m12s
CI / build (push) Successful in 42s
CI / e2e (push) Successful in 10m43s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
- scripts/updater_manifest.py : construit latest.json (version, pub_date, platforms Windows/Linux avec signature .sig et URLs des assets). - publish_release.py : genere latest.json, upload des .sig + latest.json, rappel de commit. - tauri.conf.json : endpoint updater -> raw/branch/main/desktop/latest.json. - build-windows.bat / build-linux.sh : detection automatique de obsigate-updater.key (signature) ou repli non signe. - .gitignore : ignore desktop/key/ ; tests/test_updater_manifest.py (8 tests).
This commit is contained in:
@@ -34,4 +34,6 @@ backend/VERSION
|
||||
# Tauri updater signing keys (private key — never commit)
|
||||
desktop/*.key
|
||||
desktop/*.key.pub
|
||||
desktop/key/
|
||||
|
||||
|
||||
|
||||
+12
-1
@@ -23,7 +23,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
Windows et Linux, avec repli automatique en build **non signé** si le secret est
|
||||
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,
|
||||
|
||||
+15
-7
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+11
-1
@@ -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 ───────────────
|
||||
|
||||
@@ -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=<obsigate-updater.key
|
||||
echo [2c/6] Cle updater trouvee - artefacts .sig actives
|
||||
) else (
|
||||
> 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 ───────────────────────────────────────────────
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": "<contenu .msi.sig>", "url": "<URL du .msi>" },
|
||||
"windows-x86_64": { "signature": "<contenu .exe.sig>", "url": "<URL du .exe>" },
|
||||
"linux-x86_64": { "signature": "<contenu .AppImage.sig>", "url": "<URL .AppImage>" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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** |
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": "<contenu .exe.sig>", "url": "<URL .exe>"},
|
||||
"linux-x86_64": {"signature": "<contenu .AppImage.sig>", "url": "<URL .AppImage>"}
|
||||
}
|
||||
}
|
||||
|
||||
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 `<bundle>.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: <racine>/desktop)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default=None,
|
||||
help="Fichier de sortie (défaut: <desktop>/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())
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user