Clément Farkas DauduEN

Quatre pivots vers un seul shell frontend

TL;DR

La galère, en trois couches

Chaque appli avait son propre repo, ses propres owners, ses propres déploiements et sa propre URL. Le problème se posait sur trois couches :

  1. Coordination des releases. 10 à 20 montées de version de librairies par semaine, chacune multipliée par N applis. Des centaines d'actions manuelles par semaine : des PR de dépendances, des tickets de release, des promotions manuelles (faire passer un build à l'environnement suivant). Une part non négligeable de l'année d'un ingénieur, et ça tous les ans.
  2. UX. Rechargement complet du document entre les applis. Scroll perdu, état perdu. Les utilisateurs perçoivent N produits sans lien entre eux, parce que c'est littéralement ce qu'ils sont.
  3. Dette technique. React, le design system et la librairie d'auth rebundlés dans chaque appli. Dérive des frameworks, les setups des applis qui divergent petit à petit. Pas de test E2E qui traverse les applis, pas d'état partagé entre elles.

Et trois contraintes non négociables :

C'est cette dernière qui tue la plupart des réponses élégantes avant même de commencer.

Plan 1 : auditer, puis l'option pas chère, puis peut-être la chère plus tard

La séquence sur laquelle on s'était engagés :

  1. Normaliser la flotte : amener chaque appli au même setup.
  2. Faire un spike sur un Web Component de navigation partagé (un custom element natif du navigateur que n'importe quelle appli peut embarquer).
  3. Livrer des singletons partagés via CDN : le design system, l'auth et les traductions hébergés une seule fois, en une seule copie que toutes les applis chargent.
  4. Repousser le shell runtime (une appli qui charge les autres dans la même page pendant que l'utilisateur navigue) à une décision à prendre dans un an.

Raisonnable. Le moins cher d'abord.

Ce que l'audit a trouvé

L'audit a trouvé de la dérive sur 13 applis React, répartie comme ça :

Niveau de dérive Applis
Conforme 5
Moyen 5
Fort 3

11 sur 13 divergeaient rien que sur les alias de chemins TypeScript, et 9 patterns systématiques se corrigeaient avec un seul codemod (un script qui réécrit du code dans toute une base de code) ou un changement de template.

Au départ, j'avais résumé ça par "bimodal, pas grand-chose entre les deux". Les chiffres disent le contraire et j'aurais dû lire mon propre tableau : le milieu est, à égalité, le groupe le plus gros. La moitié de la flotte en dérive moyenne, c'est une situation plus courante qu'une coupure nette, et plus utile à dire.

12 applis normalisées en cinq jours

Puis le chiffre d'exécution qui m'a le plus surpris : 12 applis normalisées en cinq jours. L'estimation était de deux à trois mois-homme. Passer toute la flotte au codemod coûte radicalement moins cher qu'il n'y paraît quand la dérive est systématique, et c'est ça, le vrai argument pour auditer d'abord.

Composer des applis dans un seul shell, en deux minutes

La suite de l'histoire alterne sans arrêt entre deux façons de mettre des applis séparées dans une seule page, alors les voici.

Le shell est la seule appli que l'utilisateur charge. Il porte la racine React et le router, et affiche les autres applis à l'intérieur, donc passer à une autre appli ne démonte plus la page. Il y a deux façons d'y faire entrer le code des autres applis :

Composition au runtime Composition au build
Comment le shell récupère une appli il télécharge son code dans le navigateur, depuis une URL il l'importe comme package interne d'un monorepo frontend, et la charge en lazy
Déploiements chaque appli peut garder les siens on renonce à une stricte indépendance des déploiements
Librairies partagées les applis se mettent d'accord dans le navigateur sur la copie à utiliser : un contrat de versions au runtime un seul lockfile, une seule politique de versions, rien sur quoi s'entendre au runtime

Où vit le code (plusieurs repos ou un monorepo) et quand les applis sont assemblées (au build ou au runtime) sont deux questions séparées. Ça compte au pivot 3.

Module Federation (MF) est une façon de faire de la composition au runtime. Le shell est le host ; chaque appli qu'il charge est un remote, buildé à part et fait pour se déployer tout seul. MF a besoin d'un support dans le bundler qui builde les applis, et c'est de ce détail que sont venus trois des quatre pivots.

Pivot 1 : ce qui a changé le plan

Cinq jours après la fin de l'audit, on a annulé purement et simplement le spike Web Component et les singletons CDN, et on a lancé tout de suite l'option chère, le shell runtime.

La conversation qui l'a imposé

Les transitions entre applis sont fréquentes et les parcours sont de longues chaînes à plusieurs sauts : un utilisateur traverse plusieurs applis à la suite. Il n'y a pas de sous-ensemble d'applis isolable par lequel commencer. Ce qui veut dire que la continuité SPA entre applis est l'exigence, pas un nice-to-have : passer d'une appli à la suivante doit marcher comme une seule et même single-page app, sans rechargement.

Les singletons CDN dédupliquent des octets, ils ne vous donnent pas ça. On aurait passé des mois à payer pour la mauvaise solution.

Le talk qui a rendu la réponse crédible

À React Paris, les 26-27 mars 2026, Taylor Quinn-Bohmann (Staff Software Engineer chez Tesla) a présenté Share Once, Ship Everywhere: Federated UI at Scale : Module Federation pour partager au runtime des composants React live et riches en données, sans releases synchronisées ni casse-tête de dépendances.

Ce qui l'a rendu utile, ce n'était pas l'architecture, c'étaient les problèmes qu'ils avaient déjà rencontrés et résolus. Un article de blog vous montre le diagramme final ; un bon talk vous donne la liste de ce qui a cassé en chemin. Alors quand la conversation sur les parcours a posé la question sept semaines plus tard, Module Federation n'était pas un projet de recherche, c'était un truc que j'avais vu quelqu'un faire tourner à une échelle bien au-dessus de la nôtre.

Je veux faire attention à la causalité, parce que mes propres notes du jour du pivot, écrites sur le moment, ne citent que la conversation. Le talk n'y figure pas. Quand je reconstitue les choses aujourd'hui, c'est lui qui a fait que "lancer l'option chère aujourd'hui" ressemblait à une décision sur deux mois plutôt que sur deux ans, mais je ne peux pas affirmer qu'il a fait basculer quoi que ce soit sur le papier. Prenez ça comme un souvenir, pas comme une trace écrite.

Ce que l'audit a quand même apporté

L'audit a quand même été rentabilisé, juste pas de la façon prévue au départ. Avec la flotte alignée (même version majeure de React, config de build canonique, client d'auth canonique), l'estimation du shell a été à peu près divisée par deux. C'est une ré-estimation, pas une économie mesurée. Personne n'a chiffré ce que ça a vraiment pris.

Pivot 2 : l'outillage n'était pas prêt

Premier spike, première semaine. Les deux plugins Module Federation pour notre bundler crashaient sur la version qu'on utilisait. Pas "un peu imparfaits", ils crashaient.

On les a remplacés par un import(url) dynamique natif, qui charge un module JavaScript depuis une URL au runtime, plus le mode librairie sur chaque remote, qui builde l'appli comme un module à importer plutôt que comme une page. Le résultat : un bundle ESM (module ES) en un seul fichier par remote, 1,6 Ko, chargé dans le host avec Suspense et une error boundary. Trois des sept critères d'acceptation prouvés dès la première semaine.

Un spike qui révèle que votre outillage est inutilisable est un spike réussi. Ça a coûté une semaine et ça nous a évité de le découvrir au quatrième mois.

Pivot 3 : la contrainte que j'ai assouplie sans le dire

Deux semaines plus tard, avec le partage des singletons validé en live (zéro rechargement, auth, store, feature flags et design system partagés, une seule racine React), on a complètement abandonné la composition au runtime et on est passés à la composition au build dans un monorepo frontend, où le shell importe chaque appli en lazy, comme un package interne.

Voici la partie qu'il serait facile de raconter à mon avantage, et je ne vais pas le faire.

L'analyse avait déjà tranché la question la veille, sur le papier : avec une contrainte non négociable de déploiements séparés (la première contrainte plus haut : les déploiements restent séparés), la composition au runtime est obligatoire quelle que soit la topologie des repos. Les axes sont orthogonaux. C'était écrit noir sur blanc.

Alors comment la composition au build a-t-elle été choisie le lendemain ? Parce que j'ai révisé la contrainte. J'ai décidé que l'objectif produit (zéro rechargement) passait avant une stricte indépendance des déploiements. Une fois qu'on assouplit ça, la composition au build est clairement plus propre, et elle l'est : un seul lockfile, une seule politique de versions, pas de négociation de singletons partagés, pas de contrat de versions au runtime.

Trois jours plus tard, on a fait marche arrière et on est revenus à Module Federation, sur un autre bundler.

La leçon transposable, ce n'est pas "on l'a construit pour apprendre que la contrainte tient". L'analyse avait la bonne réponse avant que quoi que ce soit soit construit. La leçon, c'est : l'exigence que vous assouplissez sans le dire pour rendre une option viable est celle qui revient la tuer. Si vous vous surprenez à réviser une contrainte que vous aviez notée comme non négociable, c'est le moment de vous arrêter et de la faire revalider explicitement, pas de continuer parce que la nouvelle option a l'air plus propre.

Pivot 4 : la cause racine, c'était le bundler

Le retour à Module Federation est venu avec une correction dont j'aurais préféré me passer.

On avait justifié le pivot 3 en partie par "Module Federation au runtime n'est pas assez mature". C'était une mauvaise attribution, et notre propre rétrospective le dit. MF est natif de Webpack et mature en production sur les bundlers de la famille webpack. Le blocage n'a jamais été MF ; c'était que notre bundler était en pré-release et avait abandonné le support natif de MF, en s'en remettant à un portage encore jeune (un portage de MF pour lui, pas encore mature).

En remontant le fil, trois des quatre pivots ont une seule cause :

Et le statut de MF dans le bundler était un fait public depuis le début, évaluable en un après-midi avant tout ça.

C'est l'argument "boring technology" dans sa forme la plus pure. La brique nouvelle était dans les fondations de la couche de composition, où elle ne nous a rien apporté, et elle a coûté trois revirements de plan.

Librairies partagées et manifests, la version courte

Les mesures et la panne plus bas reposent sur trois autres pièces de Module Federation.

Le share scope. Par défaut, chaque appli embarque dans son bundle sa propre copie de chaque librairie qu'elle utilise. Dans la config de fédération, le host et les remotes listent à la place les librairies qu'ils partagent : React, le router, le design system, la librairie d'auth. Au runtime, chaque appli propose ses copies à un registre commun, le share scope, et les applis utilisent les copies qui s'y trouvent. Une librairie marquée singleton demande une seule instance pour toute la page. L'appli dont la copie est utilisée la fournit, et ça peut être un remote, pas le host. La façon dont le runtime choisit cette copie, c'est le sujet de la panne.

Enregistrer les remotes. Notre shell enregistre chaque remote au démarrage, en indiquant au runtime de fédération où se trouve le code de chaque remote. Les enregistrer tous au boot est un choix, et son coût revient dans les mesures plus bas.

Les manifests. Chaque environnement a un manifest de fédération qui dit au shell quel build de chaque remote charger. Un manifest épinglé fait pointer chaque remote vers un build précis, donc un même environnement peut mélanger des builds faits à des semaines d'écart. Le dev, lui, suit un canal qui avance en continu, donc tous les remotes viennent d'un même commit.

flowchart LR
  M["Manifest d'environnement : quel build de chaque remote"] --> S["Shell (host)"]
  S -->|"enregistre au démarrage"| A["Remote A"]
  S -->|"enregistre au démarrage"| B["Remote B"]
  S -->|"propose ses copies"| SC["Share scope"]
  A -->|"propose ses copies"| SC
  B -->|"propose ses copies"| SC

Ce que ça a apporté, mesuré

Comment j'ai mesuré, et pourquoi n compte

La méthode d'abord, parce que n compte :

Premier chargement à froid : la fédération perd

Les octets sont des tailles compressées en brotli. La profondeur du waterfall en série est la longueur de la plus longue chaîne de requêtes où chacune ne peut démarrer qu'une fois la précédente terminée.

Fédéré Standalone (ajusté) Delta
Octets (brotli) 1 408 Kio 1 221 Kio +187 Kio (+15 %)
Profondeur du waterfall en série 7 niveaux 3 niveaux +4
Tout le code des applis disponible 370 ms ~209 ms +161 ms

Le +161 ms, c'est le chiffre de labo, et c'est le moins intéressant. L'écart de profondeur du waterfall grandit avec le temps d'aller-retour : en 4G à ~150 ms de RTT, ces 4 niveaux en série de plus font à peu près +600 ms. C'est ce chiffre-là qui changerait vraiment la décision de quelqu'un.

Passer d'une appli à l'autre : la fédération gagne, et de loin

Saut vers une autre appli Fédéré (dans le shell) Standalone
Téléchargé 10,4 Kio 1 251 Kio
Page pas de démontage du document rechargement complet

Dans le shell, l'état React, le store et le cache des requêtes survivent tous. (Une seconde mesure, ~41 Kio, compte le code propre à une appli sans les singletons qu'elle fournit ; une autre façon de compter, ne lisez pas les deux comme une fourchette.)

