Skip to content

fix(ci,#17217): organe de fraicheur du catalogue + deblocage de la livraison quotidienne - #17221

Merged
myia-ai-01 merged 3 commits into
mainfrom
fix/catalog-daily-publication-organ
Sep 23, 2026
Merged

myia-ai-01 merged 3 commits into
mainfrom
fix/catalog-daily-publication-organ

Conversation

@myia-ai-01

@myia-ai-01 myia-ai-01 commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Grain: MED/harness -- lane myia-ai-01:CoursIA -- paths: scripts/ci/check_catalog_freshness.py, scripts/tests/test_check_catalog_freshness.py, .github/workflows/catalog-cron.yml

Le problème

Le README promet un catalogue régénéré chaque jour. Le cron tient sa part — huit runs success d'affilée, dont celui de ce matin. Mais il ne pousse pas sur main : il livre par une PR longue durée, #15942 (chore/catalog-refresh-pending), et cette PR est BLOCKED depuis 8 jours.

Mesure sur main (dc0ccbf) :

Entrées du catalogue 1137
… dont chemins fantômes (l'entrée promet un fichier qui n'existe pas) 83 (7,3 %)
Notebooks réels absents du catalogue 301
Couverture réelle 77,7 %

Sur la branche de livraison, le même jour : 0 fantôme, 1240 entrées. La régénération était correcte depuis le début — c'est la livraison qui manquait.

Personne ne le voyait, et c'est le point : un cron qui réussit est silencieux, une PR ouverte est silencieuse, et un catalogue périmé se lit exactement comme un catalogue à jour. La promesse « eventual consistency, <24 h » vivait dans un commentaire de workflow, pas dans une mesure.

Le diagnostic inscrit dans le dépôt est réfuté par la mesure

catalog-cron.yml portait, depuis #11202 :

a push with GITHUB_TOKEN never emits [a pull_request event] (GitHub anti-recursion guard)

Les runs sont émis :

$ gh run list --branch chore/catalog-refresh-pending --limit 100 \
     --json conclusion --jq '[.[]|select(.conclusion=="action_required")]|length'
83

Ils ne sont pas absents, ils sont non approuvés. Un run action_required n'a jamais tourné : le check requis qu'il porte n'existe pas au head — il n'est ni vert ni rouge, il manque. D'où un mergeStateStatus: BLOCKED avec tous les checks visibles au vert, que rien n'explique quand on lit le rollup.

Conséquence pratique : le remède désigné comme canonique par ce commentaire — le « empty-commit wake-up » manuel — visait la mauvaise cause. Le commentaire est corrigé dans le diff, avec la mesure qui le réfute.

Vérification firsthand

Les 16 runs garés du head 8b0cc5c1 ont été approuvés à la main. Au même head, immédiatement après :

6  completed/success
1  in_progress
10 queued
0  action_required      <-- plus aucun

Le mécanisme répond. #15942 finira de virer au vert sans autre geste.

Ce que dépose cette PR

scripts/ci/check_catalog_freshness.py — le témoin permanent. Il mesure deux choses indépendantes, parce qu'elles cassent séparément :

  1. la divergence catalogue ↔ arbre (fantômes + absents), hors-ligne, sans réseau ;
  2. la livraison : âge de la PR et runs garés au head courant.
$ python scripts/ci/check_catalog_freshness.py --delivery
Catalogue : 1137 entrees pour 1351 notebooks sur disque -- couverture 77.7 %
   chemins fantomes (entree sans fichier) : 83
   notebooks absents du catalogue         : 301
Livraison : PR #15942 (BLOCKED), ouverte depuis 8.2 jour(s)
   (83 run(s) gares sur des heads perimes -- sans effet, non imputes)
La publication quotidienne ne tient pas sa promesse :
   - 384 notebook(s) divergents (tolerance 0)
   - PR de livraison #15942 ouverte depuis 8.2 j (seuil 2.0)
rc=1

.github/workflows/catalog-cron.yml — diagnostic corrigé, et un step Release the parked runs on the delivery branch qui approuve les runs garés du head après chaque poussée : actions: write, continue-on-error: true, et un ::warning nommé si le jeton n'a pas le droit d'approuver. Le geste ne peut pas casser le cron ; s'il échoue, il le dit au lieu de laisser la PR pourrir en silence.

Le contrôle qui porte la valeur des tests

Huit tests, contrôles positifs en tête — un garde qui ne peut pas échouer ne prouve rien quand il rend vert, et le cas nominal de ce garde est justement un vert.

Celui qui compte le plus est le contrôle de capacité à verdir :

def test_un_run_gare_sur_un_head_perime_n_est_pas_impute(monkeypatch):
    """Le controle qui empeche l'organe de rester rouge pour toujours."""

Une PR longue durée accumule les heads : chaque régénération quotidienne en pousse un neuf, et les runs garés des anciens ne disparaissent jamais. Ma première version les comptait tous — elle aurait affiché « 83 garés » même une fois la panne réparée, donc un organe incapable de verdir, indiscernable d'un organe débranché, et qu'on aurait fini par ignorer. Seul le head courant porte les checks requis de la PR telle qu'elle est ; les autres sont comptés à part et non imputés.

Fail-closed également : un catalogue illisible rend rc=2, jamais rc=0 — une mesure impossible n'est pas une mesure à zéro.

Validation

$ python -m pytest scripts/tests/test_check_catalog_freshness.py -q
8 passed in 0.15s

Le test est dans scripts/tests/, seul emplacement de testpaths pour les organes de scripts/ci/ — je l'avais d'abord écrit dans un scripts/ci/tests/ qui n'est pas collecté, où il n'aurait jamais tourné.

Aucun notebook touché. COURSE_CATALOG.generated.* non touché — byte-identique à main.

Non couvert par cette PR

  • Le merge de chore(catalog): scheduled auto-regenerate (long-lived PR) #15942, qui est l'acte qui répare réellement les 83 fantômes et les 301 absents. Il suit, une fois ses checks terminés.
  • L'enregistrement du garde en CI (scripts/ci/fast_lane_registry.py) : geste séparé, à poser advisory d'abord comme le reste de la famille. Cette PR dépose l'organe, elle ne le câble pas — le câbler bloquant aujourd'hui rougirait toutes les PRs tant que chore(catalog): scheduled auto-regenerate (long-lived PR) #15942 n'est pas mergée.
  • L'interblocage entre le cron et le gate de dossier Phase 4 : la régénération quotidienne déplace le head et périme toute attestation exact-head avant qu'elle soit consommable. Instruit séparément.

Correctif b3ceedda48 — l'afflux que ce step fabriquait

La première version de ce step approuvait d'un coup tous les runs garés au head. Mesuré le jour même, en approuvant 16 runs à la main sur #15942 : les checks sont restés queued 19 minutes sur un pool de runners déjà saturé, pendant lesquelles le job PR gate les a sondés toutes les ~31 s jusqu'à épuiser le quota du jeton d'installation (HTTP 403) — et il échoue fail-closed.

[pr-gate] FAIL -- cannot establish check state: gh api .../check-runs?per_page=100&page=1
failed (exit 1): gh: API rate limit exceeded for installation ... (HTTP 403)

Le quota d'installation est un budget distinct de celui d'un jeton utilisateur : le mien lisait 5000/5000 au même instant. Lire l'un pour conclure sur l'autre aurait donné le diagnostic inverse.

L'échec de PR gate sur #15942 n'est donc pas un défaut de cette PR-là, et le step tel que livré aurait refabriqué cet afflux à chaque passage quotidien du cron.

Avant Après
Approbations toutes, en rafale espacées de RELEASE_STAGGER_SECONDS (15 s)
Débit total inchangé inchangé — ce qui baisse est le taux d'arrivée
Garde-fou aucun RELEASE_CEILING (40), soupape et non plafond de débit
Dépassement — ::warning explicite, jamais silencieux

Pourquoi pas un plafond par passage. Le cron pousse un commit neuf à chaque dérive, donc le head change : des runs « reportés au lendemain » seraient garés sur un head périmé, que l'organe n'impute déjà plus (parked_stale_heads). Plafonner le débit casserait la convergence au lieu de l'étaler — et un plafond tu se lit comme « tout a été traité », ce que cette PR existe précisément pour empêcher.

Vérifié : parse YAML, bash -n sur le bloc extrait, et contrôle positif du du jq — il rend bien une tabulation réelle (cat -A → ^I), celle sur laquelle le read découpe. Les trois constructions d'échappement avaient été dépliées par un heredoc à la rédaction ; sans ce contrôle, le step serait parti avec un jq cassé.

See #17217
See #15942
See #11202

🤖 Generated with Claude Code

…vraison quotidienne

Le README promet un catalogue regenere chaque jour. Le cron tient sa part --
huit runs `success` d'affilee -- mais il ne pousse pas sur `main` : il livre
par une PR longue duree (#15942, `chore/catalog-refresh-pending`), et cette
PR est BLOCKED depuis 8 jours. Le catalogue de `main` date donc du 09-12 :
83 entrees pointent un fichier inexistant, 301 notebooks reels sont absents,
couverture reelle 77,7 % au lieu de 100 %.

Personne ne le voyait : un cron qui reussit est silencieux, une PR ouverte
est silencieuse, et un catalogue perime se lit exactement comme un catalogue
a jour. La promesse vivait dans un commentaire, pas dans une mesure.

Cause mesuree, qui refute le diagnostic inscrit dans le workflow (#11202,
« a push with GITHUB_TOKEN never emits [a pull_request event] ») : les runs
SONT emis. `gh run list --branch chore/catalog-refresh-pending` en rend 83,
event `pull_request`, tous `action_required`. Ils ne sont pas absents, ils
sont non approuves -- un run gare n'a jamais tourne, donc le check requis
qu'il porte n'existe pas au head, et la PR reste BLOCKED sans qu'aucun rouge
ne l'explique. Le remede n'est donc pas le « empty-commit wake-up » manuel
que ce commentaire designait comme canonique.

Trois gestes :

- `scripts/ci/check_catalog_freshness.py` -- le temoin permanent. Mesure deux
  choses independantes parce qu'elles cassent separement : la divergence
  catalogue/arbre (hors-ligne), et l'etat de la livraison (age de la PR,
  runs gares AU HEAD COURANT). Fail-closed : un catalogue illisible rend 2,
  jamais 0.
- `scripts/tests/test_check_catalog_freshness.py` -- 8 tests, controles
  positifs en tete. Dont celui qui garde la capacite a VERDIR : un run gare
  sur un head perime n'est pas impute, sinon l'organe reste rouge pour
  toujours une fois la panne reparee, et devient indiscernable d'un organe
  debranche.
- `catalog-cron.yml` -- diagnostic corrige, et step `Release the parked runs`
  qui approuve les runs gares du head apres chaque poussee (`actions: write`,
  `continue-on-error`, avertissement nomme si le jeton ne peut pas approuver).

Verification firsthand : les 16 runs gares du head 8b0cc5c ont ete approuves
a la main ; au meme head, plus aucun `action_required` -- 6 success,
1 in_progress, 10 queued. Le mecanisme repond.

See #17217
See #15942

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Grain tag obligatoire (#10045, bloquant).

Grain tag absent (no Grain: / in body).

Pour passer ce gate, le body doit porter en tete une ligne de la forme :

Grain: <DEEP|MED|LIGHT>/<genre> -- lane <machine:workspace> -- prev: <TIER>/<GENRE> #<PR>

Le <genre> doit figurer dans l'enumeration §1 de variation-protocol.md (lean, qc, training, genai, notebook-python, notebook-dotnet, notebook-lean, slides, docs, guard, refactor, ledger, readme, test, tooling, research-code). Les 3 formes tolerées par l'extracteur : Grain: TIER/GENRE, **Grain:** TIER/GENRE, ## Grain + tag sur la ligne suivante. La lane doit suivre le format <machine>:<workspace> (cf. lane-claim-protocol.md).

…afflux qui a tue le gate

Le step livre plus tot approuvait d'un coup TOUS les runs gares au head.
Mesure du 2026-09-21, faite en approuvant 16 runs a la main sur #15942 :
les checks sont restes `queued` 19 minutes sur un pool de runners deja
sature, pendant lesquelles le job "PR gate" les a sondes toutes les ~31 s
jusqu'a epuiser le quota du jeton d'INSTALLATION (HTTP 403, budget
distinct de celui d'un jeton utilisateur) -- et il echoue fail-closed.

Le step tel que livre aurait donc refabrique cet afflux a chaque passage
quotidien du cron.

Ce que change ce commit :

- `RELEASE_STAGGER_SECONDS` (15 s) entre deux approbations. Le debit
  TOTAL est inchange -- ce qui baisse est le taux d'ARRIVEE, donc le
  nombre de runs simultanement en file pendant que le gate sonde.
- `RELEASE_CEILING` (40) comme soupape, pas comme plafond de debit :
  au-dela, des runs s'empilent sans s'executer et approuver n'y repond
  pas. Le depassement est ECRIT (`::warning`), jamais silencieux -- un
  plafond tu se lit comme « tout a ete traite ».

Pourquoi pas un plafond par passage : le cron pousse un commit neuf a
chaque derive, donc le head change. Des runs « reportes au lendemain »
seraient gares sur un head perime, que l'organe n'impute deja plus.
Plafonner le debit casserait la convergence au lieu de l'etaler.

Verifie : YAML parse, `bash -n` sur le bloc extrait, et controle positif
du `\t` du jq (rend bien une tabulation reelle, que le `read` decoupe).

See #17221

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jsboige

jsboige commented Sep 21, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17221
head: b3ceedd
complete: true
body: read
comments-reviewed: 1
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 4cf63a123e5746023c3ab758d60d904a025983100cc48997bcc4f118a1ed2f49
diff-files: 3
diff-additions: 517
diff-deletions: 10
checks: blocked
b0: clear
scope: pass
domain: pass
verdict: BLOCKED
[/ADJOINT PREFLIGHT]

Verdict BLOCKED, cause nommee. 17 check-runs dedupliques par (started_at, id) au head b3ceedd (commit tardif inclus) : 9 pending, 1 rouge. Le rouge est PR gate (quota d'installation GitHub, echec de flotte). Les 9 pending -- dont Scripts Tests (CPU), Always-on guards, Gitleaks secret scanner -- interdisent par construction un claim vert : un dossier emis ici serait faux. mergeable = true, b0 rc=0. Re-emission requise apres stabilisation des pending.

@github-actions

Copy link
Copy Markdown
Contributor

Grain tag obligatoire (#10045, bloquant).

Grain tag absent (no Grain: / in body).

Pour passer ce gate, le body doit porter en tete une ligne de la forme :

Grain: <DEEP|MED|LIGHT>/<genre> -- lane <machine:workspace> -- prev: <TIER>/<GENRE> #<PR>

Le <genre> doit figurer dans l'enumeration §1 de variation-protocol.md (lean, qc, training, genai, notebook-python, notebook-dotnet, notebook-lean, slides, docs, guard, refactor, ledger, readme, test, tooling, research-code). Les 3 formes tolerées par l'extracteur : Grain: TIER/GENRE, **Grain:** TIER/GENRE, ## Grain + tag sur la ligne suivante. La lane doit suivre le format <machine>:<workspace> (cf. lane-claim-protocol.md).

@jsboige

jsboige commented Sep 21, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17221
head: b3ceedd
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 36a95eea92575a1aee35b7b64e195afa0ec015173756c905a828ec312ee463b8
diff-files: 3
diff-additions: 517
diff-deletions: 10
checks: blocked
b0: clear
scope: pass
domain: pass
verdict: BLOCKED
[/ADJOINT PREFLIGHT]

Verdict BLOCKED, 3 rouges nommes, 0 pending (les 9 checks en vol au cycle precedent se sont stabilises). (1) PR gate = failure : quota d'installation GitHub, echec de flotte. (2) Scripts Tests (CPU) = failure : abort natif herite de la base, corrobore par myia-po-2023 sur 7+ PRs. (3) Always-on guards -- 15 organes, 1 checkout = failure : rouge base-inherited, corroborated sur #17213/#17229/#17189 (imputations ecrites). mergeable=true, b0 rc=0. Ce qui leverait : disparition des rouges au prochain balayage.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359) — résolue

La collision de chemins signalée sur #17221 n'existe plus au passage du 2026-09-22T20:22Z : aucune autre PR ouverte ne partage désormais de chemin de fichier avec elle. Note laissée en place de l'avertissement (retraction non destructive).

@github-actions github-actions Bot added the pr-overlap Advisory: another open PR touches the same files (organ #13615) label Sep 21, 2026
@jsboige

jsboige commented Sep 22, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 17221
head: b3ceedd
complete: true
body: read
comments-reviewed: 5
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 8b8246734781bc067200dba2268fb29d443c92a0b2f8a275369d12793600031b
diff-files: 3
diff-additions: 517
diff-deletions: 10
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@github-actions github-actions Bot added variation-tag-genre-offlist GENRE hors de l'enumeration variation-protocol §1 variation-tag-prev-absent Tag Grain sans 'prev: <TIER>/<GENRE> #<PR>' (adjacence G-VAR-3 inevaluable) and removed variation-tag-missing PR sans tag Grain: <TIER>/<GENRE> (variation-protocol) labels Sep 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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.

@jsboige

jsboige commented Sep 23, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17221
head: 64991e5
complete: true
body: read
comments-reviewed: 7
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: efaf264d8bcdd256dcdcf625739fd3f6b3182015585fc1b3b06bdf02d8b64c67
diff-files: 3
diff-additions: 517
diff-deletions: 10
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Sep 23, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 17221
head: 64991e5
complete: true
body: read
comments-reviewed: 8
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 7db2053381c30f6a03f70c9b434b9a4c19e94ed77a7399040d9048c5bae112c2
diff-files: 3
diff-additions: 517
diff-deletions: 10
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
[/ADJOINT PREFLIGHT]

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

Labels

pr-overlap Advisory: another open PR touches the same files (organ #13615) variation-tag-genre-offlist GENRE hors de l'enumeration variation-protocol §1 variation-tag-prev-absent Tag Grain sans 'prev: <TIER>/<GENRE> #<PR>' (adjacence G-VAR-3 inevaluable)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants