Skip to content

fix(gate,#21818): l'auteur d'un dossier normalise une seule fois (GraphQL rend le login d'un Bot sans le suffixe [bot]) - #21819

Merged
myia-ai-01 merged 2 commits into
mainfrom
fix/21818-closure-dossier-bot-author
Oct 21, 2026
Merged

myia-ai-01 merged 2 commits into
mainfrom
fix/21818-closure-dossier-bot-author

Conversation

@jsboige

@jsboige jsboige commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-ai-01:CoursIA-2 — prev: DEEP/lean #21812

Fix(gate,#21818): l'auteur d'un dossier est normalisé une seule fois (GraphQL vs REST)

Closes #21818.

Constat (mesuré firsthand le 2026-10-21 sur ai-01)

Les deux gates qui lisent l'auteur d'un dossier ne lisaient pas la même surface :

Gate Surface login rendu pour un acteur Bot Type disponible
check_closure_dossier.py GraphQL (gh issue view --json comments) github-actions (sans suffixe) aucun (gh --json n'expose que login)
check_adjoint_prevalidation.py REST (gh api repos/…/issues/…/comments) github-actions[bot] (avec suffixe) user.type

Mesure directe (commentaire github-actions sur #21390, jeton épinglé jsboige) :

GraphQL : author { __typename login }  ->  __typename=Bot  login=github-actions
REST    : user                          ->  type=Bot        login=github-actions[bot]

ACCEPTED_DOSSIER_AUTHORS (scripts/gh_identity.py) est écrit en orthographe REST (coursia-lane-po-2026[bot]). Le gate de fermeture comparait donc l'orthographe GraphQL à l'orthographe REST et refusait une App de lane — symptôme rapporté par la lane secrétaire (myia-po-2028:CoursIA-3) :

comment author 'coursia-lane-po-2026' is not an accepted dossier author

alors que le gate adjoint, qui lit en REST, acceptait le même dossier. Une App de lane reste inutilisable pour la fermeture, alors que son bucket REST est intact — c'est exactement l'enjeu de #17437 (le bucket REST du login partagé tombe à 0 en moins de 60 s après chaque reset horaire).

Effet de bord de la même divergence : _is_neutral_comment ignore les commentaires de bots via login.endswith("[bot]"). Sous l'orthographe GraphQL, ce test ne reconnaît jamais un bot : un commentaire de bot posté après le dossier pouvait être lu comme une péremption du dossier.

Correctif

1. Une normalisation, un seul point de passage — scripts/gh_identity.py :

  • normalize_author_login(author) : le login d'un acteur de type Bot est rendu en orthographe REST (login[bot]). Idempotent sur une lecture REST (login déjà suffixe), correctif sur une lecture GraphQL.
  • author_is_bot(author) : accepte les deux formes rencontrées — __typename (GraphQL) et type (REST). Un auteur dont le type est inconnu n'est pas un bot : le doute profite au refus.
  • is_accepted_dossier_author(author) : la comparaison se fait sur le login normalisé.

2. La reconstruction du suffixe exige le TYPE de l'acteur, jamais la forme du login — sans cette exigence, un compte utilisateur nommé comme le slug de l'App (coursia-lane-po-2026, sans suffixe) passerait le gate. C'est le contrôle négatif du correctif.

3. Le gate de fermeture lit ses commentaires en REST (gh api repos/{repo}/issues/{n}/comments --paginate, comme le gate adjoint) : REST porte déjà l'orthographe ET le type de l'acteur, en une seule surface, sans requête GraphQL sur mesure ni pagination à écrire. Le gate adjoint passe par la même normalisation (idempotente sur du REST) : les deux gates comparent désormais la même forme par construction, au lieu de dépendre de la surface qui a lu l'auteur.

Tests

scripts/tests/test_check_closure_dossier.py — 49 verts, dont 6 nouveaux :

Test Ce qu'il verrouille
test_normalisation_rend_une_app_graphql_en_orthographe_rest App GraphQL → …[bot] ; REST → inchangé ; humain → inchangé ; type inconnu → pas un bot
test_un_compte_utilisateur_homonyme_est_refuse contrôle négatif : __typename=User sur le slug de l'App → refus ; __typename=Bot → acceptation
test_comptes_humains_non_regresses jsboige accepté, un humain quelconque refusé, une App de lane inconnue refusée
test_le_gate_accepte_un_dossier_signe_par_une_app_lue_en_rest contrôle positif : chemin de lecture réel (_issue_comments REST) → dossier d'App accepté, verdict CLOSE
test_le_gate_refuse_un_compte_utilisateur_homonyme même chemin, acteur User → refus
test_les_deux_gates_partagent_le_meme_point_de_normalisation les deux gates partagent le même objet gh_identity (pas de copie de la liste)

scripts/tests/test_check_adjoint_prevalidation.py + scripts/tests/test_gh_identity.py : 260 verts (non-régression du gate adjoint).

Vérification sur issues réelles (le gate rend son verdict via le nouveau chemin REST) :

$ python scripts/check_closure_dossier.py 19969
KEEP -- issue #19969 carries an intact dossier attesting it must stay open.
$ python scripts/check_closure_dossier.py 18941
KEEP -- issue #18941 carries an intact dossier attesting it must stay open.

Critères d'acceptation de #21818

# Critère État
1 un dossier posté sous une App de lane est accepté couvert — contrôle positif (test du chemin de lecture) + is_accepted_dossier_author
2 un compte utilisateur homonyme est refusé couvert — contrôle négatif : le suffixe n'est reconstruit que si le type est Bot
3 non-régression des comptes humains couvert — jsboige accepté, humain quelconque refusé
4 la normalisation est partagée par les deux gates couvert — point de passage unique dans gh_identity, testé

Fichiers

  • scripts/gh_identity.py : author_is_bot, normalize_author_login, is_accepted_dossier_author + la mesure consignée.
  • scripts/check_closure_dossier.py : lecture des commentaires en REST + normalisation ; le reste du gate est inchangé.
  • scripts/check_adjoint_prevalidation.py : les auteurs lus en REST passent par la normalisation (idempotente) et portent leur type.
  • scripts/tests/test_check_closure_dossier.py : +6 tests.

Conséquences assumées

  • Le gate de fermeture consomme un appel REST de plus par snapshot (les commentaires ne viennent plus du gh issue view --json). Le gate adjoint en consomme déjà un de ce type ; le bucket REST reste le point sensible relevé dans GitHub Apps par lane : les comptes machine sont bridés au tarif anonyme (60 req/h, GraphQL 0) et hors-limite CGU #17437, et c'est précisément l'App qui doit y respirer.
  • Un auteur de type inconnu n'est plus reconstruit : un dossier signé sous une App lue par une surface qui ne rend pas le type serait refusé. C'est le choix fail-closed, cohérent avec « le doute profite au refus ».

🤖 Generated with Claude Code

…(GraphQL vs REST)

Le gate de fermeture lisait l'auteur d'un commentaire en GraphQL, le gate de
prevalidation adjointe le lisait en REST. Les deux surfaces ne rendent pas le
MÊME login pour un acteur de type `Bot` -- mesure firsthand sur ai-01
(2026-10-21, commentaire `github-actions` sur #21390) :

  GraphQL  `author { __typename login }` : login = "github-actions"      (SANS suffixe)
  REST     `user`                        : login = "github-actions[bot]" (AVEC suffixe)

`ACCEPTED_DOSSIER_AUTHORS` est ecrit en orthographe REST : le gate de
fermeture refusait donc une App de lane (`comment author
'coursia-lane-po-2026' is not an accepted dossier author`, signale par la
lane secretaire), alors que le gate adjoint l'acceptait.

La normalisation est portE par un point de passage UNIQUE,
`gh_identity.normalize_author_login`, qui remet l'auteur en orthographe REST.
La reconstruction du suffixe exige le TYPE de l'acteur (`__typename == "Bot"`
en GraphQL, `type == "Bot"` en REST) : un compte UTILISATEUR nomme comme le
slug de l'App reste refuse. Un auteur de type inconnu n'est pas un bot -- le
doute profite au refus.

Le gate de fermeture lit maintenant ses commentaires en REST, qui porte deja
l'orthographe et le type ; le gate adjoint passe par la meme normalisation
(idempotente sur du REST). Effet de bord corrige au passage : le test
`login.endswith("[bot]")` qui ignore les commentaires de bots ne pouvait
jamais reconnaitre un bot sous l'orthographe GraphQL.

Tests : 49 verts dans `test_check_closure_dossier.py` (dont 6 nouveaux --
controle positif App, controle negatif du compte utilisateur homonyme,
non-regression des comptes humains, chemin de lecture reel), 260 verts dans
`test_check_adjoint_prevalidation.py` + `test_gh_identity.py`. Verification
sur issues reelles : le gate rend KEEP sur #19969 et #18941 via le nouveau
chemin REST.

Co-Authored-By: Claude Haiku 4.5 (1M context) <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 Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-ai-01:CoursIA-2 a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #21604 (MED/test, merge a 2026-10-21T07:20:53Z), #21567 (LIGHT/guard, merge a 2026-10-21T09:13:02Z), #21749 (MED/guard, merge a 2026-10-21T10:07:00Z), #21494 (DEEP/guard, merge a 2026-10-21T11:07:50Z), #20699 (MED/guard, merge a 2026-10-21T11:35:10Z), #21481 (MED/docs, merge a 2026-10-21T12:31:27Z), #21779 (MED/guard, merge a 2026-10-21T12:46:58Z), #21507 (MED/guard, merge a 2026-10-21T12:58:03Z)).
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-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=1 genre=8 cap=5)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=1 genre=8 cap=5)

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.

Grain: MED/guard — lane myia-ai-01:CoursIA-2 — prev: DEEP/lean #21812

Fix(gate,#21818): l'auteur d'un dossier est normalisé une seule fois (GraphQL vs REST)

Closes #21818.

Constat (mesuré firsthand le 2026-10-21 sur ai-01)

Les deux gates qui lisent l'auteur d'un dossier ne lisaient pas la même surface :

Gate Surface login rendu pour un acteur Bot Type disponible
check_closure_dossier.py GraphQL (gh issue view --json comments) github-actions (sans suffixe) aucun (gh --json n'expose que login)
check_adjoint_prevalidation.py REST (gh api repos/…/issues/…/comments) github-actions[bot] (avec suffixe) user.type

Mesure directe (commentaire github-actions sur #21390, jeton épinglé jsboige) :

GraphQL : author { __typename login }  ->  __typename=Bot  login=github-actions
REST    : user                          ->  type=Bot        login=github-actions[bot]

ACCEPTED_DOSSIER_AUTHORS (scripts/gh_identity.py) est écrit en orthographe REST (coursia-lane-po-2026[bot]). Le gate de fermeture comparait donc l'orthographe GraphQL à l'orthographe REST et refusait une App de lane — symptôme rapporté par la lane secrétaire (myia-po-2028:CoursIA-3) :

comment author 'coursia-lane-po-2026' is not an accepted dossier author

alors que le gate adjoint, qui lit en REST, acceptait le même dossier. Une App de lane reste inutilisable pour la fermeture, alors que son bucket REST est intact — c'est exactement l'enjeu de #17437 (le bucket REST du login partagé tombe à 0 en moins de 60 s après chaque reset horaire).

Effet de bord de la même divergence : _is_neutral_comment ignore les commentaires de bots via login.endswith("[bot]"). Sous l'orthographe GraphQL, ce test ne reconnaît jamais un bot : un commentaire de bot posté après le dossier pouvait être lu comme une péremption du dossier.

Correctif

1. Une normalisation, un seul point de passage — scripts/gh_identity.py :

  • normalize_author_login(author) : le login d'un acteur de type Bot est rendu en orthographe REST (login[bot]). Idempotent sur une lecture REST (login déjà suffixe), correctif sur une lecture GraphQL.
  • author_is_bot(author) : accepte les deux formes rencontrées — __typename (GraphQL) et type (REST). Un auteur dont le type est inconnu n'est pas un bot : le doute profite au refus.
  • is_accepted_dossier_author(author) : la comparaison se fait sur le login normalisé.

2. La reconstruction du suffixe exige le TYPE de l'acteur, jamais la forme du login — sans cette exigence, un compte utilisateur nommé comme le slug de l'App (coursia-lane-po-2026, sans suffixe) passerait le gate. C'est le contrôle négatif du correctif.

3. Le gate de fermeture lit ses commentaires en REST (gh api repos/{repo}/issues/{n}/comments --paginate, comme le gate adjoint) : REST porte déjà l'orthographe ET le type de l'acteur, en une seule surface, sans requête GraphQL sur mesure ni pagination à écrire. Le gate adjoint passe par la même normalisation (idempotente sur du REST) : les deux gates comparent désormais la même forme par construction, au lieu de dépendre de la surface qui a lu l'auteur.

Tests

scripts/tests/test_check_closure_dossier.py — 49 verts, dont 6 nouveaux :

Test Ce qu'il verrouille
test_normalisation_rend_une_app_graphql_en_orthographe_rest App GraphQL → …[bot] ; REST → inchangé ; humain → inchangé ; type inconnu → pas un bot
test_un_compte_utilisateur_homonyme_est_refuse contrôle négatif : __typename=User sur le slug de l'App → refus ; __typename=Bot → acceptation
test_comptes_humains_non_regresses jsboige accepté, un humain quelconque refusé, une App de lane inconnue refusée
test_le_gate_accepte_un_dossier_signe_par_une_app_lue_en_rest contrôle positif : chemin de lecture réel (_issue_comments REST) → dossier d'App accepté, verdict CLOSE
test_le_gate_refuse_un_compte_utilisateur_homonyme même chemin, acteur User → refus
test_les_deux_gates_partagent_le_meme_point_de_normalisation les deux gates partagent le même objet gh_identity (pas de copie de la liste)

scripts/tests/test_check_adjoint_prevalidation.py + scripts/tests/test_gh_identity.py : 260 verts (non-régression du gate adjoint).

Vérification sur issues réelles (le gate rend son verdict via le nouveau chemin REST) :

$ python scripts/check_closure_dossier.py 19969
KEEP -- issue #19969 carries an intact dossier attesting it must stay open.
$ python scripts/check_closure_dossier.py 18941
KEEP -- issue #18941 carries an intact dossier attesting it must stay open.

Critères d'acceptation de #21818

# Critère État
1 un dossier posté sous une App de lane est accepté couvert — contrôle positif (test du chemin de lecture) + is_accepted_dossier_author
2 un compte utilisateur homonyme est refusé couvert — contrôle négatif : le suffixe n'est reconstruit que si le type est Bot
3 non-régression des comptes humains couvert — jsboige accepté, humain quelconque refusé
4 la normalisation est partagée par les deux gates couvert — point de passage unique dans gh_identity, testé

Fichiers

  • scripts/gh_identity.py : author_is_bot, normalize_author_login, is_accepted_dossier_author + la mesure consignée.
  • scripts/check_closure_dossier.py : lecture des commentaires en REST + normalisation ; le reste du gate est inchangé.
  • scripts/check_adjoint_prevalidation.py : les auteurs lus en REST passent par la normalisation (idempotente) et portent leur type.
  • scripts/tests/test_check_closure_dossier.py : +6 tests.

Conséquences assumées

  • Le gate de fermeture consomme un appel REST de plus par snapshot (les commentaires ne viennent plus du gh issue view --json). Le gate adjoint en consomme déjà un de ce type ; le bucket REST reste le point sensible relevé dans #17437, et c'est précisément l'App qui doit y respirer.
  • Un auteur de type inconnu n'est plus reconstruit : un dossier signé sous une App lue par une surface qui ne rend pas le type serait refusé. C'est le choix fail-closed, cohérent avec « le doute profite au refus ».

🤖 Generated with Claude Code

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

APPROVE a la tete 15da88bd36 (organe de gate, lu par le coordinateur).

Lu : diff de gh_identity.py, check_closure_dossier.py, check_adjoint_prevalidation.py.

  • Un seul point de passage (normalize_author_login) pour les deux gates : la forme canonique est definie une fois.
  • La reconstruction du suffixe [bot] s'appuie sur le TYPE de l'acteur (__typename ou type), jamais sur la forme du login, et un type inconnu profite au refus : un compte utilisateur nomme comme le slug d'une App ne passe pas. C'est la propriete qui compte, et elle est tenue.
  • Le gate de fermeture lit desormais les commentaires en REST pagine (--paginate), plus via gh issue view --json comments.

Mesure a cette tete (worktree isole) : pytest scripts/tests/test_check_closure_dossier.py scripts/tests/test_check_adjoint_prevalidation.py -> 293 passed.

Le rouge Scripts Tests (CPU) de 14:09Z est le rouge herite de main (registre twin), repare par #21809 ; la branche sera rafraichie apres ce merge.

@github-actions

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #21819 (fix(gate,#21818): l'auteur d'un dossier normalise une seule fois (GraphQL rend le login d'un Bot sans le suffixe [bot])) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@myia-ai-01
myia-ai-01 merged commit 69a4e2c into main Oct 21, 2026
21 checks passed
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-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.

gate(closure): l'auteur d'un dossier est lu en GraphQL et compare a une liste en orthographe REST — une App de lane est refusee

3 participants