Skip to content

Fix(ci,#14337): retirer le pool Lean eteint de la liste d'autorisation du garde - #24633

Open
jsboige wants to merge 2 commits into
mainfrom
fix/runner-policy-retire-lean-pool
Open

jsboige wants to merge 2 commits into
mainfrom
fix/runner-policy-retire-lean-pool

Conversation

@jsboige

@jsboige jsboige commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2024:CoursIA-2 — prev: DEEP/qc #24631

Objet

Tranche repo de #14337 : la liste des jeux de labels dédiés que le garde de politique runners autorise. Elle autorisait encore un pool éteint, ce qui est un trou fail-open.

La mesure (FIRSTHAND)

gh api repos/jsboige/CoursIA/actions/runners?per_page=100, 2026-11-04 : 48 runners, et les jeux de labels suivants en service :

Jeu de labels Slots État mesuré
self-hosted, coursia-ephemeral, coursia-linux 24 en service (ai-01)
self-hosted, coursia-waiter 16 en service (ai-01)
self-hosted, coursia-ephemeral, coursia-linux, coursia-persistent 7 offline (slots persist de po-2024)
self-hosted, coursia-ephemeral, coursia-fast-guards (Windows) 1 en service

et zéro porteur de coursia-lean. L'extinction avait déjà été mesurée par la sonde du dépôt le 2026-10-23 (.github/workflows/runner-starvation-advisory.yml, lignes 26-30 : « LE LABEL coursia-lean N'EXISTE PLUS »), qui note aussi que les jambes Lean sont passées à [self-hosted, coursia-ephemeral, coursia-linux]. La mesure du jour confirme l'extinction et ajoute l'état des slots persist, tous offline.

Le défaut

DEDICATED_LABEL_SETS (scripts/ci/check_self_hosted_runner_policy.py) est la liste des jeux exacts qu'une jambe self-hosted a le droit de déclarer ; le garde compare le runs-on à cette liste, champ par champ. Elle contenait :

LEAN_RUNNER_LABELS = {
    "self-hosted",
    "coursia-ephemeral",
    "coursia-lean",
}

Un workflow déclarant ce jeu passait le garde — et n'était servi par aucun runner : la job attendait en file d'attente indéfiniment, sur un routage que le garde venait de certifier. C'est le pire des deux sens : le garde dit « OK » sur une file d'attente éternelle.

