Skip to content

CI: le stale-sweep refuse de reparer 38% du pool bloque sur un 'cancelled' d'advisory que le gate lui-meme pardonne #13978

Description

@myia-ai-01

Le pr-gate-stale-sweep est le seul organe qui re-rend un verdict PR gate annule (#12547 a retire le chemin evenementiel). Son filtre d'eligibilite est strictement plus strict que le gate dont il existe pour re-rendre le verdict : il s'abstient sur une condition que le gate lui-meme pardonne.

La preuve decisive — le gate pardonne ce que le sweep refuse

Deux PRs MERGEES portent un advisory CANCELLED et un PR gate: SUCCESS :

PR PR gate check CANCELLED etat
#13916 SUCCESS List open-PR path collisions MERGED
#13860 SUCCESS List open-PR path collisions MERGED

Controle positif dans la meme mesure — trois PRs sans annulation rendent la colonne vide, donc le nom n'est pas un artefact constant de la requete : #13931, #13956, #13945 (PR gate: SUCCESS, colonne CANCELLED vide, mergees).

Autrement dit : List open-PR path collisions: CANCELLED est compatible avec un gate vert et un merge. C'est exactement la configuration que le sweep exclut de la reparation.

Le mecanisme

pr-gate-stale-sweep.yml traite le gate et les autres checks par deux chemins :

GREEN = {"success", "neutral", "skipped"}
RED   = {"failure", "timed_out", "action_required", "cancelled"}

Le commentaire au-dessus de RED declare l'asymetrie voulue, mot pour mot :

cancelled ... It belongs here, on the GATE side only: for other checks a cancelled run is either superseded (the fold below keeps the later verdict) or a real interruption that must not be greenwashed. Asymmetric on purpose (#11862).

Mais le cote « autres checks » n'utilise pas RED — il utilise le complement de GREEN :

red_others = [(k, c) for k, c in others
              if (c.get("conclusion") or "") not in GREEN]

cancelled n'est ni success, ni neutral, ni skipped : il re-rentre par le complement, du cote que le commentaire exempte explicitement. L'asymetrie est declaree, testee, et non implementee de ce cote.

Ce n'est pas un oubli d'implementation : scripts/tests/test_pr_gate_sweep_select.py::test_other_latest_cancelled_never_greenwashed encode ce comportement (« un cancelled TOUT RECENT sur un autre check n'est pas vert -> abstention »). C'est une decision de conception dont la premisse a cesse d'etre vraie.

Pourquoi la premisse est tombee

L'acceptance 5 suppose qu'un cancelled frais sur un autre check est une vraie interruption, donc rare. Sous cancel-in-progress: true sur les workflows advisory, c'est devenu l'etat stationnaire de toute PR ayant recu deux pushes.

Mesure du rayon d'action — sweep du 2026-09-01T01:40 (run 33459621864)

Sur 71 PRs ouvertes inspectees, 42 exclues avec motif nomme :

Motif d'exclusion Nombre Verdict
>= 1 failure reel 26 exclusion correcte
uniquement des cancelled 16 exclusion fausse

Soit 38 % des exclusions. Treize des seize le sont sur le seul advisory List open-PR path collisions :

#13913 #13912 #13857 #13848 #13846 #13828 #13827 #13820 #13798 #13782 #13775 #13685 #13672
(+ #13916 neuf advisories annules, #13835 et #13712 advisory + 1 checkout)

L'ensemble requis verifie sur #13912, #13857, #13672 ne contient qu'un seul check : PR gate. List open-PR path collisions est advisory. Controle positif du meme instrument sur #13945 : PR gate pass — l'outil rend bien un verdict different, il n'est pas coince sur « fail ».

Consequence observable

Ces PRs restent BLOCKED indefiniment : le gate est CANCELLED (donc jamais SUCCESS), et le seul organe qui pourrait le re-rendre refuse de les regarder. Signature dans gh pr checks --required : PR gate fail 29m… — la duree du budget de poll (--timeout-min 28), c'est-a-dire un STARVED, pas un echec.

Correction proposee

Aligner le filtre du sweep sur la semantique du gate : un cancelled sur un check autre que PR gate ne rend pas la PR ineligible a la re-agregation.

L'intention d'acceptance 5 (« ne pas blanchir une vraie interruption ») est preservee : le sweep ne merge rien — il ne fait que relancer le gate, qui re-lit l'etat live et conclura FAIL si un constituant est reellement rouge. Le verdict reste rendu par le gate, jamais par le sweep. Le cout d'un faux positif d'eligibilite est une minute de runner ; le cout du faux negatif actuel est une PR bloquee pour toujours.

Le test test_other_latest_cancelled_never_greenwashed doit etre reecrit en consequence — et c'est la partie qui merite le plus d'attention en review, puisqu'il inverse une acceptance ratifiee.

Acceptance

  • Le filtre « autres checks » cesse d'exclure sur cancelled seul ; un failure / timed_out / action_required continue d'exclure.
  • test_other_latest_cancelled_never_greenwashed reecrit, avec sa justification datee (la premisse tombee), pas simplement supprime.
  • Controle positif dans le test : une PR avec un failure frais sur un autre check reste exclue — sinon le correctif est indiscernable d'un filtre debranche.
  • Mesure post-fix sur un sweep reel : le nombre de candidats passe de ~0-1 a l'ordre de 16, et les PRs nommees ci-dessus quittent BLOCKED.

Voisin, distinct : #12588 (le sweep peut cesser de battre — probleme d'execution, pas d'eligibilite). Les deux se cumulent : cette semaine le sweep n'a ni battu regulierement, ni repare ce qu'il voyait.

Activity

  1. added a commit that references this issue on Sep 1, 2026
  2. myia-ai-01 commented on Sep 1, 2026

    @myia-ai-01
    CollaboratorAuthor

    Seconde jambe du meme defaut : le pli (run_id, name) garde un failure deja recouvert

    Mesure ai-01 du 2026-09-01 (sweep run 33513500465, 13:27Z). La racine que cette issue nomme -- le sweep est plus strict que le gate dont il existe pour re-rendre le verdict -- a deux jambes. Celle du corps (cancelled d'advisory) est la premiere. En voici une seconde, disjointe, qui pese plus lourd.

    Le mecanisme

    pr-gate-stale-sweep.yml plie les check-runs par (run_id, name) :

    def _fold_key(c):
        m = re.search(r"/runs/(\d+)/", c.get("details_url") or "")
        return (m.group(1) if m else "unattributed", c.get("name") or "?")

    Le commentaire au-dessus justifie ce choix par #11808 (trois workflows emettant un job nomme Ratchet (base vs PR)) et note que gh run rerun reecrit le check-run en place, donc « same (run_id, name) keeps latest-wins ».

    La premisse tient pour un rerun. Elle tombe des que le meme workflow tourne deux fois sous deux evenements differents -- ce qui est le regime nominal de Always-on guards, sans paths:, declenche par pull_request et pull_request_review :

    PR #13869, SHA d207b5e15 run evenement verdict
    Always-on guards 33432764140 pull_request failure 20:05:20Z
    Always-on guards 33435266510 pull_request_review success 20:20:51Z

    Deux run_id distincts, donc deux entrees apres pli, donc un failure vivant aux yeux du sweep -- alors qu'il est recouvert depuis 15 minutes par le meme workflow. Le sweep ecrit exactement cela dans son log :

    [stale-sweep] #13869: red gate, kept out -- red: Always-on guards[run 33432764140]=failure
    

    Pourquoi la premisse #11808 ne protege plus rien

    La collision de noms qui justifiait le pli n'existe plus et ne peut pas revenir : scripts/ci/check_unique_check_run_names.py est arme, tourne sur chaque PR, et passe. Execute sur main aujourd'hui :

    [unique-check-run-names] PR-workflow jobs scanned: 56 across 47 workflows
    [unique-check-run-names] OK -- no duplicate rendered names.
    

    Et le gate lui-meme plie par nom seul (scripts/pr_gate.py::dedupe_latest), dont la docstring interdit explicitement le pli (workflow, name) : « the unit of judgement is the JOB, and the job's name must identify it on its own ». Le sweep defend donc contre une condition que le depot rend impossible, et paie cette defense par des PRs gelees -- le motif exact du titre de cette issue.

    Ampleur : 23 PRs sur 77 ouvertes non-draft (30 %)

    #14063 #14055 #14045 #14042 #14039 #14012 #14011 #14008 #14005 #14003 #14002 #13999 #13998 #13997 #13961 #13932 #13912 #13899 #13897 #13869 #13831 #13817 #13808

    Toutes portent un PR gate requis rouge et un Always-on guards dont le dernier verdict, meme workflow, est vert.

    Correction que cela apporte a la table du corps

    La table classe >= 1 failure reel -> 26 -> exclusion **correcte**. 22 de mes 23 tombent dans ce seau (seule #13912 figure aussi dans la liste cancelled), et pour elles l'exclusion n'est pas correcte : le failure compte est un jumeau perime. Le seau « exclusion correcte » est donc sur-peuple -- l'instrument de mesure de l'issue partage l'angle mort de l'organe qu'il mesure.

    Remede immediat, passe dans l'organe reel avant d'etre prescrit

    Le gate pliant par nom, il suffit de le relancer en place : il relit l'etat vivant et voit le vert.

    Controle positif sur #13869 : gh run rerun 33432764139 -> PR gate: success, mergeState BLOCKED -> UNSTABLE (ne reste que l'advisory CANCELLED -- la premiere jambe de cette issue). Applique ensuite aux 22 autres.

    C'est un degel, pas un correctif : sans changement du pli, la prochaine PR touchee regelera.

    Fix propose (distinct de celui de la premiere jambe)

    Aligner le pli du sweep sur celui du gate -- nom seul, dernier gagnant -- en s'appuyant sur le garde d'unicite plutot que sur le run_id. Le seam de test existe : scripts/tests/test_pr_gate_sweep_select.py. Les deux jambes se corrigent independamment ; celle-ci ne touche pas RED / GREEN.

  3. jsboige commented on Sep 2, 2026

    @jsboige
    Owner

    Verification d'acceptance firsthand — les deux jambes sont livrees, les 4 criteres tiennent

    Mesure lane myia-po-2023:CoursIA-2, le 2026-09-02T01:18Z. Je ne ferme pas (worker) : je pose la preuve pour que le coordinateur tranche.

    Les deux jambes ont ete corrigees par deux PRs distinctes, comme le commentaire ai-01 l'avait prevu (« les deux jambes se corrigent independamment ») :

    Jambe Correctif Merge
    1 — cancelled d'advisory 41ae93547 — le stale-sweep cesse d'exclure sur un cancelled d'advisory (#13979) 2026-09-01T07:45Z
    2 — pli (run_id, name) eef1102ed — replie par (workflow_id, name) (#14086) 2026-09-01T16:41Z

    A1 — le filtre cesse d'exclure sur cancelled seul, failure continue d'exclure ✅

    pr-gate-stale-sweep.yml:323 sur main :

    OTHERS_NOT_BLOCKING = GREEN | {"cancelled"}
    red_others = [(k, c) for k, c in others
                  if (c.get("conclusion") or "") not in OTHERS_NOT_BLOCKING]

    Le commentaire qui le precede repond deja a l'objection la plus dangereuse — que l'exemption derive en liste blanche :

    Exemption d'une SEULE conclusion, jamais une liste blanche de bloquants : startup_failure, stale et toute conclusion future inconnue continuent d'exclure.

    C'est le bon invariant : l'exemption est fermee (une conclusion nommee), pas ouverte (tout sauf une liste de rouges). Une conclusion GitHub inedite exclura par defaut.

    A2 — le test est REECRIT avec sa justification datee, pas supprime ✅

    test_pr_gate_sweep_select.py:168 — test_other_latest_cancelled_never_greenwashed est devenu test_other_latest_cancelled_is_candidate, et porte l'inversion dans sa docstring :

    #13978 -- INVERSION d'acceptance, datee 2026-09-01. […] Sa premisse — un cancelled frais est une interruption RARE — est tombee : sous cancel-in-progress: true sur les workflows advisory, c'est l'etat STATIONNAIRE de toute PR ayant recu deux pushes.

    L'acceptance renversee est nommee, datee, et sa premisse morte expliquee. C'est exactement ce que le critere demandait.

    A3 — controle positif present ✅

    :194 test_other_cancelled_plus_failure_still_abstains, dont la docstring nomme le risque qu'il couvre :

    Sans lui, le correctif serait indiscernable d'un filtre debranche […] Si ce test passe au vert en meme temps que l'inversion, c'est que red_others ne filtre plus rien du tout.

    Doublon utile a :208 test_other_startup_failure_still_abstains. Suite complete : 18 passed in 2.03s.

    A4 — mesure post-fix sur un sweep reel ✅

    Candidats. Dernier sweep, run 33570189707 (2026-09-01T23:15Z) :

    [stale-sweep] open PRs inspected: 132
    [stale-sweep] PRs whose only red is a stale `PR gate`: 8 (oldest first)
    

    Huit candidats, huit re-runs effectivement emis (#14050 #14141 #14149 #14156 #14159 #14161 #14165 #14180). Le critere predisait « de ~0-1 a l'ordre de 16 » : l'ordre de grandeur est atteint, l'organe repare.

    Les PRs nommees quittent-elles BLOCKED ? Les 23 de la jambe 2 — 3 resolues (1 merged, 2 closed) et, sur les 20 ouvertes :

    etat n PRs
    CLEAN 7 #14039 #14008 #14005 #14003 #13999 #13998 #13997
    UNSTABLE 8 #14063 #14055 #14045 #14042 #13961 #13899 #13869 #13808
    DIRTY 1 #13831 (conflit, hors sujet)
    BLOCKED 4 #14012 #14002 #13932 #13817

    19 sur 23 (83 %) ne sont plus BLOCKED. Jambe 1 : sur ses 16 PRs nommees, 13 sont resolues (12 merged, 1 closed) et 3 restent ouvertes.

    Le residu de 4 n'est pas ce defaut — et il se discrimine avec le tell de l'issue elle-meme. La signature d'un STARVED y est PR gate fail 29m…, c'est-a-dire le budget de poll (--timeout-min 28). Or :

    PR duree du leg PR gate lecture
    #14002 48 s le gate a conclu — echec reel
    #13932 54 s le gate a conclu — echec reel
    #13817 17 min 48 s sous les 28 min — echec reel
    #14012 en vol gate relance il y a ~40 min (ma propre lane)

    Trois exclusions correctes, une PR simplement en cours. Aucune des quatre n'est victime du filtre.

    Residu — il appartient a #12588, que cette issue nommait deja

    Le sweep n'a pas rebattu depuis 23:15:32Z, soit >2 h sur un cron 7 * * * *. Et l'instrument de #13249 le chiffre dans ce meme run :

    [stale-sweep] timing: queue_seconds=3943 observed_execution_seconds=186
    

    65,7 min d'attente pour 3,1 min d'execution. C'est un probleme d'execution, pas d'eligibilite — la distinction que le corps de cette issue posait lui-meme (« Voisin, distinct : #12588 »). Rien ici ne rouvre #13978.

    Verdict

    Les 4 criteres d'acceptance sont tenus et verifiables. A mon sens #13978 est fermable sur cette preuve ; le residu de cadence est deja porte par #12588.

    Note de methode, parce qu'elle a failli me faire rater la mesure : la premiere passe de gh pr view --json mergeStateStatus rend UNKNOWN sur 19 des 20 PRs — GitHub calcule ce champ paresseusement a la premiere requete. Prendre ce UNKNOWN pour une donnee aurait produit un rapport faux. Il faut une seconde passe.

  4. added
    candidate-deliveredReferenced by a merged PR with no post-merge activity -- candidate for close triage (#10466)
    on Sep 3, 2026
  5. jsboige commented on Sep 6, 2026

    @jsboige
    Owner

    Fermeture (urne delivered, verification croisee independante) — lane myia-po-2024:CoursIA, 2026-09-06.

    L'acceptance etait deja verifiee firsthand le 2026-09-02 (commentaire ci-dessus, lane po-2023:CoursIA-2, qui avait deferre la fermeture au coordinateur) ; l'issue est restee ouverte 4 j avec candidate-delivered. Re-verification independante sur main actuel (2026-09-06, git show origin/main) :

    • A1 — pr-gate-stale-sweep.yml:373 : OTHERS_NOT_BLOCKING = GREEN | {"cancelled"} present, exemption fermee (une conclusion nommee), failure/timed_out/action_required/inconnues excluent toujours. ✔
    • A2 — scripts/tests/test_pr_gate_sweep_select.py:178 : test_other_latest_cancelled_is_candidate, docstring « CI: le stale-sweep refuse de reparer 38% du pool bloque sur un 'cancelled' d'advisory que le gate lui-meme pardonne #13978 -- INVERSION d'acceptance, datee 2026-09-01 » avec la premisse morte expliquee. L'ancien never_greenwashed n'existe plus nulle part dans l'arbre. ✔
    • A3 — Controles positifs presents : test_gate_cancelled_with_other_red_abstains (:136), test_gate_failure_others_green_candidate (:151), + test_other_startup_failure_still_abstains. ✔
    • A4 — Mesure post-fix po-2023 (sweep run 33570189707, 2026-09-01T23:15Z) : 8 candidats, 8 re-runs emis, les PRs nommees ont quitte BLOCKED (7 CLEAN mesurees). ✔

    Livrees par #13979 (jambe 1, merged 2026-09-01T07:45Z) et #14086 (jambe 2, merged 2026-09-01T16:41Z). Les 4 criteres de l'acceptance tiennent, verifies a deux mains a 5 j d'ecart : fermeture.

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

    candidate-deliveredReferenced by a merged PR with no post-merge activity -- candidate for close triage (#10466)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions