Chapitre 1316 minIntermédiaire
Publier une version : SemVer, tags et releases
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- choisir le prochain numéro de version avec SemVer ;
- déduire la version des messages Conventional Commits ;
- publier une release automatiquement à partir d'un tag ;
- protéger les versions publiées.
4 exercices corrigés
Publier une version, c'est figer un état du code sous un numéro (un tag Git comme v2.5.0), produire les artefacts correspondants (image Docker, archive, exécutable) et documenter ce qui a changé (une release et un CHANGELOG). Dans un pipeline, le tag déclenche tout le reste automatiquement. Ce chapitre explique le versionnage sémantique, les messages de commit normalisés, le workflow de publication sur un tag, l'automatisation du numéro et du CHANGELOG, et la protection des versions publiées.
Le versionnage sémantique (SemVer)
Le versionnage sémantique (Semantic Versioning, SemVer) donne un sens à chaque chiffre d'un numéro MAJEUR.MINEUR.CORRECTIF (tags et versions dans Git) :
| On incrémente… | Quand | Exemple depuis 2.4.1 | Les chiffres à droite |
|---|---|---|---|
| CORRECTIF | Une correction de bug, compatible | 2.4.2 | — |
| MINEUR | Une nouvelle fonctionnalité, compatible avec l'existant | 2.5.0 | Remis à 0 |
| MAJEUR | Un changement incompatible : une route d'API supprimée, un format de réponse modifié | 3.0.0 | Remis à 0 |
Quelques règles complètent le système :
- une pré-version s'écrit avec un tiret :
2.5.0-rc.1(release candidate),3.0.0-beta.2; elle précède la version finale (2.5.0-rc.1<2.5.0) ; - en version 0.x, tout peut changer : l'API n'est pas encore considérée comme stable ; par convention (npm, Cargo), un changement incompatible incrémente alors le chiffre MINEUR et une fonctionnalité ou une correction le CORRECTIF, mais beaucoup de projets incrémentent aussi le MINEUR pour une fonctionnalité ;
- un numéro publié ne se réutilise jamais : si la 2.5.0 contient un bug, on publie la 2.5.1, on ne remplace pas la 2.5.0.
Pour une API comme sorbier-frais, « incompatible » se juge du point de vue de ceux qui l'appellent : l'application mobile et l'interface Web. Ajouter un champ à une réponse JSON est compatible ; renommer un champ ne l'est pas.
Des messages de commit normalisés
La convention Conventional Commits structure les messages de commit avec un type (Conventional Commits) :
feat: ajoute l'export CSV des notes de frais
fix: arrondit les indemnités au centime
docs: complète le README avec la procédure de déploiement
feat!: renomme le champ montant en montant_ttc dans l'API
Le type indique directement l'incrément de version :
| Commits depuis la dernière version | Incrément |
|---|---|
Uniquement fix: (et des docs:, test:, chore:, ci:, qui ne changent pas le numéro) | CORRECTIF |
Au moins un feat: | MINEUR |
Au moins un ! après le type, ou un pied de message BREAKING CHANGE: | MAJEUR |
C'est ce qui permet à un outil de calculer seul le prochain numéro et de rédiger le CHANGELOG.
Tenir un CHANGELOG
Le fichier CHANGELOG.md résume, version par version, ce qui change pour ceux qui utilisent le logiciel. Le format le plus répandu, Keep a Changelog, range les changements par catégorie (Ajouté, Modifié, Corrigé, Supprimé, Sécurité), la version la plus récente en haut, avec sa date.
C'est aussi ce que demande l'indicateur de la compétence « Intégrer en continu les versions d'une solution applicative » de l'E6 SLAM : « une documentation des versions accompagne l'intégration ». Un CHANGELOG tenu à jour, des tags et des releases en sont la preuve directe (les compétences de l'E6 SLAM).
Le workflow de publication sur un tag
Pour publier la version 2.5.0, Lina met à jour le numéro, crée le tag et pousse :
npm version minor # 2.4.1 → 2.5.0 dans package.json et package-lock.json, commit « 2.5.0 » et tag v2.5.0
git push --follow-tags # pousse le commit et le tag
Le tag v2.5.0 déclenche ce workflow :
name: Publication
on:
push:
tags: ['v*.*.*']
permissions:
contents: read
jobs:
verifier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Vérifier que le tag correspond à package.json
run: test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
- uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false
- run: npm ci
- run: npm test -- --ci
release:
needs: verifier
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- name: Construire l'archive
run: git archive --format=zip --prefix="sorbier-frais-$GITHUB_REF_NAME/" -o "sorbier-frais-$GITHUB_REF_NAME.zip" HEAD
- name: Créer la release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--title "sorbier-frais $GITHUB_REF_NAME" \
--generate-notes \
--verify-tag \
"sorbier-frais-$GITHUB_REF_NAME.zip"
| Élément | Rôle |
|---|---|
tags: ['v*.*.*'] | Seuls les tags de la forme v2.5.0 déclenchent la publication |
| La vérification du tag | Refuse de publier si le tag et le numéro de package.json ne correspondent pas |
package-manager-cache: false | Pas de cache dans un workflow de publication (l'empoisonnement du cache) |
contents: write | Nécessaire pour créer la release, accordé au seul job release |
gh release create | L'outil en ligne de commande de GitHub, préinstallé sur les runners : il crée la release, y joint l'archive, et --generate-notes rédige les notes à partir des pull requests fusionnées depuis la version précédente |
--verify-tag | Échoue si le tag n'existe pas sur GitHub, au lieu d'en créer un |
En parallèle, le workflow Docker du chapitre 9, déclenché lui aussi par les tags v*, publie les images 2.5.0 et 2.5. La production peut alors déployer 2.5.0, une étiquette qui parle à tout le monde.
Automatiser le numéro et le CHANGELOG
Choisir le numéro et écrire le CHANGELOG à la main est source d'oublis. Deux outils s'appuient sur Conventional Commits pour le faire :
- release-please (de Google) tient en permanence une pull request de publication, mise à jour à chaque fusion sur
main: elle contient le nouveau numéro et les nouvelles entrées du CHANGELOG. Quand l'équipe décide de publier, elle fusionne cette pull request ; l'outil crée alors le tag et la release. - semantic-release publie automatiquement à chaque fusion sur
mainqui contient unfeatou unfix, sans pull request intermédiaire.
name: Release Please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
publiee: ${{ steps.release.outputs.release_created }}
tag: ${{ steps.release.outputs.tag_name }}
steps:
# Action d'un éditeur tiers : dans un vrai projet, épingle-la sur une empreinte de commit
- id: release
uses: googleapis/release-please-action@v5
with:
release-type: node
image:
needs: release-please
if: needs.release-please.outputs.publiee == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Construction de l'image pour ${{ needs.release-please.outputs.tag }}"
Protéger les versions publiées
Une version publiée doit rester ce qu'elle était. Sur GitHub :
- un ruleset de tags (Settings, Rules, Rulesets, New tag ruleset) sur le motif
v*réserve la création des tags de version aux mainteneurs et interdit leur suppression et leur déplacement ; - les releases immuables, une option du dépôt, empêchent de modifier le tag et les fichiers d'une release une fois publiée ;
- un tag signé (
git tag -s v2.5.0) prouve qui a créé la version (signer ses tags) ; - une attestation de provenance des fichiers publiés, produite dans le workflow par l'action officielle
actions/attest-build-provenance(avec les permissionsid-token: writeetattestations: write), permet à quiconque de vérifier avecgh attestation verifyqu'un fichier a bien été construit par ce workflow, à partir de ce commit.
C'est exactement ce que la règle de l'action épinglée demande aux autres : des versions qui ne bougent pas.
Avec GitLab
publier-release:
stage: deploy
image: registry.gitlab.com/gitlab-org/cli:latest
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
script:
- echo "Publication de la version $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
name: "sorbier-frais $CI_COMMIT_TAG"
description: "Voir CHANGELOG.md pour le détail des changements."
Le mot-clé release crée la release GitLab à partir du tag ; l'image cli contient l'outil glab qui s'en charge. Les tags protégés (Settings, Repository, Protected tags) réservent la création des tags v* aux mainteneurs, et les variables protégées ne sont transmises qu'à leurs pipelines.
Exercices
Exercice 1 sur 4 Le prochain numéro Réussi À revoir
La version actuelle de sorbier-frais est 2.4.1. Donne le numéro de la prochaine version dans chaque cas.
- Depuis la 2.4.1, on a corrigé l'arrondi des indemnités.
- On a ajouté l'export CSV et corrigé deux bugs.
- La route
GET /notesrenvoie désormais une liste paginée{ "elements": [...], "total": 87 }au lieu d'un simple tableau. - Un autre projet est en 0.9.3 ; on ajoute une fonctionnalité.
- Avant la 3.0.0, l'équipe veut publier une première version de test.
Voir la réponseMasquer la réponse de l'exercice 1
- Correction compatible : 2.4.2.
- Au moins une fonctionnalité compatible : 2.5.0 (le correctif repart à 0, les corrections sont incluses).
- Le format de la réponse change, les applications qui lisent un tableau cassent : changement incompatible, 3.0.0.
- En 0.x, une fonctionnalité incrémente le chiffre MINEUR : 0.10.0 (et non 1.0.0 : le passage en 1.0.0 est une décision de stabilité, pas un calcul).
- Une pré-version : 3.0.0-rc.1 (ou
3.0.0-beta.1), qui précède 3.0.0 dans l'ordre des versions.
Tu avais trouvé ?
Exercice 2 sur 4 Calculer la version à partir des commits Réussi À revoir
Voici les commits fusionnés sur main depuis le tag v2.4.1 :
fix: arrondit les indemnités au centime
docs: complète le README
feat: ajoute l'export CSV des notes de frais
chore: met à jour Jest
ci: ajoute l'analyse CodeQL
- Quel numéro release-please proposera-t-il ?
- Et si l'un des messages avait été
feat!: supprime la route /notes/export-ancien?
Voir la réponseMasquer la réponse de l'exercice 2
- Il y a un
feat: incrément MINEUR, 2.5.0. Le CHANGELOG aura une section « Fonctionnalités » (l'export CSV) et une section « Corrections » (l'arrondi) ;docs,choreetcin'y figurent pas par défaut. - Le
!signale un changement incompatible : incrément MAJEUR, 3.0.0.
Tu avais trouvé ?
Exercice 3 sur 4 Une publication refusée Réussi À revoir
Hugo crée le tag v2.5.0 à la main avec git tag v2.5.0 puis git push origin v2.5.0. Le job verifier du workflow « Publication » échoue :
Run test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
Error: Process completed with exit code 1.
- Que s'est-il passé ?
- Que doit faire Hugo pour publier correctement ?
Voir la réponseMasquer la réponse de l'exercice 3
- La commande compare
vsuivi de la version depackage.jsonau nom du tag. Hugo n'a pas mis à jourpackage.json, qui indique toujours2.4.1:v2.4.1est différent dev2.5.0,testrenvoie 1, l'étape échoue, et la release n'est pas créée. C'est exactement le rôle de cette vérification : sans elle, l'application publiée en 2.5.0 afficherait 2.4.1 dans sa route/version. - Supprimer le tag erroné, en local et sur GitHub (
git tag -d v2.5.0puisgit push origin --delete v2.5.0), ce qui est possible ici parce que rien n'a été publié. Puis lancernpm version minor, qui met à jourpackage.json, commite et crée le tagv2.5.0cohérent, etgit push --follow-tags. Une fois une version vraiment publiée, on ne supprime plus son tag : on publie un correctif.
Tu avais trouvé ?
Exercice 4 sur 4 Écrire un workflow de publication Réussi À revoir
Pour l'application C# GestStock, écris un workflow qui, sur un tag v*.*.* : compile en Release sous Windows avec .NET 10, publie l'application dans un dossier publication, le compresse en GestStock-<tag>.zip, et crée une release GitHub avec ce fichier.
Voir la réponseMasquer la réponse de l'exercice 4
name: Publication
on:
push:
tags: ['v*.*.*']
permissions:
contents: read
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
- run: dotnet publish GestStock/GestStock.csproj --configuration Release --output publication
- name: Compresser
shell: pwsh
run: Compress-Archive -Path publication/* -DestinationPath "GestStock-$env:GITHUB_REF_NAME.zip"
- name: Créer la release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: gh release create $env:GITHUB_REF_NAME "GestStock-$env:GITHUB_REF_NAME.zip" --generate-notes --verify-tag
Sous Windows, le shell par défaut est PowerShell : les variables d'environnement s'y lisent avec $env:NOM. dotnet publish restaure et compile avant de publier. contents: write n'est donné qu'au job qui crée la release.
Tu avais trouvé ?