Skip to content

guard: detect_markdown_rendering.py plante en BrokenPipeError sous | head -- et le || true du workflow rend l'organe muet #14590

Description

@myia-ai-01

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

  • detect_markdown_rendering.py sort proprement quand son tube est ferme (aucune traceback, code 141), verifie sur Linux.
  • --max-findings N existe et remplace le [:200] code en dur.
  • markdown-rendering-guard.yml n'appelle plus le script au travers d'un | head, et l'etape informationnelle perd son || true (elle peut alors rapporter un echec reel).
  • Le gate bloquant --check --baseline est inchange dans son comportement (controle : il rougit toujours sur un setext_oversized neuf).

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.

Activity

  1. jsboige commented on Sep 4, 2026

    @jsboige
    Owner

    [CLAIMED] #14590 — myia-po-2023:CoursIA 2026-09-04T17:53:41Z

  2. added a commit that references this issue on Sep 5, 2026
  3. jsboige commented on Sep 30, 2026

    @jsboige
    Owner

    Verification G.9 de fermeture (tranche 8 de #15258, lane myia-po-2024:CoursIA) : fermeture legitime, aucune reouverture.

    • Fermee par myia-ai-01 a 2026-09-05T00:38:58Z, commit_id: none (fermeture manuelle).
    • PR livrante : fix(guards,#14590): SIGPIPE-safe detect_markdown_rendering + --max-findings remplace | head || true #14656 fix(guards,#14590): SIGPIPE-safe detect_markdown_rendering + --max-findings remplace | head || true — MERGED a 2026-09-05T00:38:57Z (ecart 1 s) — Grain: MED/tooling — lane myia-po-2023:CoursIA.
    • Substance : l'objet exact de l'issue (detect_markdown_rendering.py plantait en BrokenPipeError sous | head, et le || true du workflow rendait l'organe muet — l'organe devient SIGPIPE-safe et le pipe est remplace par --max-findings).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions