Environnements de release et versioning
Le cœur conceptuel de l'outil. release et next-version partagent la même tuyauterie (commands/shared.ts:buildReleasePlan()) : racine du dépôt → loadConfig → préconditions → computeNextVersion → suffixe staging/feature → nom de branche → ReleasePlan.
Les trois environnements
ReleaseEnv = 'staging' | 'prod' | 'feature'. La source de vérité du parsing est src/version.ts ; parseReleaseSubject() reconnaît chore(release): X.Y.Z[-staging.N|-<slug>.N] et classe l'environnement depuis la forme du pré-release.
prod
La version vient des commits conventionnels depuis le dernier tag stable (computeNextVersion dans version.ts). C'est le seul environnement qui commite un CHANGELOG.md et qui ouvre une MR avec les notes de release.
parseCommit()parse les Conventional Commits (types reconnus dansKNOWN_TYPES:feat,fix,perf,refactor,docs,test,build,ci,chore,revert).- Un commit est breaking via
!après le type/scope ou via un pied de pageBREAKING CHANGE:. computeNextVersion()parcourt les commits depuis le dernier tag prod et choisit le bump (major / minor / patch).
staging
Même version de base prod, plus un suffixe -staging.N. Le compteur N vient de staging.ts:nextStagingCounter(), qui prend le max sur trois signaux :
- Les tags git (définitif : la version a été publiée).
- Les sujets
chore(release):récents (une release a atterri sur le trunk mais le job tag/publish a échoué avant de pousser le tag). - Opt-in : les branches orphelines
release/X-staging.Nsurorigin— uniquement quand l'appelant l'active explicitement via--bump. Sinon on préfère échouer bruyamment sur une collision de nom plutôt que de rouler silencieusement en avant et d'orpheliner des branches.
Le staging saute le changelog : il est régénéré au cut prod, donc l'écrire à chaque itération de staging ne ferait que churner CHANGELOG.md avec des entrées réécrites de toute façon.
Le flag --bump
À utiliser quand une tentative échouée a laissé une branche release/X-staging.N sur origin. Avec --bump, release --env staging balaie les branches distantes, supprime les orphelines correspondant à la version prod planifiée, et fait avancer le compteur au-delà. Sans --bump, une collision de nom de branche fait échouer la commande au lieu de la contourner.
feature — previews
Coupé depuis une branche feature/<name>, pour produire un build de preview que la CI tague X.Y.Z-<slug>.N où X.Y.Z est la version actuelle du trunk.
- Slug : le nom après
feature/avec chaque-remplacé par_(feature.ts:slugFromBranch()) — pour garder le slug comme un seul token de pré-release semver. Rejette les branches nonfeature/*, un slug vide, ou un slug contenant encore un/(les noms imbriqués casseraient les refs de tag). - Compteur :
feature.ts:nextFeatureCounter(), analogue au compteur staging mais par slug. - Comportement : le preview commit la montée de version (sans changelog) directement sur la branche
feature/<name>et la pousse en place — pas de nouvelle branche, pas de MR. La MR de feature (déjà ouverte) couvre la revue au moment du merge. Le job tag de la CI tague le HEAD. - Précondition : la branche doit être à jour avec le trunk — la fusionner d'abord, ne jamais rebaser.
Le démantèlement d'une feature (branche + tous ses tags *-<slug>.*) se fait avec release-util cleanup <slug>.
Le cas hotfix — pas un quatrième environnement
Un hotfix (release-util hotfix) ne rajoute pas d'environnement : son tag est un tag de production ordinaire (X.Y.Z), son commit chore(release): X.Y.Z est classé prod par parseReleaseSubject(), et detect le range dans le canal production. Ce qui change, c'est d'où la version se calcule :
- Version : bump patch du tag prod le plus haut atteignable depuis la branche (
hotfix.ts:computeHotfixTarget(), viagit tag --list --merged HEAD) — et non du plus haut global commecomputeNextVersion. C'est ce qui fait marcher la boucle de re-fix : après que--applya tagué1.2.4sur la branche, le prochain--applyvoit1.2.4comme base →1.2.5. - Garde de péremption : avant de taguer,
hotfix.ts:assertHotfixNotStale()vérifie qu'aucun tag prod global (local ∪origin) n'est plus haut que la cible — taguer depuis une lignée périmée déploierait un rollback de la release plus récente. - Interaction avec le staging en cours : un hotfix livré (
1.2.4) devient la nouvelle base globale decomputeNextVersion. Si le trunk porte des commitsfeat:, la prochaine version prod visée reste1.3.0et la lignée1.3.0-staging.Ncontinue ; si le trunk ne porte que desfix:, la base passe de1.2.4à1.2.5et le compteur staging repart à 1 (les anciens tags1.2.4-staging.Ndeviennent orphelins — inoffensif). Dans tous les cas, re-couper une staging après un hotfix pour re-valider le travail en cours. - Non-Node : même bascule que
release— sanspackage.json,--applycrée le commit-marqueur viacommitEmptyau lieu de release-it.
Préconditions
Pour staging/prod (src/preconditions.ts), dans l'ordre : sur le trunk · arbre propre · le trunk correspond au défaut distant · synchronisé avec origin. En retard / divergé → échec ; en avance seulement → avertissement mais succès (ces commits partiront via la MR de release). Pour feature, la précondition « sur une branche feature/* » remplace « sur le trunk ».
La CLI ne tire jamais et ne fusionne jamais à votre place : si une vérification échoue parce que le trunk est en retard, c'est à vous de décider de git pull --ff-only et de réessayer.
L'invariant à un seul commit
La MR de release ne porte qu'un seul commit : le chore(release): X.Y.Z[...]. C'est ce sujet de commit que le job tag de la CI lit pour décider quoi taguer. Conséquences pratiques :
- Ne pas squasher les MR
release/*au merge — le squash réécrit le sujet et casse le tag. Voirintegration-ci.md. - Ne pas amender le commit
chore(release):après le merge — cela corrompt le pipeline de publication. Pour corriger le changelog, lander un commit de suivi sur le trunk et re-couper.
Pour la politique complète (hotfix, correction en avant), voir RELEASING.md.
Projets sans package.json (non-Node)
Le commit de version est normalement créé par release-it, qui le matérialise en stageant les fichiers qu'il modifie : sur un projet Node il monte la version dans package.json, ce qui donne quelque chose à committer. Sur un projet sans package.json, et dans les environnements qui n'écrivent pas de changelog (staging et feature, où skipChangelog: true), release-it n'a alors rien à stager et le commit chore(release): ne se crée jamais.
release.ts détecte l'absence de package.json à la racine (hasPackageJson) et, dans ce cas précis, court-circuite release-it pour créer le commit directement via commitEmpty (git commit --allow-empty, dans src/git.ts). Un commit vide est correct ici : la version vit dans le sujet du commit (lu par les compteurs staging/feature) et dans le tag que la CI pose à ce commit — aucun fichier n'a besoin de changer pour l'enregistrer. Le sujet provient toujours de plan.commitSubject (le canonique chore(release): <version>), jamais d'un éventuel commitMessage personnalisé dans .release-it.json, car ce sont précisément ces sujets chore(release): que les compteurs scannent.
Matrice de décision (après le retour anticipé de feature, env ne vaut plus que staging ou prod) :
package.json | env | action |
|---|---|---|
| présent | staging | release-it |
| présent | prod | release-it |
| absent | prod | release-it (le CHANGELOG.md est le fichier stagé) |
| absent | staging | commitEmpty |
| absent | feature | commitEmpty |
Le chemin Node (cas nominal) reste strictement inchangé.