Deux constats auxquels je ne m'attendais pas

Le remote, c'est 96 % de librairies partagées. Un remote de 962 Kio contenait 920 Kio de singletons qu'il fournit au share scope (dont 737 Kio pour le design system) et 41 Kio de son propre code. La déduplication marche. Mais c'est le remote qui fournit, pas le host, donc le design system ne commence à se télécharger qu'au saut 8.

La topologie standalone n'a aucune réutilisation de cache. Chaque appli standalone livre le design system sous son propre hash de contenu (un nom de fichier dérivé du contenu du fichier), donc aucune appli ne réutilise la copie d'une autre. Trois applis standalone, c'est le même design system téléchargé trois fois, ~2,3 Mo.

La pénalité du premier chargement est un choix de bootstrap

Et la conclusion qui comptait le plus : la pénalité du premier chargement n'est pas intrinsèque à Module Federation. À peu près 94 Kio et 3 à 4 des 4 sauts en plus viennent de l'enregistrement eager de chaque remote au boot, avant que l'appli ne s'affiche. C'est un choix de bootstrap, et il grandit linéairement avec le nombre d'applis absorbées (94 Kio à 4 applis, ~470 Kio projetés à 15) tout en étant payé à chaque chargement de page, quelle que soit la route.

Ce que les tableaux laissent de côté

