Simpl-Resultat/docs/qa-update-cycle.md
le king fu c859405deb
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m4s
fix(release): refuse an empty or multi-document latest.json, test the Publish step
Follow-ups of the review of PR #418.

- check-latest-json.jq wants exactly one document and release.yml calls it
  with -s: under jq 1.6, the release container's, an empty file passed the
  check (no output, status 0) and two documents were judged on the last one,
  so the job would have deleted the published manifest and uploaded the bad
  one. Two tests cover both cases.
- build-latest-json.test.mjs runs the text of release.yml's "Publish
  latest.json to package registry" step against a fake curl: DELETE then PUT
  on a valid manifest, exit 1 and no request on a refused one.
- check-frontend no longer ignores .claude/** and release.yml (decision of
  Max): the test reads verify-release.sh and runs the Publish step, so "on
  every pull request" holds. REVIEW.md, CLAUDE.md and architecture.md follow.
- release.yml: stale ".deb fallback" comment fixed.
- qa-update-cycle.md: a tauri dev build on Linux shows TargetsNotFound,
  expected since #418.

Refs #461

Resolves #419

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:00:40 -04:00

133 lines
14 KiB
Markdown

# QA — Cycle de mise à jour automatique
Checklist manuelle pour valider qu'une release est réellement installable **par la mise à jour automatique**, et pas seulement téléchargeable. À dérouler après chaque release qui touche `tauri-plugin-updater`, ses dépendances (`reqwest`/`rustls`/`rustls-webpki`), `release.yml`, ou la configuration `updater` de `tauri.conf.json`.
Elle existe parce que la CI ne peut rien prouver ici : `cargo check` et `cargo test` prouvent que le chemin de mise à jour **compile**, jamais qu'il fonctionne. Une régression s'y manifeste chez l'utilisateur, au moment de la mise à jour — et la mise à jour automatique étant une fonctionnalité **Base+**, ce sont des utilisateurs payants.
> **Cette checklist tourne après que la release est publiée.** `release.yml` a déjà poussé `latest.json` vers le registre de paquets, qui est l'endpoint configuré dans `tauri.conf.json`. Tout utilisateur Base+ qui clique sur « Vérifier les mises à jour » reçoit donc déjà la nouvelle version pendant que vous déroulez ceci. En cas d'échec, voir « Si ça casse » en bas — et lire cette section **avant** de commencer, pas pendant l'incident.
---
## Prérequis
Ce sont les conditions sans lesquelles le test ne teste rien. Chacune a un mode d'échec où la checklist se coche « OK » sans avoir rien vérifié.
- [ ] **Une machine Windows** (hôte ou VM) et **une machine Linux de bureau**, plus, pour le §3, **une machine Fedora ou openSUSE** installée depuis le `.rpm`. Les flux sont réellement différents, ils ne se substituent pas l'un à l'autre.
- [ ] Sur chaque machine Linux, **un agent polkit qui tourne**. L'installation du `.deb` comme du `.rpm` passe par `pkexec` ; sans agent, l'app retombe en cascade sur `zenity`/`kdialog` puis sur un `sudo` dans un terminal inexistant, et **paraît figée au lieu d'échouer — sur l'écran de téléchargement, pas sur `readyToInstall`** (voir §2, c'est le piège qui produit le mauvais rapport de bug). Noter plus bas quelle invite est réellement apparue : si ce n'est pas polkit, c'est un chemin de repli qui a été validé, pas le chemin nominal.
- [ ] **La version précédente installée sur chaque machine**, depuis les artefacts de la release précédente (`.deb`, `.rpm` et `-setup.exe` attachés à la release Forgejo). Ne pas tester sur la machine qui vient de builder la nouvelle version.
> Sans ça, `check()` compare `release.version > version_courante`, trouve faux, et l'UI affiche « à jour ». La checklist se coche alors comme « rien à mettre à jour » — soit exactement le saut silencieux qu'elle est censée éliminer, avec en prime une trace écrite affirmant qu'elle est passée.
- [ ] **Une clé de licence Base ou Premium valide sur chaque machine.** La mise à jour automatique est verrouillée par édition : `useUpdater` appelle `check_entitlement("auto-update")` et bascule en `notEntitled` **avant** même d'appeler `check()`. Une installation Gratuite ne télécharge jamais rien, et les builds de release n'ont pas de `dev-override`.
- [ ] Sur chaque machine, **supprimer tout `activation.token` provenant d'une autre machine** (VM clonée, snapshot restauré) dans le répertoire de données de l'app. Une `license.key` seule fonctionne — l'absence de token est un état de pré-activation toléré. Dans les versions antérieures à #347 (v0.15.0 et avant), un token émis pour un autre `machine_id` fait retomber l'édition en Gratuite, et le testeur voit une carte « non éligible » anodine plutôt qu'un échec ; depuis #347, il ne touche plus l'édition et la carte Licence affiche seulement « Non activée ». La suppression reste nécessaire tant que la version de départ du cycle est antérieure à #347.
---
## Ce que ce test prouve — et ce qu'il ne prouve pas
À écrire explicitement, parce que la tentation est de croire qu'un cycle réussi couvre tout le chemin de mise à jour.
**Prouvé** sur chaque cible déroulée : le téléchargement TLS (donc `rustls-webpki`), la vérification de signature minisign, l'exécution de l'installeur, et le redémarrage sur la nouvelle version.
**Non prouvé** : l'extraction d'archive, et pour cause : depuis #455, `tauri-plugin-updater` est compilé sans sa feature `zip`, donc sans `zip` sous Windows ni `tar` sous Linux. Avec `createUpdaterArtifacts: true`, aucun de nos paquets de mise à jour n'est une archive : le NSIS écrit le `.exe` dans un répertoire temporaire et le lance, le `.deb` passe par `pkexec dpkg -i` et le `.rpm` par `pkexec rpm -U` (§3) ; une AppImage brute, si on en livrait une, serait réécrite en place par la branche « rewriting AppImage », qui ne dépend pas de la feature. Seul `createUpdaterArtifacts: "v1Compatible"` produit des archives (`.nsis.zip`, `.msi.zip`, `.AppImage.tar.gz`). **Passer à ce mode exige de rétablir la feature** (`src-tauri/Cargo.toml`) : sans elle, un zip échoue sous Windows (`InvalidUpdaterFormat`, rien n'est installé) et un `.tar.gz` serait écrit par-dessus l'AppImage sous Linux. La branche macOS `.app.tar.gz` garde `tar`, mais l'app n'est pas livrée sur macOS.
---
## 1. Windows (NSIS)
Point d'entrée : **Paramètres → Systèmes → carte Mises à jour → « Vérifier les mises à jour »**. Il n'y a **aucune vérification automatique dans l'app** : les quatre déclencheurs sont manuels — ce bouton, l'icône rafraîchir de l'état `upToDate`, le « réessayer » de l'état `error`, et celui de la page d'erreur, qui **détecte seulement** et n'offre aucun chemin de téléchargement.
- [ ] La carte passe en `checking`, puis affiche `available` avec le **numéro de la nouvelle version** et les notes extraites du CHANGELOG.
- [ ] Cliquer sur télécharger → état `downloading`, **la progression avance** (elle vient de `contentLength`, donc un serveur qui ne le renvoie pas se voit ici).
- [ ] L'installeur NSIS s'ouvre et **l'application se ferme d'elle-même**.
> Attendu, et propre à Windows : `downloadAndInstall` ne rend jamais la main — le processus appelle `exit(0)` et laisse l'installeur prendre le relais. Les états `readyToInstall` et `installing` **ne s'affichent jamais** sur cette cible. Ne pas les attendre, ne pas les cocher.
- [ ] Terminer l'installation, relancer l'app, vérifier la version dans Paramètres → Systèmes.
- [ ] Re-cliquer sur « Vérifier les mises à jour » → `upToDate`.
## 2. Linux (.deb)
- [ ] Même point d'entrée, mêmes états jusqu'à `downloading`.
- [ ] **Pendant que la carte affiche encore « Téléchargement en cours… »**, une invite d'authentification apparaît. **Noter laquelle** : polkit (nominal), zenity/kdialog, ou rien du tout (repli `sudo` → l'app reste figée **sur l'écran de téléchargement**).
Invite observée : `________________`
> C'est bien le moment nominal, et c'est contre-intuitif : `downloadAndInstall` télécharge **et** installe dans le même appel (`updater.rs:723-729`), et l'événement `Finished` est un no-op côté UI (`useUpdater.ts:119-121`). L'écran reste donc sur `downloading` pendant tout le `dpkg -i`. Un testeur qui guette l'invite **après** `readyToInstall` ne la verra jamais arriver au bon moment : il rapportera « le téléchargement bloque » là où le vrai diagnostic est « pas d'agent polkit ».
- [ ] Authentifier → l'app **reste vivante** et passe en `readyToInstall` (« Mise à jour prête à installer » + bouton « Installer et redémarrer »).
> À ce stade le `.deb` est **déjà installé sur le disque** : `installAndRestart` ne fait que `relaunch()`. Le libellé du bouton ment sur cette cible — qui s'arrête là croit que rien n'a été installé, et qui voit la version changer après le clic attribue l'installation au clic.
- [ ] Cliquer → état `installing`, l'app redémarre.
- [ ] Vérifier la version, re-vérifier → `upToDate`.
## 3. Linux (.rpm)
Même parcours que le §2, sur la machine Fedora ou openSUSE. L'updater y lance `pkexec rpm -U` au lieu de `pkexec dpkg -i` (`install_rpm`, même cascade polkit → zenity/kdialog → `sudo`) : mêmes états, même moment pour l'invite, même piège.
- [ ] Même point d'entrée ; la carte affiche `available` avec le **numéro de la nouvelle version**.
- [ ] **Pendant que la carte affiche encore « Téléchargement en cours… »**, une invite d'authentification apparaît. **Noter laquelle.**
Invite observée : `________________`
- [ ] Authentifier → `readyToInstall`, puis cliquer → `installing`, l'app redémarre.
- [ ] Vérifier la version, re-vérifier → `upToDate`.
> Une erreur `invalid updater binary format` sur la carte, juste après le téléchargement, est le défaut d'avant #320 : l'installation a reçu le `.deb` et l'a refusé. Vérifier que le `latest.json` publié porte la clé `linux-x86_64-rpm` (`verify-release.sh`, étape 9 de `/release`).
**Pas encore déroulé** : aucune machine Fedora ou openSUSE n'était disponible quand #320 a ajouté la clé `linux-x86_64-rpm`. Le dérouler dès qu'il y en a une ; d'ici là, l'écrire « non déroulé », avec la raison, dans la trace de la release.
Les installations `.rpm` existantes n'ont rien à réinstaller : le binaire du `.rpm` v0.17.1 publié porte le marqueur `__TAURI_BUNDLE_TYPE_VAR_RPM` (vérifié le 2026-10-10), donc il cherche `linux-x86_64-rpm` dès qu'une release publie cette clé. La première release qui la publie est la première que ces installations peuvent recevoir.
### Les clés de `latest.json`
`tauri-plugin-updater` cherche la clé `{os}-{arch}-{installer}`, puis `{os}-{arch}` (`get_urls`) ; l'installeur est le type de paquet que le bundler grave dans le binaire de chaque paquet. `scripts/build-latest-json.sh`, appelé par `release.yml`, écrit donc :
| Clé | Fichier | Lue par |
|---|---|---|
| `linux-x86_64-deb` | `.deb` | les installations `.deb` |
| `linux-x86_64-rpm` | `.rpm` | les installations `.rpm` (#320) |
| `windows-x86_64` | `-setup.exe` | les installations NSIS, qui cherchent d'abord `windows-x86_64-nsis` |
**Pas de clé générique `linux-x86_64`** (retirée le 2026-10-11, décision de Max sur #320). Toutes les versions qui embarquent l'updater (v0.2.1 et suivantes) verrouillent `tauri-plugin-updater` 2.10.0 et `tauri-utils` 2.8.2, et leurs paquets portent le marqueur de type (vérifié dans le `.deb` et le `.rpm` de la v0.17.1) : une installation `.deb` ou `.rpm` trouve sa clé avant de chercher `linux-x86_64`. Cette clé servait le `.deb`, et ses derniers lecteurs ne savaient pas l'installer :
- les AppImage des v0.4.3 à v0.4.5, publiées sur Forgejo pour la v0.4.3 et sur le miroir GitHub pour les v0.4.3 à v0.4.5. Leur updater lit le registre et cherche `linux-x86_64-appimage`, absente, puis `linux-x86_64`. La v0.4.2 vise `…/releases/latest/download/latest.json`, qui répond 404 sur cette instance Forgejo, et les versions antérieures lisent GitHub ;
- un binaire lancé hors de son paquet, sans marqueur, qui ne cherche que `linux-x86_64`. C'est le cas d'un build `npm run tauri dev` sous Linux : sa carte Mises à jour affiche l'erreur `TargetsNotFound` à chaque vérification. Elle est attendue depuis le retrait de la clé, ce n'est pas une régression (#419).
Avec la clé, l'updater les traitait en AppImage (`install_appimage`) et écrivait les octets du `.deb` à la place du fichier lancé, ce qui cassait l'installation. Sans elle, leur vérification échoue avec `TargetsNotFound` et rien n'est installé : la carte Mises à jour affiche une erreur. La voie, pour ces installations, est de télécharger le `.deb` ou le `.rpm` de la dernière release.
---
## Si ça casse
L'ordre compte, et la première action ne répare pas ce qu'on croit.
1. **Republier le `latest.json` précédent** sur `generic/simpl-resultat/latest` (`DELETE` puis `PUT`, avec le `PACKAGE_TOKEN` qu'utilise `release.yml`). Le corps est récupérable depuis les assets de la release précédente — sa copie dans le registre a été détruite par le `DELETE` que `release.yml` fait avant chaque envoi.
**Les deux verbes ne visent pas le même préfixe d'API**, et l'étape « Publish latest.json to package registry » de `release.yml` porte un commentaire explicite là-dessus — le piège est déjà tombé une fois :
```
DELETE {serveur}/api/v1/packages/{owner}/generic/simpl-resultat/latest
PUT {serveur}/api/packages/{owner}/generic/simpl-resultat/latest
```
Rejouer les deux sur la même URL donne un 404 sur l'un des deux, au moment où on peut le moins se permettre de le débugger.
2. **Comprendre ce que ça arrête, et ce que ça n'arrête pas.** `check()` compare strictement `release.version > version_courante`. Republier vN-1 **stoppe la propagation** vers ceux qui n'ont pas encore cliqué. Les utilisateurs déjà passés à la vN cassée ne se verront **jamais** proposer vN-1 : ils sont bloqués dessus.
3. **Le vrai correctif est donc une vN+1**, pas un retour en arrière. Le republication de vN-1 n'achète que du temps — et si elle reste en place, elle prive aussi les utilisateurs restés en vN-1 de tout chemin de mise à jour.
---
## Trace
- [ ] Consigner le résultat **dans le corps de la release Forgejo**, en section « QA — cycle de mise à jour automatique » ajoutée sous les notes : cibles déroulées, versions de départ et d'arrivée, invite d'authentification observée sous Linux. Forgejo n'a pas de commentaires sur les releases, et éditer le corps ne touche pas `latest.json`, déjà publié dans le registre de paquets. Le corps est **remplacé en entier** par l'API — relire, concaténer, puis :
```bash
# id : GET /releases/tags/vX.Y.Z → .id ; payload.json = {"body": "<notes existantes + section QA>"}
curl -X PATCH -H "Authorization: token $(cat ~/.forgejo-token)" -H "Content-Type: application/json" \
--data-binary @payload.json "https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-resultat/releases/{id}"
```
- [ ] Si la checklist **n'a pas été déroulée**, l'écrire explicitement avec la raison, plutôt que de laisser le silence. Une étape non tracée est indiscernable d'une étape passée — c'est le mode d'échec que cette page combat.