Trois tickets du retrofit de nuage, et une porte d'audit qui dormait #95

Merged
nicolas-fryder merged 3 commits from nicolas-fryder/dev into main 2026-09-30 16:08:21 +00:00 AGit

Corrige #92 et #93, redresse la premisse de #94 et ajoute le controle qui manquait. Releve quatre advisories au passage. Detail dans les messages de commit.

Corrige #92 et #93, redresse la premisse de #94 et ajoute le controle qui manquait. Releve quatre advisories au passage. Detail dans les messages de commit.
`compose/e2e.yml` posait `BREAK_GLASS_SECRET: ${EQS_E2E_SECRET:-e2e-not-a-secret-…}`.
`SECRETS_INTERDITS` de `@eqs/config` cherche la sous-chaîne `secret` : elle la
trouvait dans une valeur qui annonce précisément n'être pas un secret. Le
conteneur sortait en 1 sur « BREAK_GLASS_SECRET ressemble à une valeur
d'exemple », et `eqs e2e up` rendait « dependency failed to start » — un message
qui ne nomme aucune variable. Mesuré sur `eqs-demo` contre 0.13.0 fraîchement
publié.

Le correctif tient en une valeur. Ce qui compte est la mesure qui manquait :
c'est la QUATRIÈME panne de cette pile trouvée en construisant une image et en
lisant les journaux d'un conteneur mort — `npmrc` absent (0.10.1), sonde sur
`/health` pour un profil `front` (0.11.2), `SERVICE_NAME` absent (0.13.0), ce
secret. On ne découvre la panne suivante qu'en réparant la précédente, ce qui
est le signe qu'il manquait un contrôle et pas un correctif.

`test/pile-e2e.test.ts` lit le compose livré, résout les valeurs de repli comme
Docker le fera pour quelqu'un qui ne pose rien, et passe l'environnement obtenu
par `baseSchema` et `coherence(…, reglesDeBase)` — les mêmes portes que
l'application appliquera au démarrage. Aucun Docker, 600 ms.

Éprouvé en le cassant, parce qu'un contrôle qui ne rougit jamais ne prouve rien :

  compose tel quel                  schéma OK     | bloquants : aucun
  sans SERVICE_NAME                 schéma REFUSE | bloquants : aucun
  avec l'ancien BREAK_GLASS_SECRET  schéma OK     | bloquants : « ressemble à
                                                    une valeur d'exemple »

Chacun des deux défauts rougit, et dans un test distinct.

`variablesPosees` passe par la racine du paquet : elle lit les deux formes
d'`environment` de Compose et sert autant au contrôle `coolify.*` qu'ici.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La chasse partie du secret de la pile e2e (commit précédent) a montré une classe,
pas un défaut : le socle livre des étages EXÉCUTABLES — une pile e2e, un compose
de déploiement, une répétition générale — et rien ne vérifiait la boucle : *ce
que je livre, est-ce que je l'accepterais ?*

Sept compose des trois dépôts passés par `baseSchema` et `coherence` : un seul
service refusé, celui d'e2e, déjà corrigé. Puis la reconstitution de
l'environnement de RÉPÉTITION, qui a sorti la vraie prise.