Le coût que ce tableau ne montre pas, et devrait montrer. On a regroupé une douzaine de zones de panne indépendantes en une seule. Avant, une appli cassée était une appli cassée. Après, un seul enregistrement de remote défaillant, c'est toute la plateforme derrière une seule page d'erreur. C'est le compromis d'architecture central de cette migration, et le mesurer en octets et en millisecondes en laissant la disponibilité hors du bilan, c'est exactement l'erreur que je relèverais dans l'article de quelqu'un d'autre.

Et on n'a jamais remesuré la couche 1. La migration a été justifiée par la galère de coordination des releases, chiffrée en temps d'ingénieur. Tout ce qui précède est mesuré en Kio et en ms. Avec 8 applis sur 13 absorbées, on a aussi ajouté de la plomberie :

Si le solde est positif sur la métrique d'origine, je n'en sais rien. On ne l'a pas mesuré. C'est un manque, pas une concession rhétorique.

La panne

Enregistrer un nouveau remote a fait tomber toute la plateforme : la production derrière une seule page d'erreur, et, avant elle, test et staging.

Comment un remote de plus a fini par fournir le router du shell

Chaque entrée de la config des librairies partagées peut être marquée eager, ce qui charge la copie de cette appli dès le départ au lieu de la charger en lazy. Ce eager est un réglage sur une librairie partagée, pas l'enregistrement eager des remotes vu dans les mesures.

