fix(release): apply the review follow-ups of the release-deps stack, drop the linux-x86_64 key #418

Merged
maximus merged 5 commits from review-followups-release-deps into main 2026-10-12 19:33:03 +00:00
16 changed files with 212 additions and 67 deletions

View file

@ -2,7 +2,7 @@
name: release name: release
description: Release a new version of Simpl-Resultat (bump, changelog, tag, push) description: Release a new version of Simpl-Resultat (bump, changelog, tag, push)
user-invocable: true user-invocable: true
updated: 2026-10-10 updated: 2026-10-11
--- ---
# /release — Release Simpl-Resultat # /release — Release Simpl-Resultat
@ -25,6 +25,12 @@ updated: 2026-10-10
- `package.json` (champ `"version"`) - `package.json` (champ `"version"`)
- `package-lock.json` (deux champs `"version"` — root ~ligne 3 et le package racine `""` ~ligne 9) - `package-lock.json` (deux champs `"version"` — root ~ligne 3 et le package racine `""` ~ligne 9)
- Si `package-lock.json` est stale (hygiene warning `package-lock.json plus ancien que package.json`) : `npm install --package-lock-only --no-audit --no-fund` pour resync. Note : peut ajouter des entrees bundled optionnelles (tailwindcss oxide wasm etc.) — cosmetique, pas d'install effective. - Si `package-lock.json` est stale (hygiene warning `package-lock.json plus ancien que package.json`) : `npm install --package-lock-only --no-audit --no-fund` pour resync. Note : peut ajouter des entrees bundled optionnelles (tailwindcss oxide wasm etc.) — cosmetique, pas d'install effective.
- Puis rejouer le garde `windows-sys` de `check-rust.yml` sur le lock édité : le commit de release touche `Cargo.lock` directement sur `main`, sans PR, et `check-rust` ne le verrait qu'au lundi suivant, après le build du tag ([ADR 0023](../../../docs/adr/0023-windows-sys-pinning.md)). Depuis la racine du dépôt, le texte exact de l'étape avec les options du runner :
```bash
python3 -c 'import sys, yaml; steps = yaml.safe_load(open(".forgejo/workflows/check-rust.yml"))["jobs"]["rust"]["steps"]; sys.stdout.write(next(s["run"] for s in steps if s.get("name") == "Verify windows-sys pinning"))' \
| bash --noprofile --norc -e -o pipefail
```
Attendu : code de sortie 0 et dernière ligne `windows-sys pinning: N checks, exit 0`. Une ligne `FAIL:` arrête la release : suivre la procédure de l'ADR 0023 avant de tagger.
3. Mettre a jour les 2 changelogs — format **Keep a Changelog** : 3. Mettre a jour les 2 changelogs — format **Keep a Changelog** :
- `CHANGELOG.md` (EN) : header `## [Unreleased]` - `CHANGELOG.md` (EN) : header `## [Unreleased]`
- `CHANGELOG.fr.md` (FR) : header `## [Non publié]` (PAS `[Unreleased]` — ne pas le confondre avec un changelog vide) - `CHANGELOG.fr.md` (FR) : header `## [Non publié]` (PAS `[Unreleased]` — ne pas le confondre avec un changelog vide)
@ -43,10 +49,10 @@ updated: 2026-10-10
```bash ```bash
bash .claude/skills/release/scripts/verify-release.sh vX.Y.Z bash .claude/skills/release/scripts/verify-release.sh vX.Y.Z
``` ```
Le script (token `~/.forgejo-token`) lit la release par tag, compte les 7 artefacts **par suffixe** (`.exe` NSIS, `.deb`, `.rpm`, leurs 3 signatures `.sig`, `latest.json` — les noms exacts varient selon la version de Tauri), puis télécharge `latest.json` (il pilote l'auto-update des installations existantes) et vérifie `version` (il affiche `pub_date` et la longueur des notes), puis exige **exactement quatre clés** (#320) : `linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64`. Pour chacune : l'extension de l'URL (`.deb`, `.deb`, `.rpm`, `.exe`), une signature non vide dont le `file:` (commentaire de confiance minisign, signature décodée du base64) est le nom de fichier de l'URL, décodé, et la version dans l'URL. **Il sort en 1 sur tout écart** (`MANQUANT`, `VIDE`, `MAUVAISE VERSION`, `MAUVAISE EXTENSION`, `DIFFÉRENT`, `INATTENDUE`) et finit par `VERDICT : OK` ou `VERDICT : ÉCHEC` : pas d'annonce de la release ni d'étape 10 avant un verdict OK. Un second argument (`verify-release.sh vX.Y.Z fichier.json`) contrôle un `latest.json` local au lieu de l'asset, pour éprouver les contrôles sur un fichier fabriqué. Le script (token `~/.forgejo-token`) lit la release par tag, compte les 7 artefacts **par suffixe** (`.exe` NSIS, `.deb`, `.rpm`, leurs 3 signatures `.sig`, `latest.json` — les noms exacts varient selon la version de Tauri), puis télécharge `latest.json` (il pilote l'auto-update des installations existantes) et vérifie `version` (il affiche `pub_date` et la longueur des notes), puis exige **exactement trois clés** (#320) : `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64`, sans clé générique `linux-x86_64` depuis le 2026-10-11 (`docs/qa-update-cycle.md`). Pour chacune : l'extension de l'URL (`.deb`, `.rpm`, `.exe`), une signature non vide dont le `file:` (commentaire de confiance minisign, signature décodée du base64) est le nom de fichier de l'URL, décodé, et la version dans l'URL. **Il sort en 1 sur tout écart** (`MANQUANT`, `VIDE`, `MAUVAISE VERSION`, `MAUVAISE EXTENSION`, `DIFFÉRENT`, `INATTENDUE`) et finit par `VERDICT : OK` ou `VERDICT : ÉCHEC` : pas d'annonce de la release ni d'étape 10 avant un verdict OK. Un second argument (`verify-release.sh vX.Y.Z fichier.json`) contrôle un `latest.json` local au lieu de l'asset, pour éprouver les contrôles sur un fichier fabriqué.
**Lire aussi l'étape « Generate latest.json » du journal du run** (même route web que ci-dessous) : `scripts/build-latest-json.sh` y nomme le paquet retenu pour chaque type (`build-latest-json: .rpm <- …`), puis le `latest.json` généré est affiché. Ce script, le délimiteur aléatoire des notes et l'assertion placée avant le `DELETE` du registre ne s'exercent qu'au tag : une assertion en échec arrête le job sur `ERROR: release-assets/latest.json failed its checks`, registre intact, et la release Forgejo reste créée sans mise à jour servie. **Lire aussi l'étape « Generate latest.json » du journal du run** (même route web que ci-dessous) : `scripts/build-latest-json.sh` y nomme le paquet retenu pour chaque type (`build-latest-json: .rpm <- …`), puis le `latest.json` généré est affiché. Le script et le filtre `scripts/check-latest-json.jq` passent par les tests de `check-frontend` à chaque PR ; leur appel par `release.yml` et le délimiteur aléatoire des notes ne s'exercent qu'au tag. Un manifeste refusé par le filtre arrête le job sur `ERROR: release-assets/latest.json failed scripts/check-latest-json.jq`, avant le `DELETE` : registre intact, et la release Forgejo reste créée sans mise à jour servie.
Durée observée du run : 22 à 30 min sur les 12 dernières releases — poser le `Monitor` puis passer à autre chose. Durée observée du run : 22 à 30 min sur les 12 dernières releases — poser le `Monitor` puis passer à autre chose.
**Lire aussi le Node.js du run.** `release.yml` n'est exercé qu'au tag : une montée de Node (Node 20 → 24 par #376) ne se prouve que dans ce journal. Il passe par la route web (l'API v1 des journaux répond 404). Le `run_number` vient de `/actions/tasks`, qui rend toute la liste, la plus récente en tête (le paramètre `limit` y est ignoré) : **Lire aussi le Node.js du run.** `release.yml` n'est exercé qu'au tag : une montée de Node (Node 20 → 24 par #376) ne se prouve que dans ce journal. Il passe par la route web (l'API v1 des journaux répond 404). Le `run_number` vient de `/actions/tasks` : sans paramètre `page`, la route rend toute la liste, la plus récente en tête, et ignore `limit` (avec `page`, elle pagine par 30 ou par `limit`, mesuré le 2026-10-12) :
```bash ```bash
TAG=vX.Y.Z TAG=vX.Y.Z
RUN=$(curl -s -H "Authorization: token $(cat ~/.forgejo-token)" \ RUN=$(curl -s -H "Authorization: token $(cat ~/.forgejo-token)" \
@ -83,3 +89,5 @@ updated: 2026-10-10
- 2026-09-13 — Étape 0 : prérequis « clés testeurs saisies avant le tag » précisé (déclencheurs `FEATURE_TIERS`, gating, `auto-update` ; cas réel v0.15.0, deux clés Premium émises le jour même). Étape 9 : script `scripts/verify-release.sh` (7 artefacts par suffixe + contenu de `latest.json`) au lieu d'un script réécrit à chaque release ; durée de run observée 22-30 min. Source : session 690a6129 (release v0.15.0, run 402) via /skills-review-inbox (draft 2026-09-12). - 2026-09-13 — Étape 0 : prérequis « clés testeurs saisies avant le tag » précisé (déclencheurs `FEATURE_TIERS`, gating, `auto-update` ; cas réel v0.15.0, deux clés Premium émises le jour même). Étape 9 : script `scripts/verify-release.sh` (7 artefacts par suffixe + contenu de `latest.json`) au lieu d'un script réécrit à chaque release ; durée de run observée 22-30 min. Source : session 690a6129 (release v0.15.0, run 402) via /skills-review-inbox (draft 2026-09-12).
- 2026-10-10 — Étape 9 (post-CI) : lire `node --version` dans le journal du run de release (route web, seule ligne `vX.Y.Z` du journal). `release.yml` n'est exercé qu'au tag, donc la montée Node 20 → 24 de #376 ne se prouve qu'à la release suivante. Source : caveat de planification de #376 (milestone `planned-2026-10-10-release-deps`). - 2026-10-10 — Étape 9 (post-CI) : lire `node --version` dans le journal du run de release (route web, seule ligne `vX.Y.Z` du journal). `release.yml` n'est exercé qu'au tag, donc la montée Node 20 → 24 de #376 ne se prouve qu'à la release suivante. Source : caveat de planification de #376 (milestone `planned-2026-10-10-release-deps`).
- 2026-10-10 — Étape 9 (post-CI) : `verify-release.sh` exige les quatre clés de `latest.json` (`linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm`, `windows-x86_64`), l'extension de chaque URL et le `file:` minisign de chaque signature, et sort en 1 sur tout écart (verdict en dernière ligne) ; lire l'étape « Generate latest.json » du journal. Source : #320 (milestone `planned-2026-10-10-release-deps`). - 2026-10-10 — Étape 9 (post-CI) : `verify-release.sh` exige les quatre clés de `latest.json` (`linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm`, `windows-x86_64`), l'extension de chaque URL et le `file:` minisign de chaque signature, et sort en 1 sur tout écart (verdict en dernière ligne) ; lire l'étape « Generate latest.json » du journal. Source : #320 (milestone `planned-2026-10-10-release-deps`).
- 2026-10-11 — Étape 9 (post-CI) : `latest.json` à trois clés, la clé générique `linux-x86_64` étant retirée (décision de Max sur #320 : ses derniers lecteurs, AppImage v0.4.3 à v0.4.5 et binaires hors paquet, ne savaient pas installer le `.deb` qu'elle servait). Le filtre d'avant le `DELETE` vit dans `scripts/check-latest-json.jq`, que les tests de `check-frontend` exercent à chaque PR, avec la comparaison des trois listes de clés. Source : revue de la PR #416.
- 2026-10-11 — Étape 2 : rejouer le garde `windows-sys` de `check-rust.yml` (ADR 0023) juste après l'édition de `Cargo.lock`, puisque le commit de release n'a pas de PR et que `check-rust` ne le verrait qu'au lundi suivant. Commande éprouvée sur le lock de la pile (exit 0) et sur une arête dérivée (exit 1). Source : revue de la PR #415.

View file

@ -63,10 +63,14 @@ print(f"version={v} {'OK' if v == version else 'MAUVAISE VERSION'} | pub_date={p
ok &= v == version ok &= v == version
print(f"notes: {len(notes)} caractères — début: {notes[:120]!r}") print(f"notes: {len(notes)} caractères — début: {notes[:120]!r}")
# The four keys (#320): tauri-plugin-updater looks up "{os}-{arch}-{installer}", # The three keys (#320): tauri-plugin-updater looks up "{os}-{arch}-{installer}",
# then "{os}-{arch}". Each with the extension its URL must end with. # then "{os}-{arch}". Each with the extension its URL must end with. No generic
EXPECTED = {"linux-x86_64": ".deb", "linux-x86_64-deb": ".deb", # linux-x86_64 since 2026-10-11, so a manifest that still has it fails here as
"linux-x86_64-rpm": ".rpm", "windows-x86_64": ".exe"} # INATTENDUE. Same set as scripts/build-latest-json.sh writes and
# scripts/check-latest-json.jq checks: scripts/build-latest-json.test.mjs reads
# this line and compares.
EXPECTED = {"linux-x86_64-deb": ".deb", "linux-x86_64-rpm": ".rpm",
"windows-x86_64": ".exe"}
def signed_file(sig): def signed_file(sig):
"""The file: field of the trusted comment, in the base64 minisign signature.""" """The file: field of the trusted comment, in the base64 minisign signature."""

View file

@ -28,7 +28,9 @@ on:
workflow_dispatch: workflow_dispatch:
# What this key does on Forgejo 8.0.3: see the comment on `concurrency` in # What this key does on Forgejo 8.0.3: see the comment on `concurrency` in
# check-rust.yml (#376). # check-rust.yml (#376). That measurement covers push and pull_request runs;
# this workflow only runs on schedule and workflow_dispatch, which it does
# not cover.
concurrency: concurrency:
group: ci-audit-${{ github.ref }} group: ci-audit-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true

View file

@ -202,11 +202,15 @@ jobs:
# docs/adr/0023-windows-sys-pinning.md holds the measurements, the exit # docs/adr/0023-windows-sys-pinning.md holds the measurements, the exit
# condition and what to do when this step fails. # condition and what to do when this step fails.
# #
# awk reads Cargo.lock, since the ubuntu:22.04 container has no Python. # awk reads Cargo.lock: mawk is part of the ubuntu:22.04 base image,
# Its exit code is tested apart from its output, so a read failure fails # while python3 is only here as a transitive dependency of the apt
# the step instead of passing for a clean, empty result. As in the step # packages installed above (run 517), which nothing guarantees. Its exit
# above, every check echoes its result, including the passing ones, and a # code is tested apart from its output: an empty output already fails the
# canary must be read and checked, or the loop's silence proves nothing. # checks below (version set, missing blocks, canary), but an awk that
# fails after printing every fact would pass without that test. As in the
# step above, every check echoes its result, including the passing ones,
# and a canary must be read and checked, or the loop's silence proves
# nothing.
# #
# No cargo call, so neither the warm index nor --locked matters here: the # No cargo call, so neither the warm index nor --locked matters here: the
# step reads the lockfile as cargo check left it, which is the committed # step reads the lockfile as cargo check left it, which is the committed

View file

@ -194,17 +194,12 @@ jobs:
# Check the manifest before the DELETE below (#320): once the old # Check the manifest before the DELETE below (#320): once the old
# version is deleted, installed apps find no update until a valid # version is deleted, installed apps find no update until a valid
# one is uploaded. A failed check stops the job, registry untouched. # one is uploaded. A failed check stops the job, registry untouched.
# Same four keys as scripts/build-latest-json.sh writes. # The filter lives in scripts/check-latest-json.jq, which
if ! jq -e --arg version "${GITHUB_REF_NAME#v}" ' # scripts/build-latest-json.test.mjs also runs on every pull
.version == $version # request, so its key list cannot drift from the script's unseen.
and (.platforms | keys) == ["linux-x86_64", "linux-x86_64-deb", "linux-x86_64-rpm", "windows-x86_64"] if ! jq -e --arg version "${GITHUB_REF_NAME#v}" \
and all(.platforms[]; (.signature | type) == "string" and (.signature | length) > 0) -f scripts/check-latest-json.jq release-assets/latest.json > /dev/null; then
and (.platforms["linux-x86_64"].url | endswith(".deb")) echo "ERROR: release-assets/latest.json failed scripts/check-latest-json.jq, registry left as is:"
and (.platforms["linux-x86_64-deb"].url | endswith(".deb"))
and (.platforms["linux-x86_64-rpm"].url | endswith(".rpm"))
and (.platforms["windows-x86_64"].url | endswith(".exe"))
' release-assets/latest.json > /dev/null; then
echo "ERROR: release-assets/latest.json failed its checks, registry left as is:"
cat release-assets/latest.json cat release-assets/latest.json
exit 1 exit 1
fi fi

View file

@ -4,7 +4,7 @@
### Corrigé ### Corrigé
- Les installations faites depuis le paquet `.rpm` reçoivent désormais les mises à jour automatiques. Le manifeste de mise à jour ne listait que le `.deb`, qu'une installation `.rpm` téléchargeait puis refusait ; il liste maintenant chaque paquet Linux sous sa propre clé. Les installations `.rpm` existantes n'ont rien à réinstaller : elles recevront la prochaine version (vérifié sur le paquet v0.17.1 publié). Les instructions de téléchargement de la page de release ne mentionnent plus d'AppImage, qui n'est pas construite, et nomment désormais le `.rpm` (#320). - Les installations faites depuis le paquet `.rpm` reçoivent désormais les mises à jour automatiques. Le manifeste de mise à jour ne listait que le `.deb`, qu'une installation `.rpm` téléchargeait puis refusait ; il liste maintenant chaque paquet Linux sous sa propre clé. Les installations `.rpm` existantes n'ont rien à réinstaller : elles recevront la prochaine version (vérifié sur le paquet v0.17.1 publié). Les instructions de téléchargement de la page de release ne mentionnent plus d'AppImage, qui n'est pas construite, et nomment désormais le `.rpm`. Le manifeste n'a plus d'entrée Linux générique : les AppImage des versions 0.4.3 à 0.4.5, qui ne savaient pas installer le `.deb` qu'elle désignait et en étaient cassées, affichent désormais une erreur à la vérification des mises à jour ; installez plutôt le paquet `.deb` ou `.rpm` (#320).
### Sécurité ### Sécurité

View file

@ -4,7 +4,7 @@
### Fixed ### Fixed
- Installs from the `.rpm` package now receive automatic updates. The update manifest only listed the `.deb`, which an `.rpm` install downloaded and then refused; it now lists each Linux package under its own key. Existing `.rpm` installs need no reinstall: they will receive the next version (checked on the published v0.17.1 package). The download instructions of the release page no longer mention an AppImage, which is not built, and now name the `.rpm` (#320). - Installs from the `.rpm` package now receive automatic updates. The update manifest only listed the `.deb`, which an `.rpm` install downloaded and then refused; it now lists each Linux package under its own key. Existing `.rpm` installs need no reinstall: they will receive the next version (checked on the published v0.17.1 package). The download instructions of the release page no longer mention an AppImage, which is not built, and now name the `.rpm`. The manifest no longer has a generic Linux entry: the AppImage builds of 0.4.3 to 0.4.5, which could not install the `.deb` it pointed to and were broken by it, now report an error when checking for updates instead; install the `.deb` or `.rpm` package (#320).
### Security ### Security

View file

@ -163,7 +163,7 @@ Pour maintenir l'éligibilité aux crédits d'impôt R&D (RS&DE fédéral + CRIC
Workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** — les jobs se suivent, ils ne tournent pas en parallèle. Workflows Forgejo Actions dans `.forgejo/workflows/`. Le runner est à **capacité 1** — les jobs se suivent, ils ne tournent pas en parallèle.
- **`check-rust.yml`** — déclenché sur les PR touchant `src-tauri/**` ou `.cargo/**`, plus chaque lundi à 06:30 UTC sur `main` et par `workflow_dispatch`. Lance `cargo check`, une vérification bloquante que les advisories acceptées restent non atteignables, une autre que les sept arêtes `windows-sys` à borne large du lock restent sur `0.61.2` ([ADR 0023](docs/adr/0023-windows-sys-pinning.md)), `cargo test` et un `cargo audit` informatif, avec le cache `Swatinem/rust-cache`. Doit être vert avant tout merge. Un `cargo update` qui re-résout ces arêtes (`-p hyper` et `-p h2` l'ont fait) fait rougir le garde `windows-sys` : remettre les arêtes à la main selon l'ADR, ne jamais modifier la liste gardée pour le faire passer. - **`check-rust.yml`** — déclenché sur les PR touchant `src-tauri/**` ou `.cargo/**`, plus chaque lundi à 06:30 UTC sur `main` et par `workflow_dispatch`. Lance `cargo check`, une vérification bloquante que les advisories acceptées restent non atteignables, une autre que les sept arêtes `windows-sys` à borne large du lock restent sur `0.61.2` ([ADR 0023](docs/adr/0023-windows-sys-pinning.md)), `cargo test` et un `cargo audit` informatif, avec le cache `Swatinem/rust-cache`. Doit être vert avant tout merge. Un `cargo update` qui re-résout ces arêtes (`-p hyper` et `-p h2` l'ont fait) fait rougir le garde `windows-sys` : remettre les arêtes à la main selon l'ADR, ne jamais modifier la liste gardée pour le faire passer.
- **`check-frontend.yml`** — déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown. Lance `npm run build` (tsc + vite) et `npm test` (vitest). Doit être vert avant tout merge. **Aucune étape `npm audit`** : les advisories npm ne sont pas un gate de CI. `npm audit` à 0 au 2026-10-10 ([#358](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/358)), un constat daté que rien ne réévalue en CI. - **`check-frontend.yml`** — déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown. Lance `npm run build` (tsc + vite) et `npm test` (vitest). Doit être vert avant tout merge. **Aucune étape `npm audit`** : les advisories npm ne sont pas un gate de CI. `npm audit` à 0 au 2026-10-10 ([#358](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/358)), un constat daté : aucun job ne le fait échouer, mais `npm ci` en imprime le compte à chaque run de `check-frontend`, et le Défenseur relève les advisories de son côté (#358 en vient).
- **`audit.yml`** — audit RustSec quotidien (06:00 UTC) + `workflow_dispatch`, pour couvrir les avis de sécurité entre deux PR Rust. Échec bloquant. **Un run vert signifie « zéro advisory hors de la liste acceptée »**, pas « zéro advisory » : cette liste est dans `.cargo/audit.toml` (preuve de non-atteignabilité et condition de retrait par entrée), encadrée par l'[ADR 0018](docs/adr/0018-suppression-advisories-non-atteignables.md). Ne jamais y ajouter une advisory atteignable ni élargir une entrée à un crate entier. - **`audit.yml`** — audit RustSec quotidien (06:00 UTC) + `workflow_dispatch`, pour couvrir les avis de sécurité entre deux PR Rust. Échec bloquant. **Un run vert signifie « zéro advisory hors de la liste acceptée »**, pas « zéro advisory » : cette liste est dans `.cargo/audit.toml` (preuve de non-atteignabilité et condition de retrait par entrée), encadrée par l'[ADR 0018](docs/adr/0018-suppression-advisories-non-atteignables.md). Ne jamais y ajouter une advisory atteignable ni élargir une entrée à un crate entier.
- **`release.yml`** — déclenché par les tags `v*`. Build Windows (NSIS `.exe`) + Linux (`.deb`, `.rpm`), signe les binaires et publie le JSON d'updater pour les mises à jour automatiques. - **`release.yml`** — déclenché par les tags `v*`. Build Windows (NSIS `.exe`) + Linux (`.deb`, `.rpm`), signe les binaires et publie le JSON d'updater pour les mises à jour automatiques.

View file

@ -75,7 +75,7 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA
- Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers. - Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers.
- **Vérifications dues à la prochaine release** (après le tag, étape 9 de `/release`) : - **Vérifications dues à la prochaine release** (après le tag, étape 9 de `/release`) :
- `release.yml` sous Node 24 (#376) : `node --version` à `v24.x` dans le journal du run de release, et `verify-release.sh` vert. Le diff touchant `release.yml`, l'étape 10 de `/release` (cycle réel de mise à jour, `docs/qa-update-cycle.md`) s'applique aussi, selon la règle du skill. - `release.yml` sous Node 24 (#376) : `node --version` à `v24.x` dans le journal du run de release, et `verify-release.sh` vert. Le diff touchant `release.yml`, l'étape 10 de `/release` (cycle réel de mise à jour, `docs/qa-update-cycle.md`) s'applique aussi, selon la règle du skill.
- `latest.json` à quatre clés (#320) : étape « Generate latest.json » du journal lue (un paquet retenu par type), `verify-release.sh` en `VERDICT : OK` sur `linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64` (extensions, `file:` des signatures). Cycle réel depuis une installation `.rpm` (§3 de `docs/qa-update-cycle.md`) dès qu'une machine Fedora ou openSUSE est disponible, sinon « non déroulé » avec la raison dans la trace de la release. - `latest.json` à trois clés (#320, clé générique `linux-x86_64` retirée le 2026-10-11) : étape « Generate latest.json » du journal lue (un paquet retenu par type), `verify-release.sh` en `VERDICT : OK` sur `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64` (extensions, `file:` des signatures). Cycle réel depuis une installation `.rpm` (§3 de `docs/qa-update-cycle.md`) dès qu'une machine Fedora ou openSUSE est disponible, sinon « non déroulé » avec la raison dans la trace de la release.
- **Le paiement est live : « aucun tag avant le go-live » n'a plus de raison d'être, et la release desktop qu'attendait le go-live est publiée (v0.16.0, 2026-10-03).** Bascule live le 2026-09-29 (Product et Price live à 19 $ CAD, webhook, variables Stripe de production, redeploy `3673d09` de maximus-api, URL `cs_live_` vérifiée), puis achat réel de contrôle le 2026-09-30 : clé sur `/merci` et par courriel, activation dans l'app, remboursement complet et licence révoquée par `charge.refunded`. **Volet serveur clos le 2026-10-03** (derniers commentaires relus le 2026-10-04) : maximus-api#62 fermée, tous critères cochés (maximus-api 0.4.2 en prod, secrets en runtime seul), avec la milestone `spec-paiements` de maximus-api (12/12) ; la-compagnie-maximus#150 (Link dans les textes légaux) livrée et fermée le 2026-10-01. **Go-live clos : la-compagnie-maximus#141** fermée le 2026-10-10 sur décision de Max, l'updater vérifié (Free refuse et invite à acheter, Base accepte ; confirmé le même jour). Restent pour mémoire, sans issue : le Dashboard Stripe (logo, paramètres Checkout, reçus automatiques, alertes d'échec de webhook) et les relevés du commentaire de fermeture. la-compagnie-maximus#184 (politique de confidentialité : données d'activation, contrôle au démarrage) est en `status:review` ; sa PR reste en brouillon jusqu'au déploiement de maximus-api#85 (commentaire du 2026-10-03). Les deux textes des suites de licence (contrôle à la saisie, retrait de la clé) y sont postés depuis le 2026-10-05 (commentaire 19724). **Vérifications manuelles sur la release publiée, toutes faites** : #365 le 2026-10-03, #346 et #350 le 2026-10-04 (voir Décisions). L'étape 10 sous Windows est faite (Michel, confirmée le 2026-10-10). - **Le paiement est live : « aucun tag avant le go-live » n'a plus de raison d'être, et la release desktop qu'attendait le go-live est publiée (v0.16.0, 2026-10-03).** Bascule live le 2026-09-29 (Product et Price live à 19 $ CAD, webhook, variables Stripe de production, redeploy `3673d09` de maximus-api, URL `cs_live_` vérifiée), puis achat réel de contrôle le 2026-09-30 : clé sur `/merci` et par courriel, activation dans l'app, remboursement complet et licence révoquée par `charge.refunded`. **Volet serveur clos le 2026-10-03** (derniers commentaires relus le 2026-10-04) : maximus-api#62 fermée, tous critères cochés (maximus-api 0.4.2 en prod, secrets en runtime seul), avec la milestone `spec-paiements` de maximus-api (12/12) ; la-compagnie-maximus#150 (Link dans les textes légaux) livrée et fermée le 2026-10-01. **Go-live clos : la-compagnie-maximus#141** fermée le 2026-10-10 sur décision de Max, l'updater vérifié (Free refuse et invite à acheter, Base accepte ; confirmé le même jour). Restent pour mémoire, sans issue : le Dashboard Stripe (logo, paramètres Checkout, reçus automatiques, alertes d'échec de webhook) et les relevés du commentaire de fermeture. la-compagnie-maximus#184 (politique de confidentialité : données d'activation, contrôle au démarrage) est en `status:review` ; sa PR reste en brouillon jusqu'au déploiement de maximus-api#85 (commentaire du 2026-10-03). Les deux textes des suites de licence (contrôle à la saisie, retrait de la clé) y sont postés depuis le 2026-10-05 (commentaire 19724). **Vérifications manuelles sur la release publiée, toutes faites** : #365 le 2026-10-03, #346 et #350 le 2026-10-04 (voir Décisions). L'étape 10 sous Windows est faite (Michel, confirmée le 2026-10-10).
- Backlog ouvert : **vague `planned-2026-09-24-paiements` close 3/3** (milestone fermée le 2026-09-30) : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, #347 (PR #364, ferme aussi #360) mergée le 2026-09-30 ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). **#365 fermée le 2026-10-03** après sa vérification manuelle. **Milestone `planned-2026-10-03-suites-licence` fermée 3/3 le 2026-10-05** (#373, #370, #367 mergées, voir Décisions ; suivi #389 en `status:triage`). Hors milestone, en `status:ready` : **#369** (réserver le fractionnement de transactions à l'édition Base) et **#372** (la liste complète des versions repousse le Journal en bas de Paramètres → Systèmes). **#350 fermée le 2026-10-04** (étape 3 vérifiée en production, voir Décisions) ; #363 fermée le 2026-10-10 sur décision de Max (ses tests, absorbés par #347, sont livrés dans la v0.16.0) ; réglés côté serveur et site : maximus-api#63 (clés de 100 ans) et maximus-api#70 (réactivation d'une machine désactivée), fermées et contenues dans `3673d09`, le commit déployé à la bascule du 2026-09-29, et la-compagnie-maximus#157, fermée, dont les CGV sont publiées en tronc commun plus annexe Simpl-Résultat (annexe relue en production le 2026-09-30, plus aucune durée de 10 ans) ; côté serveur, `/v1/prices` ne vérifie pas qu'une machine est encore active (une machine désactivée garde les cours jusqu'à l'expiration de son jeton), issue maximus-api à ouvrir ; suites de #350 ouvertes le 2026-10-01 : maximus-api#82 (`status:triage` : identifiants machine bruts déjà stockés, lignes désactivées comprises ; à rehacher avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne) et la-compagnie-maximus#184 (`status:review`, voir ci-dessus) ; go-live clos (la-compagnie-maximus#141 fermée le 2026-10-10, maximus-api#62 le 2026-10-03) ; `spec-paiements` fermée ici 5/5 ; onboarding phase 2 (parcours guidé) **mergée** : milestone `planned-2026-10-06-onboarding-parcours-guide` fermée 6/6 le 2026-10-08 (#396 à #401, `ee17f31`), suivis **#408** et **#409** (`status:triage`) ; onboarding phase 1 **livrée** : milestone `planned-2026-10-03-onboarding-phase1` fermée 7/7 le 2026-10-04 (#351 à #357, mergées en avance rapide, `667e058`) ; suivis ouverts : **#384** (`status:ready`, suivis non bloquants des revues) et **#371** (`status:blocked`, hors `/autopilot` : en-têtes CSV BMO à fournir par Max depuis un compte réel, un par type de compte, chèques/épargne et Mastercard, avec des valeurs fictives) ; analyse d'origine dans `docs/analyse-onboarding-2026-09.md`, plan détaillé dans les `spec-*-onboarding-phase1.md` locaux, ignorés par git ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **fermée 4/4** le 2026-10-05 (#232 livrée le 2026-07-27 ; #234 close le 2026-10-05 avec ses quatre preuves : dispatch run 464, PR #386 run 480, run planifié 488, `actcache` à 999 Mo) ; **#376** (`status:triage` : Node 20, en fin de vie depuis le 2026-04-30, encore installé par `check-frontend.yml` et `audit.yml`, et commentaire `concurrency` de `check-rust.yml` démenti par `forgejo-ops.md` pour les runs de PR). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist. - Backlog ouvert : **vague `planned-2026-09-24-paiements` close 3/3** (milestone fermée le 2026-09-30) : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, #347 (PR #364, ferme aussi #360) mergée le 2026-09-30 ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). **#365 fermée le 2026-10-03** après sa vérification manuelle. **Milestone `planned-2026-10-03-suites-licence` fermée 3/3 le 2026-10-05** (#373, #370, #367 mergées, voir Décisions ; suivi #389 en `status:triage`). Hors milestone, en `status:ready` : **#369** (réserver le fractionnement de transactions à l'édition Base) et **#372** (la liste complète des versions repousse le Journal en bas de Paramètres → Systèmes). **#350 fermée le 2026-10-04** (étape 3 vérifiée en production, voir Décisions) ; #363 fermée le 2026-10-10 sur décision de Max (ses tests, absorbés par #347, sont livrés dans la v0.16.0) ; réglés côté serveur et site : maximus-api#63 (clés de 100 ans) et maximus-api#70 (réactivation d'une machine désactivée), fermées et contenues dans `3673d09`, le commit déployé à la bascule du 2026-09-29, et la-compagnie-maximus#157, fermée, dont les CGV sont publiées en tronc commun plus annexe Simpl-Résultat (annexe relue en production le 2026-09-30, plus aucune durée de 10 ans) ; côté serveur, `/v1/prices` ne vérifie pas qu'une machine est encore active (une machine désactivée garde les cours jusqu'à l'expiration de son jeton), issue maximus-api à ouvrir ; suites de #350 ouvertes le 2026-10-01 : maximus-api#82 (`status:triage` : identifiants machine bruts déjà stockés, lignes désactivées comprises ; à rehacher avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne) et la-compagnie-maximus#184 (`status:review`, voir ci-dessus) ; go-live clos (la-compagnie-maximus#141 fermée le 2026-10-10, maximus-api#62 le 2026-10-03) ; `spec-paiements` fermée ici 5/5 ; onboarding phase 2 (parcours guidé) **mergée** : milestone `planned-2026-10-06-onboarding-parcours-guide` fermée 6/6 le 2026-10-08 (#396 à #401, `ee17f31`), suivis **#408** et **#409** (`status:triage`) ; onboarding phase 1 **livrée** : milestone `planned-2026-10-03-onboarding-phase1` fermée 7/7 le 2026-10-04 (#351 à #357, mergées en avance rapide, `667e058`) ; suivis ouverts : **#384** (`status:ready`, suivis non bloquants des revues) et **#371** (`status:blocked`, hors `/autopilot` : en-têtes CSV BMO à fournir par Max depuis un compte réel, un par type de compte, chèques/épargne et Mastercard, avec des valeurs fictives) ; analyse d'origine dans `docs/analyse-onboarding-2026-09.md`, plan détaillé dans les `spec-*-onboarding-phase1.md` locaux, ignorés par git ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **fermée 4/4** le 2026-10-05 (#232 livrée le 2026-07-27 ; #234 close le 2026-10-05 avec ses quatre preuves : dispatch run 464, PR #386 run 480, run planifié 488, `actcache` à 999 Mo) ; **#376** (`status:triage` : Node 20, en fin de vie depuis le 2026-04-30, encore installé par `check-frontend.yml` et `audit.yml`, et commentaire `concurrency` de `check-rust.yml` démenti par `forgejo-ops.md` pour les runs de PR). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist.
- Suivis des advisories : **#312, #313 et #315 fermées le 2026-08-15** (PRs #321/#322 mergées). **#314 fermée le 2026-08-15, prémisse infirmée** — elle affirmait « zéro run `schedule` », or `audit.yml` tire tous les jours à 06:00 UTC (**19 runs `schedule` verts** du 07-28 au 08-15, sans trou) ; elle a été écrite le 07-27 vers 23:56Z, ~6 h avant le premier tir programmé possible, et décrivait donc une fenêtre trop courte, pas un scheduler en panne. Ces 19 runs confirment au passage que `.cargo/audit.toml` est bien lu en conteneur **sur `main`** — l'ADR 0018 ne l'avait vérifié que par un `workflow_dispatch` sur une branche. **#343 fermée le 2026-09-04** (PR #344) — RUSTSEC-2026-0258 sur `h2`, la seule vulnérabilité qui rougissait l'audit depuis le 08-19 ; l'audit est de nouveau vert sur `main`, prouvé par `workflow_dispatch` (run 393) et non déduit du run de PR. **#348 fermée le 2026-09-21** (PR #349) — RUSTSEC-2026-0285 sur `rustls`, seule vulnérabilité rougissant l'audit depuis le 09-15 (7 runs, 405→411) ; vert reprouvé par `workflow_dispatch` sur `main` (run 414 : 666 paquets, zéro vulnérabilité, 12 warnings autorisés). Deux advisories en cinq semaines sur le même chemin, celui de la mise à jour automatique. Restent **#317** (prémisse caduque depuis le 2026-09-26 : `react-router-dom` 7.18.2 et suivantes corrigent l'advisory par un bump de patch dans `^7.18.1`, sans migration v8 ; absorbée par #358, dont la PR la fermera), **#319** (déclencheur de retrait pour `rsa`, désormais **seule** entrée de `.cargo/audit.toml` — le garde-fou est passé à 2 vérifications) et **#345** (domicilier la contrainte `windows-sys 0.61.2` — analysée le 09-04 : le corps d'origine, écrit de mémoire en fin de revue, se trompait sur trois points. Le déclencheur n'est pas « n'importe quel `cargo update` » mais tout bump atteignant le sous-arbre réseau/TLS — mesuré : `-p anyhow` ne bouge rien, `-p hyper` fait descendre les 7 — soit précisément là où tombent les advisories. **Prémisse restreinte le 2026-09-21 par #348** : `-p rustls`, au cœur de ce même sous-arbre, n'en déplace **aucun** — le déclencheur tient à `hyper` et à ce qu'il tire, pas à la couche TLS en général, donc « sous-arbre réseau/TLS » prédit une dérive là où il n'y en a pas et ferait écrire une strophe à la main sans nécessité. D'où la suggestion portée en commentaire sur l'issue : formuler le garde-fou sur la **version résolue de `windows-sys`** plutôt que sur une liste de crates déclencheurs — la liste est ce qui vient d'être trouvé faux, la version est ce qui est protégé. #310 avait **bien** vu la dérive et l'avait jugée inoffensive *sur Linux*, où elle l'est ; son coût tombe sur le build Windows que personne ne mesurait : **3 versions de `windows-sys` compilées contre 5**. Aucun risque de correction — les 7 crates déclarent des bornes larges couvrant les deux états. Et cargo annonce « Locking 1 package » en écrivant 8 changements, donc le silence est le sien. Décision : garde-fou CI sur la version + ADR portant la condition de sortie. Le check croisé Windows depuis Linux a été **testé et écarté** — `cc-rs` s'arrête sur `lib.exe`, il faudrait `cargo-xwin`). Côté npm, **#358** ouverte le 2026-09-26 par `/analyse-vulnerabilite` : `npm audit` remonte 7 findings (4 high, 3 moderate) sur 4 dépendances racines, contre les 2 acceptés au 07-27, et chaque correctif tient dans la borne déjà déclarée ; le lock se régénère avec `npx npm@11` (npm 10 plante sur un bump de vitest 4.1.x). - Suivis des advisories : **#312, #313 et #315 fermées le 2026-08-15** (PRs #321/#322 mergées). **#314 fermée le 2026-08-15, prémisse infirmée** — elle affirmait « zéro run `schedule` », or `audit.yml` tire tous les jours à 06:00 UTC (**19 runs `schedule` verts** du 07-28 au 08-15, sans trou) ; elle a été écrite le 07-27 vers 23:56Z, ~6 h avant le premier tir programmé possible, et décrivait donc une fenêtre trop courte, pas un scheduler en panne. Ces 19 runs confirment au passage que `.cargo/audit.toml` est bien lu en conteneur **sur `main`** — l'ADR 0018 ne l'avait vérifié que par un `workflow_dispatch` sur une branche. **#343 fermée le 2026-09-04** (PR #344) — RUSTSEC-2026-0258 sur `h2`, la seule vulnérabilité qui rougissait l'audit depuis le 08-19 ; l'audit est de nouveau vert sur `main`, prouvé par `workflow_dispatch` (run 393) et non déduit du run de PR. **#348 fermée le 2026-09-21** (PR #349) — RUSTSEC-2026-0285 sur `rustls`, seule vulnérabilité rougissant l'audit depuis le 09-15 (7 runs, 405→411) ; vert reprouvé par `workflow_dispatch` sur `main` (run 414 : 666 paquets, zéro vulnérabilité, 12 warnings autorisés). Deux advisories en cinq semaines sur le même chemin, celui de la mise à jour automatique. Restent **#317** (prémisse caduque depuis le 2026-09-26 : `react-router-dom` 7.18.2 et suivantes corrigent l'advisory par un bump de patch dans `^7.18.1`, sans migration v8 ; absorbée par #358, dont la PR la fermera), **#319** (déclencheur de retrait pour `rsa`, désormais **seule** entrée de `.cargo/audit.toml` — le garde-fou est passé à 2 vérifications) et **#345** (domicilier la contrainte `windows-sys 0.61.2` — analysée le 09-04 : le corps d'origine, écrit de mémoire en fin de revue, se trompait sur trois points. Le déclencheur n'est pas « n'importe quel `cargo update` » mais tout bump atteignant le sous-arbre réseau/TLS — mesuré : `-p anyhow` ne bouge rien, `-p hyper` fait descendre les 7 — soit précisément là où tombent les advisories. **Prémisse restreinte le 2026-09-21 par #348** : `-p rustls`, au cœur de ce même sous-arbre, n'en déplace **aucun** — le déclencheur tient à `hyper` et à ce qu'il tire, pas à la couche TLS en général, donc « sous-arbre réseau/TLS » prédit une dérive là où il n'y en a pas et ferait écrire une strophe à la main sans nécessité. D'où la suggestion portée en commentaire sur l'issue : formuler le garde-fou sur la **version résolue de `windows-sys`** plutôt que sur une liste de crates déclencheurs — la liste est ce qui vient d'être trouvé faux, la version est ce qui est protégé. #310 avait **bien** vu la dérive et l'avait jugée inoffensive *sur Linux*, où elle l'est ; son coût tombe sur le build Windows que personne ne mesurait : **3 versions de `windows-sys` compilées contre 5**. Aucun risque de correction — les 7 crates déclarent des bornes larges couvrant les deux états. Et cargo annonce « Locking 1 package » en écrivant 8 changements, donc le silence est le sien. Décision : garde-fou CI sur la version + ADR portant la condition de sortie. Le check croisé Windows depuis Linux a été **testé et écarté** — `cc-rs` s'arrête sur `lib.exe`, il faudrait `cargo-xwin`). Côté npm, **#358** ouverte le 2026-09-26 par `/analyse-vulnerabilite` : `npm audit` remonte 7 findings (4 high, 3 moderate) sur 4 dépendances racines, contre les 2 acceptés au 07-27, et chaque correctif tient dans la borne déjà déclarée ; le lock se régénère avec `npx npm@11` (npm 10 plante sur un bump de vitest 4.1.x).

View file

@ -2,7 +2,7 @@
- Status: **Accepted** - Status: **Accepted**
- Date: 2026-10-10 - Date: 2026-10-10
- Issues: #345 (cette décision), #344 (troisième dérive, contournée en n'appliquant que la strophe `h2` à la main), #348 (mesure `rustls` : la couche TLS en général ne déclenche pas la dérive) - Issues: #345 (cette décision), PR #344 (deuxième dérive, contournée en n'appliquant que la strophe `h2` à la main), #348 (mesure `rustls` : la couche TLS en général ne déclenche pas la dérive)
- S'appuie sur l'[ADR 0018](0018-suppression-advisories-non-atteignables.md) : même motif de garde-fou (chaque vérification loggée, canari, code de sortie de l'outil testé à part de sa sortie) - S'appuie sur l'[ADR 0018](0018-suppression-advisories-non-atteignables.md) : même motif de garde-fou (chaque vérification loggée, canari, code de sortie de l'outil testé à part de sa sortie)
## Contexte ## Contexte
@ -26,7 +26,7 @@ Les sept pointent **`0.61.2`**. C'est l'état du tag v0.14.0 (2026-07-18), dont
|---|---| |---|---|
| #310 (2026-07-27) | `cargo update -p rustls-webpki -p tar` fait descendre les sept arêtes ; vu, et jugé inoffensif parce que sans effet sur Linux | | #310 (2026-07-27) | `cargo update -p rustls-webpki -p tar` fait descendre les sept arêtes ; vu, et jugé inoffensif parce que sans effet sur Linux |
| PR #321 (2026-08-15) | la revue les remonte à `0.61.2`, crate par crate contre le tag v0.14.0 | | PR #321 (2026-08-15) | la revue les remonte à `0.61.2`, crate par crate contre le tag v0.14.0 |
| #344 (2026-09-04) | `cargo update -p h2` recommence ; contourné en n'appliquant que la strophe `h2` à la main | | PR #344 (2026-09-04) | `cargo update -p h2` recommence ; contourné en n'appliquant que la strophe `h2` à la main |
| #348 (2026-09-21) | `cargo update -p rustls --precise 0.23.45` ne bouge rien, mais il a fallu remesurer pour le savoir | | #348 (2026-09-21) | `cargo update -p rustls --precise 0.23.45` ne bouge rien, mais il a fallu remesurer pour le savoir |
Hors du lock, la contrainte ne vivait que dans de la prose (`STATE.md`, des messages de commit) : la prochaine mise à jour la régresserait, et il faudrait qu'un relecteur reconnaisse sept noms de crates pour le voir. C'est le mode d'échec que l'ADR 0018 traite pour les suppressions d'advisories : une décision qui ne vit que dans de la prose n'est pas une décision tenue. Hors du lock, la contrainte ne vivait que dans de la prose (`STATE.md`, des messages de commit) : la prochaine mise à jour la régresserait, et il faudrait qu'un relecteur reconnaisse sept noms de crates pour le voir. C'est le mode d'échec que l'ADR 0018 traite pour les suppressions d'advisories : une décision qui ne vit que dans de la prose n'est pas une décision tenue.
@ -39,7 +39,7 @@ Hors du lock, la contrainte ne vivait que dans de la prose (`STATE.md`, des mess
| `cargo update -p rustls --precise 0.23.45` | aucun ; le `diff` de toutes les lignes `windows-sys` du lock est vide | 2026-09-21 (#348) | | `cargo update -p rustls --precise 0.23.45` | aucun ; le `diff` de toutes les lignes `windows-sys` du lock est vide | 2026-09-21 (#348) |
| `cargo update -p hyper` | les sept descendent : `colored` et `winapi-util` vers 0.48.0, `errno`, `rustix`, `tempfile` et `rustls-platform-verifier` vers 0.52.0, `dirs-sys` vers 0.59.0 | 2026-09-04, refaite le 2026-10-10 (`hyper` 1.8.1 → 1.12.0) | | `cargo update -p hyper` | les sept descendent : `colored` et `winapi-util` vers 0.48.0, `errno`, `rustix`, `tempfile` et `rustls-platform-verifier` vers 0.52.0, `dirs-sys` vers 0.59.0 | 2026-09-04, refaite le 2026-10-10 (`hyper` 1.8.1 → 1.12.0) |
Dans cette mesure, chaque arête est descendue vers la plus basse version déjà présente que sa borne admet, et la dérive de #310 (commit `f4b09b0`) a la même répartition : deux arêtes vers 0.48.0, quatre vers 0.52.0, une vers 0.59.0. `hyper` et ce qu'il tire (`h2` en #344) la déclenchent, la couche TLS en général non : `rustls`, en son cœur, ne déplace rien. Mais `rustls-webpki` et `tar` l'ont déclenchée en #310. Aucune règle simple ne dit quelle mise à jour la provoque, et la dernière qu'on a formulée (« tout bump du sous-arbre réseau/TLS ») a été démentie par #348. C'est donc la version résolue qu'il faut garder, pas une liste de déclencheurs. Dans cette mesure, chaque arête est descendue vers la plus basse version déjà présente que sa borne admet, et la dérive de #310 (commit `f4b09b0`) a la même répartition : deux arêtes vers 0.48.0, quatre vers 0.52.0, une vers 0.59.0. `hyper` et ce qu'il tire (`h2` dans la PR #344) la déclenchent, la couche TLS en général non : `rustls`, en son cœur, ne déplace rien. Mais `rustls-webpki` et `tar` l'ont déclenchée en #310. Aucune règle simple ne dit quelle mise à jour la provoque, et la dernière qu'on a formulée (« tout bump du sous-arbre réseau/TLS ») a été démentie par #348. C'est donc la version résolue qu'il faut garder, pas une liste de déclencheurs.
**Et cargo n'en dit rien.** Le 2026-10-10, `cargo update -p hyper` annonce : **Et cargo n'en dit rien.** Le 2026-10-10, `cargo update -p hyper` annonce :
@ -49,7 +49,7 @@ Dans cette mesure, chaque arête est descendue vers la plus basse version déjà
Updating want v0.3.1 -> v0.3.2 Updating want v0.3.1 -> v0.3.2
``` ```
pendant qu'il réécrit aussi les sept arêtes. L'ensemble des paquets ne bouge pas (666 avant et après) ; seules les arêtes changent, et cargo ne rapporte que les paquets. Or les deux dérives passées sont venues avec des correctifs d'advisories RustSec (`rustls-webpki` et `tar` en #310, `h2` en #344) : la dérive arrive avec les changements les plus pressés et les moins relus. pendant qu'il réécrit aussi les sept arêtes. L'ensemble des paquets ne bouge pas (666 avant et après) ; seules les arêtes changent, et cargo ne rapporte que les paquets. Or les deux dérives passées sont venues avec des correctifs d'advisories RustSec (`rustls-webpki` et `tar` en #310, `h2` dans la PR #344) : la dérive arrive avec les changements les plus pressés et les moins relus.
### Ce que coûte la dérive ### Ce que coûte la dérive
@ -66,14 +66,14 @@ Mesuré par `cargo tree --target x86_64-pc-windows-msvc -e normal,build`, refait
**Un garde-fou CI bloquant sur la version résolue, et cet ADR pour en porter la raison et la sortie.** **Un garde-fou CI bloquant sur la version résolue, et cet ADR pour en porter la raison et la sortie.**
L'étape `Verify windows-sys pinning` de `check-rust.yml`, placée après `cargo check` à côté du garde des suppressions, lit `src-tauri/Cargo.lock` avec `awk` (le conteneur `ubuntu:22.04` n'a pas Python) et applique quatre règles : L'étape `Verify windows-sys pinning` de `check-rust.yml`, placée après `cargo check` à côté du garde des suppressions, lit `src-tauri/Cargo.lock` avec `awk` et applique quatre règles. `awk` (mawk) fait partie de l'image de base `ubuntu:22.04`, alors que `python3` n'est présent dans ce conteneur que comme dépendance transitive des paquets `apt` installés à l'étape précédente (journal du run 517), ce que rien ne garantit :
1. **Chaque arête des sept crates.** Pour `colored`, `dirs-sys`, `errno`, `rustix`, `rustls-platform-verifier`, `tempfile` et `winapi-util`, chaque bloc `[[package]]` de ce nom doit porter au moins une arête `windows-sys`, et chacune doit nommer `0.61.2`. L'absence de bloc pour l'un de ces noms fait échouer l'étape (crate renommé ou sorti du graphe). Un suffixe de source sur une arête (` (registry+…)`) est toléré. 1. **Chaque arête des sept crates.** Pour `colored`, `dirs-sys`, `errno`, `rustix`, `rustls-platform-verifier`, `tempfile` et `winapi-util`, chaque bloc `[[package]]` de ce nom doit porter au moins une arête `windows-sys`, et chacune doit nommer `0.61.2`. L'absence de bloc pour l'un de ces noms fait échouer l'étape (crate renommé ou sorti du graphe). Un suffixe de source sur une arête (` (registry+…)`) est toléré.
2. **Arête nue.** Cargo n'écrit `"windows-sys 0.61.2"` que tant que plusieurs versions coexistent ; quand il n'en reste qu'une, il écrit `"windows-sys"`. Une arête nue ne passe que si le lock ne contient qu'un seul paquet `windows-sys`, en `0.61.2`. 2. **Arête nue.** Cargo n'écrit `"windows-sys 0.61.2"` que tant que plusieurs versions coexistent ; quand il n'en reste qu'une, il écrit `"windows-sys"`. Une arête nue ne passe que si le lock ne contient qu'un seul paquet `windows-sys`, en `0.61.2`.
3. **Ensemble des versions.** Les versions de `windows-sys` du lock, toutes cibles confondues et doublons compris, doivent égaler exactement `{0.45.0, 0.48.0, 0.52.0, 0.59.0, 0.60.2, 0.61.2}` (état du 2026-10-10). Une version qui apparaît ou disparaît fait échouer l'étape avec un renvoi à cet ADR : c'est soit le coût réel (une version de plus), soit le début de la sortie, et dans les deux cas une décision. 3. **Ensemble des versions.** Les versions de `windows-sys` du lock, toutes cibles confondues et doublons compris, doivent égaler exactement `{0.45.0, 0.48.0, 0.52.0, 0.59.0, 0.60.2, 0.61.2}` (état du 2026-10-10). Une version qui apparaît ou disparaît fait échouer l'étape avec un renvoi à cet ADR : c'est soit le coût réel (une version de plus), soit le début de la sortie, et dans les deux cas une décision.
4. **Canari.** La boucle doit avoir lu et vérifié une arête de `tempfile`, crate connu pour en porter une, faute de quoi son silence ne prouve rien et l'étape échoue. 4. **Canari.** La boucle doit avoir lu et vérifié une arête de `tempfile`, crate connu pour en porter une, faute de quoi son silence ne prouve rien et l'étape échoue.
Comme dans l'ADR 0018, chaque vérification est loggée, succès compris (`ok: rustix 1.1.3 -> windows-sys 0.61.2`), puis un bilan (`windows-sys pinning: 9 checks, exit 0`), et le code de sortie d'`awk` est testé à part de sa sortie : une panne de lecture fait échouer l'étape au lieu de passer pour un lock propre. Le garde n'appelle pas cargo, donc ni l'index chaud ni `--locked` ne le concernent ; il lit le lock tel que `cargo check` l'a laissé. Comme dans l'ADR 0018, chaque vérification est loggée, succès compris (`ok: rustix 1.1.3 -> windows-sys 0.61.2`), puis un bilan (`windows-sys pinning: 9 checks, exit 0`), et le code de sortie d'`awk` est testé à part de sa sortie. Une sortie vide échoue déjà (ensemble des versions, blocs introuvables, canari) ; ce test attrape seul un `awk` qui échoue après avoir tout imprimé, et sans lui le garde sortirait en 0 (éprouvé à la revue de la PR #415). Le garde n'appelle pas cargo, donc ni l'index chaud ni `--locked` ne le concernent ; il lit le lock tel que `cargo check` l'a laissé.
La liste gardée, la version épinglée et l'ensemble attendu sont en tête du script, sous un commentaire qui renvoie ici. On ne les modifie que sur la preuve que demandent les sections suivantes, jamais pour faire passer l'étape au vert. La liste gardée, la version épinglée et l'ensemble attendu sont en tête du script, sous un commentaire qui renvoie ici. On ne les modifie que sur la preuve que demandent les sections suivantes, jamais pour faire passer l'étape au vert.
@ -97,7 +97,7 @@ Le 2026-10-10, le texte du `run:` a été extrait du workflow et lancé tel quel
La ligne `FAIL:` dit quelle règle a cédé. La ligne `FAIL:` dit quelle règle a cédé.
- **Une arête a quitté `0.61.2`** (règle 1), en général après un `cargo update` (`-p hyper`, `-p h2` et `-p rustls-webpki -p tar` l'ont fait). Garder la mise à jour voulue et remettre les arêtes, de l'une de deux façons qui mènent au même lock : - **Une arête a quitté `0.61.2`** (règle 1), en général après un `cargo update` (`-p hyper`, `-p h2` et `-p rustls-webpki -p tar` l'ont fait). Garder la mise à jour voulue et remettre les arêtes, de l'une de deux façons qui mènent au même lock :
- n'appliquer à la main que la ou les strophes du paquet visé (version, checksum, dépendances), comme #344 l'a fait pour `h2` ; - n'appliquer à la main que la ou les strophes du paquet visé (version, checksum, dépendances), comme la PR #344 l'a fait pour `h2` ;
- ou garder la sortie de `cargo update` et ramener à la main les arêtes signalées à `"windows-sys 0.61.2"`, les seules lignes à toucher. - ou garder la sortie de `cargo update` et ramener à la main les arêtes signalées à `"windows-sys 0.61.2"`, les seules lignes à toucher.
Puis vérifier que cargo accepte le lock tel quel, sans le réécrire : Puis vérifier que cargo accepte le lock tel quel, sans le réécrire :
@ -109,6 +109,8 @@ La ligne `FAIL:` dit quelle règle a cédé.
Éprouvé le 2026-10-10 sur la copie dérivée par `cargo update -p hyper` : les sept arêtes remises, le diff contre `main` se réduit aux strophes `hyper` et `want`, `cargo metadata --locked` l'accepte sans modifier le fichier, et le garde repasse. Éprouvé le 2026-10-10 sur la copie dérivée par `cargo update -p hyper` : les sept arêtes remises, le diff contre `main` se réduit aux strophes `hyper` et `want`, `cargo metadata --locked` l'accepte sans modifier le fichier, et le garde repasse.
- **L'ensemble des versions a changé** (règle 3). Pour une version ajoutée, chercher qui la tire et si elle est compilée pour Windows (`cargo tree --manifest-path src-tauri/Cargo.toml -i windows-sys@<version> --target x86_64-pc-windows-msvc`) ; si c'est le début du passage à 0.62, la condition de sortie est atteinte (section suivante). Une version retirée est un pas vers la convergence. Dans les deux cas, `EXPECTED` change dans la PR qui change le lock, preuve à l'appui dans la PR. - **L'ensemble des versions a changé** (règle 3). Pour une version ajoutée, chercher qui la tire et si elle est compilée pour Windows (`cargo tree --manifest-path src-tauri/Cargo.toml -i windows-sys@<version> --target x86_64-pc-windows-msvc`) ; si c'est le début du passage à 0.62, la condition de sortie est atteinte (section suivante). Une version retirée est un pas vers la convergence. Dans les deux cas, `EXPECTED` change dans la PR qui change le lock, preuve à l'appui dans la PR.
- **Bloc introuvable** : le crate a été renommé ou est sorti du graphe. Retirer son nom de `CRATES` après avoir revérifié la liste des bornes larges (commande de la section Conséquences). - **Bloc introuvable** : le crate a été renommé ou est sorti du graphe. Retirer son nom de `CRATES` après avoir revérifié la liste des bornes larges (commande de la section Conséquences).
- **Plus d'arête `windows-sys` dans un bloc gardé** (`has no windows-sys edge left`) : le crate a cessé de dépendre de `windows-sys` dans cette version. Même traitement qu'un bloc introuvable ; si c'est `tempfile`, choisir aussi un autre canari parmi les crates restants.
- **Un second bloc d'un crate gardé dont la borne exclut `0.61.2`** (par exemple un `tempfile` plus ancien, d'avant les bornes larges, tiré par une autre dépendance) : la règle 1 échoue, et la réparation de la première puce est impossible, puisque `cargo metadata --locked` refuse une arête hors borne (code 101, éprouvé à la revue de la PR #415). Décider dans la PR qui change le lock : admettre ce bloc par nom et version dans le script, ou écarter la dépendance qui le tire ; dans les deux cas, amender cet ADR.
- **Canari ou `awk` en échec** : le garde lui-même est cassé. Le réparer ; ne jamais changer le canari ou la liste pour le faire taire. - **Canari ou `awk` en échec** : le garde lui-même est cassé. Le réparer ; ne jamais changer le canari ou la liste pour le faire taire.
## Condition de sortie ## Condition de sortie
@ -136,7 +138,7 @@ L'épinglage n'est pas fait pour durer indéfiniment. Il change ou disparaît da
### Négatives ### Négatives
- **Friction** : chaque mise à jour qui déplace ces arêtes demande une édition à la main du lock. - **Friction** : chaque mise à jour qui déplace ces arêtes demande une édition à la main du lock.
- **La liste des sept est tenue à la main.** Un crate à borne large qui entrerait dans le graphe ne serait pas gardé, et s'il pointait une version déjà présente, la règle 3 ne le verrait pas non plus. La commande suivante liste les exigences sur `windows-sys` qui ne sont pas un `^0.x` ; le 2026-10-10, elle sort exactement les sept crates de `CRATES` : - **La liste des sept est tenue à la main.** Un crate à borne large qui entrerait dans le graphe ne serait pas gardé, et s'il pointait une version déjà présente, la règle 3 ne le verrait pas non plus. À l'inverse, retirer de `CRATES` un autre nom que le canari passe, avec une vérification de moins dans le bilan (9 → 8) : seule la relecture du diff, sous le commentaire qui renvoie ici, l'attrape. La commande suivante liste les exigences sur `windows-sys` qui ne sont pas un `^0.x` ; le 2026-10-10, elle sort exactement les sept crates de `CRATES` :
``` ```
cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked \ cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked \
@ -145,7 +147,7 @@ L'épinglage n'est pas fait pour durer indéfiniment. Il change ou disparaît da
``` ```
- La règle 3 compte toutes les cibles : 0.45.0, 0.48.0 et 0.52.0 ne sont pas compilées pour `x86_64-pc-windows-msvc` aujourd'hui, et un changement qui ne touche qu'elles la fera rougir quand même. Assumé : le lock ne dit pas ce qui est compilé pour quelle cible, et une fausse alerte se lève en une vérification `cargo tree`. - La règle 3 compte toutes les cibles : 0.45.0, 0.48.0 et 0.52.0 ne sont pas compilées pour `x86_64-pc-windows-msvc` aujourd'hui, et un changement qui ne touche qu'elles la fera rougir quand même. Assumé : le lock ne dit pas ce qui est compilé pour quelle cible, et une fausse alerte se lève en une vérification `cargo tree`.
- Le garde ne tourne que sur les PR qui touchent `src-tauri/**`, `.cargo/**` ou le workflow, et le lundi sur `main` : exactement là où le lock change, mais pas en continu. - Le garde tourne sur les PR qui touchent `src-tauri/**`, `.cargo/**` ou le workflow, et le lundi sur `main`, pas en continu. Une exception : le commit de `/release` modifie `Cargo.lock` directement sur `main`, sans PR, et le garde ne le verrait qu'au lundi suivant, après le build du tag. Le skill `/release` le rejoue donc en local juste après avoir édité le lock, avant de tagger (étape 2).
### Neutre ### Neutre
@ -156,4 +158,4 @@ L'épinglage n'est pas fait pour durer indéfiniment. Il change ou disparaît da
- [`.forgejo/workflows/check-rust.yml`](../../.forgejo/workflows/check-rust.yml) — étape « Verify windows-sys pinning » - [`.forgejo/workflows/check-rust.yml`](../../.forgejo/workflows/check-rust.yml) — étape « Verify windows-sys pinning »
- [ADR 0018](0018-suppression-advisories-non-atteignables.md) — le garde-fou des suppressions, dont celui-ci reprend le motif - [ADR 0018](0018-suppression-advisories-non-atteignables.md) — le garde-fou des suppressions, dont celui-ci reprend le motif
- Issues #345 (cette décision), #344 (strophe `h2` appliquée à la main), #348 (mesure `rustls`), #310 (première dérive) ; PR #321 (arêtes remontées à `0.61.2`) - Issues #345 (cette décision), #348 (mesure `rustls`), #310 (première dérive) ; PR #344 (strophe `h2` appliquée à la main), PR #321 (arêtes remontées à `0.61.2`)

View file

@ -601,7 +601,7 @@ Aucun `branches:` : une PR stackée sur une autre branche de feature — ce que
Déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown (`paths-ignore`). Lance `npm ci`, `npm run build` (tsc + vite) et `npm test` (vitest). Installe aussi `jq`, dont a besoin `scripts/build-latest-json.test.mjs` : ce test lance le script de génération de `latest.json` de `release.yml` ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). Pas de cache npm : ce workflow ne tourne que sur des PR, donc un cache ne servirait qu'aux pushes suivants d'une même PR, et `npm ci` y prend 9 s. Déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown (`paths-ignore`). Lance `npm ci`, `npm run build` (tsc + vite) et `npm test` (vitest). Installe aussi `jq`, dont a besoin `scripts/build-latest-json.test.mjs` : ce test lance le script de génération de `latest.json` de `release.yml` ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). Pas de cache npm : ce workflow ne tourne que sur des PR, donc un cache ne servirait qu'aux pushes suivants d'une même PR, et `npm ci` y prend 9 s.
**Aucune étape `npm audit`** — contrairement au Rust, les advisories npm ne sont donc pas un gate de CI. `npm audit` est à 0 au 2026-10-10 ([#358](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/358)), après un rafraîchissement du lock dans les bornes de `package.json`. Ce 0 est un constat daté, pas un état garanti : rien ne le réévalue en CI, et une advisory publiée depuis n'apparaît qu'au prochain `npm audit` lancé à la main. `npm audit` n'a pas de mécanisme d'exclusion natif, il n'y a donc pas d'équivalent du `.cargo/audit.toml` côté npm. **Aucune étape `npm audit`** — contrairement au Rust, les advisories npm ne sont donc pas un gate de CI. `npm audit` est à 0 au 2026-10-10 ([#358](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/358)), après un rafraîchissement du lock dans les bornes de `package.json`. Ce 0 est un constat daté, pas un état garanti : aucun job ne le fait échouer, mais l'étape `npm ci` de `check-frontend` audite à chaque run et en imprime le compte (`found 0 vulnerabilities`, ou le nombre trouvé), et le Défenseur relève les advisories de son côté (#358 en vient). `npm audit` n'a pas de mécanisme d'exclusion natif, il n'y a donc pas d'équivalent du `.cargo/audit.toml` côté npm.
### `audit.yml` — Audit RustSec quotidien ### `audit.yml` — Audit RustSec quotidien
@ -621,8 +621,8 @@ Déclenché par les tags `v*`. Un seul job, `build-and-release`, dans un contene
Fonctionnalités : Fonctionnalités :
- Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY) - Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY)
- JSON d'updater (`latest.json`) écrit par `scripts/build-latest-json.sh` à partir des assets ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). `tauri-plugin-updater` cherche `{os}-{arch}-{installer}`, puis `{os}-{arch}` : le fichier porte `linux-x86_64-deb` et `linux-x86_64-rpm` (une clé par paquet Linux), `linux-x86_64` (le `.deb`, en repli : raison, lecteurs connus et condition de retrait dans `docs/qa-update-cycle.md`) et `windows-x86_64` (l'installeur NSIS). Le script échoue s'il ne trouve pas exactement un `.deb`, un `.rpm` et un `-setup.exe`, chacun avec un `.sig` non vide ; le JSON vient de `jq` seul (notes lues comme données par `--rawfile`), avec les noms de fichiers encodés par `@uri`, comme à l'envoi des assets. Testé par `scripts/build-latest-json.test.mjs` (vitest, lancé par `check-frontend`), qui l'exécute sous `bash -e -o pipefail` comme `release.yml`. - JSON d'updater (`latest.json`) écrit par `scripts/build-latest-json.sh` à partir des assets ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). `tauri-plugin-updater` cherche `{os}-{arch}-{installer}`, puis `{os}-{arch}` : le fichier porte `linux-x86_64-deb` et `linux-x86_64-rpm` (une clé par paquet Linux) et `windows-x86_64` (l'installeur NSIS). Pas de clé générique `linux-x86_64` depuis le 2026-10-11 : elle servait le `.deb` à des installations qui ne savaient pas l'installer (anciennes AppImage, binaire hors paquet), qui reçoivent désormais `TargetsNotFound` à la vérification (raisons et lecteurs dans `docs/qa-update-cycle.md`). Le script échoue s'il ne trouve pas exactement un `.deb`, un `.rpm` et un `-setup.exe`, chacun avec un `.sig` non vide ; le JSON vient de `jq` seul (notes lues comme données par `--rawfile`), avec les noms de fichiers encodés par `@uri`, comme à l'envoi des assets. Testé par `scripts/build-latest-json.test.mjs` (vitest, lancé par `check-frontend`), qui l'exécute sous `bash -e -o pipefail` comme `release.yml`.
- JSON publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json` (registre de paquets, endpoint de `tauri.conf.json`). Avant le `DELETE` de l'ancienne version, une assertion `jq` (version égale au tag, exactement les quatre clés, signatures non vides, extension de chaque URL) arrête le job si le fichier ne la passe pas : le registre reste intact. - JSON publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json` (registre de paquets, endpoint de `tauri.conf.json`). Avant le `DELETE` de l'ancienne version, le filtre `scripts/check-latest-json.jq` (version égale au tag, exactement les trois clés, signatures non vides, extension de chaque URL) arrête le job si le fichier ne le passe pas : le registre reste intact. Le test du script applique ce même filtre à chaque PR et compare sa liste de clés à celles du script et de `verify-release.sh`, si bien qu'un écart se voit avant le tag.
- Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md, transmises par `GITHUB_OUTPUT` sous un délimiteur aléatoire (une ligne du CHANGELOG égale à un délimiteur fixe terminait la valeur) - Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md, transmises par `GITHUB_OUTPUT` sous un délimiteur aléatoire (une ligne du CHANGELOG égale à un délimiteur fixe terminait la valeur)
**Node.js 24** ([#376](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/376)), installé comme dans les trois autres workflows par le script NodeSource (`curl … | bash`). Ce script tourne en root dans le conteneur du job, et le Node.js qu'il installe lance ensuite `npx tauri build` avec `TAURI_SIGNING_PRIVATE_KEY` dans son environnement : le job confie à NodeSource (script et dépôt apt) la clé de signature de l'updater, et les jetons de publication des étapes suivantes. Risque accepté par Max le 2026-10-10, alternative écartée : un tarball Node.js épinglé. La chaîne Rust, installée de la même façon par `rustup`, est dans le même cas. Ce workflow ne tourne qu'au tag : la première release après #376 est la première sous Node 24, à vérifier par `node --version` dans le journal du run (étape 9 de `/release`). **Node.js 24** ([#376](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/376)), installé comme dans les trois autres workflows par le script NodeSource (`curl … | bash`). Ce script tourne en root dans le conteneur du job, et le Node.js qu'il installe lance ensuite `npx tauri build` avec `TAURI_SIGNING_PRIVATE_KEY` dans son environnement : le job confie à NodeSource (script et dépôt apt) la clé de signature de l'updater, et les jetons de publication des étapes suivantes. Risque accepté par Max le 2026-10-10, alternative écartée : un tarball Node.js épinglé. La chaîne Rust, installée de la même façon par `rustup`, est dans le même cas. Ce workflow ne tourne qu'au tag : la première release après #376 est la première sous Node 24, à vérifier par `node --version` dans le journal du run (étape 9 de `/release`).

View file

@ -88,17 +88,14 @@ Les installations `.rpm` existantes n'ont rien à réinstaller : le binaire du `
|---|---|---| |---|---|---|
| `linux-x86_64-deb` | `.deb` | les installations `.deb` | | `linux-x86_64-deb` | `.deb` | les installations `.deb` |
| `linux-x86_64-rpm` | `.rpm` | les installations `.rpm` (#320) | | `linux-x86_64-rpm` | `.rpm` | les installations `.rpm` (#320) |
| `linux-x86_64` | `.deb` | un binaire Linux sans marqueur de type de paquet, et les AppImage v0.4.2 à v0.4.5 (repli, voir ci-dessous) |
| `windows-x86_64` | `-setup.exe` | les installations NSIS, qui cherchent d'abord `windows-x86_64-nsis` | | `windows-x86_64` | `-setup.exe` | les installations NSIS, qui cherchent d'abord `windows-x86_64-nsis` |
**Repli `linux-x86_64`.** Raison : un binaire sans marqueur de type de paquet ne cherche que cette clé ; sans clé à son nom, sa vérification échoue (`TargetsNotFound`) au lieu de répondre. Condition de retrait : quand aucune version supportée n'est antérieure au marqueur. Relevé le 2026-10-10 : toutes les versions qui embarquent l'updater (v0.2.1 et suivantes) ont été empaquetées par `@tauri-apps/cli` 2.10.0, d'après le `package-lock.json` de chaque tag, comme la v0.17.1 dont le `.rpm` porte le marqueur. **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 lecteurs connus de cette clé ne savent pas installer le `.deb` qu'elle sert : - 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`.
- un binaire sans marqueur, lancé hors de son paquet ; 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.
- les AppImage des v0.4.2 à v0.4.5, publiées sur Forgejo pour les v0.4.2 et v0.4.3 et sur le miroir GitHub pour les v0.4.3 à v0.4.5. Leur updater lit déjà le registre, et elles cherchent `linux-x86_64-appimage`, absente, puis cette clé.
Pour les deux, l'updater traite le téléchargement comme une AppImage (`install_appimage`) et écrit les octets du `.deb` à la place du fichier lancé, ce qui casse l'installation. C'est l'état depuis la v0.4.2, que #320 ne change pas.
--- ---

View file

@ -26,16 +26,17 @@
# linux-x86_64-deb the .deb, for .deb installs. # linux-x86_64-deb the .deb, for .deb installs.
# linux-x86_64-rpm the .rpm, for .rpm installs. Without this key they fell # linux-x86_64-rpm the .rpm, for .rpm installs. Without this key they fell
# back on linux-x86_64, downloaded the .deb and refused it. # back on linux-x86_64, downloaded the .deb and refused it.
# linux-x86_64 the .deb again: the fallback of a binary without a
# bundle-type marker, which looks up this key alone.
# Remove it once no supported version predates the
# marker. AppImage installs (v0.4.2 to v0.4.5) read it
# too, and neither they nor a marker-less binary can
# install a .deb: see docs/qa-update-cycle.md, "Les
# clés de latest.json".
# windows-x86_64 the NSIS installer. An NSIS install looks up # windows-x86_64 the NSIS installer. An NSIS install looks up
# windows-x86_64-nsis first, then this key. # windows-x86_64-nsis first, then this key.
# #
# No generic linux-x86_64 key since 2026-10-11 (Max's decision on #320). Every
# version that ships the updater (v0.2.1 and later, tauri-plugin-updater
# 2.10.0) carries its bundle-type marker, so .deb and .rpm installs never
# reach that key. Its only readers were the AppImage installs v0.4.3 to
# v0.4.5 and binaries run outside a package, and the .deb it served overwrote
# them: they now get TargetsNotFound when checking, nothing installed. See
# docs/qa-update-cycle.md, "Les clés de latest.json".
#
# Each URL ends with the file name percent-encoded by jq's @uri, as the upload # Each URL ends with the file name percent-encoded by jq's @uri, as the upload
# step of release.yml encodes it: the bundle names contain a space. # step of release.yml encodes it: the bundle names contain a space.
@ -108,7 +109,6 @@ jq -n \
notes: $notes, notes: $notes,
pub_date: $pub_date, pub_date: $pub_date,
platforms: { platforms: {
"linux-x86_64": asset($deb; $deb_sig),
"linux-x86_64-deb": asset($deb; $deb_sig), "linux-x86_64-deb": asset($deb; $deb_sig),
"linux-x86_64-rpm": asset($rpm; $rpm_sig), "linux-x86_64-rpm": asset($rpm; $rpm_sig),
"windows-x86_64": asset($exe; $exe_sig) "windows-x86_64": asset($exe; $exe_sig)

View file

@ -5,7 +5,9 @@
* app reads to find its update. tauri-plugin-updater looks up * app reads to find its update. tauri-plugin-updater looks up
* "{os}-{arch}-{installer}" first, then "{os}-{arch}". Before #320 the * "{os}-{arch}-{installer}" first, then "{os}-{arch}". Before #320 the
* manifest only had linux-x86_64 (the .deb), so an .rpm install downloaded * manifest only had linux-x86_64 (the .deb), so an .rpm install downloaded
* the .deb and refused it. * the .deb and refused it. Since 2026-10-11 it has no linux-x86_64 key at
* all: its last readers (old AppImage installs, binaries run outside a
* package) could not install the .deb it served.
* *
* Each test runs the script as release.yml does, under bash -e -o pipefail * Each test runs the script as release.yml does, under bash -e -o pipefail
* (the shell of the runner's steps), on a temporary assets directory. The * (the shell of the runner's steps), on a temporary assets directory. The
@ -13,6 +15,11 @@
* URLs must percent-encode. Expected values are literals, never rebuilt from * URLs must percent-encode. Expected values are literals, never rebuilt from
* the inputs. * the inputs.
* *
* It also runs scripts/check-latest-json.jq, the filter release.yml applies
* before deleting the published manifest, so that check is exercised on every
* pull request instead of only at the tag, and compares the key lists of the
* script, of that filter and of verify-release.sh.
*
* Plain .mjs on purpose: tsconfig only covers src/, so a .ts file here would * Plain .mjs on purpose: tsconfig only covers src/, so a .ts file here would
* look type-checked without being so. Needs bash and jq on PATH; * look type-checked without being so. Needs bash and jq on PATH;
* check-frontend.yml installs jq for this file. * check-frontend.yml installs jq for this file.
@ -20,12 +27,15 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest"; import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "build-latest-json.sh"); const HERE = dirname(fileURLToPath(import.meta.url));
const SCRIPT = join(HERE, "build-latest-json.sh");
const CHECK = join(HERE, "check-latest-json.jq");
const VERIFY_RELEASE = join(HERE, "..", ".claude", "skills", "release", "scripts", "verify-release.sh");
const BASE = "https://git.example.test/owner/Repo/releases/download/v0.17.1"; const BASE = "https://git.example.test/owner/Repo/releases/download/v0.17.1";
const DEB = "Simpl Resultat_0.17.1_amd64.deb"; const DEB = "Simpl Resultat_0.17.1_amd64.deb";
@ -62,6 +72,22 @@ function expectFailure(result, message) {
expect(result.stderr).toContain(message); expect(result.stderr).toContain(message);
} }
// The manifest the script writes for the three bundles of v0.17.1.
function builtManifest() {
writeAllAssets();
const result = run();
expect(result.status).toBe(0);
return JSON.parse(result.stdout);
}
// Runs scripts/check-latest-json.jq as release.yml does, on a manifest written
// to a file: an object is serialized, a string is written as is.
function check(manifest, version = "0.17.1") {
const file = join(work, "latest.json");
writeFileSync(file, typeof manifest === "string" ? manifest : JSON.stringify(manifest));
return spawnSync("jq", ["-e", "--arg", "version", version, "-f", CHECK, file], { encoding: "utf8" });
}
beforeAll(() => { beforeAll(() => {
const jq = spawnSync("jq", ["--version"], { encoding: "utf8" }); const jq = spawnSync("jq", ["--version"], { encoding: "utf8" });
if (jq.status !== 0) { if (jq.status !== 0) {
@ -82,7 +108,7 @@ afterEach(() => {
}); });
describe("build-latest-json.sh", () => { describe("build-latest-json.sh", () => {
it("writes the four keys, each with its own URL and its own asset's signature", () => { it("writes the three keys, each with its own URL and its own asset's signature", () => {
writeAllAssets(); writeAllAssets();
const result = run(); const result = run();
@ -92,11 +118,9 @@ describe("build-latest-json.sh", () => {
expect(manifest.version).toBe("0.17.1"); expect(manifest.version).toBe("0.17.1");
expect(manifest.notes).toBe("### Fixed\n\n- Something (#320)."); expect(manifest.notes).toBe("### Fixed\n\n- Something (#320).");
expect(manifest.pub_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/); expect(manifest.pub_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
// No generic linux-x86_64: an install without a key of its own must fail
// its check (TargetsNotFound) rather than receive the .deb.
expect(manifest.platforms).toStrictEqual({ expect(manifest.platforms).toStrictEqual({
"linux-x86_64": {
signature: "RWRkZWItc2lnbmF0dXJl",
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
},
"linux-x86_64-deb": { "linux-x86_64-deb": {
signature: "RWRkZWItc2lnbmF0dXJl", signature: "RWRkZWItc2lnbmF0dXJl",
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb", url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
@ -185,7 +209,7 @@ describe("build-latest-json.sh", () => {
'"version": "9.9.9", "platforms": {}\n', '"version": "9.9.9", "platforms": {}\n',
); );
expect(manifest.version).toBe("0.17.1"); expect(manifest.version).toBe("0.17.1");
expect(Object.keys(manifest.platforms)).toHaveLength(4); expect(Object.keys(manifest.platforms)).toHaveLength(3);
}); });
it("fails without its four arguments", () => { it("fails without its four arguments", () => {
@ -193,4 +217,86 @@ describe("build-latest-json.sh", () => {
expectFailure(run([assets, "0.17.1", notesFile]), "usage: build-latest-json.sh"); expectFailure(run([assets, "0.17.1", notesFile]), "usage: build-latest-json.sh");
}); });
// The other tests pass 0.17.1, the version the bundle names also carry: a
// script that ignored its argument would pass them all.
it("takes the version from its argument, not from the bundle names", () => {
writeAllAssets();
const result = run([assets, "0.18.0", notesFile, BASE]);
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout).version).toBe("0.18.0");
});
it("fails on an empty version", () => {
writeAllAssets();
expectFailure(run([assets, "", notesFile, BASE]), "empty version");
});
});
describe("check-latest-json.jq", () => {
it("passes the script's output for the tag's version", () => {
const result = check(builtManifest());
expect(result.status).toBe(0);
expect(result.stdout).toBe("true\n");
});
it("refuses a manifest whose version is not the tag's", () => {
expect(check(builtManifest(), "0.18.0").status).not.toBe(0);
});
it.each([
["a missing key", (m) => delete m.platforms["linux-x86_64-rpm"]],
["an extra key", (m) => (m.platforms["linux-x86_64-appimage"] = m.platforms["linux-x86_64-deb"])],
["the generic linux-x86_64 key, removed on 2026-10-11", (m) => (m.platforms["linux-x86_64"] = m.platforms["linux-x86_64-deb"])],
["the .rpm key on the .deb", (m) => (m.platforms["linux-x86_64-rpm"] = m.platforms["linux-x86_64-deb"])],
["the .deb key on the .rpm", (m) => (m.platforms["linux-x86_64-deb"] = m.platforms["linux-x86_64-rpm"])],
["the Windows key on a .sig", (m) => (m.platforms["windows-x86_64"].url += ".sig")],
["an empty signature", (m) => (m.platforms["linux-x86_64-deb"].signature = "")],
["a signature that is not a string", (m) => (m.platforms["windows-x86_64"].signature = 42)],
["a key without a URL", (m) => delete m.platforms["linux-x86_64-rpm"].url],
["a manifest without platforms", (m) => delete m.platforms],
])("refuses %s", (_, mutate) => {
const manifest = builtManifest();
mutate(manifest);
expect(check(manifest).status).not.toBe(0);
});
it("refuses the shape published up to v0.17.1, without a key per Linux package", () => {
const published = {
version: "0.17.1",
notes: "### Fixed",
pub_date: "2026-10-09T01:44:40Z",
platforms: {
"linux-x86_64": { signature: "RWRkZWItc2lnbmF0dXJl", url: `${BASE}/Simpl%20Resultat_0.17.1_amd64.deb` },
"windows-x86_64": { signature: "RWRleGUtc2lnbmF0dXJl", url: `${BASE}/Simpl%20Resultat_0.17.1_x64-setup.exe` },
},
};
expect(check(published).status).not.toBe(0);
});
it("refuses a file that is not JSON", () => {
expect(check('{"version": "0.17.1", "platforms": ').status).not.toBe(0);
});
});
describe("key lists", () => {
it("are the same in the script, in check-latest-json.jq and in verify-release.sh", () => {
const manifest = builtManifest();
const written = Object.keys(manifest.platforms).sort();
// The filter compares the exact key set: passing the script's output
// proves its list is the script's.
expect(check(manifest).status).toBe(0);
// verify-release.sh keeps its own list, in a Python dict literal.
const dict = readFileSync(VERIFY_RELEASE, "utf8").match(/^EXPECTED = \{([^}]*)\}/m);
expect(dict).not.toBeNull();
const expected = [...dict[1].matchAll(/"([a-z0-9_-]+)":/g)].map((m) => m[1]).sort();
expect(expected).toStrictEqual(written);
});
}); });

View file

@ -0,0 +1,26 @@
# Check of latest.json, the updater manifest, before release.yml deletes the
# published one from the package registry (#320). Once that version is
# deleted, installed apps find no update until a valid one is uploaded, so a
# manifest that fails this check stops the job and leaves the registry as is.
#
# Run as: jq -e --arg version <X.Y.Z> -f scripts/check-latest-json.jq latest.json
# -e turns false, null and any jq error (missing field, wrong type, invalid
# JSON) into a non-zero exit status: the check fails closed.
#
# The key set must stay the one scripts/build-latest-json.sh writes and
# .claude/skills/release/scripts/verify-release.sh expects.
# scripts/build-latest-json.test.mjs runs this file on the script's output and
# on broken manifests, and compares the three lists, on every pull request
# through check-frontend: a misaligned list shows before the tag, not at it.
# Written for jq 1.6, the version ubuntu:22.04 ships.
#
# Not checked here: that each signature belongs to its own file. The script's
# test checks it before the merge, and verify-release.sh after publication
# (the file: field of the minisign trusted comment).
.version == $version
and (.platforms | keys) == ["linux-x86_64-deb", "linux-x86_64-rpm", "windows-x86_64"]
and all(.platforms[]; (.signature | type) == "string" and (.signature | length) > 0)
and (.platforms["linux-x86_64-deb"].url | endswith(".deb"))
and (.platforms["linux-x86_64-rpm"].url | endswith(".rpm"))
and (.platforms["windows-x86_64"].url | endswith(".exe"))

View file

@ -719,7 +719,8 @@ describe("real-vs-real compare — transfer netting (Issue #243)", () => {
// income/transfer credits are not dropped before they can count or cancel. // income/transfer credits are not dropped before they can count or cancel.
// //
// There is no runnable SQLite in these unit tests (tauri-plugin-sql only // There is no runnable SQLite in these unit tests (tauri-plugin-sql only
// exists inside the Tauri WebView, and CI runs Node 20 → no node:sqlite), so // exists inside the Tauri WebView, and no harness opens node:sqlite yet:
// CI runs Node 24 since #376, which has it, and #392 plans that harness), so
// the netting is pinned two ways: (1) a reference-semantics check over // the netting is pinned two ways: (1) a reference-semantics check over
// concrete debit/credit rows that reproduces the exact per-row rule the SQL // concrete debit/credit rows that reproduces the exact per-row rule the SQL
// encodes, and (2) structural assertions that the real query on BOTH compare // encodes, and (2) structural assertions that the real query on BOTH compare