From 93164cc45aafb59f933b8e360f8781680bf84f69 Mon Sep 17 00:00:00 2001 From: jsboige Date: Sat, 12 Sep 2026 14:18:02 +0200 Subject: [PATCH 1/2] fix(ci,#15758): pr_gate_missing -- le bot s'ecrit selon l'API, les deux sites partageaient la mauvaise orthographe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Residuel de #15621, survivant au correctif merge #15622 : le collecteur lit l'auteur en REST (`github-actions[bot]`), le consommateur comparait la constante GraphQL `app/github-actions`. L'egalite etait fausse pour TOUTE PR du bot, donc `bot_missing` (verdict) et `bot` (cause) etaient structurellement inatteignables -- la PR du bot etait publiee « gate absent, cause inconnue » avec « investigation manuelle » reclamee. - `BOT_LOGINS` + `is_bot_author()` : point de comparaison UNIQUE, les trois orthographes mesurees sur #15678, comme les autres organes qui lisent un auteur (`guard_comment_upsert.py`, `pick_idle_grain.py`, `review_coverage.py`). - Les deux sites de comparaison (`classify`, `prescribe`) l'appellent. - Le detail nomme l'orthographe MESUREE, jamais une chaine supposee. - Tests : les trois orthographes ; le chemin collecteur -> consommateur avec la valeur REST reelle ; et un contrat du PRODUCTEUR (le collecteur lit `.user.login`) -- c'est ce contrat qui manquait, les tests passaient la valeur de la constante du consommateur a la main. Controle positif : contre la source d'origine, `github-actions[bot]` rend `missing` et `prescribe` rend `unknown` ; les deux verdicts attendus sont donc bien absents avant ce commit. Closes #15758. See #15621, See #15622. Co-Authored-By: Claude Sonnet 5 --- scripts/pr_gate_missing.py | 52 +++++++++++++++---- scripts/tests/test_pr_gate_missing.py | 73 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/scripts/pr_gate_missing.py b/scripts/pr_gate_missing.py index 44d3a3133f..d064b7de35 100644 --- a/scripts/pr_gate_missing.py +++ b/scripts/pr_gate_missing.py @@ -13,9 +13,10 @@ - #10898 : the head commit's SUBJECT contained the literal ``[skip ci]`` -- GitHub skipped every ``pull_request`` workflow (only CodeQL ran). Fixed by a re-push whose message does not carry the token. - - #10558 : PR opened by the bot (``app/github-actions``). A push made with - ``GITHUB_TOKEN`` does not create a new workflow run (GitHub anti-recursion) - -- structural, by design, but nowhere written. + - #10558 : PR opened by the bot (``github-actions[bot]`` in REST, + ``app/github-actions`` in GraphQL -- both recognised since #15758). A push + made with ``GITHUB_TOKEN`` does not create a new workflow run (GitHub + anti-recursion) -- structural, by design, but nowhere written. - #10902 : unknown cause; the PR was ``DIRTY`` and its rebase re-triggered CI. This tool is an ADVISORY organ, never blocking (it cannot block: the missing @@ -79,7 +80,31 @@ # required by main's branch protection. Renaming here silently detaches the # detector (same invariant as pr-gate.yml: keep the string stable). GATE_NAME = "PR gate" -BOT_LOGIN = "app/github-actions" + +# Le bot du depot n'a pas UNE orthographe : elle depend de l'API qui le rend. +# Mesure #15758 (2026-09-12, sur #15678) : REST -- la source du collecteur, +# `/pulls` puis `.user.login` -- ecrit `github-actions[bot]` ; GraphQL (`gh pr +# view --json author`) ecrit `app/github-actions` ; le champ brut +# `author.login` ecrit `github-actions`. La constante unique d'origine portait +# l'orthographe GraphQL pendant que le collecteur lisait en REST, donc +# l'egalite etait fausse pour TOUTE PR du bot : `bot_missing` et la cause +# `bot` etaient structurellement inatteignables. Les trois orthographes +# mesurees sont ici, comme dans les autres organes qui lisent un auteur +# (`ci/guard_comment_upsert.py` GUARD_BOT_LOGINS, `pick_idle_grain.py` +# AUTOMATION_AUTHORS, `review_coverage.py`). +BOT_LOGINS = frozenset({"app/github-actions", "github-actions[bot]", + "github-actions"}) + + +def is_bot_author(login: object) -> bool: + """L'auteur est-il le bot du depot, quelle que soit l'orthographe rendue ? + + Point de comparaison UNIQUE : les deux sites (`classify`, verdict + `bot_missing`, et `prescribe`, cause `bot`) l'appellent, donc une + quatrieme orthographe ne peut pas n'en corriger qu'un seul. + """ + return (login or "") in BOT_LOGINS + # Marker framing the advisory comment, so re-runs can find and update it. COMMENT_MARKER_START = "" @@ -146,7 +171,9 @@ ) REMEDIATION_BOT = ( - "PR ouverte par le bot (`app/github-actions`) sans `PR gate` dans son " + "PR ouverte par le bot du depot (son login est nomme ci-dessus ; il " + "s'ecrit `github-actions[bot]` en REST et `app/github-actions` en GraphQL) " + "sans `PR gate` dans son " "rollup : cas **structurel** (issue #10928). Un push fait avec " "`GITHUB_TOKEN` ne cree pas de nouveau workflow run (regle anti-recursion " "GitHub), donc le contexte requis ne sera jamais rapporte par un push du " @@ -196,8 +223,10 @@ def classify(pr: dict) -> tuple[str, str]: return ("draft", f"#{number} draft PR, non mergeable") if GATE_NAME in rollup_names(pr): return ("has_gate", f"#{number} PR gate present (conclusion: {len(rollup_names(pr))} checks)") - if pr.get("author_login") == BOT_LOGIN: - return ("bot_missing", f"#{number} bot PR, no PR gate (structural)") + if is_bot_author(pr.get("author_login")): + return ("bot_missing", + f"#{number} bot PR, no PR gate (structural; auteur " + f"{pr.get('author_login')})") return ("missing", f"#{number} PR gate absent du rollup") @@ -224,7 +253,8 @@ def prescribe(pr: dict) -> tuple[str, str]: retarget -> un ``base_ref_changed`` postérieur au dernier run ``pull_request`` : remede = commit vide a arbre identique skip_ci -> token ``[skip ci]`` dans le sujet de tete (cause #10898) - bot -> PR ``app/github-actions`` (cause #10558) + bot -> auteur = bot du depot, quelle que soit l'orthographe + rendue (cause #10558 ; multi-orthographe #15758) unknown -> aucune des quatre : nommer les mesures, ne rien prescrire Returns: @@ -240,8 +270,10 @@ def prescribe(pr: dict) -> tuple[str, str]: f"base_ref_changed={changed}, dernier run PR gate={last or 'aucun'}") if "[skip ci]" in head_subject(pr): return ("skip_ci", f"sujet de tete porte le token [skip ci] : {head_subject(pr)[:72]!r}") - if pr.get("author_login") == BOT_LOGIN: - return ("bot", "auteur app/github-actions -- push GITHUB_TOKEN sans run") + if is_bot_author(pr.get("author_login")): + return ("bot", + f"auteur {pr.get('author_login')} (bot du depot) -- " + "push GITHUB_TOKEN sans run") return ("unknown", f"mergeable_state={ms}, pas de base_ref_changed, sujet sans [skip ci], " f"auteur {pr.get('author_login')}") diff --git a/scripts/tests/test_pr_gate_missing.py b/scripts/tests/test_pr_gate_missing.py index b79ac93e21..f6be4d1b9a 100644 --- a/scripts/tests/test_pr_gate_missing.py +++ b/scripts/tests/test_pr_gate_missing.py @@ -37,6 +37,7 @@ rollup_names, list_open_prs, has_label, + is_bot_author, GATE_NAME, LABEL_BOT_DESC, LABEL_CONFLICT_DESC, @@ -314,6 +315,78 @@ def test_collector_output_carries_author_and_labels(): assert not has_label(rows[0], "pr-gate-conflict") +# --- #15758 : le bot du depot n'a pas UNE orthographe ----------------------- +# +# Mesure firsthand sur #15678 (2026-09-12) : +# REST `gh api repos/jsboige/CoursIA/pulls/15678 --jq '.user.login'` +# -> github-actions[bot] +# GraphQL `gh pr view 15678 --json author` -> app/github-actions +# GraphQL `author.login` (brut) -> github-actions +# +# Le collecteur lit en REST, donc la PREMIERE est ce que `classify_input` range +# sous `author_login`. La constante unique d'origine portait la deuxieme : la +# comparaison etait fausse pour toute PR du bot, et `bot_missing` comme la cause +# `bot` etaient inatteignables (#15758). +REST_BOT = "github-actions[bot]" +GQL_BOT = "app/github-actions" +RAW_BOT = "github-actions" + + +def test_bot_verdict_for_every_measured_spelling(): + for spelling in (REST_BOT, GQL_BOT, RAW_BOT): + verdict, why = classify(_pr(10558, author=spelling, + rollup=_codeql_only_rollup())) + assert verdict == "bot_missing", f"{spelling!r} -> {verdict} ({why})" + + +def test_prescribe_names_the_bot_cause_for_the_rest_spelling(): + # L'autre site de comparaison : c'est `prescribe` qui choisit le label et le + # commentaire. Le defaut les rendait muets tous les deux. + cause, detail = prescribe(_candidate(10558, author=REST_BOT)) + assert cause == "bot", detail + assert REST_BOT in detail, detail + + +def test_human_author_is_not_absorbed_by_the_bot_predicate(): + for author in ("jsboige", "myia-ai-01", ""): + assert not is_bot_author(author), author + assert prescribe(_candidate(10558, author=author))[0] == "unknown", author + assert classify(_pr(10558, author="jsboige", + rollup=_codeql_only_rollup()))[0] == "missing" + + +def test_collector_contract_reads_the_login_in_rest(): + # Contrat du PRODUCTEUR. Rien n'epinglait l'orthographe produite : les tests + # passaient `app/github-actions` a la main -- la valeur de la constante du + # consommateur, jamais celle que le collecteur rend vraiment. Changer la + # source d'auteur (REST -> GraphQL) change l'orthographe rendue ; ce test + # doit rougir a ce moment-la plutot que suivre en silence. + seen = {} + + def fake_rows(args): + seen["args"] = args + return [] + + with mock.patch("pr_gate_missing._gh_rows", fake_rows): + list_open_prs("jsboige/CoursIA") + args = seen["args"] + jq = args[args.index("--jq") + 1] + assert "author: .user.login" in jq, jq + assert "author: .author.login" not in jq, jq + assert "state=open" in args[1] and "repos/" in args[1], args[1] + + +def test_collector_emits_the_rest_spelling_end_to_end(): + # Chemin reel producteur -> consommateur, avec l'orthographe que le + # collecteur produit. C'est ce couple que le test d'origine ratait : il + # pilotait le collecteur mais lui donnait la chaine du consommateur. + rows = _collector_output([_collector_row(10558, author=REST_BOT)], + check_run_names=("CodeQL",)) + assert rows[0]["author_login"] == REST_BOT + verdict, why = classify(rows[0]) + assert verdict == "bot_missing", why + + def test_classify_input_is_the_only_shape(): # Epingle le contrat : toucher a `classify_input` sans mettre a jour # classify() (ou l'inverse) fait rougir ici au lieu de desactiver un verdict From 5fbb7d8669f67e1d45f9561570b217a85099861e Mon Sep 17 00:00:00 2001 From: jsboige Date: Sat, 12 Sep 2026 14:20:32 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci,#15758):=20le=20texte=20du=20remede?= =?UTF-8?q?=20bot=20renvoie=20a=20la=20ligne=20=C2=AB=20Cause=20mesuree=20?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_comment_body` compose le commentaire dans l'ordre : texte du remede d'abord, puis « Cause mesuree : ». Le login du bot est donc nomme SOUS le texte, pas au-dessus -- « ci-dessus » etait faux dans le rendu reel. Co-Authored-By: Claude Sonnet 5 --- scripts/pr_gate_missing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/pr_gate_missing.py b/scripts/pr_gate_missing.py index d064b7de35..26172c02b4 100644 --- a/scripts/pr_gate_missing.py +++ b/scripts/pr_gate_missing.py @@ -171,9 +171,9 @@ def is_bot_author(login: object) -> bool: ) REMEDIATION_BOT = ( - "PR ouverte par le bot du depot (son login est nomme ci-dessus ; il " - "s'ecrit `github-actions[bot]` en REST et `app/github-actions` en GraphQL) " - "sans `PR gate` dans son " + "PR ouverte par le bot du depot (son login est nomme dans la ligne " + "« Cause mesuree » ci-dessous ; il s'ecrit `github-actions[bot]` en REST et " + "`app/github-actions` en GraphQL) sans `PR gate` dans son " "rollup : cas **structurel** (issue #10928). Un push fait avec " "`GITHUB_TOKEN` ne cree pas de nouveau workflow run (regle anti-recursion " "GitHub), donc le contexte requis ne sera jamais rapporte par un push du "