**`eqs-satellite-template` ne pouvait pas passer son propre
`eqs coolify verify --up`.** Son `.env.example` laisse `SERVICE_NAME` vide à
dessein — un clone doit se nommer, sinon deux clones parlent à la même base (#62)
— son compose écrit `SERVICE_NAME: ${SERVICE_NAME}` sans repli, et le dépôt
n'avait AUCUN `docker-compose.repetition.yml`, contrairement à `eqs-demo` qui en
a un précisément pour ça. Le dépôt que trente satellites clonent livrait donc une
porte infranchissable, et son verdict après trois minutes d'attente et une
construction d'image était « le conteneur n'est jamais devenu sain » — sans
nommer une seule variable.

`coolify.repetition-demarrable` reconstitue l'environnement que `--up` utilisera
vraiment et le passe par `defineEnv` et `coherence` — les portes exactes que
l'application franchira. Quelques millisecondes, et le message nomme la variable :

  gabarit AVEC sa nouvelle surcouche          vert
  gabarit SANS surcouche (l'état d'avant)     ROUGE  api ne démarrerait pas en
                                                     répétition : SERVICE_NAME
                                                     est requise
  demo telle quelle                           vert
  demo, surcouche avec un secret d'exemple    ROUGE  AUTH_SECRET : 32 caractères
                                                     au moins

Il ne refuse que ce qu'il peut PROUVER, comme `coolify.port-epingle` à côté. Sa
première version accusait une fixture de trois variables de ne pas déclarer
`PUBLIC_URL` — ce qui est le travail de `config.env-example`, et accuser deux fois
la même cause est du bruit qu'on finit par éteindre. Resserré : il ne se prononce
que sur les variables que CE compose ou CET exemple nomment. Et sa limite est
écrite dans le fichier — il juge avec `baseSchema`, pas avec le schéma du
satellite, qui n'est pas lisible statiquement.

Un bug trouvé en chemin, dans mon propre code : `coherence(env, regles)` préfixe
DÉJÀ `reglesDeBase`. Lui repasser `reglesDeBase` jouait chaque règle deux fois et
sortait le même refus en double. Vu parce qu'un test comptait les problèmes au
lieu de se contenter de leur contenu. Corrigé aux deux endroits.

`minor` et non `patch` : l'API grandit d'un contrôle et d'une option.

Chasse 21 consignée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois tickets ouverts par le rétrofit de `nuage`, et une prémisse à redresser.

## #92 — le ticket disait trois entrées fausses sur quatre, il y en avait quatre

`docs/profils.md` annonçait « Dépôts concernés aujourd'hui : ATLAS, photofront,
metalfrom, nuage » pour le profil `next-fullstack`. Chaque ligne vérifiée contre
l'origine git et contre la forme de l'arbre, pas relue :

  ATLAS       EQS/ATLAS, pas de apps/, 103 route.ts          exact
  photofront  EQS/LUMIX, apps/api NestJS + apps/web Next     nom faux ET profil faux
  metalfrom   git.nicolasfryder.ovh/nico/metalfrom.eu        autre forge, hors flotte
  nuage       EQS-Sandbox/NUAGE, apps/api NestJS + front     profil faux : c'est `nest`

Le ticket proposait un renommage pour `photofront`. C'en était deux : le dépôt ne
s'appelle pas comme ça **et** il n'a pas ce profil. LUMIX est `apps/api` en
`NestFactory.create()` + `apps/web` en Next, zéro route handler — la forme exacte
du gabarit. Une entrée fausse cache volontiers une seconde faute derrière la
première qu'on voit.

## La chasse a rendu trois instances de plus

La classe : **un dépôt nommé dans un document, jamais confronté au dépôt.**
Consignée en chasse 22, avec les trois pièges qui l'alimentent.

  reference/satellites.md   nuage « hors socle », « (hors flotte EQS) »
  reference/satellites.md   Topologie : dépôt EQS/eqs-homepage
  docs/glossaire.md         Topologie : dépôt eqs-homepage

Les deux premières se contredisaient **sur le même dépôt** : profils.md donnait à
`nuage` un profil de la flotte, satellites.md le mettait hors de la flotte, et
aucun des deux n'avait raison. Quand deux listes tenues à la main divergent, la
tentation est d'en croire une.

Et Topologie est `EQS/Topologie`, pas `EQS/eqs-homepage` — le nom du dossier local
n'est pas le nom du dépôt, exactement comme photofront/LUMIX. Le même piège deux
fois dans le même fichier.

Parade : pas de contrôle neuf, on **retire l'autorité à la liste**. Le relevé de
`profils.md` porte sa date, la mention que chaque ligne a été mesurée sur l'arbre,
et les deux commandes qui le démentiraient. C'est ce que la chasse 18 appelait
« à défaut, la dater et dire d'où elle vient ». Un contrôle qui comparerait le
profil déduit au profil déclaré demanderait de lire les dépôts depuis
`eqs-platform` — ce que la chasse 18 a montré impossible, et qui attend `eqs-fleet`.

## #93 — `docs/retrofitter-un-existant.md`

Le symétrique d'`initialiser-un-satellite.md` : une étape, un critère de
vérification. Ce qu'il dit et qui n'était écrit nulle part :

- **l'étage 0**, le tableau complet des `peerDependencies` du socle, relevé sur
  les dix `package.json`. C'est la marche la plus haute et elle est avant la
  première. Deux pièges dedans : `@nestjs/swagger` a deux planchers (`>=11` dans
  `errors`, `^12.0.1` dans `platform-nest`, donc `^12` pour un profil `nest`), et
  `zod` est en `^4`, la migration la plus coûteuse de l'étage et la seule qui n'a
  rien à voir avec le socle ;
- **les deux baselines de D-048** avec le geste exact : le second `tsconfig.json`
  daté et chiffré, `--suppress-all` une seule fois, `--prune-suppressions` après
  correction, et l'interdiction de `--pass-on-unpruned-suppressions` ;
- **`eqs.check.taches` de D-049**, et ses deux silences par dessein : une tâche
  dont l'outil manque se retire en le disant, un manifeste mal déclaré ne lève
  pas. Donc il faut vérifier que ses tâches tournent — « ma tâche passe » et « ma
  tâche n'a pas tourné » affichent le même vert ;
- **ce qu'on garde** parce que c'est au-dessus du standard (doctrine §3), et le
  cas limite : un mécanisme meilleur que celui du socle se garde avec une
  **dispense datée**, pas un contournement ;
- **`eqs migrate inspect` d'abord, toujours** — et que le profil se déduit des
  route handlers, pas de la présence de Next. C'est ce critère qui a mal classé
  trois dépôts.

Trouvé en l'écrivant : le retour d'expérience d'ATLAS existe — 583 lignes, l'ordre
des étages, les arbitrages, six risques du socle repérés avant d'écrire une ligne
— et il est sur une branche locale `epic/retrofit` **qui n'est pas poussée**. Seul
le poste qui l'a écrite peut le lire. Le guide en reprend ce qui est vrai de tout
rétrofit ; rien n'a été touché dans ATLAS, le dépôt est un autre chantier.

## #94 — la prémisse était fausse, le manque était réel

Le ticket dit « 0.13.0 est taguée mais pas publiée ». Elle est publiée : elle est
sous l'étiquette `next` depuis le 2026-09-21, exactement là où D-037 la met.

  @eqs/tooling   latest=0.12.2   next=0.13.0      (les dix, idem)

Ce qui n'a pas eu lieu, c'est l'étape 5. Et elle n'a pas eu lieu **parce que
l'étape 4 était rouge** : le bac à sable d'`eqs-demo` a trouvé le
`BREAK_GLASS_SECRET` de la pile e2e, ce que dit c21ad5c lui-même. La procédure a
fonctionné. Il n'y a pas de publication ratée à rattraper.

Le ticket écrivait « je ne sais pas si `changeset publish` a échoué ou n'a jamais
été lancé ; les deux laissent le même état ». La question était bonne, et il
manquait la commande qui y répond. `pnpm check:publication` distingue quatre
états que `latest` seul confondait :

  promue                 rien à faire
  candidate en attente   l'étape 4 n'a pas rendu son verdict
  candidate dépassée     le dépôt a avancé depuis le tag — NE PAS promouvoir
  jamais publiée         le seul vrai défaut, celui que #94 croyait voir

Sur le dépôt aujourd'hui il rend « candidate dépassée, 2 commits après le tag »,
et dit d'aller chercher une version neuve plutôt que de promouvoir. C'est la
bonne réponse, et aucune commande ne savait la donner.

Éprouvé en le cassant, sur quatre fixtures hors du dépôt : version absente du
registre (code 1), versions divergentes en mode `fixed` (code 1), version promue
(code 0), candidate dépassée (code 0). Chacune rend le bon message **et** le bon
code.

Un bug trouvé en l'écrivant, dans mon propre code : `process.exit()` juste après
un `fetch` lève `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)` sous
Windows et le processus rend **127** — y compris sur le chemin vert. Un contrôle
dont le code de sortie ment est pire que pas de contrôle. Corrigé en posant
`process.exitCode` et en laissant Node se fermer seul ; c'est ce qui a rendu les
quatre fixtures utiles, la première version les passait toutes en 127.

Il n'est **pas** dans `pnpm check` : celui-ci tient hors ligne et le pre-push
dépasse déjà son budget. Il est joué en étape 0 et en étape 6 de la sortie, là où
sa réponse change une décision — comme `check:paquets`. Hors ligne, il le dit et
sort en 0.

`ETAT.md` porte enfin la distinction que le ticket demandait en point 3 : ce qui
est **versionné** et ce qui est **installable** sont deux tableaux, pas un.

## Trouvé en commitant : une porte rouge que rien ne déclenchait

Le `pre-commit` ne joue `pnpm audit` que si `package.json` ou le verrou est dans
l'index. J'ai ajouté un script, donc la porte s'est réveillée — rouge. Rien
n'avait changé dans le dépôt : **c'est l'advisory qui est apparu**, et aucun
commit depuis le 2026-09-28 ne touchait un manifeste.

Les quatre relevés, et leur portée, parce que « HIGH en production » a deux sens :

  brace-expansion  2 HIGH, atteints UNIQUEMENT par @next/eslint-plugin-next, pair
                   OPTIONNEL de @eqs/tooling. Chaîne de LINT : jamais le runtime
                   d'un satellite. `pnpm audit --prod` compte les pairs comme
                   production, d'où le verdict de la porte.
  multer, fast-uri MODERATE, mais par les VRAIES dépendances de
                   @eqs/platform-nest. Celles-là partent chez les satellites.

Gravité plus basse, portée plus grande : c'est l'inverse de brace-expansion, et
c'est le cas qui compte pour la flotte.

`overrides` existait déjà dans `pnpm-workspace.yaml`, avec `multer: ^2.3.0` et un
commentaire qui prévoyait exactement ceci (« les deux dépôts doivent la retirer
ensemble le jour où Nest remontera sa borne »). Un quatrième advisory porte sur
`>=2.2.0 <2.4.0` : la borne est relevée, pas doublée. **La borne d'un override se
périme sans que personne ne touche au code.**

Ce qu'un override ne fait pas, et qui reste ouvert : il tient dans ce dépôt et sa
CI. Un satellite résout `@nestjs/throttler` depuis les intervalles publiés par
`@eqs/platform-nest`. L'y protéger demande de relever les intervalles du paquet ;
c'est un changement de paquet publié et il mérite son propre passage.

`pnpm audit --prod` : aucune vulnérabilité connue, tous niveaux. `pnpm check`
rejoué vert après le changement de verrou.

## Ce que je n'ai pas vérifié

Les profils `nest + front` annoncés pour Topologie, MCP Hub et CODEX — leurs
origines git sont vérifiées, leurs profils non. Et `atelier`, déclaré `nest`,
porte huit `route.ts` dans `apps/web` : à regarder. Les deux sont dans le « reste
à regarder » de la chasse 22.

Le guide n'a pas été déroulé de bout en bout par quelqu'un qui le suit : il est
tiré d'un rétrofit fait, pas d'un rétrofit refait en le lisant.

#94 n'est pas fermé ici : il le sera quand 0.14.0 sera promue.

Closes #92
Closes #93

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-platform!95
No description provided.