Mécanisme : la config des singletons partagés traitait eager comme une préférence par entrée, décidée librairie par librairie. Seuls React et React-DOM étaient fournis par le host. Tous les autres singletons de l'arbre de boot synchrone du host (ce dont le shell a besoin avant de pouvoir s'afficher) étaient lazy, donc dépendants de l'ordre. Enregistrer un remote de plus a inversé l'ordre, et un remote a fini par fournir le router du shell. L'appel qui échouait se trouvait au-dessus de la frontière des applis, donc l'error boundary racine l'a attrapé et a tout emporté avec elle.

flowchart TD
  H["Le shell démarre"] --> E["React, React-DOM : eager, fournis par le host"]
  H --> L["Router et autres singletons de l'arbre de boot : lazy, donc dépendants de l'ordre"]
  L --> N["Un remote de plus enregistré : l'ordre s'inverse"]
  N --> X["Un remote fournit le router du shell"]
  X --> F["Appel en échec au-dessus de la frontière des applis : error boundary racine, une seule page d'erreur"]

On avait vu ce risque, et on l'a laissé revenir

On avait identifié exactement ce risque, puis on l'a laissé revenir. La décision de composition au build du pivot 3, neuf semaines plus tôt, citait comme première raison qu'elle éliminait structurellement le contrat de versions des singletons partagés : un seul lockfile, une politique à version unique, pas de négociation. On a choisi cette architecture en partie pour tuer ce risque, on est revenus en arrière trois jours plus tard pour l'indépendance des déploiements, et on n'a emporté aucune mitigation dans ce retour en arrière. La mitigation a été inventée après la panne.

