Simpl-Resultat/docs/qa-update-cycle.md
le king fu f6418f79cd docs(release): add a real update-cycle QA checklist to step 9
Step 9 inspected latest.json and stopped there. That proves the file is well
formed, not that it installs: the TLS download, signature verification,
installer execution and relaunch are exercised by no test at all — cargo check
and cargo test only prove that code compiles. A regression there surfaces at
update time, on a user's machine, and automatic updates are a Base+ feature.

Written as a manual checklist rather than automation because it needs two real
desktop environments. The value is in the preconditions, which are what make
the difference between running the test and only appearing to:

- Test from a machine still on the PREVIOUS version. check() compares
  release.version > current strictly, so testing on the machine that just
  built the release shows "up to date" and the checklist gets ticked as
  "nothing to update" — the silent skip it exists to prevent, now with a
  paper trail claiming it passed.
- A Base+ key on each machine, with any activation.token from another machine
  removed. Auto-update is entitlement-gated: useUpdater dispatches NOT_ENTITLED
  before check() ever runs, and a token bound to a different machine_id
  silently resolves the edition back to Free.
- A live polkit agent on Linux, and record which prompt actually appeared —
  install_deb cascades pkexec -> zenity/kdialog -> terminal sudo, so without an
  agent the app appears to hang instead of failing.

The two target flows are written separately because they genuinely differ: on
Windows downloadAndInstall never returns (the process exits and NSIS takes
over, so readyToInstall/installing never render), while on Linux the app stays
alive through a polkit prompt and a restart control.

The doc states what the test does NOT prove: `tar` stays unexercised. Its
vulnerable path is only reached by install_appimage and the macOS .app.tar.gz
branch; our .deb goes through `pkexec dpkg -i` and our NSIS path launches the
.exe. That corrects #315's own premise.

The failure path is documented as an incident playbook rather than a
"rollback", because it is not one. Republishing the previous latest.json stops
propagation, but check()'s strict comparison means users already on the broken
version are never offered the older one — the real fix is a vN+1.

rpm is recorded as known-broken, not unverified, and tracked in #320.

Resolves #315

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:51:51 -04:00

83 lines
7.4 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-webpki`, `tar`), `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**. Les deux flux sont réellement différents, ils ne se substituent pas l'un à l'autre.
- [ ] Sur la machine Linux, **un agent polkit qui tourne**. L'installation du `.deb` 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**. 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` 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é — mais 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.
---
## 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 les deux cibles : 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, donc `tar`. Dans `tauri-plugin-updater`, `tar` n'est atteint que par `install_appimage` et la branche macOS `.app.tar.gz`. Aucun de nos bundles ne passe par là — le `.deb` fait `pkexec dpkg -i`, le NSIS écrit le `.exe` dans un répertoire temporaire et le lance. **Tant qu'on ne livre pas d'AppImage, le chemin `tar` reste mort** et aucune checklist manuelle ne le couvrira.
---
## 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 — ce bouton et celui de la page d'erreur sont les deux seuls déclencheurs.
- [ ] 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`.
- [ ] Après le téléchargement, l'app **reste vivante** et passe en `readyToInstall`.
- [ ] Une invite d'authentification apparaît. **Noter laquelle** : polkit (nominal), zenity/kdialog, ou rien du tout (repli `sudo` → l'app paraît figée).
Invite observée : `________________`
- [ ] Authentifier → état `installing`, puis le contrôle de redémarrage apparaît.
- [ ] Redémarrer, vérifier la version, re-vérifier → `upToDate`.
## 3. RPM — cassé, connu
- [ ] **Ne pas tester** : ce chemin est cassé, pas seulement non vérifié.
`latest.json` ne porte qu'une entrée `linux-x86_64`, construite à partir du `.deb` (`release.yml:104-122`). Une installation rpm reçoit donc des octets deb, que `Installer::Rpm` rejette après vérification de la signature magique du payload. Les artefacts `.rpm` continuent pourtant d'être publiés comme assets de release. Suivi en **#320**.
---
## 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.
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 en commentaire de la release Forgejo : cibles déroulées, versions de départ et d'arrivée, invite d'authentification observée sous Linux.
- [ ] 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.