diff --git a/.forgejo/workflows/check-rust.yml b/.forgejo/workflows/check-rust.yml index ecb5240..219131e 100644 --- a/.forgejo/workflows/check-rust.yml +++ b/.forgejo/workflows/check-rust.yml @@ -190,6 +190,136 @@ jobs: echo "Suppression guard: $checks checks, exit $rc" exit $rc + # Pin guard for windows-sys (#345). Seven crates in the lockfile accept + # windows-sys through a wide bound, so cargo may point each of them at + # any version the graph already holds. All seven point at 0.61.2, the + # state every Windows release has shipped with since v0.14.0. An update + # such as `cargo update -p hyper` re-resolves the seven edges down to + # 0.48/0.52/0.59, and cargo reports only the packages it bumped, never + # the edges: the Windows build then compiles five windows-sys versions + # instead of three, and nothing says so. Which updates do it cannot be + # told from their names (#348), so the step guards the resolved version. + # docs/adr/0023-windows-sys-pinning.md holds the measurements, the exit + # condition and what to do when this step fails. + # + # awk reads Cargo.lock, since the ubuntu:22.04 container has no Python. + # Its exit code is tested apart from its output, so a read failure fails + # the step instead of passing for a clean, empty result. 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 + # step reads the lockfile as cargo check left it, which is the committed + # one unless cargo had to update it. + - name: Verify windows-sys pinning + run: | + set -u + # The guarded list. Why 0.61.2, why these versions, and when either + # may change: docs/adr/0023-windows-sys-pinning.md. Edit CRATES, PIN + # or EXPECTED only on the proof that ADR asks for, never to turn this + # step green. + CRATES="colored dirs-sys errno rustix rustls-platform-verifier tempfile winapi-util" + PIN="0.61.2" + # Every windows-sys version in the lockfile, all targets included, + # as of 2026-10-10. + EXPECTED="0.45.0 0.48.0 0.52.0 0.59.0 0.60.2 0.61.2" + CANARY="tempfile" + LOCK="src-tauri/Cargo.lock" + ADR="docs/adr/0023-windows-sys-pinning.md" + rc=0 + checks=0 + canary_checked=0 + # One fact per line: "wsys " for each windows-sys package, + # and "block [...]" for each [[package]] + # block of a crate in CRATES, with one per windows-sys + # edge: the version the edge names, or "-" for a bare edge (cargo + # drops the version when the lockfile holds a single windows-sys). + # A " (registry+...)" source suffix on an edge is dropped. + facts=$(awk -v crates="$CRATES" ' + function flush( i, line) { + if (inpkg && name == "windows-sys") print "wsys", version + if (inpkg && (name in wanted)) { + line = "block " name " " version + for (i = 1; i <= n; i++) line = line " " target[i] + print line + } + inpkg = 0; name = ""; version = ""; n = 0 + } + BEGIN { k = split(crates, list, " "); for (i = 1; i <= k; i++) wanted[list[i]] = 1 } + /^\[\[package\]\]$/ { flush(); inpkg = 1; next } + /^\[/ { flush(); next } + inpkg && /^name = "/ { name = $3; gsub(/"/, "", name) } + inpkg && /^version = "/ { version = $3; gsub(/"/, "", version) } + inpkg && /^[ \t]*"windows-sys[ "]/ { + s = $0; sub(/^[ \t]*"/, "", s); sub(/".*$/, "", s) + m = split(s, f, " "); target[++n] = (m >= 2 ? f[2] : "-") + } + END { flush() } + ' "$LOCK") && awk_rc=0 || awk_rc=$? + if [ "$awk_rc" -ne 0 ]; then + echo "FAIL: awk exited $awk_rc reading $LOCK, so nothing was checked." + echo "windows-sys pinning: $checks checks, exit 1" + exit 1 + fi + # The exact set of windows-sys versions, duplicates kept: a version + # that appears or disappears is a decision, not a side effect. + found=$(printf '%s\n' "$facts" | awk '$1 == "wsys" { print $2 }' | sort | paste -sd ' ' -) + want=$(printf '%s\n' $EXPECTED | sort | paste -sd ' ' -) + checks=$((checks + 1)) + if [ "$found" = "$want" ]; then + echo "ok: windows-sys versions in the lockfile: $found" + else + echo "FAIL: windows-sys versions in the lockfile are \"$found\", expected exactly \"$want\". A version was added or removed: follow $ADR before editing EXPECTED." + rc=1 + fi + # Every edge of every block of each guarded crate must name PIN. A + # bare edge passes only when the lockfile's one windows-sys is PIN. + for crate in $CRATES; do + blocks=$(printf '%s\n' "$facts" | awk -v c="$crate" '$1 == "block" && $2 == c') + if [ -z "$blocks" ]; then + checks=$((checks + 1)) + echo "FAIL: no [[package]] block named $crate in $LOCK (renamed, or gone from the graph?). See $ADR." + rc=1 + continue + fi + while read -r kind name version targets; do + if [ -z "$targets" ]; then + checks=$((checks + 1)) + echo "FAIL: $crate $version has no windows-sys edge left. See $ADR." + rc=1 + continue + fi + for target in $targets; do + checks=$((checks + 1)) + if [ "$target" = "$PIN" ]; then + echo "ok: $crate $version -> windows-sys $PIN" + elif [ "$target" != "-" ]; then + echo "FAIL: $crate $version -> windows-sys $target, pinned at $PIN. See $ADR." + rc=1 + elif [ "$found" = "$PIN" ]; then + echo "ok: $crate $version -> windows-sys (bare edge: the lockfile's only windows-sys is $PIN)" + else + echo "FAIL: $crate $version -> windows-sys with no version, while the lockfile holds \"$found\". See $ADR." + rc=1 + fi + if [ "$crate" = "$CANARY" ]; then + canary_checked=1 + fi + done + done <<<"$blocks" + done + # Canary: the loop must have read and checked an edge of a crate + # known to carry one. Otherwise its silence proves nothing. + checks=$((checks + 1)) + if [ "$canary_checked" -eq 1 ]; then + echo "ok: canary '$CANARY' found, its windows-sys edge read and checked above" + else + echo "FAIL: canary '$CANARY' was not read and checked. The guard is not proving anything." + rc=1 + fi + echo "windows-sys pinning: $checks checks, exit $rc" + exit $rc + - name: cargo test run: cargo test --manifest-path src-tauri/Cargo.toml --all-targets diff --git a/CLAUDE.md b/CLAUDE.md index 0ba2249..b29a72f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,7 +162,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. -- **`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, `cargo test` et un `cargo audit` informatif, avec le cache `Swatinem/rust-cache`. Doit être vert avant tout merge. +- **`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` remonte 2 high en permanence — une seule advisory `react-router` (mode RSC, inatteignable dans une app de bureau sans serveur), acceptée et suivie en [#317](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/317). - **`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. diff --git a/docs/adr/0023-windows-sys-pinning.md b/docs/adr/0023-windows-sys-pinning.md new file mode 100644 index 0000000..77e1b7e --- /dev/null +++ b/docs/adr/0023-windows-sys-pinning.md @@ -0,0 +1,159 @@ +# ADR 0023 — Épinglage de `windows-sys` 0.61.2 : garde-fou CI sur la version résolue, condition de sortie écrite + +- Status: **Accepted** +- 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) +- 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 + +Sept crates du fichier de verrouillage déclarent `windows-sys` par une borne qui couvre plusieurs mineures : + +| crate | exigence déclarée sur `windows-sys` | +|---|---| +| `colored` 3.1.1, `winapi-util` 0.1.11 | `>=0.48, <=0.61` | +| `errno` 0.3.14, `rustix` 1.1.3, `tempfile` 3.24.0 | `>=0.52, <0.62` | +| `rustls-platform-verifier` 0.6.2 | `>=0.52.0, <0.62.0` | +| `dirs-sys` 0.5.0 | `>=0.59.0` | + +Ce sont les seuls du graphe dans ce cas (relevé par `cargo metadata` le 2026-10-10). Toutes les autres déclarations sont des `^0.x`, qui n'admettent qu'une mineure, donc une seule des versions présentes : elles ne peuvent pas dériver. Le lock contient six versions de `windows-sys`, toutes cibles confondues (0.45.0, 0.48.0, 0.52.0, 0.59.0, 0.60.2, 0.61.2), et les sept crates peuvent pointer n'importe laquelle de celles que leur borne admet, sans ajouter de paquet au lock et sans violer aucun contrat. + +Les sept pointent **`0.61.2`**. C'est l'état du tag v0.14.0 (2026-07-18), dont la release (run 322) a traversé un vrai build Windows NSIS, et celui de chaque release depuis : v0.15.0 (run 402), v0.16.0 (run 451), v0.17.0 (run 491), v0.17.1 (run 508), vérifié le 2026-10-10 en rejouant le garde ci-dessous sur le lock de chaque tag. L'état dérivé n'a traversé aucun build Windows : aucune release n'a eu lieu pendant la seule fenêtre où `main` l'a porté, de #310 (2026-07-27) à la PR #321 (2026-08-15). + +### La contrainte n'était tenue par rien + +| | | +|---|---| +| #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 | +| #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 | + +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. + +### Le déclencheur, mesuré + +| commande, sur une copie du lock | effet sur les sept arêtes | mesure | +|---|---|---| +| `cargo update -p anyhow` | aucun | 2026-09-04 | +| `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) | + +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. + +**Et cargo n'en dit rien.** Le 2026-10-10, `cargo update -p hyper` annonce : + +``` + Locking 2 packages to latest compatible versions + Updating hyper v1.8.1 -> v1.12.0 + 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. + +### Ce que coûte la dérive + +Pas un risque de correction : les deux états sont dans les bornes déclarées par les sept auteurs. Le coût est de taille et de temps de compilation, et il tombe sur le build Windows, que personne ne mesurait : + +| état | versions de `windows-sys` compilées pour `x86_64-pc-windows-msvc` | +|---|---| +| épinglé (`0.61.2`) | **3** : 0.59.0, 0.60.2, 0.61.2 | +| dérivé | **5** : les mêmes, plus 0.48.0 et 0.52.0 | + +Mesuré par `cargo tree --target x86_64-pc-windows-msvc -e normal,build`, refait le 2026-10-10 sur le lock de `main` et sur sa copie dérivée par `cargo update -p hyper`. Deux crates `windows-sys` complets de plus dans chaque build Windows, ajoutés en effet de bord, dans une configuration qu'aucun build Windows n'a traversée. + +## Décision + +**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 : + +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`. +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. + +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é. + +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. + +**Friction assumée** : chaque mise à jour qui déplace ces arêtes (celles qui atteignent `hyper`, entre autres) fera échouer l'étape et demandera une édition à la main du lock. C'est l'objet : un effet de bord silencieux devient une décision explicite. + +### Éprouvé par mutation + +Le 2026-10-10, le texte du `run:` a été extrait du workflow et lancé tel quel, avec les options du runner (`bash --noprofile --norc -e -o pipefail`), sur des copies du lock : + +| lock | sortie | +|---|---| +| celui de `main`, et ceux des tags v0.14.0 à v0.17.1 | exit 0, 9 vérifications | +| dérivé par `cargo update -p hyper` | exit 1, les sept arêtes en échec | +| un bloc `windows-sys` 0.53.0 ajouté | exit 1, ensemble des versions | +| `colored` renommé | exit 1, bloc introuvable | +| absent (`awk` sort en 2) | exit 1, 0 vérification | +| `tempfile` retiré de la liste gardée, ou liste vide (script modifié) | exit 1, canari | + +## Procédure quand le garde échoue + +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 : + - n'appliquer à la main que la ou les strophes du paquet visé (version, checksum, dépendances), comme #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. + + Puis vérifier que cargo accepte le lock tel quel, sans le réécrire : + + ``` + cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked > /dev/null + ``` + + É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@ --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). +- **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 + +L'épinglage n'est pas fait pour durer indéfiniment. Il change ou disparaît dans deux cas. + +- **Au passage de l'écosystème à `windows-sys` 0.62.** La borne `<=0.61` de `colored` et `winapi-util`, comme le `<0.62` d'`errno`, `rustix`, `tempfile` et `rustls-platform-verifier`, les empêche de suivre ; seul `dirs-sys` (`>=0.59.0`, sans plafond) le peut. Quand un crate du graphe tirera 0.62, la règle 3 rougira et il faudra décider sur preuve : une nouvelle configuration n'est tenue qu'après un vrai build Windows, c'est-à-dire un run `release.yml`. `PIN`, `EXPECTED` et au besoin `CRATES` changent alors dans la même PR que le lock, avec un amendement de cet ADR. Le garde est mis à jour, jamais contourné. +- **À la convergence vers un seul `windows-sys`**, la sortie souhaitée. Plus aucune arête n'a où dériver, et le coût mesuré disparaît. Le garde le signalera (règle 3 en échec, arêtes nues admises par la règle 2) ; l'étape peut alors être retirée, avec un amendement de cet ADR. + +## Alternatives considérées + +- **Check croisé Windows en CI** (`cargo check --target x86_64-pc-windows-msvc` depuis Linux). L'option la plus attirante, puisqu'elle prouverait que le graphe Windows compile quel que soit l'état. Testée à l'analyse de #345, elle échoue : `cargo check` a besoin du toolchain natif de la cible dès qu'un build script compile du C, et `cc-rs` s'arrête sur `failed to find tool "lib.exe"`. Il faudrait installer sur chaque PR Rust le toolchain croisé que `release.yml` n'installe qu'au tag (`cargo-xwin` compilé depuis les sources, `clang`, `lld`), sur un runner à capacité 1 : un chantier, pas une étape bon marché. À rouvrir si le besoin d'une preuve Windows revient pour d'autres raisons. +- **Garde sur la propriété** (« au plus 3 `windows-sys` dans le graphe Windows »). Il encode le dommage mesuré plutôt qu'un numéro, survit au passage à 0.62 et couvrirait un nouveau crate à borne large. Mais il reste indirect à la lecture, tolère un échange à compte constant, et demande `cargo tree` : le lock ne dit pas quelle arête est compilée pour quelle cible. Écarté au profit de la cohérence avec le motif existant. +- **Garde sur une liste de crates déclencheurs** (`hyper`, la couche TLS). La liste est précisément ce que #348 a trouvé faux. +- **Commentaire seul dans `Cargo.toml`.** `Cargo.toml` n'est pas ce qu'on lit avant de lancer `cargo update`, et rien ne l'exécute. + +## Conséquences + +### Positives + +- La dérive ne passe plus en silence : une mise à jour qui déplace une arête rougit `check-rust` sur la PR même, avec le nom de chaque arête déplacée, au lieu d'attendre qu'un relecteur reconnaisse sept noms de crates dans un diff que cargo n'a pas annoncé. +- La contrainte est domiciliée : qui voit l'étape rougir trouve ici pourquoi, et quoi faire. +- Le motif est celui que le dépôt connaît déjà (ADR 0018), pièges compris. + +### Négatives + +- **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` : + + ``` + cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked \ + | python3 -c 'import json, sys; [print(p["name"], p["version"], d["req"]) for p in json.load(sys.stdin)["packages"] for d in p["dependencies"] if d["name"] == "windows-sys"]' \ + | grep -v ' \^0\.' + ``` + +- 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. + +### Neutre + +- Le garde lit le lock, il ne prouve pas que le graphe Windows compile. Cette preuve reste le run `release.yml`. +- Rien ne change pour l'application : #345 lit `Cargo.lock` sans le modifier, et ne déplace aucune dépendance. + +## Liens + +- [`.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 +- 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`) diff --git a/docs/architecture.md b/docs/architecture.md index ca83372..c2edec4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -585,6 +585,8 @@ Déclenché sur les PR qui touchent `src-tauri/**`, `.cargo/**` (ou le workflow Entre `cargo check` et `cargo test`, une étape **bloquante** rejoue la justification des advisories acceptées dans `.cargo/audit.toml` : elle vérifie par `cargo tree --locked` que chaque crate supprimé reste absent des deux cibles livrées, et échoue si l'un d'eux redevient atteignable. Un canari (`tar`, réellement présent) garantit que son silence prouve quelque chose. Voir [ADR 0018](adr/0018-suppression-advisories-non-atteignables.md). +Juste après, une seconde étape **bloquante**, `Verify windows-sys pinning` ([#345](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/345)), lit `src-tauri/Cargo.lock` avec `awk`. Les sept crates qui déclarent `windows-sys` par une borne couvrant plusieurs mineures (`colored`, `dirs-sys`, `errno`, `rustix`, `rustls-platform-verifier`, `tempfile`, `winapi-util`) doivent tous pointer `0.61.2`, l'état de chaque release Windows depuis v0.14.0, et l'ensemble des versions de `windows-sys` du lock ne doit pas changer. Une mise à jour comme `cargo update -p hyper` re-résout ces arêtes sans que cargo l'annonce, et le build Windows compile alors cinq versions de `windows-sys` au lieu de trois. Chaque vérification est loggée avec un bilan, un canari (`tempfile`) doit être lu et vérifié, et une panne d'`awk` fait échouer l'étape. Quand elle rougit, on remet les arêtes à la main : procédure et condition de sortie dans l'[ADR 0023](adr/0023-windows-sys-pinning.md). + Aucun `branches:` : une PR stackée sur une autre branche de feature — ce que produit `/autopilot` — déclenche donc bien la CI. **Cache** ([#234](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/234)) : `Swatinem/rust-cache` garde `~/.cargo` et les dépendances compilées de `src-tauri/target`, sous une clé faite de la version de rustc, des variables `CARGO*` et `RUST*` du job, des dépendances déclarées dans les manifestes et le lockfile Cargo (la version de l'app n'en fait pas partie), et du job. Il passe par le proxy de cache du runner, sur le port 28100 qu'ufw n'ouvre qu'aux réseaux Docker des jobs (procédure et retour arrière dans `docs/forgejo-ops.md` de la-compagnie-maximus). Le runner (v12.7.0) garde ce qu'écrit un run de PR pour cette seule PR : ses pushes suivants le relisent, les autres PR non. Le run du lundi et `workflow_dispatch` sont donc les seuls à écrire le cache partagé, sur lequel une PR se rabat quand rien ne correspond dans sa propre portée. Le runner supprime une entrée inutilisée depuis 7 jours ou créée il y a plus de 30 jours, et ne garde qu'une entrée par clé, toutes portées confondues. Un run à froid est donc normal dans les cas suivants ; seul un délai dépassé signale un retour de #234 : @@ -651,3 +653,4 @@ Les ADRs documentent les décisions techniques structurantes. Ils vivent dans `d | [0020](adr/0020-identifiant-machine-derive.md) | Identifiant machine dérivé : un hachage à clé fixe remplace l'identifiant du système auprès du serveur de licences | 2026-10-01 | Accepted | | [0021](adr/0021-migrations-profils-secondaires.md) | Les profils secondaires rattrapent leurs migrations à l'ouverture | 2026-10-06 | Accepted | | [0022](adr/0022-canal-evenements-donnees.md) | Canal d'événements de données : les écritures signalent, le parcours de démarrage relit | 2026-10-08 | Accepted | +| [0023](adr/0023-windows-sys-pinning.md) | Épinglage de `windows-sys` 0.61.2 : garde-fou CI sur la version résolue, condition de sortie écrite | 2026-10-10 | Accepted |