Sept choses à piquer

  1. Une égalité de versions est le cas dangereux, pas un écart. Un écart (les applis proposent des versions différentes) logge un warning et continue avec une seule instance. Une égalité (elles proposent toutes la même version) s'en remet en silence à l'ordre de chargement. Tous les remotes étaient sur exactement la même version du router, et c'est précisément pour ça que ça a cassé.

    Notez que ça dépend du fait de tourner avec strictVersion: false, et qu'avec strictVersion: true un écart lève une erreur à la place. false reste le bon choix, ça empêche un décalage de patch (des applis à une version de patch d'écart) de faire tomber une route, mais le prix, c'est que votre seul signal est un warning dans la console d'un navigateur que personne n'instrumente.

  2. Le hors-prod a cassé 58 minutes avant la production. Test et staging étaient déjà tombés avec exactement le même défaut quand le changement a été promu. C'est le constat sur le process, et il est pire que le constat technique : il n'y avait aucun contrôle qui charge le shell composé avec le manifest épinglé de l'environnement cible avant la promotion. Le défaut n'est dans aucun artefact, il est dans la combinaison, donc seule la combinaison peut être testée.

  3. On ne connaît pas le temps de détection. Il n'a jamais été chiffré, et c'est structurellement difficile ici : tous les assets renvoyaient 200, l'origine (le serveur) était saine, la panne était entièrement dans le navigateur. Une panne de composition côté client ne produit absolument aucun signal côté serveur. Les contrôles qui manquent sont le reporting d'erreurs sur la boundary racine du shell et un test synthétique, un check automatisé qui charge le shell composé dans chaque environnement.

  4. L'endroit du throw décide de l'étendue des dégâts. L'appel qui échouait se trouvait au-dessus de la frontière des applis, donc l'error boundary racine a tout avalé. Mais attention à ne pas généraliser : ce qui a cassé, c'est le router, et si le router ne peut pas se résoudre, il n'y a pas de segments vers lesquels router, donc une boundary par segment (une error boundary par partie routée de la page) n'aurait rien attrapé. Le placement des boundaries aurait contenu les trois providers après lui dans la chaîne. Il ne peut structurellement pas contenir le router.

  5. Corriger le symptôme signalé a coûté un deuxième cycle. On a livré le passage du seul router en eager, et ça a immédiatement fait apparaître le provider suivant dans la chaîne. Quatre providers, même mécanisme. Corrigez la règle, pas l'instance.

  6. Le dev ne pouvait pas le reproduire, et ne le pourra jamais. Le dev suit un canal qui avance en continu, donc tous les remotes viennent d'un même commit. Les environnements épinglés mélangent des builds faits à des semaines d'écart. Cette asymétrie est la classe de bug, et c'est une propriété permanente de la topologie, pas un trou qu'on comble en testant plus fort sur le dev.

  7. Le levier de rollback n'a eu besoin d'aucun déploiement. Dans une topologie avec un manifest épinglé par environnement, le manifest est la surface de rollback, et c'est bien plus rapide que de livrer quoi que ce soit : on retire les clés du remote fautif et la plateforme revient. Bon à savoir.

    Il faut aussi dire que chez nous, c'était une modif à la main sur un CDN, faite en live par trois personnes, sans review et sans piste d'audit, qui a immédiatement divergé de la source de vérité gitops (le repo git que les déploiements sont censés suivre). C'est un deuxième incident en attente derrière le premier. Les manifests sont déjà versionnés par environnement, donc un revert relu, en une seule commande, c'est peu de travail et une grosse baisse du risque à 3 h du matin.