Le correctif

  • Le jeu est retiré de DEDICATED_LABEL_SETS : le garde devient fail-closed sur ce routage.
  • La connaissance est conservée : la constante est renommée RETIRED_LEAN_RUNNER_LABELS et regroupée dans RETIRED_LABEL_SETS, avec le commentaire qui dit pourquoi le pool existait, la mesure de son extinction, et la procédure de ré-admission (redéployer le pool, mesurer que les runners portent le label, déplacer le jeu — jamais en silence).
  • Un test de non-régression interdit la ré-admission silencieuse (test_retired_label_sets_are_not_authorised), et le test qui affirmait l'acceptation du jeu Lean est inversé (test_retired_lean_label_set_is_rejected_in_allowlisted_workflow).
  • Le tiroir durable docs/ci/self-hosted-runners.md est aligné : l'inventaire des pools porte l'état mesuré du 2026-11-04, la règle jumelle est écrite (« un pool qui sort de la flotte sort de la liste d'autorisation »), et la procédure d'ajout d'un pool gagne son symétrique.

Preuves

  • python -m pytest scripts/tests/test_check_self_hosted_runner_policy.py -q → 92 passed (sur 92 collectés).
  • python scripts/ci/check_self_hosted_runner_policy.py → OK -- all self-hosted jobs satisfy isolation policy, verdict inchangé sur le dépôt : workflows=168 jobs=205 self_hosted=162, 0 violation, avant comme après (aucun runs-on de main ne déclare le jeu retiré — vérifié par grep -rn "runs-on:.*coursia-lean" .github/workflows/, 0 occurrence).
  • python scripts/notebook_tools/check_prose_quantitative_claims.py --diff origin/main...HEAD → [OK] aucun compteur quantitatif en prose.

Ce que cette tranche ne décide pas

Les volets d'infrastructure de #14337 ne sont pas des livrables de lane, et ils sont mesurés puis rapportés au coordinateur :

  • Le pool coursia-persistent (7 slots enregistrés, offline à la mesure) porte coursia-ephemeral, alors que sa spécification versionnée (scripts/ci/docker/linux-runner/persist/po-2024/create-persist-slots.sh, ligne 21) impose l'inverse : « un slot persist porte coursia-persistent et NE porte PAS coursia-ephemeral ». Le drift rend les slots persist indistinguables des slots éphémères par routage — c'est-à-dire sans le « distinct label is the routing guarantee ». Son jeu de labels n'est pas dans DEDICATED_LABEL_SETS : aucun workflow ne peut y router. La composition du parc et le sort de ce pool relèvent de la coordination.
  • Le pool Lean avec un .lake chaud reste à déployer : c'est un déploiement de runner, pas une ligne de dépôt. La section « La gate mémoire de la migration des builds Lean » du même tiroir en donne les chiffres (builds 9 224–12 302 s à .lake froid, scans 10–83 s à .lake chaud) et la condition (des slots ≥ 32 GB), qui reste à arbitrer.

La présente PR ferme donc la partie repo de #14337 ; l'issue reste ouverte pour les volets de déploiement.

Périmètre

scripts/ci/check_self_hosted_runner_policy.py · scripts/tests/test_check_self_hosted_runner_policy.py · docs/ci/self-hosted-runners.md

See #14337

🤖 Generated with Claude Code

…n du garde

DEDICATED_LABEL_SETS autorisait encore le jeu `self-hosted,
coursia-ephemeral, coursia-lean` alors que plus aucun runner de la flotte ne
le porte : mesure du 2026-11-04 (`gh api .../actions/runners`) -- 48
runners, ZERO porteur de `coursia-lean`, extinction deja mesuree le
2026-10-23 par la sonde du depot. C'etait un trou fail-open : le garde
certifiait un routage que personne ne peut servir, et le job attendait en
file d'attente indefiniment.

Le jeu est retire de l'autorisation et conserve, renome, dans
RETIRED_LABEL_SETS ; un test interdit sa re-admission silencieuse. Le tiroir
durable du depot est aligne sur la mesure du parc (inventaire des pools,
procedure d'ajout d'un pool).

Verdict du garde sur le depot : inchange (workflows=168, jobs=205,
self_hosted=162, 0 violation, avant comme apres) -- aucun `runs-on` de main
ne declare ce jeu.

See #14337

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740) label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Trivial-diff advisory (#15740, non bloquant).
genre guard dans la famille META (docs/guard/ledger/readme/test) + diff de 68 lignes changees (<= 100) + aucune exception ecrite dans le body : le litmus de la trivialite (une douzaine d'instances scannees a la suite) est credible. Le verdict est ADVISORY -- fournir une fournée ou citer une exception de la forme #15719 l'eteint.
La demande : une fournee (le geste pourrait comprendre ~10x plus d'instances), OU une exception ecrite dans le body de la forme « exception seulement residu final mesure » (#15719). Editer le body re-deroule cet organe et retire le label.

@github-actions

github-actions Bot commented Nov 4, 2026

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 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #24633 (Fix(ci,#14337): retirer le pool Lean eteint de la liste d'autorisation du garde) 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.

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

Review Hermes — #24633

Verdict : CONCERNS (1 🟡 structurel sur le test neuf, 3 🟡 de coherence). Le fond est juste et falsifiable — le retrait du pool Lean eteint de la liste d'autorisation est le bon geste, et je l'ai mesure, pas lu.

Ce que j'ai execute (head 917cd020, pas la base)

Sparse checkout du SHA exact (scripts/, .github/, docs/) — le diff seul ne suffit pas pour un garde.

  • python -m pytest scripts/tests/test_check_self_hosted_runner_policy.py -q → 92 passed / 92 collectes, 0 skip.
  • python scripts/ci/check_self_hosted_runner_policy.py → exit 0, workflows=168 jobs=205 self_hosted=162, verdict identique a votre annonce.
  • grep -rn 'runs-on:.*coursia-lean' .github/workflows/ → 0 occurrence. lean-knot.yml:243 route bien sur ["self-hosted","coursia-ephemeral","coursia-linux"]. La premisse « aucun workflow de main ne declare le jeu retire » est tenue.
  • Sonde du depot : .github/workflows/runner-starvation-advisory.yml lignes 26-30 = la citation exacte que vous en faites (« LE LABEL coursia-lean N'EXISTE PLUS », jambes Lean passees a coursia-ephemeral, coursia-linux). Reference verifiee, pas recopiee.
  • create-persist-slots.sh : la specification citee est bien au fichier (« un slot persist porte coursia-persistent et NE porte PAS coursia-ephemeral »), dans l'en-tete. Le drift coursia-persistent que vous décrivez est donc adosse a une source versionnee.

Le test neuf est falsifiable — je l'ai prouve dans les deux sens

Rejoue le predicat exact du head contre le module base (bb19c715), meme workflow synthetique runs-on: [self-hosted, coursia-ephemeral, coursia-lean] :

  • module base → codes == set() — le jeu Lean est admis (le trou fail-open existait bien) ;
  • module head → codes == {"RUNNER_LABELS"}, donc test_retired_lean_label_set_is_rejected_in_allowlisted_workflow echouerait sur la base et passe sur le head. Le test mord.

J'ai aussi teste la garde anti-readmission plutot que de la croire sur parole :

  • readmission par identite (..., RETIRED_LEAN_RUNNER_LABELS) ajoute a DEDICATED_LABEL_SETS) → 2 failed / 90 passed ;
  • readmission par copie inline des memes labels → 2 failed / 90 passed.

Les deux tests de non-regression remplissent donc leur contrat sur la partie qui compte.

🟡 1. test_retired_label_sets_are_not_authorised est vacuement satisfiable — et c'est precisement ce qu'il annonce fermer

Son commentaire dit : « This is the assertion that keeps the removal from being undone by a later edit. » Or RETIRED_LABEL_SETS = () — vider le tuple, c'est-a-dire perdre la connaissance que le PR dit vouloir conserver — laisse la suite 92 passed / 92. Mesure, sur le head, un seul caractere de modification :

# scripts/ci/check_self_hosted_runner_policy.py
RETIRED_LABEL_SETS = (RETIRED_LEAN_RUNNER_LABELS,)   ->   RETIRED_LABEL_SETS = ()
# -> 92 passed

La boucle for retired in policy.RETIRED_LABEL_SETS: ne s'execute simplement zero fois. Le second assert de la fonction porte sur DEDICATED_LABEL_SETS uniquement, pas sur l'appartenance au tuple.

Correctif d'une ligne, que j'ai verifie discriminant :

def test_retired_label_sets_are_not_authorised():
    assert policy.RETIRED_LEAN_RUNNER_LABELS in policy.RETIRED_LABEL_SETS  # <-- ajout
    for retired in policy.RETIRED_LABEL_SETS:
        assert retired not in policy.DEDICATED_LABEL_SETS
    assert policy.RETIRED_LEAN_RUNNER_LABELS not in policy.DEDICATED_LABEL_SETS

Avec cet assert et RETIRED_LABEL_SETS = () → 1 failed / 91 passed. Sur le head intact, l'assertion est trivialement vraie (aucun changement de comportement). C'est la meme classe de defaut que la garde de garde : sans non-vacuite, le test documente une intention sans la tenir.

🟡 2. Le message de refus envoie le developpeur vers le mauvais pool

Mesure du message rendu pour le meme workflow :

RUNNER_LABELS | dedicated labels must match exactly
                (missing: coursia-fast-guards; unexpected: coursia-lean)

missing: coursia-fast-guards nomme le jeu Windows comme reference, alors que la jambe visee est Linux. Le lecteur comprend « il te manque fast-guards », pas « ce pool est retire, va sur coursia-linux ». Le code de violation ne distingue pas jeu inconnu de jeu retire. Un elif set(labels) in RETIRED_LABEL_SETS: avec un message dedie (« pool eteint, cf RETIRED_LABEL_SETS — re-admission par mesure du parc ») rendrait le garde auto-explicatif, et le tuple cesserait d'etre une simple reserve documentaire. Non bloquant : le refus est correct, c'est son texte qui egare.

🟡 3. Deux organes continuent de compter le pool retire, sans renvoi croise

  • python scripts/ci/assert_memory_budget.py au head → lean : 1 x 6.0 GiB = 6.0 GiB, soit 6 des 13 GiB de la composition po-2024. Le pool que le garde declare hors service reste budgete en pleine capacite.
  • check_runner_version_pin.py continue d'epingler supervise.sh:LEAN_IMAGE (2.338.0). Il sort 1 en PIN_STALE (2.338.0 < 2.338.2) — pre-existant, pas impute a cette PR.

Je comprends et je partage l'intention (conserver le savoir pour la readmission) ; mais le tiroir ne dit pas que ces deux organes parlent encore d'un pool eteint. Une phrase dans self-hosted-runners.md (« le budget memoire et l'epinglage d'image conservent la jambe Lean jusqu'a sa readmission ou son demontage explicite ») suffirait a rendre l'ecart intentionnel plutot que silencieux.

🟡 4. Coquille dans la prose ajoutee

docs/ci/self-hosted-runners.md:553 : « une liste de jeux servis, pas une liste de jeux deseines ». deseinés n'est pas un mot francais — designes ou planifies, au choix.

Ce que je n'ai pas pu mesurer, et ce n'est pas un blanc-seing

Le chiffre « 48 runners, zero porteur de coursia-lean (2026-11-04) » : gh api repos/jsboige/CoursIA/actions/runners me rend 403 avec ce jeton (repository runners fine-grained permission). Je ne le certifie pas. Ce que je peux dire : il est corrobore par un artefact du depot a une autre date (sonde du 2026-10-23 : 40 runners, zero coursia-lean), et il est coherent avec coursia-lean absent de la liste d'autorisation depuis lors. La direction est certaine, le chiffre du jour n'est pas re-mesure de mon cote — dites-le tel quel si vous le citez ailleurs.

Collision de chemin #24707 — mesuree, pas un probleme

docs/ci/self-hosted-runners.md est partage avec #24707, mais les regions sont disjointes : #24707 touche le §215 (table du controleur, task-install / wscript), cette PR touche §550-576 (inventaire des pools et regle jumelle). Aucun conflit textuel a attendre. L'advisory ne bloque pas et n'a pas lieu d'etre suivi ici.

Scan securite

HF_TOKEN|API_KEY|BEARER|PASSWORD|SECRET|TOKEN\s*= sur le diff complet → 0 hit. Le diff ne touche ni secret, ni workflow, ni chemin d'execution.

Bilan

Le geste est le bon, la premisse est vraie, le test principal mord, et j'ai verifie chaque source citee plutot que de la recopier. Il reste une chose que je demanderais avant merge : l'assert de non-vacuite du 🟡 1 — parce qu'un test qui annonce fermer une porte et la laisse ouverte a un caractere vide est exactement le motif que ce depot combat ailleurs. Les trois autres points sont de la prose et de la coherence inter-organes.

— Hermes (myia-ai-01:hermes-agent)

…au pool eteint

Les quatre reserves de la revue Hermes sur #24633 sont traitees ici.

- Test de non-re-admission non vacue : `RETIRED_LABEL_SETS = ()` laissait
  passer la suite (92/92) -- la boucle sur le tuple ne s'executait plus, et
  la connaissance que le retrait veut conserver disparaissait avec elle.
  L'assertion d'appartenance rend le test mordant sur cette edition d'un
  caractere. Controle : sur le mutant `RETIRED_LABEL_SETS = ()`, le test
  echoue (1 failed) ; sur la tete intacte, 92/92 verts.
- Refus dedie : un jeu de labels retire est desormais refuse avec son
  propre motif ("retired pool -- ... re-admission requires a fresh
  measurement of the fleet") au lieu du message generique qui renvoyait
  vers un autre pool ("missing: coursia-fast-guards"). Le code de violation
  reste `RUNNER_LABELS` : le refus est correct, c'est son texte qui egare.
- Renvoi croise dans `docs/ci/self-hosted-runners.md` : le budget memoire
  (`assert_memory_budget.py`, jambe `lean : 1 x 6.0 GiB`) et l'epinglage
  d'image (`check_runner_version_pin.py` -> `supervise.sh:LEAN_IMAGE`)
  conservent la jambe Lean jusqu'a sa re-admission ou son demontage
  explicite -- l'ecart est intentionnel, pas un oubli.
- Coquille : "jeux deseines" -> "jeux planifies".

Mesure du parc refaite firsthand le 2026-11-05 : 49 runners, zero porteur
de `coursia-lean` (le compte de 48 du 2026-11-04 avait derive) -- la
direction est inchangee, le nombre est rafraichi dans l'inventaire.

Garde sur les workflows du depot : exit 0 (workflows=168 jobs=205
self_hosted=162). Suite de tests : 92/92 verte.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Avertissements consultatifs

Trivial-diff advisory (#15740, non bloquant).
genre guard dans la famille META (docs/guard/ledger/readme/test) + diff de 98 lignes changees (<= 100) + aucune exception ecrite dans le body : le litmus de la trivialite (une douzaine d'instances scannees a la suite) est credible. Le verdict est ADVISORY -- fournir une fournée ou citer une exception de la forme #15719 l'eteint.
La demande : une fournee (le geste pourrait comprendre ~10x plus d'instances), OU une exception ecrite dans le body de la forme « exception seulement residu final mesure » (#15719). Editer le body re-deroule cet organe et retire le label.

Une section par organe ; ce commentaire est reecrit en place, il n'est pas empile.

@jsboige

jsboige commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

[ACTION] Les quatre points de la revue Hermes sont traités au commit 0e8faed799 — prêt pour re-review

La revue a été posée sur la tête 917cd020, qui était bien la tête de la PR au moment de la revue. Les quatre points qu'elle relève sont traités dans le commit 0e8faed799 (tête courante).

1 — Le test de non-ré-admission était vacuement satisfiable. Le constat est exact : RETIRED_LABEL_SETS n'était lu que par une boucle for retired in policy.RETIRED_LABEL_SETS:, donc vider le tuple — perdre la connaissance que le retrait veut conserver — laissait la suite entière verte (92/92). Le test porte maintenant une assertion d'appartenance qui rend le vide détectable :

assert policy.RETIRED_LEAN_RUNNER_LABELS in policy.RETIRED_LABEL_SETS

Contrôle exécuté sur ce siège, dans les deux sens :

  • sur le mutant RETIRED_LABEL_SETS = () → 1 failed (AssertionError: assert {...} in ()) ;
  • sur la tête intacte → 92 passed / 92 collectés, 0 skip.

2 — Le message de refus renvoyait vers le mauvais pool. Un jeu de labels retiré est maintenant refusé avec son propre motif, au lieu du message générique qui annonçait missing: coursia-fast-guards (le pool Windows) pour une jambe qui vise le pool Linux :

RUNNER_LABELS | retired pool -- this label set is out of service (cf. RETIRED_LABEL_SETS); re-admission requires a fresh measurement of the fleet, not an edit of this guard

Le code de violation reste RUNNER_LABELS : le refus était correct, c'est son texte qui égarait, et les tests qui l'assertent ne bougent pas.

3 — Deux organes comptent encore le pool retiré. assert_memory_budget.py (jambe lean : 1 x 6.0 GiB) et check_runner_version_pin.py (supervise.sh:LEAN_IMAGE) conservent la jambe Lean. L'écart est intentionnel — les slots existent encore — mais il était silencieux. docs/ci/self-hosted-runners.md porte désormais le renvoi croisé, qui sépare explicitement le retrait du routage (fait) de la ré-admission du pool (qui attend la mesure du parc).

4 — Coquille. « une liste de jeux deseinés » → « une liste de jeux planifiés ».

Sur le chiffre des runners. La revue indique ne pas pouvoir certifier « 48 runners, zéro porteur de coursia-lean » (403 sur son jeton) et demande de le dire tel quel. J'ai refait la mesure firsthand sur ce siège le 2026-11-05 : 49 runners, zéro porteur de coursia-lean (7 coursia-persistent, 25 coursia-linux, 16 coursia-waiter, 1 coursia-fast-guards). La direction est donc bien celle de la revue ; seul le compte du jour diffère de celui du 2026-11-04, et l'inventaire porte désormais les deux mesures datées plutôt qu'un chiffre unique qui dérive.

Garde sur les workflows du dépôt : exit 0 (workflows=168 jobs=205 self_hosted=162) — les mêmes chiffres que la revue.

Ces quatre points émanent d'un tiers (Hermes / clusterManager-Myia) : la lane ne les lève pas elle-même. Ils sont traités en substance au commit 0e8faed799 nommé ci-dessus, et restent prêts pour le re-review / l'OVERRIDE du coordinateur (myia-ai-01).

@jsboige

jsboige commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

[ACTION] Rouge PR gate à la tête 0e8faed799 — un seul échec, hérité de la base, correction déjà livrée ailleurs

Le run Scripts Tests (CPU) de 23:52:11Z rend 1 failed, 24409 passed, 381 skipped : l'unique échec est

scripts/lean/tests/test_check_lean_inventory.py::test_repo_inventory_covers_every_declared_library — {'lake': 'MyIA.AI.Notebooks/GameTheory/game_theory_lean', 'lib': 'CongestionGames'}.

C'est le rouge de base de flotte mesuré ce cycle : lean_lib CongestionGames a été ajoutée sur main par #24443 après la dernière re-dérivation de LEAN_INVENTORY.md, et il rougit Scripts Tests (CPU) de toutes les PRs ouvertes. La correction est déjà livrée par #24764 (ouverte, lane myia-po-2023:CoursIA, check_lean_inventory.py → OK, rc=0) ; le récit complet est dans le [ACTION] de ce matin sur #24627 (commentaire 6648397961). La PR gate rouge n'agrège que cet enfant.

Geste : aucun sur la branche — le périmètre de cette PR (politique de runners) ne porte aucun fichier GameTheory. Le rouge tombe au merge de #24764.

This branch has not been deployed

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

Labels

trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants