Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions scripts/ci/merge_dwell.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,14 @@ def _default_branch(repo: str, fetch=_gh_json) -> str:
("scripts-tests.yml", "Scripts & Notebook-Tools Tests"),
)

#: #19180 : conclusions qui portent un verdict ROUGE sur main. `timed_out`
#: (le workflow a epuise son temps) et `startup_failure` (il n'a pas pu
#: demarrer) sont des rouges reels : main n'a pas rendu de vert. Seules les
#: conclusions SANS verdict -- `cancelled`, `skipped`, et toute autre valeur
#: hors des deux tuples -- se sautent (#19069).
MAIN_RED_CONCLUSIONS = ("failure", "timed_out", "startup_failure")
MAIN_VERDICT_CONCLUSIONS = ("success",) + MAIN_RED_CONCLUSIONS


def _main_red_motif(repo: str, fetch=_gh_json) -> "str | None":
"""#18686 + #18790 + #18796 : motif de rouge observable sur la branche
Expand All @@ -657,8 +665,10 @@ def _main_red_motif(repo: str, fetch=_gh_json) -> "str | None":
verdict (`cancelled`, `skipped` : `status=completed` les inclut, et en
passe de merge en rafale la concurrence du workflow annule les runs
intermediaires -- le dernier run rendu masquait alors le rouge reel,
#19069). Le premier run `success`/`failure` rendu est le verdict le
plus frais de ce workflow sur main, **independamment de son anciennete**
#19069). Le premier run a verdict rendu -- `success`, ou un rouge de
`MAIN_RED_CONCLUSIONS` (#19180 : `timed_out` et `startup_failure` en
sont) -- est le verdict le plus frais de ce workflow sur main,
**independamment de son anciennete**
(limite de la fenetre globale du commit de tete : un merge non lie aux
paths du workflow peut evict le run hors de la fenetre de 100 -- CR
ai-01 2026-10-02 18:55Z sur #18796).
Expand Down Expand Up @@ -718,13 +728,15 @@ def _main_red_motif(repo: str, fetch=_gh_json) -> "str | None":
# runs intermediaires et le dernier run rendu n'a alors AUCUN
# verdict : la derogation restait fermee alors que le dernier run
# reellement conclu sur main etait rouge. On saute les runs sans
# verdict et on prend le premier reellement conclu.
# verdict et on prend le premier reellement conclu. #19180 : un
# `timed_out` ou un `startup_failure` EST un verdict (rouge) ; le
# sauter remontait jusqu'au vert precedent et declarait main vert.
run = next(
(
r
for r in entries
if isinstance(r, dict)
and r.get("conclusion") in ("success", "failure")
and r.get("conclusion") in MAIN_VERDICT_CONCLUSIONS
),
None,
)
Expand All @@ -735,15 +747,20 @@ def _main_red_motif(repo: str, fetch=_gh_json) -> "str | None":
# PAS evoluer silencieusement le verdict : on ignore le run.
if run.get("name") != display_name:
continue
if run.get("conclusion") != "failure":
if run.get("conclusion") not in MAIN_RED_CONCLUSIONS:
continue
created = run.get("created_at") or ""
if latest is None or created > (latest.get("created_at") or ""):
latest = run
if latest is None:
return None
return "main rouge: workflow `{}` en echec sur {} (run {})".format(
conclusion = latest.get("conclusion")
# Le motif nomme la conclusion quand ce n'est pas un `failure` ordinaire,
# pour que la derogation reste justifiable a la relecture (#19180).
qualifier = "" if conclusion == "failure" else " ({})".format(conclusion)
return "main rouge: workflow `{}` en echec{} sur {} (run {})".format(
latest.get("name") or "?",
qualifier,
branch,
(latest.get("html_url") or "").rsplit("/", 1)[-1] or "?",
)
Expand Down
52 changes: 52 additions & 0 deletions scripts/tests/test_merge_dwell.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,58 @@ def test_19069_runs_tous_sans_verdict_ferme_la_derogation():
) is None


# --- #19180 : un run timed_out sur main est un rouge, pas un run sans verdict -

#: Fenetre de l'issue #19180 : le run le plus recent a epuise son temps
#: (`timed_out`), le precedent etait vert. Avant le correctif, le filtre
#: `success`/`failure` sautait le `timed_out` comme un `cancelled` et
#: remontait jusqu'au vert : main etait declare vert alors qu'il ne l'etait pas.
_TIMED_OUT_THEN_GREEN = [
{"id": 2002, "name": "Scripts & Notebook-Tools Tests",
"conclusion": "timed_out", "created_at": "2026-10-05T00:40:00Z",
"html_url": "https://github.com/o/r/actions/runs/2002"},
{"id": 2001, "name": "Scripts & Notebook-Tools Tests",
"conclusion": "success", "created_at": "2026-10-05T00:10:00Z",
"html_url": "https://github.com/o/r/actions/runs/2001"},
]


def test_19180_timed_out_puis_success_la_derogation_souvre():
motif = merge_dwell._main_red_motif(
"o/r", fetch=_runs_fetch(_TIMED_OUT_THEN_GREEN)
)
assert motif is not None
assert "2002" in motif
assert "timed_out" in motif


def test_19180_startup_failure_compte_aussi_rouge():
runs = [dict(_TIMED_OUT_THEN_GREEN[0], conclusion="startup_failure"),
_TIMED_OUT_THEN_GREEN[1]]
motif = merge_dwell._main_red_motif("o/r", fetch=_runs_fetch(runs))
assert motif is not None
assert "startup_failure" in motif


def test_19180_controle_negatif_cancelled_puis_success_reste_vert():
# Le comportement #19069 ne change pas : un `cancelled` n'a pas de
# verdict, il se saute, et le vert qui le precede ferme la derogation.
runs = [dict(_TIMED_OUT_THEN_GREEN[0], conclusion="cancelled"),
_TIMED_OUT_THEN_GREEN[1]]
assert merge_dwell._main_red_motif("o/r", fetch=_runs_fetch(runs)) is None


def test_19180_motif_failure_inchange():
# Le message d'un `failure` ordinaire garde sa forme d'avant #19180.
motif = merge_dwell._main_red_motif(
"o/r", fetch=_runs_fetch(_CANCELLED_THEN_RED)
)
assert motif == (
"main rouge: workflow `Scripts & Notebook-Tools Tests` en echec "
"sur main (run 37225392464)"
)


def test_head_committed_at_leve_sans_date():
with pytest.raises(merge_dwell.DwellError):
merge_dwell.head_committed_at(
Expand Down
Loading