Le fix, et le check qui l'aurait attrapé

La réparation a été un changement de code : rendre eager chaque singleton du host, livré en deux cycles. Ce qui empêche la récidive, c'est une assertion en CI qui vérifie que chaque entrée du host est eager, qu'aucune entrée de remote ne l'est, et que les deux maps (la liste des librairies partagées du host et celle des remotes) portent les mêmes noms. Ne me faites pas dire qu'une règle de lint a réparé une panne de production.

Et cette assertion est nécessaire mais pas suffisante. Elle impose une règle tirée d'une seule panne, sur un fichier de config. La classe de panne, c'est la composition au runtime d'artefacts versionnés indépendamment, et sa prochaine occurrence ne sera pas une erreur sur le réglage eager, ce sera un décalage de peer-dep (un écart de version sur une peer dependency) ou un remote buildé contre une version majeure du design system que le host n'a pas. Aucun lint de config ne voit ça.

Le contrôle qui correspond à la classe, c'est un smoke test qui conditionne la promotion :

  1. Démarrer le shell composé avec le vrai manifest épinglé de chaque environnement.
  2. Vérifier qu'il s'affiche.
  3. Ne promouvoir que si c'est le cas.

C'est aussi le contrôle qui aurait attrapé la fenêtre de 58 minutes.

Ce que j'en retiens

Mise à jour, novembre 2026

Le fond de ce qui précède reste tel que publié ; quatre points ont tourné autrement.

La ligne de statut est périmée, et son dénominateur aussi. J'ai écrit "8 applis sur ~13 sont absorbées". Fin octobre, le shell servait 14 remotes, tous en production : 11 portages d'applis standalone et un d'un template, anciens repos archivés, plus une nouvelle appli construite dans le monorepo et une page de paramètres. Par rapport aux 8 d'avant, le chiffre le plus comparable, ce sont les 11 portages.

Le registre de déploiement liste encore 13 front-ends standalone, dont un laissé dehors exprès parce qu'il a besoin d'un outillage que le monorepo n'a pas. Donc : 14 dedans, 13 dehors. Je n'arrive pas à faire coller ça avec les 13 applis React de l'audit : mes notes ne disent pas lesquelles ont fini où, et je ne vais pas inventer une correspondance.

