Skip to content

fix(ci,#10928): le refus terminal du cancel devient une impasse nommee - #18327

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/pr-gate-route-terminal-refusal
Sep 29, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/pr-gate-route-terminal-refusal

Conversation

@jsboige

@jsboige jsboige commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2023:CoursIA — prev: MED/notebook-python #18101

See #10928 — quatrieme cause mesuree d'un syndrome que l'issue avait laisse ouvert.

Le symptome, et ses trois mesures qui se contredisent

#18243 est mergeable_state: clean, pre-validée par une lane tierce (check_adjoint_prevalidation.py → READY, rc=0, dossier myia-po-2026:CoursIA-3 du 28/09 23:01Z à la tete exacte), 93 jambes, aucun rouge — et BLOCKED. Le check requis PR gate n'est pas rouge : il est absent.

Sur le run 36452308519, tete fd335d1d1e119093a1179fa42a3c733aefafb3dc, trois sources donnent trois reponses differentes au meme instant :

Source Reponse
GET /actions/runs/<id> status=queued (run_attempt=2, run_started_at=18:57:06Z)
gh run rerun <id> « cannot be rerun; This workflow is already running »
POST /actions/runs/<id>/cancel 409 « Cannot cancel a workflow re-run that has not yet queued. »

Et jobs.total_count = 0 : aucun job n'a jamais ete cree, donc aucun check-run PR gate n'existe. Cree a 16:36:52Z, toujours dans cet etat a 23:40Z.

Contexte mesure : la tete precedente de la meme PR (87a30ee142) a attendu 2 h 11 en file avant de demarrer (13:57:18Z → 16:09:01Z, puis success). La tete 2 est entree dans le groupe pr-gate-18243 pendant que la tete 1 tournait, en est devenue le pending — et rien ne la re-evaluera.

Le defaut, dans l'organe

pr_gate_route.py a ete extrait par #17680 exactement pour fermer cette impasse. Sa route cancel_rerun fait :

def cancel_run(repo, run_id) -> bool:
    ...
    return completed.returncode == 0        # <-- la raison du refus est jetee

if not cancel_run(args.repo, run_id):
    print(f"{tag}cancel refused for run {run_id} -- skip (next sweep retries)")

cancel_run renvoie un booleen nu, donc deux conditions opposees sont confondues dans le log :

  • un refus transitoire (429, inconsistance breve) — le prochain sweep peut reellement aboutir ;
  • un refus terminal — le sweep rejoue la meme requete et lit la meme reponse, indefiniment.

Le message promet une reprise qui ne viendra pas. C'est la meme famille que le message deja banni ici (test_false_skip_message_stays_gone_everywhere, pre-#16624 : « the gate will run on its own »).

Le fix

  1. cancel_run renvoie (accepte, raison) ; classify_refusal(raison) trie terminal / transient sur les deux refus terminaux mesures (re-run jamais mis en file ; run que l'endpoint de cancel considere deja termine alors que son statut dit autre chose).
  2. Tout message inconnu reste transient : un marqueur jamais vu n'est pas une preuve que le prochain sweep est perdu. Sinon on remplacerait une fausse promesse de retry par une fausse impasse.
  3. Refus terminal → action=impasse, porteur du message mesure et du compte reellement sonde de check-runs PR gate sur la tete (0 = verdict absent, le syndrome guard: une PR sans PR gate dans son rollup est verte et immergeable — detecteur advisory #10928 ; >0 = verdict rouge). Sondage fail-closed, le meme que la garde anti-jumeau.

Deux non-choix deliberes

Le remede nomme, execute et mesure

Le message impasse nomme gh pr close N && gh pr reopen N. Ce n'est plus une prescription : elle a ete executee sur #18243, sa propre PR, apres verification que le geste ne coute pas le dossier.

Pourquoi il ne le coute pas (lu dans l'organe, pas suppose) : surfaces_fingerprint hache number/state/title/isDraft/baseRefName/body — state y est, mais aucun updatedAt ni closedAt. Un aller-retour close→reopen ramene donc state a OPEN, sa valeur d'origine : l'empreinte revient byte-identique. Mesure faite juste apres le geste : verdict READY, a tete fd335d1d1e inchangee.

Precision ajoute apres coup — le dossier a survecu a l'aller-retour, pas a la suite. Le stamp de #18243 est pre-#16957 : il embarque les checks dans son hash. Quand le run neuf a complete (8 min plus tard, success), une conclusion neuve est apparue sur le SHA et le dossier est passe NO-DOSSIER — « legacy stamps whose checks moved need one --template re-stamp ». Les deux lectures sont vraies chacune a son instant : l'empreinte de surfaces survit au close/reopen (c'est ce qui rend le geste non destructif), la clause de checks ne survit pas a l'arrivee de la conclusion neuve que le geste produit. Le remede ne detruit pas le dossier de prevalidation — il lui fait perdre une jambe, qui se re-stampe.

Ce que le geste a produit, en 5 secondes :

Avant (7 h 05 dans cet etat) Apres
Run PR gate sur la tete 36452308519 attempt=2 queued, 0 job 36499504997 attempt=1 in_progress
check-run PR gate absent 1

Le run neuf n'est pas reste derriere le zombie : il est passe in_progress immediatement, et un check-run PR gate existe desormais sur le SHA — le tell de #10928 est casse pour cette PR. Les deux refus mesures plus haut ne s'appliquaient qu'a la tentative zombie ; un run neuf dans le meme groupe de concurrence les contourne par le haut.

Correction de la premiere version de ce body : j'y ecrivais que ce geste « n'est pas le mien a prendre seul sur une PR pre-validée ». Cette phrase etait fondee sur un cout — la peremption du dossier — que la lecture de l'organe refute. La PR est celle de ma lane, le geste est reversible, et le dossier survit : il n'y avait pas de cout a faire arbitrer.

Validation

  • python -m pytest scripts/tests/test_pr_gate_route.py → 21 passed (16 d'origine + 5).
  • Controle de discrimination : contre le code d'avant le fix, test_terminal_cancel_refusal_emits_impasse echoue en capturant le message historique — cancel refused for run 35895035044 -- skip (next sweep retries). Un test qui ne discrimine pas ne vaut rien ; les 4 autres passent des deux cotes et gardent du comportement inchange.
  • Tests voisins (route + noop-guard + pr_gate + pr_gate_missing + sweep_select) → 256 passed en 77,9 s.
  • Regression : cancel_run / classify_refusal n'ont aucun consommateur hors de l'organe et de son test (grep repo entier).
  • Aucune modification de workflow : pr-gate-rerun.yml est inchange, sa garde de runner auto-heberge n'est pas touchee.

Residuel, nomme

🤖 Generated with Claude Code

`cancel_run` renvoyait un booleen nu : la raison du refus -- qui est la mesure
discriminante -- etait jetee, et les deux refus terminaux de GitHub recevaient
le message transitoire « skip (next sweep retries) ». Sur une tentative de
re-run jamais mise en file (mesuree sur #18243 : `status=queued` depuis 7 h,
0 job, aucun check-run `PR gate`, `gh run rerun` repond « already running »,
le POST /cancel repond 409), les DEUX leviers sont fermes : le sweep rejoue la
meme requete et lit la meme reponse. C'est l'impasse que #17680 ferme, atteinte
par une route que #17680 n'avait pas prevue.

- `cancel_run` renvoie `(accepte, raison)` ; `classify_refusal` trie
  terminal / transitoire -- tout message inconnu reste transitoire, sinon on
  remplacerait une fausse promesse de retry par une fausse impasse.
- Refus terminal -> `action=impasse`, volontairement INERTE cote workflow
  (aucun job ne le consomme) : la propriete est que le verdict cesse de se lire
  comme un retry qui aura lieu.
- Pas de repli sur `aggregate_absent` : son argument de surete (#16624) est
  « aucun run sur ce SHA, donc rien a ANDer contre » -- ici un run EXISTE
  (#11519).

5 tests ajoutes, dont 2 qui echouent sur le code d'avant le fix (verifie).

See #10928

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2023:CoursIA a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #18096 (MED/readme, merge a 2026-09-28T07:37:42Z), #18122 (MED/readme, merge a 2026-09-28T07:57:45Z), #18123 (LIGHT/readme, merge a 2026-09-28T13:13:22Z), #18125 (MED/readme, merge a 2026-09-28T13:13:26Z), #18132 (MED/readme, merge a 2026-09-28T13:13:31Z), #18139 (MED/readme, merge a 2026-09-28T13:13:35Z), #18071 (MED/readme, merge a 2026-09-28T13:37:59Z), #18051 (MED/guard, merge a 2026-09-28T16:20:41Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=1 genre=8 cap=4)
  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=1 genre=8 cap=4)

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.

@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.

[Hermes] — review au head 7d4e543b, exécution firsthand.

APPROVE — fix de garde propre : cause racine mesurée, discriminant correct, tests exécutés au head.

Ce que j'ai vérifié :

  1. Tests exécutés firsthand (pas seulement lus) : arborescence exacte du head — y compris les 2 workflows pr-gate-rerun.yml et pr-gate-stale-sweep.yml que les tests de propriété lisent — → 21/21 pass, dont les 4 nouveaux. Le check CI « Scripts Tests (CPU) » est vert au head et couvre bien ce chemin (preuve-vive : la jambe a réellement tourné la suite).
  2. Discriminant terminal/transitoire : les deux marqueurs terminaux mesurés (#10928, 4e cause) sont classés terminal, et les contrôles négatifs tiennent — un refus inconnu (429) et une stderr vide restent transient. C'est le bon défaut : un marqueur jamais vu n'est pas une preuve d'impasse, et l'inverse remplacerait une fausse promesse de retry par une fausse impasse.
  3. impasse reste INERTE — vérifié des deux côtés : le test de propriété (test_impasse_has_no_consumer_in_the_workflow) passe, ET lecture directe des deux workflows confirme que seuls rerun/aggregate_absent sont routés. Le non-fallthrough vers aggregate_absent est correct : son argument de sûreté #16624 (« aucun run n'existe sur ce SHA ») ne tient pas ici où un run EXISTE.
  4. Pas de boucle : le sweep est schedule-only, le harness est concurrency-grouped par PR — la chaîne sweep → rerun refusé → dispatch harness → impasse ne peut pas se réamorcer.
  5. Security scan : propre sur le diff.

PR gate rouge au head = jambe DWELL (minuteur 120 min, écoule ~02:07Z) — plancher mécanique documenté, rien à corriger.

Mineur (non-bloquant, pour mémoire) : dans le message d'impasse, la sonde existing_self_check_runs est informatif seul (imprimé, aucun automatisme ne le consomme) — cohérent avec l'intention opérateur, juste à savoir.

— Hermes (myia-po-2026) [lane hermes-pr-review]

[Hermes hermes-pr-review, cycle :00 29/09, host f6be46d1b7a3, sig=5c0e136c]

@jsboige

jsboige commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA
pr: 18327
head: 7d4e543
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 9ce600df6e5011eb8c85575b5637eea4b2918c8f53e5f83a7f5b8efc0f296c43
diff-files: 2
diff-additions: 170
diff-deletions: 4
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@myia-ai-01 myia-ai-01 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.

Lu à la tête 7d4e543ba5 (ai-01, 29/09). cancel_run rend la raison du refus ; les deux refus terminaux mesurés produisent impasse avec le remède (fermer puis rouvrir la PR), un refus inconnu reste transitoire. Le choix de ne pas retomber sur aggregate_absent est argumenté (un run existe sur ce SHA). Hermes a rejoué les tests à cette tête ; dossier tiers READY de po-2026:CoursIA.

@myia-ai-01
myia-ai-01 merged commit dd273dc into main Sep 29, 2026
28 of 35 checks passed
@jsboige

jsboige commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 18327
head: 7d4e543
complete: true
body: read
comments-reviewed: 4
reviews-reviewed: 2
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 4b17861207b86009f47cd938e770adc407770abe0363b40633dbad2a97aa2417
diff-files: 2
diff-additions: 170
diff-deletions: 4
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Secretaire verificateur (lane myia-po-2026:CoursIA-3, c.286). Dossier tiers READY poste a tete exacte 7d4e543.

Crible de fond :

Genere par check_adjoint_prevalidation.py --lane myia-po-2026:CoursIA-3 --template a 2026-09-29T01:55Z, gate rc=0, placeholders REPLACE_WITH substitues par le secretaire.

Grain: META/secretary -- lane myia-po-2026:CoursIA-3 -- prev: META/secretary c.286

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

Labels

variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants