Pourquoi vos branches d'environnement vous ralentissent, comment les équipes les plus rapides du monde s'en sont débarrassées, et ce que ça change concrètement dans votre code. L'article progresse en trois parties, de plus en plus profondes : les experts sont servis à la fin.
Il est 18 h 47, un jeudi
La feature est prête depuis neuf jours. Testée, validée, approuvée. Mais elle ne partira pas en production ce soir, ni demain : elle est coincée sur la branche staging, mélangée à deux autres features qui, elles, ne sont pas prêtes. Impossible de livrer l'une sans embarquer les autres. Alors on attend. Ou pire : on tente un cherry-pick chirurgical vers master, en croisant les doigts pour que les commits ne se marchent pas dessus.
Si cette scène vous parle, votre équipe utilise des branches d'environnement : une branche dev ou staging qui accumule les features en test, une branche master qui représente la production, et des merges rituels entre les deux. C'est le modèle le plus répandu du monde. C'est aussi celui que Google, Meta, Netflix et toutes les équipes mesurées comme les plus performantes ont abandonné il y a quinze ans.
Le problème n'est pas votre équipe. Le problème est qu'une branche Git est un très mauvais outil pour répondre à la question « qu'est-ce qui est activé, pour qui, en ce moment ? »
L'anatomie du mal
Le modèle à branches d'environnement repose sur une équivalence séduisante, une branche = un environnement = un état du produit, qui se dégrade pour trois raisons mécaniques.
D'abord, la branche d'intégration dérive. Chaque feature mergée dans staging mais pas encore en prod creuse l'écart avec master : features abandonnées, correctifs en double, conflits résolus différemment de chaque côté. Ce qu'on teste n'est plus ce qu'on livrera.
Ensuite, livrer devient un problème de soustraction. Sortir la feature A sans la B exige de l'extraire de la masse : cherry-picks, merges sélectifs, et un code en prod qui n'est jamais exactement celui qui a été validé.
Enfin, déployer et livrer sont le même geste. Activer une feature exige de pousser du code ; la désactiver aussi. Chaque mise en prod devient un événement qu'on espace, donc qui grossit, donc qui fait peur, donc qu'on espace encore. Le cercle est vicieux et bien documenté.
Ce que disent les chiffres
Le programme de recherche DORA (Google Cloud) mesure depuis 2014 la performance de milliers d'équipes, et son résultat le plus contre-intuitif est solide : les équipes qui déploient le plus souvent sont aussi celles qui ont le moins d'incidents. La sécurité ne vient pas de la prudence des gros déploiements espacés ; elle vient de la petitesse de chaque changement.
Les solutions, dans l'ordre où on les découvre
Il n'y a pas un modèle à adopter d'un bloc, et c'est le point que la plupart des articles ratent. Le chemin passe par des solutions intermédiaires, et chacune est un état stable où une équipe peut s'installer durablement. Ce qui pousse à la suivante n'est pas la mode ; c'est un échec précis, qu'on va raconter à chaque fois.
Branches d'environnement dev, staging, master, et des merges rituels
│ la branche d'intégration dérive, livrer devient une soustraction
▼
Une branche + des tags branches courtes, validation avant merge
│ la validation bloque le merge, les features font la queue
▼
Artefact unique + review apps un build par commit, une scène par feature
│ déployer reste le seul interrupteur, tout ou rien
▼
Trunk + feature flags on merge tôt, éteint ; livrer = allumer
Une seule branche de vérité
La première réponse à la dérive des branches d'environnement ne demande aucun outillage nouveau, seulement trois règles. La branche principale est protégée : push direct interdit, merge request obligatoire, pipeline vert requis. Les branches de feature sont courtes : quelques jours au plus, une feature = une branche = une MR. Et les environnements se déploient depuis des tags, jamais depuis des branches dédiées : staging redevient un endroit où l'on déploie un commit précis de main, pas une branche qui accumule.
La règle de livraison est alors simple : on ne merge que ce qu'on est prêt à livrer. La validation se fait avant le merge, sur la branche de feature. C'est le « GitHub Flow » classique, et il résout déjà le cœur du problème : plus de dérive, ce qu'on teste est ce qu'on livre.
Vous pouvez vous arrêter ici si vous livrez une feature à la fois et que la validation est rapide. Beaucoup de petites équipes vivent très bien avec ce modèle, et n'ont besoin de rien de plus.
Trois mois plus tard, mardi, 9 h 05. Le tableau des merge requests affiche quatre features « en attente de validation ». La QA n'a pas fini la première. La quatrième a déjà deux conflits avec la branche principale ; son auteur passera la matinée à rebaser du code validé la semaine dernière. En rétrospective, quelqu'un propose : « et si on avait une branche commune où on merge tout ce qui est à tester ? » Silence gêné. C'est la branche
stagingqui frappe à la porte, sous un faux nom.
L'échec est instructif : le problème n'est pas de savoir où poser le code à tester, c'est que la validation bloque le merge. Tant qu'une seule scène de test existe, les features font la queue devant, ou s'y mélangent. La réponse n'est pas une branche commune ; c'est de donner à chaque feature sa propre scène.
Le même artefact, des scènes jetables
Deux mécanismes s'ajoutent, toujours sans toucher au code applicatif. Le premier : build once, promote everywhere. Un artefact unique par commit de main ; dev, staging et prod reçoivent ce même binaire, promu par tag, jamais reconstruit. Ce qui a été validé est, bit à bit, ce qui part en prod. Le second : des environnements éphémères par MR, les « review apps ». Chaque branche de feature est déployée sur une URL jetable, détruite au merge. La QA valide chaque feature isolément, avant le merge, sans qu'une branche d'intégration mélange tout.
Concrètement, en GitLab CI (l'équivalent existe chez GitHub Actions et les autres), l'essentiel tient en trois jobs :
# 1. Un seul build, adressé par commit : c'est lui qui voyagera partout
build:
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
# 2. Une review app par merge request, détruite à sa fermeture
review:
script:
- deploy --image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop_review
rules:
- if: $CI_MERGE_REQUEST_IID
# 3. La prod reçoit l'image déjà validée : un retag, jamais un rebuild
promote_prod:
script:
- crane tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" prod
rules:
- if: $CI_COMMIT_TAG =~ /^prod-/
C'est la réponse « sans flags » au besoin qui pousse les équipes vers les branches d'environnement : tester plusieurs features en parallèle. Chacune a son environnement, aucune ne bloque les autres.
Vous pouvez vous arrêter ici si déployer et livrer peuvent rester le même geste chez vous : pas besoin d'activer une feature client par client, ni de l'éteindre en urgence sans redéployer. C'est le régime de croisière de très nombreuses équipes produit, et il est déjà excellent.
Six mois plus tard, vendredi, 16 h 40. La release de mardi contenait quatre features, validées une à une sur leurs environnements éphémères, irréprochables. Mais l'une d'elles touche au même cache que le module de facturation, et sous le trafic réel du vendredi, ça casse. Le seul levier disponible est brutal : redéployer l'image précédente, en emportant les trois features innocentes. Pendant l'incident, un commercial passe une tête : « on peut activer la nouveauté seulement pour le client pilote qui l'a demandée ? » Non. On ne peut pas. Le déploiement est le seul interrupteur de la maison, et il éteint tout en même temps.
Deuxième échec instructif : la validation isolée ne prouve pas le comportement en conditions réelles, et le déploiement est un interrupteur trop gros, qui allume et éteint tout d'un bloc. Il manque un levier plus fin que le déploiement. C'est exactement ce qu'est un feature flag.
L'interrupteur : découpler livrer de déployer
La dernière marche renverse la logique : on merge avant que la feature soit finie, masquée par un interrupteur, et c'est l'interrupteur qui livre. Déployer et livrer deviennent deux gestes distincts. Déployer, c'est pousser du code en production, éteint ; c'est le geste technique, fréquent, automatisé, sans enjeu produit, qui peut arriver dix fois par jour. Livrer, c'est allumer : le geste produit, une feature, un client, un instant choisi, réversible en secondes, sans redéploiement.
Au fond, un flag est trivial : une condition qui lit une configuration au moment de l'exécution.
if (flags.isEnabled('new_checkout')) {
renderNewCheckout();
} else {
renderLegacyCheckout();
}
Toute la valeur tient dans une question : d'où vient la valeur du flag ? Trois réponses, du plus simple au plus puissant, chacune légitime à son échelle :
| Niveau | Source des valeurs | Un « flip » c'est… | Granularité |
|---|---|---|---|
| 1. Config runtime | Un fichier de config chargé au démarrage | Changer une variable du déploiement, redémarrer le pod | Par environnement |
| 2. Backend | Un objet feature_flags servi par votre API |
Un appel d'API ou un champ en base | Par client, par compte |
| 3. Service dédié | Unleash, LaunchDarkly, Flagsmith… | Un clic dans une UI, effectif en secondes | Par utilisateur, par pourcentage, par règle |
Un piège spécifique au frontend mérite d'être nommé : dans une SPA, process.env.MA_VARIABLE n'est pas une variable d'environnement. Elle est substituée au moment du build et cuite dans le bundle. Un flag lu ainsi exige un build par environnement, ce qui casse « même artefact partout ». La solution standard : un fichier config.js non bundlé, généré au démarrage du conteneur et chargé par index.html avant l'application. Le bundle reste identique partout ; seule cette config diffère.
Reste le cycle de vie. Un flag de release n'est pas une décoration permanente : c'est un échafaudage, avec une date de démontage prévue dès sa pose. Il naît éteint partout, le jour où la feature démarre, ce qui permet de merger le premier commit dans le trunk le jour même. Il s'allume en dev, où toutes les features en cours cohabitent. Puis en staging, seul : on isole ce qu'on évalue. Puis en prod, progressivement si l'enjeu le mérite : équipe interne, client pilote, pourcentage, tout le monde. Et quelques semaines après la généralisation, le flag et l'ancien chemin de code sont supprimés. Cette dernière étape n'est pas optionnelle ; on y revient dans la partie qui suit.
Le chemin est le plan de migration
Le plan de transition est déjà écrit : c'est le chemin qu'on vient de parcourir, dans le même ordre, en ne décommissionnant l'ancien mécanisme que quand plus rien ne le lit ; une branche morte gardée « au cas où » redeviendra un entrepôt. Pour une équipe de 5 à 20 personnes, comptez un à deux trimestres. L'étape difficile n'est jamais technique : c'est le raccourcissement des branches, qui oblige à découper le travail autrement.
Deux disciplines font tenir le système. Merger petit et souvent : le trunk ne fonctionne que si l'intégration est quotidienne, et une grosse MR est un problème à découper, pas à relire plus fort. Et tout nouveau développement naît derrière un flag : le flag remplace la branche comme unité de livraison ; si une feature n'a pas de flag, elle doit être livrable le jour de son merge.
Les objections que vous allez entendre
Cette migration se gagne en réunion d'équipe avant de se gagner dans Git. Trois phrases reviennent dans toutes les équipes ; autant arriver avec les réponses.
« On va merger du code pas fini dans master ?! » On merge du code fini au sens du pipeline : compilé, testé, revu. Ce qui n'est pas fini, c'est la feature, et elle est éteinte. Le vrai danger est ailleurs : une branche de trois semaines, c'est du code que personne n'exécute contre le reste du système ; le code éteint en production, lui, est intégré et compilé avec tout le monde, chaque jour.
« La QA doit valider avant la prod. » Elle valide toujours, et mieux qu'avant : l'objet change. Au lieu de valider une release, un paquet de features enchevêtrées, elle valide une feature, seule, sur sa review app ou derrière son flag en staging. Le contrôle ne disparaît pas ; il se déplace du paquet vers l'unité.
« Des if partout dans le code, c'est sale. » Un flag de release bien posé, c'est un point de divergence, à la frontière du module, avec une date de mort. Une branche longue est aussi un if : simplement il est invisible, il n'est jamais exécuté en continu, et il se paie d'un coup au merge final. La dette de flags se gère, on y vient ; la dette de merge se subit.
Ce qui se passe à grande échelle
Tout ce qui précède fonctionne tel quel pour une équipe. Voici ce qui casse quand on grossit, et les mécanismes que les grandes organisations ont construits en réponse.
La dette de flags est une dette exponentielle
Chaque flag vivant double le nombre de chemins d'exécution possibles : dix flags actifs, c'est en théorie 1024 combinaisons d'états, dont votre suite de tests n'en couvre qu'une poignée. Les incidents de flags sont un genre à part entière ; le plus célèbre reste Knight Capital en 2012, où du code mort réactivé par un flag recyclé a coûté 460 millions de dollars en 45 minutes.
Les défenses standard : une date d'expiration par flag, posée à la création, avec un rapport automatique des flags périmés (certaines équipes font échouer le build) ; un registre central avec nom, owner, ticket et type (la taxonomie de Hodgson fait référence), car un flag sans owner est un flag mort qu'on n'ose pas supprimer ; ne jamais recycler un nom de flag, jamais ; et tester les deux états des flags proches de la prod, plus l'état par défaut de tous les autres. On ne teste pas les 1024 combinaisons, on teste les frontières qui bougent.
Rollout progressif : le bucketing, pas la loterie
« Allumer pour 10 % des utilisateurs » ne peut pas être un Math.random() par requête : un même utilisateur verrait la feature apparaître et disparaître entre deux pages. Les SDK de flags font du bucketing déterministe : hash(user_id + flag_name) % 100, comparé au pourcentage cible. Le résultat est stable par utilisateur, indépendant entre flags grâce au nom dans le hash, et passer de 10 à 30 % conserve les 10 % déjà exposés. C'est aussi la brique de base de l'A/B testing : le flag renvoie une variante au lieu d'un booléen, et l'analyse compare les cohortes.
Le schéma de base de données ne connaît pas les flags
Le code a deux chemins (flag on, flag off), mais les deux parlent à la même base. Une migration destructive, renommer une colonne, changer un type, casse le chemin éteint. La réponse standard est le pattern expand-contract : on ajoute la nouvelle colonne sans toucher à l'ancienne, le code écrit dans les deux et lit l'ancienne (expand) ; on bascule la lecture derrière le flag et on backfille l'historique (migrate) ; et quand le flag est généralisé puis supprimé, on supprime l'ancienne colonne dans une release ultérieure (contract). Corollaire : chaque version du schéma doit supporter la version courante et la précédente du code, exigence de toute façon imposée par les déploiements progressifs.
Garder le trunk vert quand cinquante personnes mergent
À petite échelle, « pipeline vert avant merge » suffit. À grande échelle, deux MR vertes individuellement peuvent casser le trunk une fois combinées : le merge skew. D'où les merge queues (GitHub Merge Queue, GitLab Merge Trains) : chaque MR candidate est testée contre le résultat des merges qui la précèdent dans la file. Avec la sélection de tests par analyse d'impact, c'est ce qui permet à un trunk de tenir des milliers de commits par jour.
Observer les flips comme des déploiements
Quand livrer = allumer un flag, le flip devient l'événement à risque, et il mérite les honneurs d'un déploiement : un marqueur dans les dashboards, la liste des flags actifs comme dimension dans les erreurs et les traces, et idéalement des gardes-fous automatiques qui ramènent le flag à zéro si le taux d'erreur de la cohorte exposée dévie. Le rollback n'est plus un geste d'urgence, c'est une politique.
Les anti-patterns qui reviennent toujours
La branche dev réinventée : « on garde juste une branche d'intégration pour regrouper les features en test ». C'est l'ancien modèle sous un nouveau nom ; ce regroupement est le rôle de l'environnement d'intégration alimenté par le trunk, flags allumés.
Le flag architectural : un flag de release protège un point de divergence, à la frontière du module. Quand le même flag apparaît dans quinze fichiers, ce n'est plus un interrupteur, c'est une branche logicielle non versionnée.
Le flag qui dépend d'un flag : les combinaisons deviennent intestables et les flips cessent d'être indépendants. Les interrupteurs doivent rester orthogonaux.
Le staging-entrepôt : dix flags allumés en permanence « pour voir », et on a recréé la branche d'intégration en configuration. On valide une chose à la fois, pour les flags comme pour les branches.
Quand ce modèle n'est pas le bon
L'honnêteté oblige à tracer les limites. Le trunk-based avec livraison continue suppose qu'on peut déployer à volonté, et ce n'est pas toujours vrai. Sur mobile, la review des stores impose des déploiements espacés ; les flags y sont paradoxalement plus précieux encore, seul levier de livraison entre deux releases. En embarqué ou en on-premise, quand le client déploie lui-même, la promotion d'artefact devient une gestion de versions supportées, et les flags de la configuration produit. Dans les domaines régulés, les gates de conformité ne disparaissent pas ; ils se déplacent dans le pipeline, revue, traçabilité, signature, plutôt que dans les branches. Et pour une équipe de deux personnes qui livre une fois par mois, le coût du système de flags peut dépasser son bénéfice : le modèle se justifie par la fréquence de livraison qu'on veut atteindre, pas par la mode.
Par où commencer demain matin
Trois gestes, par ordre de rendement.
Protégez votre branche principale et exigez le pipeline vert avant merge : dix minutes de configuration, effet immédiat. Livrez ensuite votre prochaine feature derrière un flag, même un simple booléen dans une config runtime ; vous découvrirez les questions concrètes, où vivent les valeurs, qui flippe, quand on nettoie, sur un cas réel et sans risque. Et mesurez votre fréquence de déploiement et votre délai commit → prod : ce sont deux des quatre métriques DORA, et elles diront, dans six mois, si la migration a tenu ses promesses.
Ces trois gestes vous installent à la première étape du chemin. Les suivantes, unifier l'artefact, monter les review apps, poser les flags, se prendront une par une, quand leur échec fondateur se produira chez vous. Le plus dur n'est pas technique : c'est d'accepter que la sécurité vienne de la fréquence, pas de la prudence.
Pour aller plus loin. Feature Toggles de Pete Hodgson, la taxonomie de référence des flags et de leur cycle de vie. trunkbaseddevelopment.com de Paul Hammant, le site de référence sur la méthode. Patterns for Managing Source Code Branches de Martin Fowler, sur les branches d'environnement comme anti-pattern. dora.dev, la recherche qui fonde empiriquement tout ce qui précède. Et Software Engineering at Google, chapitres 16 et 24, en accès libre.
Top comments (0)