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

7.4 KiB

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.