ci: restore the Rust cache with rust-cache and a weekly run on main (#234) #375
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#375
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-234-rust-cache"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #234
Refset 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.ymlSwatinem/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.main(lundi 06:30 UTC, aprèsaudit.yml) etworkflow_dispatch.node24depuis la v2.9 et embarque@actions/cache6 (ESM seul), et le runner lance les actions JavaScript avec lenodedu conteneur.zstdinstallé : archives du cache plus petites et plus rapides que le repli gzip.taiki-e/install-actioncomme 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-rusttourne aussi chaque semaine ;taiki-e/install-actionest composite).CLAUDE.md,docs/architecture.md: section CI/CD (deuxième commit).Fait hors de cette PR (VPS, 2026-10-03)
Le corps de #234 proposait de fixer
cache.hostsur 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é.172.16.0.0/12, alors que/etc/docker/daemon.json(réglé par Coolify) alloue les réseaux Docker dans10.0.0.0/8: les conteneurs de jobs ne correspondaient à aucune règle.cache.proxy_portvalait0, donc un port tiré au hasard à chaque démarrage.forgejo-runner generate-configavecproxy_port: 28100, fichier complémentaire systemd/etc/systemd/system/forgejo-runner.service.d/config.confqui passe--config, règleufw 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).Cache savedpuisCache restored). Détail dans ses commentaires.Request timeout» dedocs/forgejo-ops.mddans la-compagnie-maximus (commit66787ea), retour arrière compris.Décisions (Max, 2026-10-03)
Le runner v12.7.0 isole ce qu'écrit un run
pull_requestsous 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.mainqui, avecworkflow_dispatch, est le seul écrivain du cache partagé. Sur 120 jours : 22 runscheck-rustpour 11 PR. Sans écrivain partagé, la moitié des runs, le premier de chaque PR, resterait à froid.check-frontendne tourne que sur des PR, donc un cache ne servirait qu'aux pushes suivants d'une même PR, etnpm ciy prend 9 s (run 456).Runs à froid attendus, qui ne signifient pas un retour de #234 (seul un délai dépassé le signalerait) :
env:du job, jusqu'au lundi suivant : la clé hache rustc et les variablesCARGO*,RUST*,CC*,CFLAGS*,CXX*etCMAKE*. Une montée de version de l'app ne change pas la clé ;main. Deux PR ouvertes sur un même lockfile, différent de celui demain, s'évincent l'une l'autre ;Options laissées à Max :
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 ;Vérification
working-directorysur une étapeuses). Il ne vérifie pas le blocon:: il accepteon: 42oucrontab:, 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.check-rustpart à 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
concurrencydecheck-rust.yml(« Cancel obsolete runs ») est contredit parforgejo-ops.mdpour les runs de PR sur Forgejo 8.0.3. Il précède cette PR, je le laisse.forgejo-ops.md(la-compagnie-maximus), proposée à part.Post-merge checklist (hence
Refs, not a closing keyword)check-rustsurmainparworkflow_dispatch. Le journal doit d'abord afficherNo 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 afficherCache 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é./warmuple surveille)./home/ubuntu/.cache/actcachesur le VPS (Max,du -sh).STATE.md.🤖 Generated with Claude Code
Résultats CI : sauvegarde au premier run, restauration au suivant
Lus dans les journaux, pas dans le statut des jobs.
No cache found., immédiat, sans attenteCache restored successfully,full match: true, environ 20 scargo checkcargo testCache Size: ~999 MB,Cache saved successfully, environ 25 s avec zstdCache up-to-date., rien de re-sauvegardév0-rust-rust-Linux-x64-6448e498-86c598be. Elle porte rustc 1.99.0, les variablesCARGO_*,Cargo.locketCargo.toml; le serveur la stocke en minuscules et la compare sans casse.Failed to saveniRequest timeoutdans les deux journaux.check-frontendreste 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 deactcachesur 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é.
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/logset non dans le statut :cf1da2b) :No cache found.immédiat ; en post, nettoyage desrc-tauri/target, tar--use-compress-program zstdmt,Cache Size: ~999 MB,Cache saved successfullyen 25 s ;20db766, la tête) :Cache restored successfullypuisRestored from cache key "v0-rust-rust-linux-x64-6448e498-86c598be" full match: trueen 20 s,cargo checken 9,9 s, compilation des tests en 15,7 s,Cache up-to-date.,Job succeeded;Failed to save,Request timeout,reserveCache,getCacheEntry,ETIMEDOUT,ECONNREFUSEDni d'avertissement d'action dans les deux journaux ; Node v24.21.0 ; 187 tests Rust verts aux deux runs ;check-frontend458 et 460 verts, avecadded 175 packages … in 9sdans les deux : le chiffre de D2 tient.Code du runner v12.7.0 (
bb4d56f, code.forgejo.org) :getWriteIsolationKey(internal/app/run/runner.go:192) : unpull_requestécrit sous sa refrefs/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 etkeepUsedà 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-commandecache-serversur 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
node24depuis v2.9.0 (v2.8.2 :node20) etpost-if: "success() || env.CACHE_ON_FAILURE == 'true'";actions/checkout@v4déclarenode20;taiki-e/install-action@v2est composite. Les commentaires réécrits sont exacts.Diff sémantique YAML (base
0e4fef5→ tête) :check-frontend.ymlidentique ;audit.ymlne diffère que par des lignes de commentaire shell dansrun:;check-rust.ymldiffère de ce que la PR annonce et de rien d'autre (schedule,workflow_dispatch, étapeRust cache,zstd,setup_24.x). La formeschedule: [{cron: '30 6 * * 1'}]+workflow_dispatch: nullest celle d'audit.yml, qui tire chaque jour à 06:00Z.Le reste : timeline de #234 sans
closes(référence de #375 enref_action: none), aucune formeClose/Fix/Resolve #Ndans le corps ni dans les commits ; commits conventionnels (ci:,docs:) ;git diff --checkpropre ; base =mainactuel, merge en avance rapide ; #374 fermée sans merge ;release.ymln'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 parKeyetVersionseulement, garde la plus récente et supprime les autres dès qu'elles n'ont pas servi depuis 5 minutes.WriteIsolationKeyn'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) :Journal du serveur :
deleted cache: &{ID:1 … Key:k_a … WriteIsolationKey:}etdeleted cache: &{ID:2 … Key:k_b … WriteIsolationKey:refs/pull/999/head}. Le regroupement est identique en v13.2.0 et surmaindu runner : une mise à jour ne le corrige pas.Trois conséquences :
workflow_dispatchdu 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é ;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 sonCargo.lockredevient celui demain(rebase sur la même montée déjà mergée, retour arrière) ;Cargo.lock, différent de celui demain(une pile/autopilotposé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.mdet à l'entréeforgejo-ops.mddé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 pasUsedAt, 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 decheck-rust.yml: unworking-directorysur une étapeusesest refusé (exit 1), maison: 42, un événement inconnu,workflow-dispatch,pathz:,crontab:à la place decron:etcron: 5passent 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 afficherNo cache found., preuve que l'entrée de la PR était isolée comme l'affirment trois textes, puisCache 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.mdla 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éfixesCARGO,CC,CFLAGS,CXX,CMAKEetRUST(le journal listeCARGO_INCREMENTAL,CARGO_PROFILE_TEST_DEBUG,CARGO_TERM_COLOR). Une modification duenv:du job, unRUSTFLAGSajouté 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 remetpackage.versionà0.0.0dansCargo.tomlet ne hache, dansCargo.lock(format 4, analysé sans repli d'après le journal), que les paquets àsourceouchecksum, ce qui exclutsimpl-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
/warmuprepère les workflows àschedule:et compte la série d'échecs par nom de workflow.check-rust.ymlmêle désormais des runspull_requestetschedule: un rouge de PR en cours de travail y serait lu comme une tâche planifiée en échec. Filtrer surevent == "schedule"pour ce workflow, et lireCache saved successfullyouCache up-to-date.dans le journal du lundi, puisqu'une sauvegarde ratée laisse le job vert.check-frontend.ymletaudit.ymlinstallent encore Node 20, en fin de vie depuis le 2026-04-30.Merge
Merge demandé par Max (« corrige et merge »).
f821492est 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.f821492, lue dans les journaux :check-rustrun 463 (restaurationfull match: true,cargo checken 10,6 s, 187 tests,Cache up-to-date., 3 min 21 s) etcheck-frontendrun 462, tous deux verts.CLAUDE.md,docs/architecture.md) : l'arbre de l'application est celui demain.f821492. #234 reste ouverte (Refs) jusqu'à ce que la liste post-merge soit faite.