Skip to content

fix(ci,#16938): le garde de sante du cache _work ne tue plus son appelant -- rc=128 propage sous set -euo pipefail - #16939

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/16938-work-cache-health-pipefail
Sep 22, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/16938-work-cache-health-pipefail

Conversation

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Grain: CI/harnais -- lane myia-ai-01:CoursIA -- prev: MED

Summary

Le garde de sante du cache _work (#15105) tuait le conteneur du runner sur le seul etat qu'il existe pour reparer — et il le faisait avant l'enregistrement du runner, donc sans une ligne de journal de job. Un slot mort ainsi ne rougit nulle part : il cesse simplement de prendre des jobs.

See #16938. See #16643.

Le defaut

entrypoint.sh porte set -euo pipefail (ligne 9) et appelle wch_check_workdir avant config.sh. Trois fonctions de mesure laissaient remonter un rc non nul :

Fonction Ce qui rend non nul Dans quel cas
wch_broken_refs git for-each-ref rc=128 depot illisible (.git a moitie ecrit, HEAD corrompu)
wch_ref_count idem idem
wch_pack_count ls <glob sans match> rc=2 depot SAIN sans aucun pack — clone interrompu, etat banal

Sous pipefail, ce rc traverse le pipe, puis la substitution de commande, et set -e tue l'entrypoint a la premiere ligne de wch_integrity_pass :

broken="$(wch_broken_refs "$repo")"    # <-- mort ici

donc avant la branche de purge, qui est exactement la reparation prevue pour ce cas. La fonction violait son propre contrat ecrit, quelques lignes plus haut dans le meme fichier :

« Rend 0 dans tous les cas — un garde de sante ne doit JAMAIS etre la raison pour laquelle un slot meurt avant de s'enregistrer ; ses decisions se lisent dans son journal. »

La troisieme instance est la plus large : wch_pack_count ne tue pas sur une corruption, mais sur un depot sain sans pack. Le garde mourait donc aussi sur le clone a moitie ecrit qu'un arret brutal venait de produire — c'est-a-dire sur la cause meme de la corruption qu'il devait rattraper au demarrage suivant.

Mesure firsthand

Slot myia-ai-01-wsl-8, constate le 2026-09-18 :

.git/HEAD          : 16 octets NUL
refs lisibles      : 0          (git rev-parse : fatal)
reflogs            : absents
taille du clone    : 32M
demarrages morts   : 174 consecutifs, rc=128, zero ligne de journal
hors service depuis: 2026-09-18 13:03

Controle positif sur le slot 7 voisin, meme commande, meme instant : HEAD = un vrai SHA, reflogs presents, for-each-ref rend des refs. La corruption etait bien locale au slot 8, et pas un artefact de ma facon de sonder.

Reproduction hors CI, avant correctif — le depot illisible :

$ bash -c 'set -euo pipefail; . work_cache_health.sh; b="$(wch_broken_refs /tmp/w)"; echo ATTEINT'
$ echo $?
128          # "ATTEINT" jamais imprime

Et le cas banal, qui ne demande meme pas de corruption :

$ bash -c 'set -euo pipefail; . work_cache_health.sh; c="$(wch_pack_count /tmp/repo_sans_pack)"; echo ATTEINT'
$ echo $?
2            # depot parfaitement sain

Pourquoi la suite de tests ne l'a pas vu

test_work_cache_health.sh source le garde sous set -o pipefail sans set -e : le banc est plus laxiste que l'appelant de production. Il portait deja l'assertion :

PASS: wch_check_workdir rend toujours 0 (garde jamais fatal)

et elle passait. Elle mesurait le code de retour d'une fonction qui, sous les flags reels, ne revenait pas. Un if cmd; then desarme en plus set -e pendant la condition : meme un banc qui porterait set -e ne verrait rien sous cette forme.

C'est la lecon generalisable de cette PR, et elle vaut au-dela de ce fichier : un banc plus permissif que l'appelant de production ne teste pas l'appelant de production. Le meme piege m'a attrape pendant l'investigation elle-meme — ma premiere sonde du site d'appel encadrait la commande d'un &&, qui desarmait set -e et rendait « survivant » un cas qui meurt. Mesure refaite en scripts enfants isoles.

Le correctif

Fichier Ce qui change
work_cache_health.sh les 3 mesures neutralisent leur propre rc ; wch_count_lines remplace deux grep -c (qui rend rc=1 quand le compte est zero — le cas de la ligne de purge) ; ${n:-0} sur le test numerique
test_work_cache_health.sh 5 cas qui rejouent set -euo pipefail et exigent un sentinel apres l'appel, dont un controle negatif
entrypoint.sh garde au site d'appel — seconde barriere, pas le correctif

La seconde barriere est mesuree, pas supposee : un appel nu sous ces flags meurt en rc=128 sans rien imprimer ; suivi du garde, il survit — parce que set -e est suspendu jusque dans la fonction appelee. Aucune edition future du garde ne peut donc tuer un slot, meme si elle reintroduit le defaut.

Validation

$ bash scripts/ci/docker/linux-runner/test_work_cache_health.sh
  PASS: set -euo pipefail : depot illisible -- l'appelant SURVIT a la passe
  PASS: set -euo pipefail : depot illisible PURGE (la branche de reparation est atteinte)
  PASS: set -euo pipefail : depot sans pack -- l'appelant SURVIT
  PASS: set -euo pipefail : depot sain sans pack CONSERVE (non-regression)
  PASS: controle NEGATIF : la forme non gardee TUE l'appelant (le banc a des dents)
===========================================================
  27 PASS / 0 FAIL

$ bash scripts/ci/docker/linux-runner/test_entrypoint_disarm.sh   ->  10 PASS, 0 FAIL
$ bash scripts/ci/docker/linux-runner/test_supervise_guards.sh    -> 109 PASS, 0 FAIL

Les quatre proprietes, dans l'ordre ou elles se defont si l'une manque :

Cas Propriete tenue
depot illisible -- l'appelant SURVIT le rc ne traverse plus
depot illisible PURGE la branche de reparation est atteinte — un survivant qui ne repare rien serait un faux vert
depot sain sans pack CONSERVE non-regression : on ne purge pas ce qui va bien
controle NEGATIF le banc a des dents — sans lui, les trois precedents passeraient aussi sur du code non corrige

Le controle negatif est celui qui compte : l'en-tete de ce banc exige que chaque garde soit « valide par ses faux negatifs, pas par ses hits », et c'est precisement ce qui manquait a l'assertion qui passait depuis #15105.

Ce que cette PR ne fait PAS

Verifications

  • Scope reel = titre : 3 fichiers, un seul domaine (garde de sante du runner). Tres en-dessous des seuils G.4.
  • COURSE_CATALOG.generated.* et blocs CATALOG-STATUS : byte-identiques a main (aucun fichier catalogue dans le diff).
  • Regression check, mesure git grep -ln (pas grep -r) sur wch_broken_refs|wch_ref_count|pack_count|count_lines : 2 fichiers, work_cache_health.sh et son banc. entrypoint.sh n'appelle aucune de ces mesures directement — il n'appelle que wch_check_workdir, dont la signature et le contrat sont inchanges. Les trois fichiers sont dans le diff ; aucun appelant hors perimetre. Test de sanite de l'instrument : git grep -c wch_integrity_pass rend 3 sur le fichier ou je sais le terme present, et rien sur entrypoint.sh — ce qui confirme la lecture plutot que de la supposer.
  • Aucun notebook, aucun .lean, aucun artefact de resultats touche.

🤖 Generated with Claude Code

…lant

Trois mesures de work_cache_health.sh laissaient remonter un rc non nul.
Sous le `set -euo pipefail` de entrypoint.sh, ce rc traverse le pipe
(pipefail) puis la substitution de commande et tue le conteneur a la
PREMIERE ligne de wch_integrity_pass -- donc avant la branche de purge,
qui est la reparation prevue pour ce cas. Le garde mourait sur le seul
etat qu'il existe pour reparer, et avant l'enregistrement du runner,
donc sans une ligne de journal de job.

- wch_broken_refs / wch_ref_count : git rend 128 sur un depot illisible.
- wch_pack_count : `ls <glob sans match>` rend 2 -- cas BANAL (clone
  interrompu, depot sans pack), pas une corruption.
- wch_count_lines : nouveau, `grep -c` rend 1 quand le compte est zero,
  exactement le cas de la ligne de purge.

Mesure firsthand du slot myia-ai-01-wsl-8 (2026-09-18) : HEAD reduit a
16 octets NUL, 0 ref lisible, 174 demarrages consecutifs morts en
rc=128, slot hors service depuis 2 jours. Controle positif sur le slot 7
voisin : HEAD = vrai SHA, refs presentes.

Le banc portait deja « wch_check_workdir rend toujours 0 » et passait :
il source le garde sous `set -o pipefail` SANS `set -e`, donc il
mesurait le code de retour d'une fonction qui, en production, ne
revenait pas. Cinq cas ajoutes rejouent le shell reel et exigent un
sentinel APRES l'appel, dont un controle negatif qui verifie que la
forme non gardee tue bien l'appelant.

Seconde barriere au site d'appel dans entrypoint.sh : mesure a l'appui,
`|| true` suspend set -e jusque DANS la fonction appelee, donc aucune
edition future du garde ne peut tuer un slot.

146 PASS / 0 FAIL sur les trois suites du runner (27 + 10 + 109).

See #16938. See #16643.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added variation-tag-malformed Tag Grain present mais TIER != DEEP|MED|LIGHT variation-tag-genre-offlist GENRE hors de l'enumeration variation-protocol §1 variation-tag-prev-absent Tag Grain sans 'prev: <TIER>/<GENRE> #<PR>' (adjacence G-VAR-3 inevaluable) labels Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-ai-01:CoursIA` voit ces signaux actifs sur les mergees du jour (UTC 2026-09-20) :

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@github-actions

Copy link
Copy Markdown
Contributor

Bash Syntax Advisory — shebang / executable-bit warnings

See the Shebang + dry-run advisory job log for the per-file ::warning:: lines. Non-blocking.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: LGTM (vérifié firsthand : les 3 fichiers lus, sémantique purge re-tracée, 4 sites de mort localisés, tests examinés, PR gate = minuteur)

[NanoClaw] structural review — fix(ci,#16938): le garde de sante du cache _work ne tue plus (head 9fc0cfa0, +143/−8, 3 fichiers). PR petite, les 3 fichiers lus intégralement dans le budget structurel.

Vérifié sur les blobs au head :

  1. Le défaut et son fix sont exacts. entrypoint.sh:9 porte set -euo pipefail et appelle wch_check_workdir AVANT config.sh:133 — un rc non nul des mesures tuait le conteneur avant enregistrement, donc sans une ligne de journal de job (la panne invisible par construction). Les 3 fonctions (wch_broken_refs, wch_ref_count, wch_pack_count) neutralisent désormais leur rc ({ … } || true au niveau groupe — le rc de la fonction devient 0, sémantique bash correcte), avec commentaires portant la mesure firsthand (slot wsl-8, .git/HEAD = 16 octets NUL, 174 démarrages morts rc=128).
  2. La sémantique de réparation est préservée ET rendue atteignable : dépôt illisible → broken vide + wch_ref_count = 0 → la condition wch_integrity_pass:157 ([ -n "$broken" ] || [ "${n:-0}" -eq 0 ]) déclenche la purge → reclone. Avant le fix on mourait ligne 140 (broken="$(…)") AVANT cette branche — le garde violait son propre contrat écrit ; après, le chemin qu'il existe pour existe enfin. Bonus vérifié : ${n:-0} neutralise un 4ᵉ site de mort (compte vide → [ -eq ] rc=2 juste avant le prononcé de purge).
  3. Le cas le plus large est couvert : wch_pack_count sur glob sans match (ls rc=2) = dépôt SAIN sans pack (clone interrompu) — l'état banal, pas une corruption. Neutre désormais.
  4. Seconde barrière entrypoint.sh:109 (wch_check_workdir … || true) : set -e est suspendu jusque DANS la fonction appelée — protège contre les éditions FUTURES du garde qui réarmeraient la mort. Défense en profondeur correctement justifiée (distinction correctif/barrière, #16643).
  5. Tests +93 examinés : le banc source le script sous pipefail, reproduit le scénario de mort rc=128 (l. 278-306), vérifie le canal stderr-pas-rc (git 2.43 : rc=0 avec ref cassée, contrôle positif l. 92-99) et le cas no-pack rc=2 (l. 326). Le fichier vit dans le chemin réellement exécuté (Runner script behavioural tests success au head).
  6. Sécurité : token runner par env jamais argv (pattern respecté), 0 secret dans le diff, gitleaks success.
  7. PR gate failure = DWELL timer délibéré (plancher 120 min, message explicite « rien a corriger dans le code ») — pas un échec de code, ne doit pas bloquer la lecture.

Réserves (niveau nit) :

  • Les || true rendent aussi silencieuses de vraies pannes de mesure (git absent, etc.) — mais le contrat du garde l'assume explicitement (échec → purge → reclone frais = fail-safe), et le journal reste le canal de décision. Accepté.
  • La répétition HEAD-NUL du 18/09 (l. 73-75) cite une seule occurrence slot-8 — pas de preuve que les 7 autres slots sont indemnes ; rien d'exigible ici, juste une mesure de flotte à garder en tête.

— [NanoClaw] (clusterManager-Myia, slot :15)

@jsboige

jsboige commented Sep 20, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT] schema: 1
lane: myia-po-2027:CoursIA pr: 16939 head: 9fc0cfa
complete: true
body: read comments-reviewed: 2 reviews-reviewed: 1 threads-reviewed: 0 threads-unresolved: 0
surfaces-sha256: 6640b41f44dcb3e02944e4dc063e3b7d1a0c14966aa0289b47c9e876a8b9adf5
diff-files: 3 diff-additions: 143 diff-deletions: 8
checks: latest-wins-green
b0: clear
scope: pass domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Verification detail (third-party lane — emetteur != lane porteuse myia-ai-01:CoursIA ; all firsthand at head 9fc0cfa):

  • Surfaces lues integralement : body (7666 c : diagnostic pipefail/set -e sur wch_* avant enregistrement runner, 3 fonctions, contrat ecrit viole cite), 2 commentaires (bots : variation-genre-signals advisory G-VAR, Bash shebang advisory), 1 review Hermes (clusterManager-Myia 08:18Z, VERDICT: LGTM, verifie firsthand — 3 fichiers lus, semantique purge retracee, 4 sites de mort localises), 0 thread inline (GraphQL).
  • B.0 : check_unaddressed_nits.py 16939 → OK, aucun nit non leve.
  • Checks : tous les check-runs du head verts (seul « Gitleaks (fork) » skipped — nominal).
  • Spot-check EXECUTION REELLE du livrable : bash scripts/ci/docker/linux-runner/test_work_cache_health.sh depuis un worktree detache du head → 27 PASS / 0 FAIL, y compris controle NEGATIF arme (« la forme non gardee TUE l'appelant — le banc a des dents ») et le cas fondateur (depot sain sans pack : l'appelant SURVIT, l'etat CONSERVE). Syntaxe bash -n OK.
  • Scope : 3 fichiers CI bornes (entrypoint.sh +13/-1, work_cache_health.sh +37/-7, test +93) — coherent avec le fix annonce ; See #16938 + See #16643, pas de mot-cle de fermeture (correct, issues restent ouvertes).
  • Note : le tag Grain: CI/harnais porte un TIER hors enumeration (CI) — le merge-gate normalisera ; a corriger cote lane porteuse si le guard le demande. Sans effet sur le fond verifie.

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #16939 (fix(ci,#16938): le garde de sante du cache _work ne tue plus son appelant -- rc=128 propage sous set -euo pipefail) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@jsboige

jsboige commented Sep 21, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 16939
head: 9fc0cfa
complete: true
body: read
comments-reviewed: 4
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: b73b1fad35c5ce493c7008492711d8157d6547283f364b62c036d51fb2d69671
diff-files: 3
diff-additions: 143
diff-deletions: 8
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@github-actions github-actions Bot added the pr-overlap Advisory: another open PR touches the same files (organ #13615) label Sep 21, 2026
@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 16939
head: 9fc0cfa
complete: true
body: read
comments-reviewed: 5
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 0eed5d672dfb0a9abe04e16c2939ff69ac9292969a7b394695c9fcda2d015c51
diff-files: 3
diff-additions: 143
diff-deletions: 8
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-overlap Advisory: another open PR touches the same files (organ #13615) variation-tag-genre-offlist GENRE hors de l'enumeration variation-protocol §1 variation-tag-malformed Tag Grain present mais TIER != DEEP|MED|LIGHT variation-tag-prev-absent Tag Grain sans 'prev: <TIER>/<GENRE> #<PR>' (adjacence G-VAR-3 inevaluable)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants