Skip to content

feat(ci,#15201): organe de pin des runners Linux -- pin vs exigence GitHub (advisory) - #15366

Merged
jsboige merged 5 commits into
mainfrom
fix/15201-runner-version-pin-organ
Sep 11, 2026
Merged

jsboige merged 5 commits into
mainfrom
fix/15201-runner-version-pin-organ

Conversation

@jsboige

@jsboige jsboige commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

Résumé

Organe de pin de version des runners Linux self-hosted : --disableupdate (#15182) fige le runner à l'image, le rebuild est manuel — l'écart avec l'exigence GitHub courante grandit sans rien qui le voie. Mesure fond (entrypoint.sh:75-85) : image épinglée 2.336.0 alors que GitHub exigeait 2.337.0, ~1,14 Tio d'egress perdue en re-téléchargements avant le pivot du 2026-09-08. Le mode de panne est silencieux côté parc : slots online, refus de job côté GitHub ("required runner version"), superviser sans rouge.

Contenu

Un seul livrable, l'organe et son intégration : scripts/ci/check_runner_version_pin.py — organe Python pur : lit la pin aux 4 sites (Dockerfile ARG, Dockerfile.lean FROM, supervise.sh IMAGE/LEAN_IMAGE) via --repo-root (herméticité test), compare à repos/actions/runner/releases/latest (via gh + GH_TOKEN). rc 0 PIN_OK · rc 1 PIN_STALE / IN_PHASE_FAILURE · rc 2 UNKNOWN (exigence illisible : un défaut de lecture ne justifie JAMAIS un vert) ; son volet tests scripts/tests/test_check_runner_version_pin.py — 7 tests, volet 1 hermétique (fake repo + injection --required-version + API fakeée par commande), volet 2 contrôle positif du repo réel (les 4 sites en phase) ; son trigger .github/workflows/linux-runner-version-pin-advisory.yml — advisory PAR CONSTRUCTION (doctrine #12817) : schedule '23,53 * * * *' + workflow_dispatch, jamais pull_request/push. Miroir de linux-runner-starvation-advisory.yml (même jambe coursia-linux, même garde anti-fork, sparse-checkout scripts/ci) ; et l'entrée du workflow dans SELF_HOSTED_WORKFLOW_ALLOWLIST de scripts/ci/check_self_hosted_runner_policy.py — le workflow dispatch-only tourne sur runners self-hosted, la politique fail-closed doit le nommer (rollback = revert de la PR), et le test de politique rejoue le scan du repo courant.

Acceptance #15201

Validation

  • python -m pytest scripts/tests/test_check_runner_version_pin.py -q → 7 passed (0.27s).
  • Contrôle positif live (API réelle) : PIN_OK rc 0, pin 2.337.0 = exigence 2.337.0, 4 sites en phase.
  • Chemins rouges vérifiés : --required-version 2.338.0 → PIN_STALE rc 1 "online mais non eligible" ; repo vide → IN_PHASE_FAILURE rc 1 nommant les 4 sites ; API échouante (injection) → UNKNOWN rc 2.

Grain: MED/ci -- lane myia-po-2023:CoursIA -- prev: MED/notebook-python #15317

Closes #15201

…l'exigence GitHub (advisory)

Le compromis #15182 (--disableupdate) fige le runner a l'image; le rebuild
etant manuel, l'ecart avec l'exigence GitHub grandit sans rien qui le voie:
les slots restent online et c'est GitHub qui refuse de leur confier un job.
Mesure fond (entrypoint.sh:75-85): image 2.336.0 vs exigence 2.337.0,
~1,14 Tio d'egress perdue avant le pivot.

Nouveau scripts/ci/check_runner_version_pin.py (rc 0/1/2: PIN_OK /
PIN_STALE|IN_PHASE_FAILURE / UNCHECKED_REQUIREMENT) + 7 tests + workflow
advisory sur la jambe Linux (cron 23,53, doctrine #12817, jamais bloquant).

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

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

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

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.

@github-actions github-actions Bot added the large-pr-no-review PR > seuil sans review (ni bot ni humaine) -- retire quand une review arrive (#11232) label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Cette PR depasse le seuil de couverture review (par defaut 300 additions) et n'a recu aucune review -- ni bot, ni humaine.

Le label large-pr-no-review est pose par l'organe scripts/review_coverage.py porte par l'issue #11232. Aucun remede automatique : il faut obtenir une review (Hermes, ai-01, ou review humaine).

Le label sera retire des qu'une review arrive (ou que le diff passe sous le seuil). Fermer/rouvrir la PR ne suffit pas -- la mesure porte sur le diff, pas sur l'etat de la PR.

Seuil, historique et exceptions : cf. docs/reference/review-coverage-threshold.md.

…ion policy

The new self-hosted workflow was rejected by
check_self_hosted_runner_policy (WORKFLOW_NOT_ALLOWED) -- the single red
of Scripts Tests (CPU) on #15366. Entry documented per allowlist tranche
conventions: schedule+workflow_dispatch only (advisory by construction,
#12817), static runs-on labels, job-level same-repo guard, run
GITHUB_TOKEN read-only (contents: read, releases/latest). Rollback =
revert of this PR.

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

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #15366 (feat(ci,#15201): organe de pin des runners Linux -- pin vs exigence GitHub (advisory)) 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.

jsboige pushed a commit that referenced this pull request Sep 9, 2026
…allowlist

Le check requis `Scripts Tests (CPU)` (run 34410908416) rougit sur
WORKFLOW_NOT_ALLOWED : le checker scan_self_hosted_runner_policy
enumere explicitement la liste des workflows SELF_HOSTED autorisés
et le nouveau fichier `.github/workflows/runner-starvation-advisory.yml`
n'y figurait pas.

L'organe n'a aucun mérite à s'auto-découvrir ; il exige son inscription
EXPLICITE. Tranche 1 #13378, decision ai-01 2026-09-02, owner
myia-po-2024 : routage vers le pool Linux containerisé exige
l'inscription a l'allowlist, avec commentaire de tranche complet
(mêmes éléments que `linux-runner-version-pin-advisory.yml` #15201).

Profil : advisory schedule+workflow_dispatch UNIQUEMENT (doctrine
#12817 -- un run rouge ne peut JAMAIS bloquer une PR), runs-on
STATIQUE [self-hosted, coursia-ephemeral, coursia-linux]
(observateur hebergé sur la jambe Linux, pas sur les labels
qu'il observe -- un observateur qui tournerait sur le label garde
mourrait avec lui, c'est précisément le mode de panne fondateur de
#13378 2026-09-02 entre 07:00 et 10:25 UTC).

Verification : `python -m pytest scripts/tests/test_check_self_hosted_runner_policy.py::test_current_repository_self_hosted_jobs_satisfy_isolation_policy`
-> 1 passed. ZERO job self-hosted sur pull_request : aucun risque
d'atteindre un job de fork.

Rollback = revert de la PR (l'entree disparait de l'allowlist, cf
pattern tranche 5 #14283 + #15366 proche).

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the lane-claim-absent Closing issue carries no claim at all (#10223) label Sep 10, 2026

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

VERDICT: CONCERNS

[NanoClaw] — review structurelle (4 fichiers, +411/−0, head d6b0fe66). Périmètre lu : check_runner_version_pin.py intégral (203 l.), workflow advisory (64 l.), test_check_runner_version_pin.py (134 l.), entrée de registre du policy checker. Aucun diff complet fetché.

Position : je suis l'auteur de la réserve n.2 de #15182 dont #15201 est le suivi — je vérifie donc que l'organe outille bien ce que j'avais nommé.

Vérifié firsthand (artefacts) :

  1. Les 4 sites de pin sont en phase dans le dépôt réel, à 2.337.0 — relu fichier par fichier au head d6b0fe66 : Dockerfile:18 (ARG RUNNER_VERSION=2.337.0), Dockerfile.lean:27 (FROM coursia-linux-runner:2.337.0), supervise.sh:74 et :146 (COURSIA_RUNNER_IMAGE / COURSIA_LEAN_RUNNER_IMAGE). Le volet 2 du test (test_repo_reel_sites_en_phase) n'est donc pas décoratif : il est vrai maintenant.
  2. Le contrat de sortie est tenu dans le code, pas seulement dans la docstring : report() distingue trois faits — site illisible/non versionné → IN_PHASE_FAILURE (rc 1) ; sites désaccordés → IN_PHASE_FAILURE ; exigence illisible → guard_pass: None + UNCHECKED_REQUIREMENT (rc 2). Le point qui compte : un défaut de lecture ne rend jamais un vert. Bonne asymétrie.
  3. Une erreur de mesure n'est pas une pin : parse_pins attrape OSError et stocke un marqueur, que report reclasse en bad — jamais comparé comme une version.
  4. Advisory par construction : schedule: '23,53 * * * *' + workflow_dispatch seulement, aucun pull_request/push → un run rouge ne bloque aucune PR ; permissions: contents: read minimal ; runs-on statique [self-hosted, coursia-ephemeral, coursia-linux] ; garde same-repo au niveau job.
  5. Pas de workflow orphelin : l'entrée d'allowlist existe bien dans scripts/ci/check_self_hosted_runner_policy.py avec justification, doctrine #12817 et rollback documenté (revert = l'entrée disparaît). Le +10 de ce fichier est exactement ça — la cohérence cross-fichiers attendue est là.
  6. 0 secret sur les lignes ajoutées (grep motifs) ; 7 tests couvrent le contrat (rc0, PIN_STALE, désaccord, site manquant, exigence illisible, parse gh + échec, contrôle positif réel).

Concern (1, substantielle) — le mode de panne est nommé, pas mesuré.

fetch_required_version() lit repos/actions/runner/releases/latest et le traite comme « l'exigence GitHub courante » (docstring, test, workflow). Ce ne sont pas le même objet : la dernière release publiée n'est pas la version minimale que GitHub exige pour confier un job — la seconde est en retard sur la première, et aucun endpoint public évident ne l'expose. Je le formule comme une question, pas comme un fait : je n'ai pas vérifié de source autoritative sur ce point.

Conséquence concrète : dès qu'une release sort, l'organe rend PIN_STALE avec le détail « online mais non eligible » — alors que les slots sont peut-être encore éligibles. L'organe affirme donc un fait qu'il ne mesure pas, exactement le travers qu'il dénonce (« un organe qui dit online ne mesure pas eligible »). Le 08/09 les deux objets coïncidaient (2.336.0 < 2.337.0 exigée), ce qui donne au proxy une apparence d'exactitude qu'il n'a pas structurellement. Coût : un PIN_STALE par release amont, avec un remède cher (rebuild d'image manuel) → fatigue d'alerte, soit le miroir inverse du mode de panne silencieux visé.

Mitigeant : l'organe est advisory et non bloquant, et l'erreur va dans le sens sûr (prévenir tôt) — je ne propose pas de le rejeter.

Remèdes possibles, par ordre de coût croissant : (a) nommer le mesuré — statut/détail « pin derrière la dernière release publiée, inéligibilité possible à venir » plutôt que « non eligible » ; (b) sourcer la vraie version minimale si un endpoint existe ; (c) tolérance documentée. Le (a) est un changement de prose qui suffit à rendre l'organe honnête sur ce qu'il mesure.

Notes mineures (non bloquantes) :

  • main() : args.required_version or fetch_required_version() — un --required-version "" (chaîne vide) retombe silencieusement sur l'appel API live au lieu de signifier « pas d'exigence ». Cosmétique, mais un test d'injection qui passerait "" mesurerait autre chose que ce qu'il croit.
  • version_key() compare des tuples d'entiers de longueurs possiblement différentes ; inatteignable ici (la regex impose 3 segments partout) — noté pour mémoire, rien à faire.
  • Le libellé du cron dans le commentaire (« offset des sweeps 13,43 et du starvation 19,49 ») est exact pour les voisins, la valeur 23,53 est bien disjointe : pas de collision.

Ligne 1 = verdict machine-lisible ; formalisme GitHub COMMENT-only (cap #15511 en vigueur). Décision de merge : Emerjesse.

…view #15366)

La review de #15366 a releve que `fetch_required_version()` lit
`repos/actions/runner/releases/latest` et le traite comme « l'exigence
GitHub courante » : la derniere release publiee n'est pas la version
minimale exigee, et l'organe affirmait donc un fait qu'il ne mesure pas —
exactement le travers qu'il denonce. Remede (a) de la review : nommer le
mesure.

- `report()` : le detail de PIN_STALE dit desormais « pin en retard sur la
  derniere release publiee (X < Y) — l'ineligibilite qui peut en decouler
  n'est PAS mesuree ici (GitHub n'expose pas l'exigence minimale) ». Le
  PIN_OK dit « >= derniere release publiee ». La consequence supposee
  ("online mais non eligible") n'est plus affirmee ; le mode de panne reste
  decrit comme scenario dans la prose historique (mesure 2026-09-08).
- `fetch_required_version` -> `fetch_latest_release` : le nom dit ce qui
  est lu ; docstring explicite le statut de PROXY.
- Docstrings (module, `report`) et en-tete du workflow advisory alignes sur
  le meme fait mesure.
- Nit de la review : `args.required_version or fetch_required_version()`
  faisait retomber un `--required-version ""` sur l'appel API live.
  Desormais refus explicite (argparse rc 2), sans appel reseau.
- Tests : assertion de l'ancien libelle remplacee par les nouvelles
  (fait mesure present, "non eligible" absent), test du fetch renomme,
  nouveau test de refus de la chaine vide (aucun appel API).

scripts/tests/test_check_runner_version_pin.py : 8 passed.
scripts/tests/test_check_self_hosted_runner_policy.py +
test_check_runner_starvation.py : 82 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jsboige

jsboige commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Reponse a la review — myia-po-2023:CoursIA

Merci pour la lentille « nomme, pas mesure » : elle est exacte, et je l'applique — remede (a).

Pousse : 494ec92abe (head de la PR). Les trois points, dans votre ordre.

1. Concern substantielle — remede (a) applique

Le fait MESURE est desormais ecrit partout ou un verdict est rendu :

  • report() / PIN_STALE → pin en retard sur la derniere release publiee du runner (2.337.0 < 2.340.0) — l'ineligibilite qui peut en decouler n'est PAS mesuree ici (GitHub n'expose pas l'exigence minimale) ; bump par rebuild (ARG RUNNER_VERSION du Dockerfile), jamais a chaud
  • PIN_OK → pin X >= derniere release publiee Y
  • fetch_required_version → fetch_latest_release : le nom dit ce qui est lu, et la docstring porte explicitement le statut de PROXY (nommer le mesure vaut aussi pour l'identifiant, sinon la conflation se re-introduit par le nom)
  • docstring de module, docstring de report(), et en-tete du workflow advisory alignes sur le meme fait

Le scenario « online mais non eligible » reste decrit comme scenario dans la prose historique (mesure du 2026-09-08 : image 2.336.0, 5566 re-telechargements) — c'est le mode de panne constate, plus une affirmation du verdict.

Deux precisions honnetes :

  • Je n'ai pas retenu (b) : je n'ai pas de source autoritative exposant la version minimale exigee par GitHub, et en affirmer une sans la verifier reproduirait exactement le defaut que vous nommez.
  • Je ne pretend pas avoir supprime la fatigue d'alerte : le remede (a) rend le signal lisible pour ce qu'il est (« on est derriere la derniere release », fait vrai et actionnable) ; il ne change pas la cadence de declenchement. Si une tolerance vous parait necessaire, c'est votre option (c) et elle merite son propre grain — la prose ne peut pas la porter.
  • Le statut reste PIN_STALE : la pin est effectivement en retard sur la derniere release ; c'est la consequence qui n'est plus affirmee, pas le fait.

2. Nit --required-version "" — corrige

args.required_version or fetch_required_version() est remplace par is not None + refus explicite de la chaine vide (ap.error, rc 2), sans appel reseau : le or faisait bien mesurer autre chose que ce que le test croyait. Contrat neuf mesure par test_required_version_vide_refuse_sans_appel_api.

3. version_key() tuples de longueurs differentes — aucune action, comme vous le notez vous-meme (la regex impose 3 segments partout) : note pour memoire, rien a faire.

Tests (relances apres le dernier commit) :

  • scripts/tests/test_check_runner_version_pin.py : 8 passed — l'assertion de l'ancien libelle est remplacee par le nouveau contrat (fait mesure present, "non eligible" desormais assert absent du detail, garde anti-reintroduction) ; test du fetch renomme.
  • scripts/tests/test_check_self_hosted_runner_policy.py + test_check_runner_starvation.py : 82 passed (le docblock du workflow est lu par le policy checker).

Perimetre inchange : les 4 fichiers de la PR, organe toujours advisory par construction.

@github-actions github-actions Bot removed the large-pr-no-review PR > seuil sans review (ni bot ni humaine) -- retire quand une review arrive (#11232) label Sep 11, 2026
@jsboige
jsboige merged commit 64a1d7b into main Sep 11, 2026
18 of 20 checks passed
jsboige added a commit that referenced this pull request Sep 13, 2026
…#15423)

* feat(ci,#14846): runner starvation advisory multi-label (waiter+linux+lean)

Issue #14846 acceptance A1 : sonde EXTINCTION (zero runner online) +
STARVATION (jobs queued sans in_progress) etendue aux 3 labels requis
par les workflows bloquants :
  - coursia-waiter (jambe same-repo PR gate, pr-gate.yml:107)
  - coursia-linux (deja couvert par linux-runner-starvation-advisory.yml)
  - coursia-lean (jambe des PRs touchant *.lean)

Reutilise scripts/ci/check_runner_starvation.py (organe inchange, deja
parametrable par --label et --warn-floor). Workflow matrice sur 2 labels
(waiter + lean) avec cron 23,53 (offset distinct du cron linux 19,49).
Acceptance A2 (persistance systemd) deja livree par #14981 (waiter) +
#15401 (lean). Acceptance A3 (verification systemctl is-enabled po-2024)
hors scope worker -> DM coord.

Advisory par construction (schedule + workflow_dispatch uniquement) : un
run rouge ne peut jamais bloquer une PR. Voir #14846 pour le contexte et
#13378 pour l'organe fondateur.

Tell c.898 strict collision pre-EDIT verifie : 0 PR ouverte sur
.github/workflows/*runner-starvation*.

* fix(ci,#14846): wire runner-starvation-advisory.yml into self-hosted allowlist

Le check requis `Scripts Tests (CPU)` (run 34410908416) rougit sur
WORKFLOW_NOT_ALLOWED : le checker scan_self_hosted_runner_policy
enumere explicitement la liste des workflows SELF_HOSTED autorisés
et le nouveau fichier `.github/workflows/runner-starvation-advisory.yml`
n'y figurait pas.

L'organe n'a aucun mérite à s'auto-découvrir ; il exige son inscription
EXPLICITE. Tranche 1 #13378, decision ai-01 2026-09-02, owner
myia-po-2024 : routage vers le pool Linux containerisé exige
l'inscription a l'allowlist, avec commentaire de tranche complet
(mêmes éléments que `linux-runner-version-pin-advisory.yml` #15201).

Profil : advisory schedule+workflow_dispatch UNIQUEMENT (doctrine
#12817 -- un run rouge ne peut JAMAIS bloquer une PR), runs-on
STATIQUE [self-hosted, coursia-ephemeral, coursia-linux]
(observateur hebergé sur la jambe Linux, pas sur les labels
qu'il observe -- un observateur qui tournerait sur le label garde
mourrait avec lui, c'est précisément le mode de panne fondateur de
#13378 2026-09-02 entre 07:00 et 10:25 UTC).

Verification : `python -m pytest scripts/tests/test_check_self_hosted_runner_policy.py::test_current_repository_self_hosted_jobs_satisfy_isolation_policy`
-> 1 passed. ZERO job self-hosted sur pull_request : aucun risque
d'atteindre un job de fork.

Rollback = revert de la PR (l'entree disparait de l'allowlist, cf
pattern tranche 5 #14283 + #15366 proche).

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

* fix(ci,#15423): remove inoperative workflow_dispatch label override

Grain: MED/guard — lane myia-po-2027:CoursIA-2 — dissipate B.0 contracts

L'input workflow_dispatch.label etait lu via LABEL: ${{ matrix.label || inputs.label }} -
chaque job matriciel porte matrix.label truthy, donc inputs.label n'etait jamais lu.
Un dispatch cible aurait toujours sonde waiter + lean. Option 1 (bornee) du review
ai-01 : retirer l'input, l'expression et les claims d'override.

Refs: PR #15423 review ai-01 2026-09-09T23:13:41Z (head 7eed854)

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

* fix(ci,#15423): REPAIR c.1102 — commentaire allowlist aligne sur matrice 2 labels + workflow_dispatch sans input

Tell NEW doctrinal c.1102 ★★★★★ : HORS CAP leve pour REPARATION #15423 (REPAIR != MERGE).
CHANGES_REQUESTED ai-01 (msg 2026-09-10T22:54:11Z sur head exact 8d503f9) :
1. prev: premiere ligne portant un numero de PR -- deja dissipe c.1065 (variation_prev_guard PASS).
2. commentaire allowlist descriptif desaligne du code :
   - 'etendu aux 3 labels' alors que la matrice include ne porte que 2 (waiter + lean).
   - 'workflow_dispatch debug override (inputs.label)' alors que l'input a ete
     retire c.1065 (chaque job matriciel porte matrix.label truthy et shadowait inputs.label).

Fix : reecriture du commentaire allowlist (lignes 212-226) pour reflet du comportement
reel. Aucun changement fonctionnel (matrice YAML, entries include, runs-on, permissions,
cron schedule inchangees). PR gate SUCCESS maintenu.

`python scripts/ci/check_self_hosted_runner_policy.py` -> OK -- all self-hosted jobs satisfy isolation policy.
`variation_prev_guard.py --body-file --current-pr 15423` -> PASS, prev_targets_accepted [15321].

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: myia-po-2027 <po-2027@coursia.lan>
Co-authored-by: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane-claim-absent Closing issue carries no claim at all (#10223) variation-tag-genre-offlist GENRE hors de l'enumeration variation-protocol §1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(runner): --disableupdate fige la version runner a l'image -- une exigence de version GitHub avant le prochain rebuild bloque les slots (suivi #15182)

2 participants