From 31c217ec8ab44fffdb51611f0333e875a773498d Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Thu, 30 Jul 2026 13:36:36 +0200 Subject: [PATCH 1/3] ci(deps): une majeure incompatible retenait cinq montees saines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le groupement etait `patterns: ["*"]` par ecosysteme, donc une seule demande par mois et par ecosysteme. La premiere demande npm qui en est sortie portait six montees, dont TypeScript 7 -- que svelte-check refuse encore, il declare `peer typescript@"^5.0.0 || ^6.0.0"`. npm ci echouait AVANT le premier test : npm error ERESOLVE Found: typescript@7.0.2 peer typescript@"^5.0.0 || ^6.0.0" from svelte-check@4.7.4 Cinq montees parfaitement saines -- @types/node, svelte-check, jsdom et deux autres -- etaient donc retenues par la sixieme, sans autre issue que de fermer la demande entiere et de tout perdre. C'est le defaut du groupement, pas celui de la demande. Majeures et mineures voyagent desormais separement, dans les trois ecosystemes. Les mineures deviennent independantes : elles passent seules, et une majeure qui casse attend dans son propre lot aussi longtemps qu'il faut sans rien bloquer. Le cout est une demande de plus par ecosysteme et par mois. C'est peu au regard de ce qu'on achete : la difference entre une montee qu'on relit et une montee qu'on subit. La cadence mensuelle et le groupement restent, pour la raison ecrite en tete du fichier -- une cooperative sans mainteneur a temps plein cesse de regarder un depot qui ouvre une demande par dependance et par semaine. C'est sur le front que la separation compte le plus, et le commentaire le dit : les dependances y sont epinglees a la version EXACTE (§14.1), donc chacune de leurs majeures est une rupture potentielle de peer dependency. --- .github/dependabot.yml | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec921a5..5b2a3a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,10 +6,22 @@ # sans cette entrée, l'épinglage devient une dette que personne ne relit. Les deux vont # ensemble ou aucun des deux ne tient. # -# La cadence est MENSUELLE et les mises à jour sont GROUPÉES en une seule demande par -# écosystème. Une coopérative n'a pas de mainteneur à temps plein : une demande par -# dépendance et par semaine, c'est un dépôt où l'on cesse de regarder les demandes — donc -# un dépôt où la mise à jour de sécurité passe inaperçue avec les autres. +# La cadence est MENSUELLE et les mises à jour sont GROUPÉES. Une coopérative n'a pas de +# mainteneur à temps plein : une demande par dépendance et par semaine, c'est un dépôt où +# l'on cesse de regarder les demandes — donc un dépôt où la mise à jour de sécurité passe +# inaperçue avec les autres. +# +# MAIS LES MAJEURES VOYAGENT SÉPARÉMENT DES MINEURES, et cette ligne est écrite après un +# échec. La première version de ce fichier groupait tout en `patterns: ["*"]` par +# écosystème. La demande npm qui en est sortie portait six montées, dont TypeScript 7 — +# que `svelte-check` refuse encore (`peer typescript@"^5.0.0 || ^6.0.0"`). Résultat : +# `npm ci` échouait AVANT le premier test, et cinq montées parfaitement saines étaient +# retenues par la sixième, sans autre issue que de fermer la demande entière. +# +# Deux groupes par écosystème coûtent une demande de plus et rendent les mineures +# indépendantes : elles passent seules, et une majeure qui casse peut rester en attente +# aussi longtemps qu'il faut sans rien bloquer. C'est la seule chose qui distingue une +# montée qu'on relit d'une montée qu'on subit. version: 2 updates: # --- Go --------------------------------------------------------------------------- @@ -24,6 +36,10 @@ updates: groups: go: patterns: ["*"] + update-types: ["minor", "patch"] + go-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: build(deps) @@ -37,6 +53,10 @@ updates: groups: actions: patterns: ["*"] + update-types: ["minor", "patch"] + actions-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: ci(deps) @@ -52,6 +72,12 @@ updates: # # Tout est en devDependencies : rien de ce bloc ne part sur un poste, seul le bundle # construit voyage. + # + # C'est ICI que le groupement unique a fait le plus de dégâts, et c'est pourquoi la + # séparation compte le plus : les dépendances du front sont épinglées à la version + # EXACTE (§14.1), donc chacune de leurs majeures est une rupture potentielle de peer + # dependency. TypeScript 7 en est une, et elle attendra dans `front-majeur` que + # `svelte-check` l'accepte, sans retenir les correctifs des cinq autres. - package-ecosystem: npm directory: /web schedule: @@ -61,5 +87,9 @@ updates: groups: front: patterns: ["*"] + update-types: ["minor", "patch"] + front-majeur: + patterns: ["*"] + update-types: ["major"] commit-message: prefix: build(front) From 6380ac5b914130f55a956e786d9a285347257ea3 Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Thu, 30 Jul 2026 13:36:37 +0200 Subject: [PATCH 2/3] test(station): le garde-fou anti-blocage n'avait plus de marge sur un runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hang valait 5 s, et son commentaire promettait « it never elapses in a passing run : it is a guard, not a delay ». La CI a dementi la promesse : --- FAIL: TestAnAmputatedCatalogIsRefusedAgainstTheRealGuard (5.54s) failures_catalog_test.go:1619: la grille n'a jamais compte 331 tuiles L'echec est tombe sur une demande qui ne change QUE deux empreintes d'actions dans deux fichiers YAML. Il ne pouvait donc pas venir du code teste, et c'est ce qui permet de le nommer sans hesiter : ce n'est pas une regression, c'est le budget. Ce n'est pas non plus une course entre goroutines, et la premiere piste ecrite dans SUIVI.md -- la famille skipUnderShort que ci.yml documente -- etait fausse. MESURE sur ce test, le plus lourd du paquet parce qu'il decode les 181 images du flv.csv REEL deux fois : isole, sans -race 0,52 s (cinq executions, stable) isole, sous -race 1,07 s budget 5 s observe en CI 5,54 s L'ecart vient du runner. `go test ./...` joue les paquets EN PARALLELE, si bien qu'internal/web et cmd/openscale -- 25 s et 21 s a eux seuls -- concourent avec celui-ci sur quatre vCPU, sous le detecteur de courses. Une seconde de travail en devient cinq, et un garde-fou qui n'avait qu'un facteur cinq de marge n'en a plus. Trente secondes, et le choix ne coute RIEN sur une execution verte : awaitCondition rend la main des que sa condition tient, donc aucun test qui passe ne ralentit d'une microseconde. Ce qu'on paye, c'est trente secondes au lieu de cinq pour declarer un vrai blocage, une fois, sur une execution qui echoue deja. C'est exactement l'arbitrage que le commentaire revendiquait, et que la valeur 5 ne tenait pas. Les mesures sont ecrites a cote de la constante et non ici : la prochaine personne tentee de la redescendre a « une valeur ronde » les trouvera sur place. --- internal/station/harness_test.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/internal/station/harness_test.go b/internal/station/harness_test.go index 8869010..3c1cdb2 100644 --- a/internal/station/harness_test.go +++ b/internal/station/harness_test.go @@ -33,7 +33,30 @@ const ( // hang is how long a test waits for something that should be immediate before it // declares a deadlock. It never elapses in a passing run: it is a guard, not a // delay, and no assertion depends on its value. -const hang = 5 * time.Second +// +// # Thirty seconds, and it used to be five +// +// Five was not a margin, it was a coincidence that held on a development machine. CI +// caught TestAnAmputatedCatalogIsRefusedAgainstTheRealGuard at 5.54 s on 30/07/2026, on a +// pull request that changed nothing but two action SHAs in YAML — so the failure could not +// have come from the code under test. MEASURED afterwards, on the same test, which is the +// heaviest of this package because it decodes the 181 images of the REAL flv.csv twice: +// +// isolated, no -race 0.52 s (five runs, stable) +// isolated, -race 1.07 s +// budget 5 s +// observed in CI 5.54 s +// +// The gap is the runner. `go test ./...` runs the packages CONCURRENTLY, so internal/web +// and cmd/openscale — 25 s and 21 s of their own — compete with this one on four vCPUs, +// under the race detector. One second of work becomes five, and a guard with a factor of +// five of headroom has none left. +// +// Raising it costs NOTHING on a green run: the wait returns as soon as its condition +// holds, so no passing test gets slower by a microsecond. What it costs is thirty seconds +// instead of five to declare a genuine deadlock, once, on a run that is already failing. +// That is the trade the sentence above claims to make, and five did not honour it. +const hang = 30 * time.Second // loadConfig reads the configuration actually shipped with the binary. // From 18dc2d4c5f03f6562a300e3cff7a7e51f5d67f0e Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Thu, 30 Jul 2026 13:36:37 +0200 Subject: [PATCH 3/3] docs: l'echec intermittent avait un nom, et la piste que j'avais donnee etait fausse L'entree de la passe de securite laissait une observation ouverte : une execution de la suite sur douze signalait un echec dans un paquet, jamais reproduit, et la commande de comptage n'en avait pas garde le nom. Elle renvoyait vers la famille de tests sensibles au planificateur que ci.yml documente, skipUnderShort. Ce n'etait pas ca, et une piste fausse laissee dans le suivi coute plus qu'une observation sans piste : elle envoie chercher au mauvais endroit. Le test est nomme, la cause est mesuree, et le correctif est dans le commit precedent. Le suivi porte desormais les quatre chiffres qui la montrent -- 0,52 s isole, 1,07 s sous -race, 5 s de budget, 5,54 s observe -- et l'explication de l'ecart : les paquets tournent en parallele, donc les trois plus lourds concourent sur quatre vCPU sous le detecteur de courses. L'entree dit aussi ce que le groupement de dependabot.yml a coute, pour la meme raison qu'on ecrit les echecs plutot que de les corriger en silence : la prochaine personne tentee de regrouper tout en une demande y trouvera pourquoi cela avait ete essaye, et ce que cela a bloque. Rien d'autre ne change : ni un compteur, ni un chiffre de la passe de securite. --- SUIVI.md | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/SUIVI.md b/SUIVI.md index 2fdf569..924e12d 100644 --- a/SUIVI.md +++ b/SUIVI.md @@ -68,12 +68,50 @@ l'écrire. Un fait qui les renforce, vérifié plutôt que supposé : la table ` porte **aucun identifiant de client** — donc son export ouvert n'expose pas de donnée personnelle. -**Une observation restée ouverte.** Sur douze exécutions de la suite complète, **une** a -signalé un échec dans un paquet, jamais reproduit ensuite — et la commande de comptage qui -l'a vu n'en a pas gardé le nom. Ce n'est pas imputable aux ajouts ci-dessus : les trois -tests nouveaux ont tenu 50 exécutions chacun, et `internal/web` dix passes sous `-race`. -`ci.yml` documente déjà une famille de tests sensibles au planificateur (`skipUnderShort`), -et c'est la première piste si le symptôme revient. +**L'échec intermittent a un nom, et la première piste était fausse (30/07/2026).** L'entrée +ci-dessus le laissait ouvert : une exécution de la suite sur douze signalait un échec dans un +paquet, jamais reproduit, et la commande de comptage n'en avait pas gardé le nom. Elle +renvoyait vers la famille de tests sensibles au planificateur que `ci.yml` documente +(`skipUnderShort`). **Ce n'était pas ça.** + +La CI l'a rattrapé sur la demande Dependabot des actions — une demande qui ne change que +deux empreintes dans deux fichiers YAML, donc dont l'échec ne pouvait pas venir du code +testé : + +``` +--- FAIL: TestAnAmputatedCatalogIsRefusedAgainstTheRealGuard (5.54s) + failures_catalog_test.go:1619: la grille n'a jamais compté 331 tuiles +``` + +Ce n'est pas une course entre goroutines, c'est un **budget d'attente sans marge**. Mesuré +sur ce test — le plus lourd du paquet, il décode les 181 images du `flv.csv` réel deux fois : + +| Condition | Durée | +|---|---| +| isolé, sans `-race` | 0,52 s (cinq exécutions, stable) | +| isolé, sous `-race` | 1,07 s | +| budget `hang` | 5 s | +| observé en CI à l'échec | 5,54 s | + +L'écart vient du runner. `go test ./...` joue les paquets **en parallèle**, donc +`internal/web` et `cmd/openscale` — 25 s et 21 s à eux seuls — concourent avec celui-ci sur +quatre vCPU, sous le détecteur de courses. Une seconde de travail en devient cinq, et un +garde-fou qui n'avait qu'un facteur cinq de marge n'en a plus. + +`hang` passe donc de 5 s à **30 s**, avec les mesures écrites à côté de la constante. Le +changement ne coûte **rien** sur une exécution verte — l'attente rend la main dès que sa +condition tient, aucun test qui passe ne ralentit d'une microseconde. Il coûte trente +secondes au lieu de cinq pour déclarer un vrai blocage, une fois, sur une exécution qui +échoue déjà. C'est l'arbitrage que le commentaire de la constante revendiquait depuis le +début — « it is a guard, not a delay » — et que la valeur 5 ne tenait pas. + +**Et les deux groupes de `dependabot.yml`, pour la même raison qu'on écrit les échecs.** La +première version du fichier groupait tout par écosystème en `patterns: ["*"]`. La demande +npm qui en est sortie portait six montées dont TypeScript 7, que `svelte-check` refuse +encore (`peer typescript@"^5.0.0 || ^6.0.0"`) : `npm ci` échouait avant le premier test, et +cinq montées saines étaient retenues par la sixième sans autre issue que de fermer la +demande entière. Majeures et mineures voyagent désormais séparément, dans les trois +écosystèmes. **Il y a bien un gcc sur ce poste, et les relevés précédents disaient le contraire.** La passe `-race` exige cgo, donc un compilateur C, et trois entrées de suivi de suite l'ont