Versienummers zouden geen beslissing moeten zijn. Ze zouden een consequentie moeten zijn van de wijzigingen die je maakte.
Semantic versioning (semver) heeft duidelijke regels:
- MAJOR: Breaking changes
- MINOR: Nieuwe features, backwards compatible
- PATCH: Bug fixes, backwards compatible
Maar handmatig beslissen “is dit een minor of patch?” is foutgevoelig en inconsistent. Laten we het automatiseren.
De Kern: Conventional Commits
Het magische ingrediënt is conventional commits — een gestandaardiseerd commit message formaat dat tooling vertelt welk soort wijziging je maakte.
<type>(<scope>): <description>
[optional body]
[optional footer]
Types die ertoe doen voor versioning:
fix:→ PATCH bump (1.0.0 → 1.0.1)feat:→ MINOR bump (1.0.0 → 1.1.0)BREAKING CHANGE:of!→ MAJOR bump (1.0.0 → 2.0.0)
Voorbeelden:
fix(auth): correct password validation regex
feat(api): add user preferences endpoint
feat(core)!: change configuration format
BREAKING CHANGE: config.yml no longer supports legacy format
semantic-release Opzetten
semantic-release is de standaard tool hiervoor. Het:
- Analyseert commits sinds laatste release
- Bepaalt de versie bump
- Genereert release notes
- Maakt Git tags aan
- Publiceert (npm, Docker, etc.)
Installatie
Maak een .releaserc.json:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/gitlab",
"@semantic-release/git"
]
}
GitLab CI Configuratie
stages:
- test
- release
variables:
GITLAB_TOKEN: $GITLAB_TOKEN # CI/CD variable met api scope
release:
stage: release
image: node:20
before_script:
- npm install -g semantic-release @semantic-release/gitlab @semantic-release/changelog @semantic-release/git
script:
- semantic-release
rules:
- if: $CI_COMMIT_BRANCH == "main"
Vereiste GitLab Token
Maak een Project Access Token met:
- Role: Maintainer
- Scopes: api, write_repository
Voeg het toe als CI/CD variable GITLAB_TOKEN (protected, masked).
Hoe Het Werkt
Wanneer je naar main pusht:
flowchart TD
subgraph analysis["Commit Analyse"]
A["Commits sinds v1.2.3"]
A --> B["fix(api): handle null response"]
A --> C["feat(ui): add dark mode"]
A --> D["docs: update readme"]
B --> E["→ PATCH"]
C --> F["→ MINOR"]
D --> G["→ (geen release)"]
end
E --> H["Hoogste bump: MINOR"]
F --> H
H --> I["Nieuwe versie: v1.3.0"]
Dan doet semantic-release:
- Maakt
CHANGELOG.mdentry - Commit de changelog
- Maakt Git tag
v1.3.0 - Maakt GitLab Release met notes
Complete .releaserc.json
Hier is mijn productie configuratie:
{
"branches": [
"main",
{ "name": "beta", "prerelease": true },
{ "name": "alpha", "prerelease": true }
],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "docs", "release": false },
{ "type": "refactor", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "chore", "scope": "deps", "release": "patch" }
]
}],
["@semantic-release/release-notes-generator", {
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance" },
{ "type": "refactor", "section": "Refactoring" },
{ "type": "docs", "section": "Documentation", "hidden": true },
{ "type": "chore", "section": "Maintenance", "hidden": true }
]
}
}],
["@semantic-release/changelog", {
"changelogFile": "CHANGELOG.md"
}],
["@semantic-release/gitlab", {
"gitlabUrl": "https://gitlab.example.com"
}],
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}]
]
}
Conventional Commits Afdwingen
Automatisering werkt alleen als commits de conventie volgen. Dwing het af:
commitlint
Maak .commitlintrc.json:
{
"extends": ["@commitlint/config-conventional"]
}
Pre-commit Hook
Met husky:
npm install --save-dev husky @commitlint/cli @commitlint/config-conventional
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
GitLab CI Check
Voor projecten zonder husky:
lint-commits:
stage: test
image: node:20
before_script:
- npm install -g @commitlint/cli @commitlint/config-conventional
script:
- |
if [ -n "$CI_MERGE_REQUEST_IID" ]; then
# Check commits in MR
git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
commitlint --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to HEAD
fi
rules:
- if: $CI_MERGE_REQUEST_ID
Container Image Versioning
Voor Docker images, gebruik semantic-release om images te taggen:
release:
stage: release
image: docker:24
services:
- docker:24-dind
before_script:
- apk add --no-cache nodejs npm git
- npm install -g semantic-release @semantic-release/exec
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- semantic-release
rules:
- if: $CI_COMMIT_BRANCH == "main"
Met .releaserc.json:
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/exec", {
"publishCmd": "docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:${nextRelease.version} && docker push $CI_REGISTRY_IMAGE:${nextRelease.version}"
}],
"@semantic-release/gitlab"
]
}
Nu krijgen je Docker images automatisch semantic version tags.
Monorepo Versioning
Voor monorepos met meerdere packages, gebruik semantic-release-monorepo of onafhankelijke releases per package:
release-api:
stage: release
script:
- cd packages/api && semantic-release
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- packages/api/**/*
release-web:
stage: release
script:
- cd packages/web && semantic-release
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- packages/web/**/*
Elk package behoudt zijn eigen versie.
Integratie met GitOps
Nadat semantic-release een tag aanmaakt, trigger deployment:
deploy:
stage: deploy
script:
- |
# Update GitOps repo met nieuwe versie
cd gitops-repo
kustomize edit set image myapp:${CI_COMMIT_TAG}
git commit -am "Deploy ${CI_COMMIT_TAG}"
git push
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/
De tag triggert de deploy job, die de GitOps repository updatet met de nieuwe versie.
Breaking Changes Afhandelen
Breaking changes verdienen speciale aandacht:
feat(api)!: change authentication to OAuth2
BREAKING CHANGE: Basic auth is no longer supported.
Users must migrate to OAuth2. See migration guide at /docs/oauth-migration
De ! na de scope of BREAKING CHANGE: in de footer triggert een MAJOR bump.
Documenteer breaking changes in de commit body — ze verschijnen in de changelog.
Pre-release Versies
Voor beta/alpha releases:
{
"branches": [
"main",
{ "name": "beta", "prerelease": true },
{ "name": "alpha", "prerelease": true }
]
}
Commits naar beta branch maken versies zoals 1.3.0-beta.1.
Mijn Workflow
- Ontwikkel op feature branch met conventional commits
- MR naar main — commitlint valideert messages
- Merge naar main — semantic-release bepaalt versie
- Tag aangemaakt — triggert deployment pipeline
- GitOps updated — ArgoCD deployt nieuwe versie
Geen handmatige versie bumps. Geen vergeten changelog entries. Geen discussies over “is dit een minor of patch?”
Troubleshooting
“No release” terwijl je er een verwacht
Check commit types. Alleen fix, feat, en breaking changes triggeren standaard releases.
“Permission denied” bij push
Zorg dat GITLAB_TOKEN write_repository scope heeft en de geassocieerde user naar protected branches kan pushen.
Dubbele releases
Voeg [skip ci] toe aan de release commit message (al in de config hierboven).
Waarom Dit Ertoe Doet
Geautomatiseerde versioning gaat niet over lui zijn. Het gaat over:
- Consistentie: Elke release volgt dezelfde regels
- Documentatie: Changelog is altijd actueel
- Traceerbaarheid: Versie → commits → wijzigingen
- Snelheid: Geen handmatige stappen betekent snellere releases
- Verminderde frictie: Developers focussen op code, niet proces
Versienummers moeten een verhaal vertellen. Geautomatiseerde semantic versioning zorgt dat dat verhaal accuraat, consistent, en altijd up to date is.
