ci: restore the Rust cache with rust-cache and a weekly run on main (#234) #375

Merged
maximus merged 3 commits from issue-234-rust-cache into main 2026-10-03 22:11:18 +00:00
Owner

Refs #234

Refs et non une fermeture : le cache partagé que lisent les PR ne se vérifie qu'après le merge (liste en fin de PR).

Ce que la PR change

  • check-rust.yml
    • Étape Swatinem/rust-cache@v2 (workspaces: "src-tauri -> target") juste après le checkout, avec ses valeurs par défaut : sauvegarde seulement si le job réussit et si la clé n'a pas été trouvée à l'identique.
    • Run hebdomadaire sur main (lundi 06:30 UTC, après audit.yml) et workflow_dispatch.
    • Node.js 24 au lieu de 20 : rust-cache déclare node24 depuis la v2.9 et embarque @actions/cache 6 (ESM seul), et le runner lance les actions JavaScript avec le node du conteneur.
    • zstd installé : archives du cache plus petites et plus rapides que le repli gzip.
    • Commentaires mis à jour, dont celui qui présentait taiki-e/install-action comme une action JavaScript : elle est composite.
  • check-frontend.yml : pas de cache npm, par décision (voir plus bas) ; commentaire réécrit.
  • audit.yml : deux commentaires que cette PR rendait faux (check-rust tourne aussi chaque semaine ; taiki-e/install-action est composite).
  • CLAUDE.md, docs/architecture.md : section CI/CD (deuxième commit).
  • Pas d'entrée CHANGELOG : configuration CI, aucun changement visible pour l'utilisateur, comme pour #232.

Fait hors de cette PR (VPS, 2026-10-03)

Le corps de #234 proposait de fixer cache.host sur une adresse joignable. Ça n'aurait pas suffi : l'adresse annoncée, l'IP publique du VPS, était joignable ; c'est le port qui était filtré.

  • Cause, deux problèmes cumulés :
    1. ufw refuse l'entrant par défaut et n'ouvre aux conteneurs que la source 172.16.0.0/12, alors que /etc/docker/daemon.json (réglé par Coolify) alloue les réseaux Docker dans 10.0.0.0/8 : les conteneurs de jobs ne correspondaient à aucune règle.
    2. Le runner tournait sans fichier de config : cache.proxy_port valait 0, donc un port tiré au hasard à chaque démarrage.
  • Correctif : config produite par forgejo-runner generate-config avec proxy_port: 28100, fichier complémentaire systemd /etc/systemd/system/forgejo-runner.service.d/config.conf qui passe --config, règle ufw allow proto tcp from 10.0.0.0/8 to any port 28100, redémarrage du runner. Les ports du proxy et du serveur interne restent fermés depuis Internet (vérifié de l'extérieur).
  • Preuve : PR de diagnostic #374, rouge au run 455 (port du cache sans réponse, ports 22 et 443 ouverts), verte au run 457 (Cache saved puis Cache restored). Détail dans ses commentaires.
  • Doc : section « Cache Actions : Request timeout » de docs/forgejo-ops.md dans la-compagnie-maximus (commit 66787ea), retour arrière compris.

Décisions (Max, 2026-10-03)

Le runner v12.7.0 isole ce qu'écrit un run pull_request sous la référence de la PR (getWriteIsolationKey) : seuls les runs suivants de cette PR le relisent, et une PR ne lit le cache partagé que si rien ne correspond dans sa propre portée. Écrivent dans le cache partagé : schedule, workflow_dispatch, push, et la fermeture d'une PR mergée. La spec de juillet disait l'inverse, d'après le code de gitea/act.

  • D1 : rust-cache sur les PR, plus un run hebdomadaire sur main qui, avec workflow_dispatch, est le seul écrivain du cache partagé. Sur 120 jours : 22 runs check-rust pour 11 PR. Sans écrivain partagé, la moitié des runs, le premier de chaque PR, resterait à froid.
  • D2 : pas de cache npm. check-frontend 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 (run 456).

Runs à froid attendus, qui ne signifient pas un retour de #234 (seul un délai dépassé le signalerait) :

  • après chaque nouvelle version stable de Rust ou une modification de l'env: du job, jusqu'au lundi suivant : la clé hache rustc et les variables CARGO*, RUST*, CC*, CFLAGS*, CXX* et CMAKE*. Une montée de version de l'app ne change pas la clé ;
  • quand une PR sauvegarde sous la clé de l'entrée partagée : le runner ne garde qu'une entrée par clé, toutes portées confondues, et l'entrée partagée disparaît pour toutes les PR jusqu'au lundi suivant. Cas typique : une PR qui a monté une dépendance, puis est revenue au lockfile de main. Deux PR ouvertes sur un même lockfile, différent de celui de main, s'évincent l'une l'autre ;
  • quand une entrée qui n'a eu que des correspondances exactes, donc jamais re-sauvegardée, atteint 30 jours ;
  • un lundi après une semaine sans requête au cache, une restauration en 404 : la recherche ne rafraîchit pas l'entrée, et le ménage qu'elle déclenche peut la supprimer (forgejo/runner#1741). Le run repart alors à froid et re-sauvegarde.

Options laissées à Max :

  • un second run le vendredi (30 6 * * 1,5), premier créneau sûr après une sortie de Rust (un jeudi), qui garde aussi chaque écart sous les 7 jours ;
  • un déclenchement sur la fermeture des PR mergées, pour rafraîchir le cache partagé à chaque merge Rust.

Vérification

  • Les trois workflows passent le validateur de schéma du runner v12.7.0 : binaire officiel, somme SHA-256 vérifiée, étalonné en local (il refuse bien un working-directory sur une étape uses). Il ne vérifie pas le bloc on: : il accepte on: 42 ou crontab:, d'après la revue. Les deux déclencheurs ajoutés ne se prouvent donc qu'après le merge, par les points 1 et 3 de la liste.
  • Suites locales vertes, pour la forme puisqu'aucun code source ne change : vitest 1348, Rust 187.
  • CI : le premier run check-rust part à froid et doit sauvegarder ; le run suivant, déclenché par le commit de doc, doit restaurer. Les résultats viendront en commentaire, lus dans les journaux et non dans le statut du job : une sauvegarde ratée laisse le job vert.

Hors de cette PR

  • Le commentaire concurrency de check-rust.yml (« Cancel obsolete runs ») est contredit par forgejo-ops.md pour les runs de PR sur Forgejo 8.0.3. Il précède cette PR, je le laisse.
  • L'isolation des caches de PR mérite une entrée dans forgejo-ops.md (la-compagnie-maximus), proposée à part.

Post-merge checklist (hence Refs, not a closing keyword)

  1. Lancer check-rust sur main par workflow_dispatch. Le journal doit d'abord afficher No cache found., preuve que l'entrée de 999 Mo de cette PR était bien isolée : s'il la restaure, la description de l'isolation est fausse. Il doit ensuite afficher Cache saved successfully, cette fois dans le cache partagé. L'entrée de cette PR disparaît alors au ménage suivant, puisqu'une entrée plus récente porte la même clé.
  2. Premier run de la prochaine PR Rust : il doit restaurer l'entrée partagée.
  3. Premier run planifié, lundi 2026-10-05 à 06:30 UTC : vert (/warmup le surveille).
  4. Taille de /home/ubuntu/.cache/actcache sur le VPS (Max, du -sh).
  5. Synchronisation de STATE.md.
  6. #234 est alors fermée à la main, avec les preuves.

🤖 Generated with Claude Code

Refs #234 `Refs` et non une fermeture : le cache partagé que lisent les PR ne se vérifie qu'après le merge (liste en fin de PR). ## Ce que la PR change - **`check-rust.yml`** - Étape `Swatinem/rust-cache@v2` (`workspaces: "src-tauri -> target"`) juste après le checkout, avec ses valeurs par défaut : sauvegarde seulement si le job réussit et si la clé n'a pas été trouvée à l'identique. - Run hebdomadaire sur `main` (lundi 06:30 UTC, après `audit.yml`) et `workflow_dispatch`. - Node.js 24 au lieu de 20 : rust-cache déclare `node24` depuis la v2.9 et embarque `@actions/cache` 6 (ESM seul), et le runner lance les actions JavaScript avec le `node` du conteneur. - `zstd` installé : archives du cache plus petites et plus rapides que le repli gzip. - Commentaires mis à jour, dont celui qui présentait `taiki-e/install-action` comme une action JavaScript : elle est composite. - **`check-frontend.yml`** : pas de cache npm, par décision (voir plus bas) ; commentaire réécrit. - **`audit.yml`** : deux commentaires que cette PR rendait faux (`check-rust` tourne aussi chaque semaine ; `taiki-e/install-action` est composite). - **`CLAUDE.md`, `docs/architecture.md`** : section CI/CD (deuxième commit). - **Pas d'entrée CHANGELOG** : configuration CI, aucun changement visible pour l'utilisateur, comme pour #232. ## Fait hors de cette PR (VPS, 2026-10-03) Le corps de #234 proposait de fixer `cache.host` sur une adresse joignable. Ça n'aurait pas suffi : l'adresse annoncée, l'IP publique du VPS, était joignable ; c'est le port qui était filtré. - **Cause**, deux problèmes cumulés : 1. ufw refuse l'entrant par défaut et n'ouvre aux conteneurs que la source `172.16.0.0/12`, alors que `/etc/docker/daemon.json` (réglé par Coolify) alloue les réseaux Docker dans `10.0.0.0/8` : les conteneurs de jobs ne correspondaient à aucune règle. 2. Le runner tournait sans fichier de config : `cache.proxy_port` valait `0`, donc un port tiré au hasard à chaque démarrage. - **Correctif** : config produite par `forgejo-runner generate-config` avec `proxy_port: 28100`, fichier complémentaire systemd `/etc/systemd/system/forgejo-runner.service.d/config.conf` qui passe `--config`, règle `ufw allow proto tcp from 10.0.0.0/8 to any port 28100`, redémarrage du runner. Les ports du proxy et du serveur interne restent fermés depuis Internet (vérifié de l'extérieur). - **Preuve** : PR de diagnostic #374, rouge au run 455 (port du cache sans réponse, ports 22 et 443 ouverts), verte au run 457 (`Cache saved` puis `Cache restored`). Détail dans ses commentaires. - **Doc** : section « Cache Actions : `Request timeout` » de `docs/forgejo-ops.md` dans la-compagnie-maximus (commit `66787ea`), retour arrière compris. ## Décisions (Max, 2026-10-03) Le runner v12.7.0 isole ce qu'écrit un run `pull_request` sous la référence de la PR (`getWriteIsolationKey`) : seuls les runs suivants de cette PR le relisent, et une PR ne lit le cache partagé que si rien ne correspond dans sa propre portée. Écrivent dans le cache partagé : `schedule`, `workflow_dispatch`, `push`, et la fermeture d'une PR mergée. La spec de juillet disait l'inverse, d'après le code de gitea/act. - **D1** : rust-cache sur les PR, plus un run hebdomadaire sur `main` qui, avec `workflow_dispatch`, est le seul écrivain du cache partagé. Sur 120 jours : 22 runs `check-rust` pour 11 PR. Sans écrivain partagé, la moitié des runs, le premier de chaque PR, resterait à froid. - **D2** : pas de cache npm. `check-frontend` 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 (run 456). **Runs à froid attendus**, qui ne signifient pas un retour de #234 (seul un délai dépassé le signalerait) : - après chaque nouvelle version stable de Rust ou une modification de l'`env:` du job, jusqu'au lundi suivant : la clé hache rustc et les variables `CARGO*`, `RUST*`, `CC*`, `CFLAGS*`, `CXX*` et `CMAKE*`. Une montée de version de l'app ne change pas la clé ; - quand une PR sauvegarde sous la clé de l'entrée partagée : le runner ne garde qu'une entrée par clé, toutes portées confondues, et l'entrée partagée disparaît pour toutes les PR jusqu'au lundi suivant. Cas typique : une PR qui a monté une dépendance, puis est revenue au lockfile de `main`. Deux PR ouvertes sur un même lockfile, différent de celui de `main`, s'évincent l'une l'autre ; - quand une entrée qui n'a eu que des correspondances exactes, donc jamais re-sauvegardée, atteint 30 jours ; - un lundi après une semaine sans requête au cache, une restauration en 404 : la recherche ne rafraîchit pas l'entrée, et le ménage qu'elle déclenche peut la supprimer ([forgejo/runner#1741](https://code.forgejo.org/forgejo/runner/issues/1741)). Le run repart alors à froid et re-sauvegarde. **Options laissées à Max** : - un second run le vendredi (`30 6 * * 1,5`), premier créneau sûr après une sortie de Rust (un jeudi), qui garde aussi chaque écart sous les 7 jours ; - un déclenchement sur la fermeture des PR mergées, pour rafraîchir le cache partagé à chaque merge Rust. ## Vérification - Les trois workflows passent le validateur de schéma du runner v12.7.0 : binaire officiel, somme SHA-256 vérifiée, étalonné en local (il refuse bien un `working-directory` sur une étape `uses`). Il ne vérifie pas le bloc `on:` : il accepte `on: 42` ou `crontab:`, d'après la revue. Les deux déclencheurs ajoutés ne se prouvent donc qu'après le merge, par les points 1 et 3 de la liste. - Suites locales vertes, pour la forme puisqu'aucun code source ne change : vitest 1348, Rust 187. - CI : le premier run `check-rust` part à froid et doit sauvegarder ; le run suivant, déclenché par le commit de doc, doit restaurer. Les résultats viendront en commentaire, lus dans les journaux et non dans le statut du job : une sauvegarde ratée laisse le job vert. ## Hors de cette PR - Le commentaire `concurrency` de `check-rust.yml` (« Cancel obsolete runs ») est contredit par `forgejo-ops.md` pour les runs de PR sur Forgejo 8.0.3. Il précède cette PR, je le laisse. - L'isolation des caches de PR mérite une entrée dans `forgejo-ops.md` (la-compagnie-maximus), proposée à part. ## Post-merge checklist (hence `Refs`, not a closing keyword) 1. Lancer `check-rust` sur `main` par `workflow_dispatch`. Le journal doit d'abord afficher `No cache found.`, preuve que l'entrée de 999 Mo de cette PR était bien isolée : s'il la restaure, la description de l'isolation est fausse. Il doit ensuite afficher `Cache saved successfully`, cette fois dans le cache partagé. L'entrée de cette PR disparaît alors au ménage suivant, puisqu'une entrée plus récente porte la même clé. 2. Premier run de la prochaine PR Rust : il doit restaurer l'entrée partagée. 3. Premier run planifié, lundi 2026-10-05 à 06:30 UTC : vert (`/warmup` le surveille). 4. Taille de `/home/ubuntu/.cache/actcache` sur le VPS (Max, `du -sh`). 5. Synchronisation de `STATE.md`. 6. #234 est alors fermée à la main, avec les preuves. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-10-03 19:09:54 +00:00
ci: restore the Rust cache with rust-cache and a weekly run on main (#234)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
PR Check — Rust / rust (pull_request) Successful in 9m37s
cf1da2bea0
The runner's cache server is reachable again since the VPS fix of
2026-10-03 (cache proxy pinned to port 28100, ufw rule scoped to the
Docker job networks; diagnosed in #374). Bring caching back to check-rust
through Swatinem/rust-cache, as #232 deferred.

forgejo-runner v12.7.0 keeps what a pull_request run saves private to
that PR, so PR runs alone would only warm later pushes to the same PR. A
weekly scheduled run on main (Mondays 06:30 UTC) and workflow_dispatch
write the shared entries that every PR falls back to.

rust-cache v2.9+ declares node24 and the runner starts JavaScript actions
with the container's node, so the job now installs Node.js 24. zstd is
installed for faster, smaller cache archives.

No npm cache in check-frontend, by decision: with per-PR isolation it
would only serve later pushes of a PR, and npm ci takes 9s there. audit.yml
loses two comments this change makes false.

No CHANGELOG entry: CI configuration only.

Refs #234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added 1 commit 2026-10-03 19:22:12 +00:00
docs: describe the Rust cache, its weekly writer and the absent npm cache (#234)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m41s
PR Check — Rust / rust (pull_request) Successful in 3m22s
20db766f52
CLAUDE.md and docs/architecture.md still said the Actions cache was
removed until #234 was fixed. They now describe rust-cache, the per-PR
isolation of what a pull_request run saves, the weekly run on main that
writes the shared cache, the expected cold runs, and why check-frontend
has no npm cache. The audit.yml paragraph no longer quotes the outdated
"1 PR in 40" figure.

Refs #234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Résultats CI : sauvegarde au premier run, restauration au suivant

Lus dans les journaux, pas dans le statut des jobs.

Run 459 (premier, à froid) Run 461 (suivant, après le commit de doc)
Restauration No cache found., immédiat, sans attente Cache restored successfully, full match: true, environ 20 s
cargo check 3 min 02 s 9,9 s
Compilation de cargo test 3 min 14 s 15,7 s
Sauvegarde Cache Size: ~999 MB, Cache saved successfully, environ 25 s avec zstd Cache up-to-date., rien de re-sauvegardé
Job complet 9 min 37 s 3 min 22 s
  • Clé : v0-rust-rust-Linux-x64-6448e498-86c598be. Elle porte rustc 1.99.0, les variables CARGO_*, Cargo.lock et Cargo.toml ; le serveur la stocke en minuscules et la compare sans casse.
  • Node v24.21.0 dans le conteneur. Aucun Failed to save ni Request timeout dans les deux journaux.
  • L'installation (apt, Node, rustup : environ 1 min 50 s) devient la plus grosse part du job. Le cache ne la couvre pas.
  • check-frontend reste vert (runs 458 et 460).

À garder en tête après le merge. Ces deux runs étaient des runs de PR : l'entrée de 999 Mo est isolée sous la référence de cette PR, et le cache partagé reste vide jusqu'au point 1 de la liste. Toute PR qui ajoute, retire ou monte une dépendance, ou qui arrive après une nouvelle version de Rust ou une modification de l'env: du job, écrira sa propre entrée d'environ 1 Go. Une entrée isolée disparaît au ménage suivant dès qu'une entrée plus récente porte la même clé, sinon après 7 jours sans usage. D'où le point 4 (taille de actcache sur le VPS).

Corrigé après la revue (commentaire 18946) : ce paragraphe rangeait les releases parmi les PR qui écrivent leur propre entrée, et disait ces entrées gardées au moins 7 jours. Or rust-cache ignore la version de l'app, et le ménage du runner ne garde qu'une entrée par clé.

## Résultats CI : sauvegarde au premier run, restauration au suivant Lus dans les journaux, pas dans le statut des jobs. | | Run 459 (premier, à froid) | Run 461 (suivant, après le commit de doc) | |---|---|---| | Restauration | `No cache found.`, immédiat, sans attente | `Cache restored successfully`, `full match: true`, environ 20 s | | `cargo check` | 3 min 02 s | 9,9 s | | Compilation de `cargo test` | 3 min 14 s | 15,7 s | | Sauvegarde | `Cache Size: ~999 MB`, `Cache saved successfully`, environ 25 s avec zstd | `Cache up-to-date.`, rien de re-sauvegardé | | Job complet | 9 min 37 s | **3 min 22 s** | - Clé : `v0-rust-rust-Linux-x64-6448e498-86c598be`. Elle porte rustc 1.99.0, les variables `CARGO_*`, `Cargo.lock` et `Cargo.toml` ; le serveur la stocke en minuscules et la compare sans casse. - Node v24.21.0 dans le conteneur. Aucun `Failed to save` ni `Request timeout` dans les deux journaux. - L'installation (apt, Node, rustup : environ 1 min 50 s) devient la plus grosse part du job. Le cache ne la couvre pas. - `check-frontend` reste vert (runs 458 et 460). **À garder en tête après le merge.** Ces deux runs étaient des runs de PR : l'entrée de 999 Mo est isolée sous la référence de cette PR, et le cache partagé reste vide jusqu'au point 1 de la liste. Toute PR qui ajoute, retire ou monte une dépendance, ou qui arrive après une nouvelle version de Rust ou une modification de l'`env:` du job, écrira sa propre entrée d'environ 1 Go. Une entrée isolée disparaît au ménage suivant dès qu'une entrée plus récente porte la même clé, sinon après 7 jours sans usage. D'où le point 4 (taille de `actcache` sur le VPS). *Corrigé après la revue (commentaire 18946) : ce paragraphe rangeait les releases parmi les PR qui écrivent leur propre entrée, et disait ces entrées gardées au moins 7 jours. Or rust-cache ignore la version de l'app, et le ménage du runner ne garde qu'une entrée par clé.*
Author
Owner

Revue adversariale — PR #375

Verdict : APPROVE

Résumé. La PR fait ce qu'elle annonce : rust-cache sauvegarde au premier run et restaure au suivant, et l'isolation décrite dans les commentaires correspond au code du runner v12.7.0, reproduite ici sur son binaire officiel. Aucun bloquant. Trois réserves : le validateur cité ne regarde pas le bloc on:, le ramasse-miettes du runner déduplique les entrées sans tenir compte de leur portée, et la description de la clé oublie l'environnement du job.

Ce qui a été vérifié, et comment

Journaux CI, lus par la route web /jobs/0/logs et non dans le statut :

  • run 459 (cf1da2b) : No cache found. immédiat ; en post, nettoyage de src-tauri/target, tar --use-compress-program zstdmt, Cache Size: ~999 MB, Cache saved successfully en 25 s ;
  • run 461 (20db766, la tête) : Cache restored successfully puis Restored from cache key "v0-rust-rust-linux-x64-6448e498-86c598be" full match: true en 20 s, cargo check en 9,9 s, compilation des tests en 15,7 s, Cache up-to-date., Job succeeded ;
  • aucune occurrence de Failed to save, Request timeout, reserveCache, getCacheEntry, ETIMEDOUT, ECONNREFUSED ni d'avertissement d'action dans les deux journaux ; Node v24.21.0 ; 187 tests Rust verts aux deux runs ;
  • check-frontend 458 et 460 verts, avec added 175 packages … in 9s dans les deux : le chiffre de D2 tient.

Code du runner v12.7.0 (bb4d56f, code.forgejo.org) :

  • getWriteIsolationKey (internal/app/run/runner.go:192) : un pull_request écrit sous sa ref refs/pull/N/…, sauf la fermeture d'une PR mergée ; tout autre événement écrit sans isolation ;
  • findCacheWithIsolationKeyFallback (act/artifactcache/caches.go:105) : portée propre d'abord (clé exacte, puis préfixe), portée partagée seulement si rien n'y correspond ;
  • keepUnused à 7 jours et keepUsed à 30 jours (caches.go:213-214) ; clés passées en minuscules à l'écriture comme à la lecture.

Reproduction sur le binaire forgejo-runner-12.7.0-linux-amd64 (SHA-256 vérifié), sous-commande cache-server sur un répertoire jetable hors dépôt, requêtes signées comme celles du proxy : avant tout GC, une entrée partagée est servie au run planifié et, en repli, à une autre PR ; la PR 999 reçoit sa propre entrée avant la partagée ; l'entrée isolée de la PR 999 n'est jamais servie à une autre PR. Les trois affirmations du commentaire des déclencheurs tiennent.

Fermeture de PR non souscrite : aucun run n'apparaît à l'heure des merges de #364 (00:57Z), #366 (01:25Z) et #368 (13:43Z). La fermeture d'une PR mergée ne déclenche donc pas ce workflow, comme le dit le commentaire.

Manifestes des actions : rust-cache déclare node24 depuis v2.9.0 (v2.8.2 : node20) et post-if: "success() || env.CACHE_ON_FAILURE == 'true'" ; actions/checkout@v4 déclare node20 ; taiki-e/install-action@v2 est composite. Les commentaires réécrits sont exacts.

Diff sémantique YAML (base 0e4fef5 → tête) : check-frontend.yml identique ; audit.yml ne diffère que par des lignes de commentaire shell dans run: ; check-rust.yml diffère de ce que la PR annonce et de rien d'autre (schedule, workflow_dispatch, étape Rust cache, zstd, setup_24.x). La forme schedule: [{cron: '30 6 * * 1'}] + workflow_dispatch: null est celle d'audit.yml, qui tire chaque jour à 06:00Z.

Le reste : timeline de #234 sans closes (référence de #375 en ref_action: none), aucune forme Close/Fix/Resolve #N dans le corps ni dans les commits ; commits conventionnels (ci:, docs:) ; git diff --check propre ; base = main actuel, merge en avance rapide ; #374 fermée sans merge ; release.yml n'utilise aucun cache, donc rien de ce que le cache contient n'atteint un binaire publié.

Suites locales non rejouées : la PR ne touche aucun code source, et la CI de la tête les a passées (lu dans les journaux).

Suggestions (non bloquantes)

1. Le ramasse-miettes du runner déduplique par clé, toutes portées confondues. La dernière passe de gcCache (caches.go:286-330) groupe les entrées complètes par Key et Version seulement, garde la plus récente et supprime les autres dès qu'elles n'ont pas servi depuis 5 minutes. WriteIsolationKey n'entre pas dans le groupe. Reproduit sur le binaire, après 5 min 20 s d'attente et un redémarrage du serveur (le GC tourne à l'ouverture) :

Entrées sous la même clé Run planifié ou dispatch Autre PR PR 999
partagée (n° 1), puis PR 999 (n° 4) avant : n° 1 ; après : rien avant : n° 1 ; après : rien n° 4
PR 999 (n° 2), puis partagée (n° 5) n° 5 n° 5 avant : n° 2 ; après : n° 5
partagée seule (n° 3, témoin) n° 3 n° 3 n° 3

Journal du serveur : deleted cache: &{ID:1 … Key:k_a … WriteIsolationKey:} et deleted cache: &{ID:2 … Key:k_b … WriteIsolationKey:refs/pull/999/head}. Le regroupement est identique en v13.2.0 et sur main du runner : une mise à jour ne le corrige pas.

Trois conséquences :

  • bénigne : après le workflow_dispatch du point 1, l'entrée isolée de 999 Mo de cette PR disparaît au GC suivant. Une entrée isolée n'est donc pas « gardée au moins 7 jours », comme le dit le commentaire de résultats, dès qu'une entrée plus récente porte la même clé ;
  • à ajouter à la liste des runs à froid : un run de PR qui sauvegarde sous la clé de l'entrée partagée, après elle, la fait supprimer pour toutes les PR jusqu'au lundi suivant. Ça arrive quand la portée propre de la PR contient une entrée de même préfixe (même rustc, même environnement) mais d'un autre Cargo.lock : le runner la sert avant de regarder la portée partagée, le run n'est pas une correspondance exacte, il sauvegarde. Cas typique : la PR a monté une dépendance, puis son Cargo.lock redevient celui de main (rebase sur la même montée déjà mergée, retour arrière) ;
  • deux PR ouvertes au même Cargo.lock, différent de celui de main (une pile /autopilot posée sur une montée de dépendance), s'évincent l'une l'autre : à son push suivant, chacune repart d'une restauration partielle et resauvegarde environ 1 Go.

Ajouter la deuxième au commentaire des déclencheurs, à docs/architecture.md et à l'entrée forgejo-ops.md déjà prévue. Le comportement ressemble à un oubli au moment où l'isolation a été ajoutée au runner ; une recherche rapide dans son suivi ne montre aucune issue sur ce point, en ouvrir une se défend.

Pour lire les journaux du lundi : le cas « peut-être le run du lundi lui-même » a une signature connue en amont, forgejo/runner#1741 (ouverte). La recherche (GET /cache) ne rafraîchit pas UsedAt, et le GC part juste après elle : après une semaine sans requête, si la dernière utilisation de l'entrée a dépassé 7 jours de quelques secondes, il supprime l'entrée que la recherche vient de renvoyer, et le téléchargement répond 404. Le run part alors à froid et resauvegarde. Un échec de restauration en 404 ce jour-là n'est pas un retour de #234, contrairement à un délai dépassé.

2. Le validateur cité ne vérifie pas le bloc on:. Étalonné sur la tête de check-rust.yml : un working-directory sur une étape uses est refusé (exit 1), mais on: 42, un événement inconnu, workflow-dispatch, pathz:, crontab: à la place de cron: et cron: 5 passent tous « workflow schema validation OK ». La vérification « les trois workflows passent le validateur » ne dit donc rien des deux déclencheurs ajoutés ; seuls les points 1 et 3 de la liste post-merge les prouveront. Le point 1 devient réfutable si son attendu est écrit en entier : le dispatch doit d'abord afficher No cache found., preuve que l'entrée de la PR était isolée comme l'affirment trois textes, puis Cache saved successfully. S'il restaure l'entrée de la PR, la description de l'isolation est fausse.

3. Composition de la clé. Le commentaire de l'étape et docs/architecture.md la décrivent comme rustc, manifestes et lockfile, job. Elle porte aussi l'environnement : rust-cache hache dans la clé de restauration, avec rustc, les variables aux préfixes CARGO, CC, CFLAGS, CXX, CMAKE et RUST (le journal liste CARGO_INCREMENTAL, CARGO_PROFILE_TEST_DEBUG, CARGO_TERM_COLOR). Une modification du env: du job, un RUSTFLAGS ajouté par exemple, donne donc un run à froid jusqu'au lundi suivant, comme une nouvelle version de Rust : à ajouter à la liste. À l'inverse, le commentaire de résultats range les releases parmi les PR qui écrivent leur propre entrée. rust-cache remet package.version à 0.0.0 dans Cargo.toml et ne hache, dans Cargo.lock (format 4, analysé sans repli d'après le journal), que les paquets à source ou checksum, ce qui exclut simpl-result. Une montée de version ne change pas la clé ; seuls un ajout, un retrait ou une montée de dépendance en ouvrent une nouvelle.

Hors PR

  • /warmup repère les workflows à schedule: et compte la série d'échecs par nom de workflow. check-rust.yml mêle désormais des runs pull_request et schedule : un rouge de PR en cours de travail y serait lu comme une tâche planifiée en échec. Filtrer sur event == "schedule" pour ce workflow, et lire Cache saved successfully ou Cache up-to-date. dans le journal du lundi, puisqu'une sauvegarde ratée laisse le job vert.
  • check-frontend.yml et audit.yml installent encore Node 20, en fin de vie depuis le 2026-04-30.
## Revue adversariale — PR #375 **Verdict : APPROVE** **Résumé.** La PR fait ce qu'elle annonce : rust-cache sauvegarde au premier run et restaure au suivant, et l'isolation décrite dans les commentaires correspond au code du runner v12.7.0, reproduite ici sur son binaire officiel. Aucun bloquant. Trois réserves : le validateur cité ne regarde pas le bloc `on:`, le ramasse-miettes du runner déduplique les entrées sans tenir compte de leur portée, et la description de la clé oublie l'environnement du job. ### Ce qui a été vérifié, et comment **Journaux CI**, lus par la route web `/jobs/0/logs` et non dans le statut : - run 459 (`cf1da2b`) : `No cache found.` immédiat ; en post, nettoyage de `src-tauri/target`, tar `--use-compress-program zstdmt`, `Cache Size: ~999 MB`, `Cache saved successfully` en 25 s ; - run 461 (`20db766`, la tête) : `Cache restored successfully` puis `Restored from cache key "v0-rust-rust-linux-x64-6448e498-86c598be" full match: true` en 20 s, `cargo check` en 9,9 s, compilation des tests en 15,7 s, `Cache up-to-date.`, `Job succeeded` ; - aucune occurrence de `Failed to save`, `Request timeout`, `reserveCache`, `getCacheEntry`, `ETIMEDOUT`, `ECONNREFUSED` ni d'avertissement d'action dans les deux journaux ; Node v24.21.0 ; 187 tests Rust verts aux deux runs ; - `check-frontend` 458 et 460 verts, avec `added 175 packages … in 9s` dans les deux : le chiffre de D2 tient. **Code du runner v12.7.0** (`bb4d56f`, code.forgejo.org) : - `getWriteIsolationKey` (`internal/app/run/runner.go:192`) : un `pull_request` écrit sous sa ref `refs/pull/N/…`, sauf la fermeture d'une PR mergée ; tout autre événement écrit sans isolation ; - `findCacheWithIsolationKeyFallback` (`act/artifactcache/caches.go:105`) : portée propre d'abord (clé exacte, puis préfixe), portée partagée seulement si rien n'y correspond ; - `keepUnused` à 7 jours et `keepUsed` à 30 jours (`caches.go:213-214`) ; clés passées en minuscules à l'écriture comme à la lecture. **Reproduction sur le binaire** `forgejo-runner-12.7.0-linux-amd64` (SHA-256 vérifié), sous-commande `cache-server` sur un répertoire jetable hors dépôt, requêtes signées comme celles du proxy : avant tout GC, une entrée partagée est servie au run planifié et, en repli, à une autre PR ; la PR 999 reçoit sa propre entrée avant la partagée ; l'entrée isolée de la PR 999 n'est jamais servie à une autre PR. Les trois affirmations du commentaire des déclencheurs tiennent. **Fermeture de PR non souscrite** : aucun run n'apparaît à l'heure des merges de #364 (00:57Z), #366 (01:25Z) et #368 (13:43Z). La fermeture d'une PR mergée ne déclenche donc pas ce workflow, comme le dit le commentaire. **Manifestes des actions** : rust-cache déclare `node24` depuis v2.9.0 (v2.8.2 : `node20`) et `post-if: "success() || env.CACHE_ON_FAILURE == 'true'"` ; `actions/checkout@v4` déclare `node20` ; `taiki-e/install-action@v2` est composite. Les commentaires réécrits sont exacts. **Diff sémantique YAML** (base `0e4fef5` → tête) : `check-frontend.yml` identique ; `audit.yml` ne diffère que par des lignes de commentaire shell dans `run:` ; `check-rust.yml` diffère de ce que la PR annonce et de rien d'autre (`schedule`, `workflow_dispatch`, étape `Rust cache`, `zstd`, `setup_24.x`). La forme `schedule: [{cron: '30 6 * * 1'}]` + `workflow_dispatch: null` est celle d'`audit.yml`, qui tire chaque jour à 06:00Z. **Le reste** : timeline de #234 sans `closes` (référence de #375 en `ref_action: none`), aucune forme `Close/Fix/Resolve #N` dans le corps ni dans les commits ; commits conventionnels (`ci:`, `docs:`) ; `git diff --check` propre ; base = `main` actuel, merge en avance rapide ; #374 fermée sans merge ; `release.yml` n'utilise aucun cache, donc rien de ce que le cache contient n'atteint un binaire publié. Suites locales non rejouées : la PR ne touche aucun code source, et la CI de la tête les a passées (lu dans les journaux). ### Suggestions (non bloquantes) **1. Le ramasse-miettes du runner déduplique par clé, toutes portées confondues.** La dernière passe de `gcCache` (`caches.go:286-330`) groupe les entrées complètes par `Key` et `Version` seulement, garde la plus récente et supprime les autres dès qu'elles n'ont pas servi depuis 5 minutes. `WriteIsolationKey` n'entre pas dans le groupe. Reproduit sur le binaire, après 5 min 20 s d'attente et un redémarrage du serveur (le GC tourne à l'ouverture) : | Entrées sous la même clé | Run planifié ou dispatch | Autre PR | PR 999 | |---|---|---|---| | partagée (n° 1), puis PR 999 (n° 4) | avant : n° 1 ; après : **rien** | avant : n° 1 ; après : **rien** | n° 4 | | PR 999 (n° 2), puis partagée (n° 5) | n° 5 | n° 5 | avant : n° 2 ; après : n° 5 | | partagée seule (n° 3, témoin) | n° 3 | n° 3 | n° 3 | Journal du serveur : `deleted cache: &{ID:1 … Key:k_a … WriteIsolationKey:}` et `deleted cache: &{ID:2 … Key:k_b … WriteIsolationKey:refs/pull/999/head}`. Le regroupement est identique en v13.2.0 et sur `main` du runner : une mise à jour ne le corrige pas. Trois conséquences : - bénigne : après le `workflow_dispatch` du point 1, l'entrée isolée de 999 Mo de cette PR disparaît au GC suivant. Une entrée isolée n'est donc pas « gardée au moins 7 jours », comme le dit le commentaire de résultats, dès qu'une entrée plus récente porte la même clé ; - à ajouter à la liste des runs à froid : un run de PR qui sauvegarde sous la clé de l'entrée partagée, après elle, la fait supprimer pour toutes les PR jusqu'au lundi suivant. Ça arrive quand la portée propre de la PR contient une entrée de même préfixe (même rustc, même environnement) mais d'un autre `Cargo.lock` : le runner la sert avant de regarder la portée partagée, le run n'est pas une correspondance exacte, il sauvegarde. Cas typique : la PR a monté une dépendance, puis son `Cargo.lock` redevient celui de `main` (rebase sur la même montée déjà mergée, retour arrière) ; - deux PR ouvertes au même `Cargo.lock`, différent de celui de `main` (une pile `/autopilot` posée sur une montée de dépendance), s'évincent l'une l'autre : à son push suivant, chacune repart d'une restauration partielle et resauvegarde environ 1 Go. Ajouter la deuxième au commentaire des déclencheurs, à `docs/architecture.md` et à l'entrée `forgejo-ops.md` déjà prévue. Le comportement ressemble à un oubli au moment où l'isolation a été ajoutée au runner ; une recherche rapide dans son suivi ne montre aucune issue sur ce point, en ouvrir une se défend. Pour lire les journaux du lundi : le cas « peut-être le run du lundi lui-même » a une signature connue en amont, forgejo/runner#1741 (ouverte). La recherche (`GET /cache`) ne rafraîchit pas `UsedAt`, et le GC part juste après elle : après une semaine sans requête, si la dernière utilisation de l'entrée a dépassé 7 jours de quelques secondes, il supprime l'entrée que la recherche vient de renvoyer, et le téléchargement répond 404. Le run part alors à froid et resauvegarde. Un échec de restauration en 404 ce jour-là n'est pas un retour de #234, contrairement à un délai dépassé. **2. Le validateur cité ne vérifie pas le bloc `on:`.** Étalonné sur la tête de `check-rust.yml` : un `working-directory` sur une étape `uses` est refusé (exit 1), mais `on: 42`, un événement inconnu, `workflow-dispatch`, `pathz:`, `crontab:` à la place de `cron:` et `cron: 5` passent tous « workflow schema validation OK ». La vérification « les trois workflows passent le validateur » ne dit donc rien des deux déclencheurs ajoutés ; seuls les points 1 et 3 de la liste post-merge les prouveront. Le point 1 devient réfutable si son attendu est écrit en entier : le dispatch doit d'abord afficher `No cache found.`, preuve que l'entrée de la PR était isolée comme l'affirment trois textes, puis `Cache saved successfully`. S'il restaure l'entrée de la PR, la description de l'isolation est fausse. **3. Composition de la clé.** Le commentaire de l'étape et `docs/architecture.md` la décrivent comme rustc, manifestes et lockfile, job. Elle porte aussi l'environnement : rust-cache hache dans la clé de restauration, avec rustc, les variables aux préfixes `CARGO`, `CC`, `CFLAGS`, `CXX`, `CMAKE` et `RUST` (le journal liste `CARGO_INCREMENTAL`, `CARGO_PROFILE_TEST_DEBUG`, `CARGO_TERM_COLOR`). Une modification du `env:` du job, un `RUSTFLAGS` ajouté par exemple, donne donc un run à froid jusqu'au lundi suivant, comme une nouvelle version de Rust : à ajouter à la liste. À l'inverse, le commentaire de résultats range les releases parmi les PR qui écrivent leur propre entrée. rust-cache remet `package.version` à `0.0.0` dans `Cargo.toml` et ne hache, dans `Cargo.lock` (format 4, analysé sans repli d'après le journal), que les paquets à `source` ou `checksum`, ce qui exclut `simpl-result`. Une montée de version ne change pas la clé ; seuls un ajout, un retrait ou une montée de dépendance en ouvrent une nouvelle. ### Hors PR - `/warmup` repère les workflows à `schedule:` et compte la série d'échecs par nom de workflow. `check-rust.yml` mêle désormais des runs `pull_request` et `schedule` : un rouge de PR en cours de travail y serait lu comme une tâche planifiée en échec. Filtrer sur `event == "schedule"` pour ce workflow, et lire `Cache saved successfully` ou `Cache up-to-date.` dans le journal du lundi, puisqu'une sauvegarde ratée laisse le job vert. - `check-frontend.yml` et `audit.yml` installent encore Node 20, en fin de vie depuis le 2026-04-30.
maximus added 1 commit 2026-10-03 22:04:53 +00:00
ci: document the cache evictions and key inputs found in review (#234)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m45s
PR Check — Rust / rust (pull_request) Successful in 3m21s
f821492274
Applies the three suggestions of the review of #375:

- The runner's cleanup keeps one entry per key, whatever its scope: a PR
  run that saves under the shared entry's key evicts it for every PR until
  the next Monday, and two PRs on the same non-main lockfile evict each
  other. Added to the list of cold runs.
- The cache key also hashes the job's CARGO*/RUST*/CC*/CFLAGS*/CXX*/CMAKE*
  variables, and ignores the app's own version: changing env: means a cold
  run until Monday, a release does not.
- A Monday restore that fails with a 404 after a quiet week is a known
  runner race (forgejo/runner#1741), not #234 coming back.

Refs #234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Merge

Merge demandé par Max (« corrige et merge »).

  • f821492 est postérieur à la revue (commentaire 18946) et n'a pas été relu. Il applique ses trois suggestions, en commentaires et en doc seulement : l'éviction par clé, toutes portées confondues ; les entrées de la clé (environnement du job, version de l'app ignorée) ; l'attendu du point 1 de la liste post-merge. Le corps de la PR et le commentaire de résultats sont corrigés en conséquence.
  • CI de la tête f821492, lue dans les journaux : check-rust run 463 (restauration full match: true, cargo check en 10,6 s, 187 tests, Cache up-to-date., 3 min 21 s) et check-frontend run 462, tous deux verts.
  • Aucun code source ne change (workflows, CLAUDE.md, docs/architecture.md) : l'arbre de l'application est celui de main.
  • Merge en avance rapide par l'API, épinglé sur f821492. #234 reste ouverte (Refs) jusqu'à ce que la liste post-merge soit faite.
## Merge Merge demandé par Max (« corrige et merge »). - **`f821492` est postérieur à la revue (commentaire 18946) et n'a pas été relu.** Il applique ses trois suggestions, en commentaires et en doc seulement : l'éviction par clé, toutes portées confondues ; les entrées de la clé (environnement du job, version de l'app ignorée) ; l'attendu du point 1 de la liste post-merge. Le corps de la PR et le commentaire de résultats sont corrigés en conséquence. - **CI de la tête `f821492`, lue dans les journaux** : `check-rust` run 463 (restauration `full match: true`, `cargo check` en 10,6 s, 187 tests, `Cache up-to-date.`, 3 min 21 s) et `check-frontend` run 462, tous deux verts. - **Aucun code source ne change** (workflows, `CLAUDE.md`, `docs/architecture.md`) : l'arbre de l'application est celui de `main`. - **Merge en avance rapide par l'API, épinglé sur `f821492`.** #234 reste ouverte (`Refs`) jusqu'à ce que la liste post-merge soit faite.
maximus merged commit f821492274 into main 2026-10-03 22:11:18 +00:00
maximus deleted branch issue-234-rust-cache 2026-10-03 22:11:18 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#375
No description provided.