Skip to content

Modèle de Release

Document de référence décrivant le modèle de release utilisé sur les projets de l'équipe. Le modèle est trunk-based, forward-only, et agnostique au langage (s'applique à n'importe quel projet, peu importe son écosystème : Node, Python, Go, Rust, etc.).

Ce document est conçu pour être consommé à la fois par les humains et par des assistants IA (Claude Code). Les règles sont explicites, les exemples concrets, et les rationales documentées pour permettre un raisonnement correct sur les cas limites.

Philosophie

Trois principes non-négociables guident l'ensemble du modèle :

  1. Trunk-based : une seule branche longue, main. Toutes les features, fixes, et MR de release s'y mergent.
  2. Forward-only : les tags sont immuables. Une release cassée ne se retag jamais, on incrémente.
  3. Releases par MR : aucun push direct sur main. Même les commits de release passent par une MR, comme n'importe quelle autre modification.

L'état de la production est identifié par le dernier tag pur X.Y.Z (sans suffixe). Les tags sont la source de vérité ; les branches bougent.

Outil de release

La CLI qui implémente ce modèle est @spektrum/release-util, distribuée via le registry npm privé de Spektrum (https://npm.internal.spektrum-suisse.ch). Elle est invoquée via npx :

bash
npx @spektrum/release-util@latest release --env staging

NPM internal

Note sur l'agnosticité au langage. Le fait que la CLI soit distribuée via npm ne contraint pas le langage du projet sur lequel elle opère. Elle fait des opérations Git génériques et lit/écrit des fichiers standards (CHANGELOG.md, fichier de version du projet). Elle s'utilise donc sur n'importe quel projet : Node, Python, Go, Rust, etc. Le seul prérequis local est d'avoir Node.js installé (une version LTS récente suffit).

Configuration du registry privé

Pour que npx trouve le package, le scope @spektrum doit pointer sur le registry privé. Configurer soit un .npmrc à la racine du projet (recommandé : la config est partagée avec l'équipe via Git), soit un .npmrc global dans le home du dev.

Le contenu à ajouter est le même dans les deux cas :

ini
@spektrum:registry=https://npm.internal.spektrum-suisse.ch

Option 1 : .npmrc au niveau du projet (recommandé) :

bash
echo "@spektrum:registry=https://npm.internal.spektrum-suisse.ch" >> .npmrc
git add .npmrc
git commit -m "chore: configure spektrum private registry"

Option 2 : .npmrc global :

bash
npm config set @spektrum:registry https://npm.internal.spektrum-suisse.ch

Vérification : npx @spektrum/release-util@latest --help doit retourner l'aide de la CLI sans erreur de résolution de package.

Convention d'écriture dans ce document

Dans tout le reste du document, release-util ... est utilisé comme raccourci pour la commande complète npx @spektrum/release-util@latest .... À l'usage réel, taper la commande complète (ou créer un alias shell local si on l'utilise fréquemment).

Vue d'ensemble du flux standard

Rôles des branches

BrancheRôleQui peut y écrire
mainTrunk d'intégration. Toutes les features, fixes, et MR de release s'y mergent. Les tags de release y vivent.Devs (via MR uniquement : branche protégée)
feature/*Branches de travail. Branchées depuis main, mergées via MR. Courtes (heures/jours) par défaut. Voir Flux pour features longues si elles doivent durer plus longtemps.Le dev qui y travaille
release/*Créée par release-util release --env staging|prod. Contient le commit chore(release): X.Y.Z.La CLI de release
hotfix/*Branchée depuis le dernier tag de production, mergée dans main via MR. Voir Hotfixes.Le dev qui y travaille

Pour savoir ce qui est en production :

bash
git describe --tags --match='[0-9]*.[0-9]*.[0-9]*' --abbrev=0

Format des tags

Trois familles de tags, distinguées par leur suffixe :

X.Y.Z                  → production, déploiement manuel + environnement protégé
X.Y.Z-staging.N        → staging, déploiement automatique
X.Y.Z-<slug>.N         → preview de feature longue, environnement preview dédié

Pas de préfixe v. La version du tag est utilisée directement comme version du build.

Le <slug> est dérivé du nom de branche après suppression du préfixe feature/. La convention sur le séparateur (tirets - ou underscores _) dépend de l'écosystème du projet : le standard semver impose strictement [0-9A-Za-z-] dans les identifiants de pre-release, mais beaucoup d'outils tolèrent les underscores. Choisir une convention par projet et s'y tenir.

feature/client_indecis       → slug "client_indecis"
1.2.4-client_indecis.0

Les tags pointent :

  • sur le merge commit sur main pour staging/prod (capture l'état canonique de main au moment de la release) ;
  • sur le HEAD de la branche feature pour les previews (il n'y a pas de merge à ce stade).

Flux standard (staging → production)

1. Développement

Les features et fixes atterrissent sur main via le flux MR habituel. Aucune intervention de l'outil de release pour le travail normal.

2. Création d'une release staging

Depuis un main propre et à jour avec origin/main :

bash
release-util release --env staging

La CLI :

  • Calcule la prochaine version à partir des conventional commits depuis le dernier tag
  • Crée une branche release/X.Y.Z-staging.N depuis main
  • Met à jour le fichier de version du projet et préfixe CHANGELOG.md
  • Commit chore(release): X.Y.Z-staging.N
  • Push la branche et ouvre la MR de release vers main

La CLI ne push jamais directement sur main. Elle refuse de tourner si :

  • la copie locale n'est pas sur main ;
  • l'arbre de travail est sale ;
  • main local est en retard sur origin/main.

3. Review et merge de la MR

Comme n'importe quelle MR.

4. Tag automatique par CI

Le job release-tag se déclenche sur main au push du merge. Il détecte le commit chore(release): ... dans le push et push le tag X.Y.Z-staging.N pointant sur le merge commit.

5. Déploiement staging

Le push du tag déclenche le pipeline de déploiement staging (automatique).

6. Validation client

Si la validation échoue, fix forward : merger les corrections sur main via MR, relancer release-util release --env staging. La CLI produira X.Y.Z-staging.N+1. Ne jamais retaguer.

7. Release production

Quand staging est validé, depuis un main propre :

bash
release-util release --env prod

Même forme que les étapes 2-5, mais :

  • Le tag est X.Y.Z (sans suffixe) ;
  • Le déploiement production est manuel et gated par un environnement protégé.

Flux pour features longues

Certaines features nécessitent plusieurs semaines de validation client avec des itérations fréquentes (UX en discussion, mockups qui évoluent, scope qui bouge). Mettre ces features sur main derrière un feature flag est l'approche canonique trunk-based, mais nécessite une infrastructure de flags propre au projet.

Ce flux alternatif garde la feature isolée sur sa branche tout en préservant les invariants du modèle (forward-only, pas de force-push, releases auditables via CI).

Principes

  • La branche feature/<nom> est maintenue synchronisée avec main via des merges réguliers de mainfeature/<nom>. Jamais de rebase (rebase = réécriture d'historique distant = orphelinise les tags de preview existants).
  • Un build de preview peut être produit depuis la branche feature à n'importe quel moment, sans toucher à main ni au flux de release standard.
  • Les tags de preview ont la forme X.Y.Z-<slug>.N, où X.Y.Z est la version actuelle de main au moment de la sync.
  • Quand le client valide finalement, la feature est mergée dans main via MR ; la dette d'intégration est minimale grâce aux syncs régulières.

Vue d'ensemble

Exemple sur l'historique Git

Le diagramme suivant illustre concrètement le flux : une feature client_indecis créée alors que main est à 1.2.3, deux syncs successives avec main (la deuxième après un bump de main à 1.2.4), plusieurs tags de preview, puis merge final qui redonne la main au flux de release standard (1.3.0-staging.0 puis 1.3.0).

Points clés à observer :

  • Chaque sync de main dans la feature est immédiatement suivie d'un tag de preview dont le préfixe X.Y.Z reflète l'état courant de main (1.2.3 puis 1.2.4 après le bump).
  • Le compteur N reset à chaque changement de préfixe : après la sync sur 1.2.4, on repart de client_indecis.0, pas de client_indecis.2.
  • Les itérations qui n'introduisent pas de sync (juste un commit sur la feature) incrémentent N à préfixe constant (client_indecis.0client_indecis.1).
  • Le merge final dans main redonne la main au flux standard : la version 1.3.0 (bump minor déclenché par les feat: dans la feature) sort en staging puis en production via les MR de release habituelles.
  • main ne reçoit jamais de tag de preview : seuls X.Y.Z et X.Y.Z-staging.N y vivent.

Règles spécifiques

  1. Sync uniquement par merge, jamais par rebase. Rebaser une branche partagée réécrit l'historique distant et orpheline les tags de preview précédents (les commits qu'ils référencent disparaissent). Merge préserve l'historique et la validité des tags.

  2. La CLI bloque si la feature est en retard sur main. Même invariant que pour staging/prod : release-util release --env feature refuse de tourner si main a des commits qui ne sont pas sur la branche feature. Sync d'abord localement, résoudre les conflits, push, puis relancer. Aucun contournement n'est offert.

  3. Le préfixe X.Y.Z reflète l'état de main au moment de la sync. Si main est à 1.4.0, la preview est 1.4.0-<slug>.0. Si main bump à 1.5.0 et qu'on resync, la prochaine preview est 1.5.0-<slug>.0 : le compteur N reset à chaque changement de préfixe. Cela permet de lire d'un coup d'œil sur quelle version de main la preview est basée.

  4. Pas de MR pour les builds de preview. La MR de la feature elle-même reste ouverte pendant les itérations ; le commit chore(release): X.Y.Z-<slug>.N est poussé directement sur la branche feature. La CI tag le HEAD de la branche. (Exception au principe "releases par MR" : justifiée parce que la MR feature englobera l'intégralité du travail au merge final, où la review se fera.)

  5. Pas de mise à jour du CHANGELOG.md pour les previews. L'entrée changelog est générée une seule fois, au moment du merge final dans main (via le flux standard de release). Cela évite de polluer le changelog avec des entrées de preview qu'il faudrait ensuite nettoyer.

  6. Le slug est dérivé du nom de branche. feature/client_indecisclient_indecis. La CLI rejette si la branche courante ne matche pas le pattern feature/*.

  7. Quand le client valide, merger normalement. Ouvrir une MR feature/<nom>main, faire reviewer, merger. Puis lancer le flux standard de release (release-util release --env staging) depuis main. La version résultante est calculée à partir des conventional commits de la feature.

Cas limites

Plusieurs features longues en parallèle. Possible et supporté. Chaque branche a son propre slug, donc pas de collision de tags. La règle "premier qui merge gagne" s'applique : la deuxième feature à merger devra d'abord absorber les changements de la première via une sync de main. C'est un coût d'intégration accepté ; les features qui ne se touchent pas paient peu.

Changement de main pendant que le client teste. Une sync change le HEAD de la branche feature et invalide donc la dernière preview testée par le client (le tag précédent existe toujours, mais le code de la branche a évolué). Le client devra revalider sur la nouvelle preview. Souvent acceptable (le client itère de toute façon), mais à communiquer explicitement à l'équipe et au client.

Feature abandonnée. Supprimer la branche et ses tags de preview :

bash
release-util cleanup <slug>

Cela supprime la branche distante et tous les tags *-<slug>.*. À faire systématiquement pour éviter l'accumulation de tags zombies.

Conflit lors de la sync. Résoudre localement, commiter le merge, push. La CLI ne fait pas de sync automatique : c'est volontaire, pour que le dev ait conscience des changements intégrés.

Forward-only : règles absolues

  • Jamais retaguer. Un 1.0.1-staging.0 cassé devient 1.0.1-staging.1, jamais un 1.0.1-staging.0 force-retagué.
  • Jamais force-push de tags.
  • Jamais force-push sur main ni sur les branches feature partagées.
  • Si un pipeline échoue après le tag : fix forward avec un nouveau tag. Les jobs de déploiement doivent être idempotents pour rendre les retries possibles.

Hotfixes

Pour corriger un bug en production sans livrer le travail non-released de main, la CLI fournit un flux dédié en trois temps :

bash
release-util hotfix --start    # branche hotfix/<X.Y.Z+1> depuis le dernier tag de production
# …fix, commit…
release-util hotfix --apply    # commit chore(release):, tag X.Y.Z+1, push du tag → déploiement
release-util hotfix --finish   # push de la branche + MR squash de retour vers main
  1. --start branche hotfix/<X.Y.Z+1> depuis le dernier tag de production (le X.Y.Z le plus haut connu en local ou sur origin). Committer le fix sur cette branche.
  2. --apply calcule la prochaine version patch à partir des tags atteignables sur la branche, fait le commit chore(release): X.Y.Z (changelog via release-it si un package.json existe), tague HEAD et pousse le tag uniquement. Un tag de hotfix est un tag de production ordinaire : le pipeline de tag le déploie comme n'importe quelle release.
  3. --finish pousse la branche et ouvre (ou rafraîchit) la MR de retour vers main, avec squash-on-merge demandé par option de push (merge_request.squash, GitLab ≥ 17.2). Merger la MR seulement une fois le fix validé en production — c'est la décision humaine du flux.

Boucle de re-fix

Le client ne valide pas ? Committer sur la même branche, relancer --apply (1.2.4 → 1.2.5) puis --finish, autant de fois que nécessaire. Le nom de branche garde la première version comme simple étiquette — il n'est jamais renommé (cela orphelinerait la MR ouverte).

Garde-fous intégrés

--apply refuse de taguer si une release de production plus récente existe ailleurs (taguer depuis une lignée périmée déploierait un rollback — repartir d'un --start frais), s'il n'y a aucun commit depuis la base, ou si une branche release/<version> ouverte sur origin vise la même version. --finish exige que tout ce qui part vers main soit couvert par un tag déjà poussé.

À savoir

  • Pousser un tag X.Y.Z requiert le droit de créer des tags protégés (Maintainer par défaut — ou assouplir le réglage du projet pour les Developers).
  • Ne pas éditer le sujet du commit squash au moment du merge : la CLI met le titre de la MR à chore(release): X.Y.Z pour que le job release-tag le reconnaisse et saute idempotemment (le tag existe déjà).
  • Un hotfix livré devient la nouvelle base de versionnement : la prochaine release de main se calcule depuis lui. Re-cutter une staging après un hotfix pour re-valider le travail en cours.

L'alternative historique — merger le fix dans main puis release-util release --env staging|prod — reste valable quand main est livrable en l'état. Important : une release depuis main livre tout ce qui est mergé dans main, pas uniquement le hotfix.

Convention de commits (Conventional Commits)

Les commits sont validés localement (hook commit-msg) contre la spec Conventional Commits. La CLI de release utilise la même convention pour décider du bump semver et générer le changelog.

feat: nouvelle fonctionnalité            → bump minor
fix: correction de bug                   → bump patch
feat!: changement breaking               → bump major
refactor: renommage interne              → pas de release
chore: maintenance                       → pas de release
docs: mise à jour de doc                 → pas de release
test: ajout/modification de tests        → pas de release

Le ! après le type (ou un footer BREAKING CHANGE: dans le corps) déclenche un bump major.

Ne jamais bypasser le hook avec --no-verify. Réécrire le message du commit si rejeté (git commit --amend).

Variables CI/CD requises

À définir dans les variables CI/CD du projet (protected + masked) :

  • RELEASE_TOKEN : token avec droit d'écriture sur le dépôt, rôle Maintainer (pour pouvoir push des tags sur les branches protégées). Utilisé par le job release-tag.
  • Tokens de déploiement : spécifiques à la plateforme cible (registre de packages, infrastructure de déploiement, etc.). À documenter projet par projet.

Paramètres du dépôt

  • Branche par défaut : main.
  • Branches protégées : main, pas de push direct, MR uniquement. Allowed to push and merge : Maintainers (pour que RELEASE_TOKEN puisse pousser les tags ; les Devs passent toujours par une MR pour les commits applicatifs).
  • Tags protégés : protéger les deux patterns pour empêcher suppression ou déplacement :
    • [0-9]*.[0-9]*.[0-9]* : tags de production
    • [0-9]*.[0-9]*.[0-9]*-* : tags de staging et de preview
  • Environnements : production (protégé, restreindre qui peut déployer), staging (libre ou semi-restreint), preview-* (créés à la demande, libres).
  • Méthode de merge : Merge commit ou Fast-forward. Pas Squash : squash réécrit le message du commit de release, et le détecteur du job release-tag cherche chore(release): X.Y.Z exactement.
  • Push rules (optionnel) : appliquer une regex sur le message de commit qui matche Conventional Commits, en doublon du hook local.

Troubleshooting

MR de release mergée mais pas de tag

Vérifier le job release-tag du pipeline déclenché par le push sur main. Causes courantes :

  • RELEASE_TOKEN manquant, sans accès aux branches/tags protégés, ou avec le mauvais rôle/scope.
  • MR mergée avec une stratégie squash qui a réécrit le message chore(release): .... Fix : changer la méthode de merge par défaut du projet en Merge commit ou Fast-forward.

Pipeline échoué après le tag

Fixer la cause, relancer release-util release --env staging|prod pour produire l'incrément suivant. Ne pas retaguer. Les jobs de déploiement doivent être idempotents (réutilisable sur retry).

Mauvais canal déployé

Vérifier le format du tag :

  • 1.0.1 → production
  • 1.0.1-staging.N → staging
  • 1.0.1-<slug>.N → preview de feature

Le job qui détecte le canal log son choix dans la pipeline.

release-util refuse de tourner : "must run from main" / "working tree dirty"

Comportement intentionnel. Commit ou stash les changements et switch sur main d'abord.

release-util refuse de tourner : "local main doesn't match origin/main"

Comportement intentionnel. La CLI ne fait pas de pull automatique : reviewer d'abord les commits entrants pour savoir ce qui va être livré.

bash
git log --oneline main..origin/main
git pull --ff-only

release-util refuse de tourner : "feature branch behind main" (mode feature)

Sync main dans la branche feature, résoudre les conflits localement, push, puis relancer :

bash
git merge main
# (résoudre les conflits si nécessaire)
git push
release-util release --env feature

Hook commit-msg rejette le format

Réécrire le message pour respecter Conventional Commits :

bash
git commit --amend

Ne pas bypasser avec --no-verify.

Tag créé manuellement avec préfixe v

La CI ignore les tags avec préfixe v, donc rien ne se déclenchera. Supprimer le tag et utiliser release-util correctement :

bash
git tag -d vX.Y.Z
git push origin :refs/tags/vX.Y.Z

Tag de preview supprimé alors que le client le référence encore

Les tags doivent être protégés (voir Paramètres du dépôt). Si la protection était mal configurée et qu'un tag a été supprimé : release-util release --env feature régénérera un nouveau tag (N+1), mais le numéro perdu est définitivement perdu (forward-only), communiquer le nouveau tag au client.

Conflit de merge bloquant lors d'une sync de feature longue

Normal si la feature est restée non-synchronisée longtemps. Résoudre les conflits localement (git mergetool ou édition manuelle), commiter le merge, push. Si les conflits sont trop nombreux/complexes, considérer que la feature a accumulé trop de dette d'intégration, discuter avec l'équipe d'une approche feature-flag à la place.

Glossaire

Définitions des termes utilisés dans ce document, utiles pour ceux (humains ou IA) qui découvrent le modèle.

  • Trunk-based : modèle Git où toutes les modifications convergent vers une seule branche longue (ici main). Pas de branches longues parallèles type GitFlow.
  • Forward-only : les corrections se font en avançant (nouveau commit, nouveau tag) jamais en réécrivant le passé (force-push, retag, suppression de tag).
  • MR de release : Merge Request contenant uniquement le commit chore(release): X.Y.Z qui bump la version. Mergée comme n'importe quelle autre MR.
  • Preview : build généré depuis une branche feature longue, déployé sur un environnement dédié pour validation client, sans toucher à staging ni production.
  • Slug : identifiant court d'une feature, dérivé du nom de branche (feature/client_indecisclient_indecis), utilisé dans le tag de preview.
  • Sync : merge de main dans une branche feature pour la maintenir à jour. Toujours merge, jamais rebase.
  • Fix forward : corriger un problème en avançant (nouveau commit + nouveau tag) plutôt qu'en modifiant le passé.
  • Conventional Commits : convention de message de commit (type: description) utilisée pour automatiser le bump semver et la génération du changelog.
  • Preflight : ensemble de vérifications exécutées par la CLI avant toute action de release (branche, propreté du working tree, à-jour avec le remote, etc.). Les preflights ne peuvent pas être contournés.

Résumé pour assistant IA

Si tu es un assistant IA aidant un dev sur ce projet, retiens :

  1. Ne propose jamais git push --force, retag, ou --no-verify. Ces opérations sont interdites par le modèle.
  2. Pour faire une release, l'utilisateur doit utiliser release-util release --env staging|prod|feature depuis la bonne branche, pas faire les opérations manuellement.
  3. Si une release a échoué, la solution est toujours fix forward : nouvelle MR, nouveau tag avec incrément.
  4. Pour une feature longue, vérifier que la branche est feature/* et bien synchronisée avec main avant de proposer un build de preview.
  5. Le format de tag révèle le canal : X.Y.Z (prod), X.Y.Z-staging.N (staging), X.Y.Z-<slug>.N (preview).
  6. Les conventional commits déterminent le bump : feat: → minor, fix: → patch, ! ou BREAKING CHANGE: → major.

Contributors

The avatar of contributor named as Calixte Mayoraz Calixte Mayoraz
The avatar of contributor named as Clément Favre Clément Favre

Changelog