Un écart peut être le cas dangereux. J'ai écrit "Une égalité de versions est le cas dangereux, pas un écart." Un mois après la panne, un écart a fait tomber test et staging. Le shell embarquait la librairie d'auth avec une mineure de retard sur sept remotes déployés après lui. La stratégie de partage par défaut, version-first, prend la version la plus haute proposée, donc la copie des remotes a gagné, une deuxième instance a tourné, et le provider d'auth du shell a été perdu. La production n'a survécu que parce que ses remotes étaient épinglés sur des builds plus anciens. La section plus haut prévoyait que la suite serait un décalage de versions. Ça l'a été, en plus petit que je ne le pensais : une mineure de la librairie d'auth.

Cinq jours plus tard, on a mergé shareStrategy: 'loaded-first' dans le shell et dans la config partagée des remotes : la copie déjà chargée, celle que le shell charge en eager, gagne quelle que soit sa version. Je ne m'y suis fié qu'après un test témoin qui échoue : même décalage fabriqué, une seule chaîne changée, et version-first a reproduit mot pour mot l'erreur de la panne. Ce n'est qu'après ce changement que ma phrase est vraie : deux jours plus tard, un vrai décalage en production, la librairie du design system du shell avec une mineure de retard sur la plage d'un remote, a loggé un warning et gardé une seule instance.

La pénalité du premier chargement : bon symptôme, mécanisme sans doute un niveau plus bas, fix non mesuré. J'avais mis "À peu près 94 Kio et 3 à 4 des 4 sauts en plus" sur le compte de l'enregistrement eager de chaque remote au boot. En lisant plus tard le runtime de fédération, je l'ai fait descendre d'un niveau : avec version-first, le démarrage charge le fichier d'entrée de chaque remote enregistré, qu'il soit sur la route courante ou non, pour collecter les versions, en plus du fetch du manifest propre au shell et du preflight. loaded-first saute cette boucle. Je n'ai aucune mesure de chargement à froid depuis qu'il a été livré, donc le "~470 Kio projetés à 15" n'est pas vérifié : il extrapole un coût dont le principal suspect a depuis été désactivé.

L'indépendance des déploiements n'était pas encore réelle. On est "revenus en arrière trois jours plus tard pour l'indépendance des déploiements", mais le shell compilait encore des traductions, des reducers et un client d'API tirés des remotes. Livrer un remote seul aurait servi une moitié de paire, donc chaque release de remote était aussi une release du shell : 31 releases du shell sur les six jours précédant la mi-septembre, la plupart sans rien changer au bundle. Sur 159 merges touchant un remote, comptés quelques jours plus tôt, seuls 75 changeaient ce que le shell compilait.

Couper ce lien nous a pris la seconde moitié de septembre, en expand puis contract, remote par remote : livrer ses propres traductions et son état au montage, puis faire en sorte que le shell ne les fournisse plus. Une règle provisoire a continué à livrer le shell avec chaque remote jusqu'à ce qu'on la supprime aussi. Le premier déploiement en prod d'un remote sans toucher au shell, une seule écriture de manifest, est arrivé à mi-parcours, ce qui dit où était le couplage : réépingler un remote n'a jamais eu besoin du shell, en livrer un, si.

Fin octobre, la prod et le test différaient sur 12 des 14 versions épinglées, sous un même shell. Ma seule nouvelle mesure de la couche 1 est mince : en 19 heures après la suppression de la règle, 2 releases du shell pour 4 releases de remotes, toutes deux avec de vrais changements dans le shell. Ça montre que le mécanisme marche, pas que le rythme tient. Les tags hebdomadaires du shell sont tombés de 48 et 51 à une fourchette de 5 à 8, revenus à leur niveau de fin août, donc je ne peux pas attribuer la baisse au fix.