Le fait
Un check-run peut porter conclusion: success et status: in_progress en meme temps, indefiniment. pr_gate.py classe alors ce check en pending et poll jusqu'a l'expiration de son budget (45 min), puis rend STARVED.
Mesure du 2026-09-06 sur #14967 (tete b30eb6ef) :
|
valeur |
| check-run |
Detect notebook changes, id 101568709885 |
| run parent |
Notebook Execution Required 34063718835 — status: completed, conclusion: success, updated_at 22:22:31Z |
| job |
started_at 22:20:17Z -> completed_at 22:20:27Z (10 s), conclusion: success |
status du job |
in_progress — encore a 23:50:39Z, soit 90 min apres son propre completed_at |
L'anomalie est confirmee sur deux endpoints independants (repos/.../commits/<sha>/check-runs et repos/.../actions/jobs/101568709885) : ce n'est pas une latence de coherence, c'est un enregistrement fige.
Consequence sur la PR : le PR gate 34063718782 demarre a 23:19:53Z avec --timeout-min 45 (deadline 00:04:53Z) et poll un job vert depuis 1 h. 64 des 65 autres check-runs etaient completed. Sans intervention, verdict STARVED sur une PR dont rien n'etait rouge.
La ligne
scripts/pr_gate.py:483
if status in STATUS_PENDING or not conclusion:
pending.append(name)
Le or court-circuite sur son premier membre : un conclusion terminal ne peut pas racheter un status fige. Or c'est conclusion qui est autoritatif — l'UI GitHub elle-meme affiche ce job en vert. La forme correcte est conclusion-first : une conclusion terminale clot le check, quel que soit status ; status ne decide que lorsque conclusion est nul.
Le piege de reparation (la partie non evidente)
Relancer l'agregateur ne repare rien : le PR gate re-lirait le meme enregistrement fige et re-poll 45 min. C'est le reflexe naturel — et il coute un second budget entier.
Ce qui repare, c'est de relancer le workflow ENFANT qui porte le check-run fige, pour qu'un enregistrement frais le supplante (dedupe_latest retient le dernier par nom) :
gh run rerun 34063718835 --repo jsboige/CoursIA # le run ENFANT, pas le PR gate
Mesure de la reparation sur #14967 : re-run lance 23:50Z -> nouveau check-run completed/success a 23:51:07Z -> PR gate settled success a 23:53:2xZ -> merge 23:53:43Z. Le budget de 45 min n'a pas ete re-paye.
Frequence — mesuree, pas supposee
Balayage des 30 PRs ouvertes a 23:55Z, comptant les check-runs status != completed AND conclusion != null sur chaque tete : 0. C'est donc un alea plateforme rare, pas un defaut recurrent. Ce ticket ne vaut pas par sa frequence mais par son cout unitaire (45 min de budget + un STARVED trompeur sur une PR verte) et par le fait que le geste de reparation est contre-intuitif.
# reconductible
for N in $(gh pr list --repo jsboige/CoursIA --state open --limit 30 --json number --jq '.[].number'); do
S=$(gh pr view $N --repo jsboige/CoursIA --json headRefOid --jq .headRefOid)
W=$(gh api "repos/jsboige/CoursIA/commits/$S/check-runs?per_page=100" \
--jq '[.check_runs[]|select(.status!="completed" and .conclusion!=null)]|length')
[ "$W" != "0" ] && echo "PR #$N wedged=$W"
done
Acceptance
classify traite une conclusion terminale comme terminale : un check {status: "in_progress", conclusion: "success"} tombe dans ok, pas dans pending ; {status:"in_progress", conclusion:"failure"} tombe dans bad. status ne decide que si conclusion est nul.
- Controle positif en test — le cas fige doit ECHOUER avant le correctif et passer apres : construire le jeu de check-runs contenant
{"name":"X","status":"in_progress","conclusion":"success"} et asserter X in ok. Sans cette assertion rouge-avant/vert-apres, le correctif n'est pas demontre (le test passerait deja sur un jeu ou tous les status sont coherents).
- Le message de
STARVED nomme, pour chaque constituant en attente, le couple status/conclusion observe — un STARVED -- timed out waiting for: Detect notebook changes ou le job est vert depuis une heure est indiscernable d'un vrai enlisement, et c'est ce qui a coute le diagnostic ici.
- Le geste de reparation (relancer le run ENFANT, jamais l'agregateur) est consigne la ou un operateur le cherchera.
Ce que ce ticket ne demande pas
Aucun changement de budget (--timeout-min), de cadence de poll, ni de settle-polls : le budget n'est pas en cause — il a ete consomme a attendre un evenement qui n'arriverait jamais.
Le fait
Un check-run peut porter
conclusion: successetstatus: in_progressen meme temps, indefiniment.pr_gate.pyclasse alors ce check en pending et poll jusqu'a l'expiration de son budget (45 min), puis rendSTARVED.Mesure du 2026-09-06 sur #14967 (tete
b30eb6ef) :Detect notebook changes, id101568709885Notebook Execution Required34063718835 —status: completed,conclusion: success,updated_at 22:22:31Zstarted_at 22:20:17Z->completed_at 22:20:27Z(10 s),conclusion: successstatusdu jobin_progress— encore a 23:50:39Z, soit 90 min apres son proprecompleted_atL'anomalie est confirmee sur deux endpoints independants (
repos/.../commits/<sha>/check-runsetrepos/.../actions/jobs/101568709885) : ce n'est pas une latence de coherence, c'est un enregistrement fige.Consequence sur la PR : le
PR gate34063718782 demarre a 23:19:53Z avec--timeout-min 45(deadline 00:04:53Z) et poll un job vert depuis 1 h. 64 des 65 autres check-runs etaientcompleted. Sans intervention, verdictSTARVEDsur une PR dont rien n'etait rouge.La ligne
scripts/pr_gate.py:483Le
orcourt-circuite sur son premier membre : unconclusionterminal ne peut pas racheter unstatusfige. Or c'estconclusionqui est autoritatif — l'UI GitHub elle-meme affiche ce job en vert. La forme correcte est conclusion-first : une conclusion terminale clot le check, quel que soitstatus;statusne decide que lorsqueconclusionest nul.Le piege de reparation (la partie non evidente)
Relancer l'agregateur ne repare rien : le
PR gatere-lirait le meme enregistrement fige et re-poll 45 min. C'est le reflexe naturel — et il coute un second budget entier.Ce qui repare, c'est de relancer le workflow ENFANT qui porte le check-run fige, pour qu'un enregistrement frais le supplante (
dedupe_latestretient le dernier par nom) :gh run rerun 34063718835 --repo jsboige/CoursIA # le run ENFANT, pas le PR gateMesure de la reparation sur #14967 : re-run lance 23:50Z -> nouveau check-run
completed/successa 23:51:07Z ->PR gatesettled success a 23:53:2xZ -> merge 23:53:43Z. Le budget de 45 min n'a pas ete re-paye.Frequence — mesuree, pas supposee
Balayage des 30 PRs ouvertes a 23:55Z, comptant les check-runs
status != completed AND conclusion != nullsur chaque tete : 0. C'est donc un alea plateforme rare, pas un defaut recurrent. Ce ticket ne vaut pas par sa frequence mais par son cout unitaire (45 min de budget + unSTARVEDtrompeur sur une PR verte) et par le fait que le geste de reparation est contre-intuitif.Acceptance
classifytraite une conclusion terminale comme terminale : un check{status: "in_progress", conclusion: "success"}tombe dansok, pas danspending;{status:"in_progress", conclusion:"failure"}tombe dansbad.statusne decide que siconclusionest nul.{"name":"X","status":"in_progress","conclusion":"success"}et asserterX in ok. Sans cette assertion rouge-avant/vert-apres, le correctif n'est pas demontre (le test passerait deja sur un jeu ou tous lesstatussont coherents).STARVEDnomme, pour chaque constituant en attente, le couplestatus/conclusionobserve — unSTARVED -- timed out waiting for: Detect notebook changesou le job est vert depuis une heure est indiscernable d'un vrai enlisement, et c'est ce qui a coute le diagnostic ici.Ce que ce ticket ne demande pas
Aucun changement de budget (
--timeout-min), de cadence de poll, ni desettle-polls: le budget n'est pas en cause — il a ete consomme a attendre un evenement qui n'arriverait jamais.