detect_markdown_rendering.py n'est pas SIGPIPE-safe : quand son appelant borne la sortie (| head), il plante apres avoir rendu son verdict. Le workflow qui l'appelle ainsi avale le plantage avec un || true, donc l'organe est bruyant en CI et muet dans son statut.
La preuve, firsthand, dans le log CI
Run du garde markdown-rendering guard (main-repo notebooks) sur la PR #14139, 2026-09-04T01:34:24Z (etape Informational -- Quarto closure scan) :
--closure: render-list=1191 closure-targets=1586
scanned: MyIA.AI.Notebooks
violations: 447 total
Traceback (most recent call last):
File ".../scripts/notebook_tools/detect_markdown_rendering.py", line 1510, in <module>
raise SystemExit(main())
File ".../scripts/notebook_tools/detect_markdown_rendering.py", line 1490, in main
print(f" {flag}{f['severity'].upper():>5} {f['file']} cell#{f['cell']} [{f['rule']}]")
BrokenPipeError: [Errno 32] Broken pipe
Le verdict etait deja produit (447 violations + le breakdown par regle). Le plantage survient dans la boucle d'affichage des findings — head -8 a ferme le tube au bout de 8 lignes, le print suivant leve BrokenPipeError, et le script meurt sur une exception non geree.
Effet de bord visible dans le log : le breakdown par regle apparait entrelace dans la traceback (warn cjk_in_prose: 6 entre line 1510 et raise SystemExit). stdout est bloc-bufferise vers un tube, stderr ne l'est pas — la traceback part avant que le buffer stdout ne soit vide. Un lecteur du log voit un organe qui a l'air d'avoir explose au milieu de son propre verdict.
L'appelant
.github/workflows/markdown-rendering-guard.yml, derniere etape :
- name: "Informational -- Quarto closure scan (render-list + linked notebooks)"
if: always()
run: |
python scripts/notebook_tools/detect_markdown_rendering.py \
--closure --quarto-yml _quarto.yml MyIA.AI.Notebooks | head -8 || true
Le shell par defaut de GitHub Actions est bash -eo pipefail : sans le || true, pipefail remonterait le code de sortie de python et l'etape rougirait. Le || true la garde verte — et il garde verte exactement de la meme facon une sortie normale, une sortie qui trouve 447 violations, et un plantage. L'etape ne peut structurellement rien rapporter d'autre que success.
Le gate bloquant n'est pas touche : l'etape HARD gate appelle --check --baseline ... sans tube, elle rend son verdict proprement. Le rouge de #14139 etait un vrai setext_oversized, pas ce defaut. C'est bien une issue d'organe, pas un faux positif a lever.
Ce que ca coute concretement
Le message d'erreur qu'un agent recoit quand il suit la consigne du workflow lui-meme (Then re-run: python scripts/notebook_tools/detect_markdown_rendering.py --report <notebook>) et qu'il borne la sortie — reflexe standard sur un outil qui affiche jusqu'a 200 findings — est une traceback. Un plantage est indiscernable d'un outil casse : le prochain a le rencontrer perdra un cycle a chercher un bug dans le scan alors que le scan a bien fonctionne.
Note de reproduction — le defaut est propre au runner Linux
Je n'ai pas pu le reproduire sur ai-01 (Git Bash / Windows), ni sur --closure ... | head -8, ni sur --report <nb> | head -8, y compris sous set -o pipefail : rc=0, stderr vide, 408 lignes de sortie. La semantique de fermeture de tube de MSYS n'envoie pas l'EPIPE que CPython voit sur le runner Ubuntu.
Consequence pratique a ecrire dans l'issue plutot que de la laisser se redecouvrir : une lane Windows qui tente de reproduire ce bug conclura « pas de defaut » et fermera l'issue. La reproduction se fait sur le runner Linux (ou en conteneur Ubuntu), pas sur un poste Windows.
Le geste
1. Rendre le script tolerant au tube ferme (c'est la correction principale, elle vaut pour tous les appelants presents et futurs). Forme canonique CPython, a poser autour du corps de main() ou dans le __main__ :
try:
rc = main()
except BrokenPipeError:
# Le consommateur (head, less, |) a ferme le tube : ce n'est pas une erreur
# du scan. Rediriger stdout vers devnull pour que le flush de sortie
# d'interpreteur ne releve pas une seconde BrokenPipeError, puis sortir 141
# (128 + SIGPIPE), la convention shell.
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
rc = 141
raise SystemExit(rc)
La redirection vers devnull n'est pas cosmetique : sans elle, CPython rejoue la BrokenPipeError au flush final et reimprime un Exception ignored in: <_io.TextIOWrapper ...> sur stderr.
2. Donner au script le moyen de se borner lui-meme — --max-findings N (defaut : les 200 actuellement codes en dur l. 1488), pour que l'appelant n'ait plus besoin de head du tout. Le workflow devient :
python scripts/notebook_tools/detect_markdown_rendering.py \
--closure --quarto-yml _quarto.yml MyIA.AI.Notebooks --max-findings 8
et perd son | head -8 || true : plus de tube, plus de masque, l'etape rapporte enfin ce qu'elle mesure.
3. Controle positif obligatoire — un test qui echoue avant le correctif :
python scripts/notebook_tools/detect_markdown_rendering.py --closure \
--quarto-yml _quarto.yml MyIA.AI.Notebooks 2>&1 | head -3
doit rendre zero ligne de traceback sur un runner Linux. Le mesurer d'abord sans le patch (traceback presente), puis avec (absente). Un patch valide par « ca ne plante plus chez moi » sur Windows ne prouve rien — cf la note de reproduction ci-dessus.
Acceptance
Contexte
Precedent de la meme classe sur le meme organe : #11850 — ImportError (pyyaml absent du runner) avalee, le garde rougissait sur toute PR notebook sans dire pourquoi. Deux fois que ce script echoue de facon illisible parce que son mode d'echec est masque par le shell qui l'appelle.
Trouve en instruisant le rouge de #14139 (po-2026). Ce n'est pas un defaut de cette PR — le rouge y est un vrai setext_oversized, corrige par ailleurs.
detect_markdown_rendering.pyn'est pas SIGPIPE-safe : quand son appelant borne la sortie (| head), il plante apres avoir rendu son verdict. Le workflow qui l'appelle ainsi avale le plantage avec un|| true, donc l'organe est bruyant en CI et muet dans son statut.La preuve, firsthand, dans le log CI
Run du garde
markdown-rendering guard (main-repo notebooks)sur la PR #14139,2026-09-04T01:34:24Z(etape Informational -- Quarto closure scan) :Le verdict etait deja produit (
447 violations+ le breakdown par regle). Le plantage survient dans la boucle d'affichage des findings —head -8a ferme le tube au bout de 8 lignes, leprintsuivant leveBrokenPipeError, et le script meurt sur une exception non geree.Effet de bord visible dans le log : le breakdown par regle apparait entrelace dans la traceback (
warn cjk_in_prose: 6entreline 1510etraise SystemExit). stdout est bloc-bufferise vers un tube, stderr ne l'est pas — la traceback part avant que le buffer stdout ne soit vide. Un lecteur du log voit un organe qui a l'air d'avoir explose au milieu de son propre verdict.L'appelant
.github/workflows/markdown-rendering-guard.yml, derniere etape :Le shell par defaut de GitHub Actions est
bash -eo pipefail: sans le|| true,pipefailremonterait le code de sortie de python et l'etape rougirait. Le|| truela garde verte — et il garde verte exactement de la meme facon une sortie normale, une sortie qui trouve 447 violations, et un plantage. L'etape ne peut structurellement rien rapporter d'autre quesuccess.Le gate bloquant n'est pas touche : l'etape
HARD gateappelle--check --baseline ...sans tube, elle rend son verdict proprement. Le rouge de #14139 etait un vraisetext_oversized, pas ce defaut. C'est bien une issue d'organe, pas un faux positif a lever.Ce que ca coute concretement
Le message d'erreur qu'un agent recoit quand il suit la consigne du workflow lui-meme (
Then re-run: python scripts/notebook_tools/detect_markdown_rendering.py --report <notebook>) et qu'il borne la sortie — reflexe standard sur un outil qui affiche jusqu'a 200 findings — est une traceback. Un plantage est indiscernable d'un outil casse : le prochain a le rencontrer perdra un cycle a chercher un bug dans le scan alors que le scan a bien fonctionne.Note de reproduction — le defaut est propre au runner Linux
Je n'ai pas pu le reproduire sur ai-01 (Git Bash / Windows), ni sur
--closure ... | head -8, ni sur--report <nb> | head -8, y compris sousset -o pipefail:rc=0, stderr vide, 408 lignes de sortie. La semantique de fermeture de tube de MSYS n'envoie pas l'EPIPE que CPython voit sur le runner Ubuntu.Consequence pratique a ecrire dans l'issue plutot que de la laisser se redecouvrir : une lane Windows qui tente de reproduire ce bug conclura « pas de defaut » et fermera l'issue. La reproduction se fait sur le runner Linux (ou en conteneur Ubuntu), pas sur un poste Windows.
Le geste
1. Rendre le script tolerant au tube ferme (c'est la correction principale, elle vaut pour tous les appelants presents et futurs). Forme canonique CPython, a poser autour du corps de
main()ou dans le__main__:La redirection vers
devnulln'est pas cosmetique : sans elle, CPython rejoue laBrokenPipeErrorau flush final et reimprime unException ignored in: <_io.TextIOWrapper ...>sur stderr.2. Donner au script le moyen de se borner lui-meme —
--max-findings N(defaut : les200actuellement codes en dur l. 1488), pour que l'appelant n'ait plus besoin deheaddu tout. Le workflow devient :et perd son
| head -8 || true: plus de tube, plus de masque, l'etape rapporte enfin ce qu'elle mesure.3. Controle positif obligatoire — un test qui echoue avant le correctif :
python scripts/notebook_tools/detect_markdown_rendering.py --closure \ --quarto-yml _quarto.yml MyIA.AI.Notebooks 2>&1 | head -3doit rendre zero ligne de traceback sur un runner Linux. Le mesurer d'abord sans le patch (traceback presente), puis avec (absente). Un patch valide par « ca ne plante plus chez moi » sur Windows ne prouve rien — cf la note de reproduction ci-dessus.
Acceptance
detect_markdown_rendering.pysort proprement quand son tube est ferme (aucune traceback, code 141), verifie sur Linux.--max-findings Nexiste et remplace le[:200]code en dur.markdown-rendering-guard.ymln'appelle plus le script au travers d'un| head, et l'etape informationnelle perd son|| true(elle peut alors rapporter un echec reel).--check --baselineest inchange dans son comportement (controle : il rougit toujours sur unsetext_oversizedneuf).Contexte
Precedent de la meme classe sur le meme organe : #11850 —
ImportError(pyyaml absent du runner) avalee, le garde rougissait sur toute PR notebook sans dire pourquoi. Deux fois que ce script echoue de facon illisible parce que son mode d'echec est masque par le shell qui l'appelle.Trouve en instruisant le rouge de #14139 (po-2026). Ce n'est pas un defaut de cette PR — le rouge y est un vrai
setext_oversized, corrige par ailleurs.