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:

  1. Analyseert commits sinds laatste release
  2. Bepaalt de versie bump
  3. Genereert release notes
  4. Maakt Git tags aan
  5. 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:

  1. Maakt CHANGELOG.md entry
  2. Commit de changelog
  3. Maakt Git tag v1.3.0
  4. 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

  1. Ontwikkel op feature branch met conventional commits
  2. MR naar main — commitlint valideert messages
  3. Merge naar main — semantic-release bepaalt versie
  4. Tag aangemaakt — triggert deployment pipeline
  5. 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:

  1. Consistentie: Elke release volgt dezelfde regels
  2. Documentatie: Changelog is altijd actueel
  3. Traceerbaarheid: Versie → commits → wijzigingen
  4. Snelheid: Geen handmatige stappen betekent snellere releases
  5. 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.