En février 2026, le code de Jewely existait en deux versions. Les deux partaient du même commit, daté du 29 août 2025. L'une portait Crown-DP. L'autre faisait tourner Louis Julian en production.
Pour faire un CMS commun, il fallait décider laquelle des deux devenait la base. Cette note raconte comment j'ai pris cette décision, ce que j'ai écarté, et ce qu'elle a coûté.
L'écart, mesuré
| Mesure | Valeur |
|---|---|
| Fichiers différents entre les deux branches | 1 714 |
| Lignes ajoutées et supprimées | +143 590 / −99 248 |
| Commits côté Crown-DP depuis la séparation | 643 |
| Commits côté Louis Julian depuis la séparation | 1 435 |
Les deux branches n'avaient pas fait le même travail. Côté Crown-DP, l'architecture avait changé : la gestion de plusieurs boutiques, des moyens de paiement interchangeables, un éditeur de pages par blocs, 198 tests, un déploiement Docker Swarm. Côté Louis Julian, la branche avait suivi la vie d'un site en production : les pages Rolex, l'ERP, la maintenance.
Un audit en lecture seule, avant tout merge
Je n'ai lancé aucun merge. J'ai d'abord comparé les deux branches fichier par fichier, couche par couche, et j'ai donné un verdict à chaque fichier.
| Verdict | Ce qu'il veut dire | Fichiers |
|---|---|---|
| Prendre | La version Crown-DP remplace l'autre | environ 1 500 |
| Garder les deux | Les thèmes de chaque maison | environ 180 |
| Fusionner à la main | Le fichier a changé des deux côtés | environ 30 |
| Garder l'ancienne | Le fichier n'existe que côté Louis Julian | environ 5 |
| Ignorer | Fichiers temporaires ou abandonnés | environ 5 |
L'audit a changé la taille du problème. Il n'y avait pas 1 714 fichiers à fusionner. Il y en avait une trentaine à relire ligne par ligne : la fiche produit, les collections, les rôles du back-office, les services Rolex, les commandes d'import.
La décision : un tronc, et l'autre maison portée dessus
J'avais trois options.
- Fusionner Crown-DP dans la branche historique. Écartée. Les conflits auraient été massifs, et la branche historique n'avait aucun test pour dire si la fusion avait cassé quelque chose.
- Garder deux codes. Écartée. Chaque fonctionnalité aurait été construite deux fois, et l'écart aurait continué de grandir.
- Faire de Crown-DP le tronc, et y porter Louis Julian. Retenue. Louis Julian devient un thème et une configuration posés sur le code commun.
Le critère n'était pas l'ancienneté de la branche, ni le nombre de commits. C'était la présence des tests : la branche qui en avait pouvait dire si la suite se passait bien.
Une base de données par maison
La deuxième décision portait sur les données. La configuration avait prévu une colonne shop_id sur chaque table, pour ranger toutes les maisons dans une seule base. Je ne l'ai pas mise en œuvre. Chaque maison a son serveur, sa base de données et sa propre image Docker.
code commun (Laravel, back-office Vue)
│
┌───────────────┴───────────────┐
APP_SLUG_NAME=crown-dp APP_SLUG_NAME=louis-julian
│ │
configuration + templates configuration + templates
image Docker image Docker
base de données base de données
Deux raisons. Une requête qui oublie un filtre ne peut pas montrer les commandes d'une maison à une autre. Une migration ratée ou un serveur en panne ne touche qu'une maison.
Le coût est connu : il y a autant de déploiements que de maisons. « Construit une fois » vaut pour le code, pas pour la mise en ligne. La colonne shop_id reste le plan B, écrit comme tel dans la feuille de route, pour le jour où le nombre de maisons rendra ce coût trop lourd.
Ce qui empêche les deux maisons de diverger à nouveau
Aucune condition sur le nom d'une maison dans le code commun. Ce qui distingue une maison est une option, déclarée dans sa configuration :
function feature_enabled(string $feature, bool $default = false): bool
{
return (bool) shop_config("features.{$feature}", $default);
}
Une option déclarée pour une maison doit exister pour l'autre, même à false. La liste des options devient un contrat entre les maisons et le code commun.
Les noms écrits en dur, cherchés avant le portage. L'audit en a trouvé : l'identifiant de Crown-DP dans un helper de templates, et une adresse d'expédition d'e-mails dans trois fichiers. Ils ont été corrigés avant de porter la deuxième maison, pas après.
Les tests, une fois par maison.
strategy:
fail-fast: false
matrix:
shop: [crown-dp, louis-julian]
fail-fast: false est voulu. Si une maison casse, je veux aussi le résultat de l'autre, pour savoir si le défaut est dans le code commun ou dans un thème.
Ce que ça a coûté
- Le ménage d'abord. Le dépôt comptait 109 branches. J'en ai gardé 8 avant de commencer, pour que personne ne reparte d'une branche morte.
- Des thèmes complets. Une maison ne peut plus emprunter le template d'une autre. Chaque maison doit donc avoir tous les siens. L'incident qui a imposé cette règle est raconté dans Un CMS pour plusieurs maisons, sans qu'elles se ressemblent.
- Une manipulation Git en plus. Chaque branche de préproduction ne doit contenir que les fichiers de sa maison. J'ai choisi des fichiers
.gitignorepar dossier plutôt qu'un sparse-checkout, parce qu'il n'y a rien à configurer sur chaque poste. Le prix : après chaque merge depuis la branche principale, il faut retirer de l'index les fichiers des autres maisons. La note de décision le dit, et dit quand reprendre la question : à la quatrième maison. - Deux étapes, pas une. Le code de Louis Julian a été porté sur le tronc commun les 9 et 10 mars 2026, et le site a tourné dessus en préproduction. Sa production est ensuite passée sur une déclinaison du CMS : une instance à part, avec sa base de données, son serveur et ses propres mises à jour.
Ce que j'en retiens
- Mesurer avant de choisir. Le chiffre qui faisait peur était 1 714. Le chiffre utile était 30.
- Choisir le tronc sur ce qui protège la suite. Ici, les tests.
- Écrire le plan B et ce qui le déclenche.
shop_idpour une base unique, le sparse-checkout à la quatrième maison. La personne suivante sait ce qui a été écarté et quand y revenir.
Le résultat côté produit est dans l'étude de cas Crown-DP et le CMS Jewely.