Home Industries Case Studies About Azure CSP Drop Table Pulse Get Started
Back to Insights
DevOps February 2026 8 min read

GitVersion Monorepo: Semantic Versioning with Conventional Commits

Complete guide to GitVersion 6 monorepo support with conventional commits for automated semantic versioning. Learn how to configure independent project versioning in monorepos.

Drop Table Team

Versioning in a monorepo has always been a challenge. With GitVersion 6's new monorepo support and the power of conventional commits, you can finally achieve automated, consistent semantic versioning across all your projects in a Monorepo. Let's explore how to make this work.

What's New in GitVersion 6

GitVersion 6 introduces first-class monorepo support, a feature the community has been requesting for years. Previously, managing multiple projects with independent version numbers in a single repository required complex workarounds. Now, GitVersion natively understands monorepo structures and can calculate versions for each project independently based on its own commit history.

Key improvements in GitVersion 6 include native monorepo support for independent project versioning, improved conventional commit integration, better performance with large repositories, enhanced configuration flexibility, and support for multiple versioning strategies within the same repo.

Why Conventional Commits Matter

Conventional commits provide a standardised way to write commit messages that both humans and machines can understand. The format follows a simple structure:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Common types include feat for new features (triggers minor version bump), fix for bug fixes (triggers patch version bump), docs for documentation changes, refactor for code refactoring, test for adding tests, and chore for maintenance tasks. Adding BREAKING CHANGE: in the footer or ! after the type triggers a major version bump.

🔑 Key Benefit

When GitVersion 6 parses conventional commits, it can automatically determine the next semantic version without any manual intervention. This eliminates version conflicts and ensures consistent releases.

Setting Up GitVersion 6 for Monorepos

Let's walk through configuring GitVersion 6 for a typical monorepo structure with multiple services.

Repository Structure

In a monorepo, each project that needs independent versioning has its own complete GitVersion.yml configuration. GitVersion does not automatically inherit or merge configurations from parent directories, so each project config must be self-contained:

my-monorepo/
├── src/
│   ├── api-gateway/
│   │   └── GitVersion.yml
│   ├── user-service/
│   │   └── GitVersion.yml
│   └── order-service/
│       └── GitVersion.yml
└── shared/
    └── common-lib/
        └── GitVersion.yml

Project Configuration

Each project needs a complete GitVersion.yml that includes all settings. Here's a comprehensive example using the TrunkBased workflow with ignore.paths to scope versioning to just that project:

workflow: TrunkBased/preview1
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
tag-prefix: 'user-service-v'
version-in-branch-pattern: (?<version>[vV]?\d+(\.\d+)?(\.\d+)?).*
no-bump-message: \+semver:\s?(none|skip)
tag-pre-release-weight: 60000
commit-date-format: yyyy-MM-dd
merge-message-formats: {}
update-build-number: true
semantic-version-format: Strict
strategies:
  - ConfiguredNextVersion
  - Mainline
branches:
  main:
    mode: ContinuousDeployment
    label: ''
    prevent-increment:
      of-merged-branch: true
    track-merge-target: false
    track-merge-message: false
    regex: ^master$|^main$|^trunk$
    source-branches: []
    is-source-branch-for: []
    tracks-release-branches: false
    is-release-branch: false
    is-main-branch: true
    pre-release-weight: 55000
  pull-request:
    increment: Patch
    is-main-branch: false
mode: ContinuousDelivery
label: '{BranchName}'
increment: Inherit
track-merge-target: false
track-merge-message: false
commit-message-incrementing: Enabled
ignore:
  paths:
    - '^(?!src[\\/]+user-service(?:[\\/]|$))(?!shared[\\/]+common-lib(?:[\\/]|$)).+'

This configuration uses the TrunkBased workflow which is ideal for teams practising continuous deployment. The regex patterns for version bumps cover all conventional commit types. The ignore.paths at the bottom scopes this configuration to only consider commits affecting src/user-service or shared/common-lib.

Each project in your monorepo needs its own complete configuration file with a unique tag-prefix and appropriate ignore.paths pattern.

Alternative: Explicit Ignore Patterns

Instead of using negative lookahead regex, you can explicitly list paths to ignore. This can be easier to read but requires updating when new projects are added:

# src/api-gateway/GitVersion.yml - using explicit ignore patterns
workflow: TrunkBased/preview1
tag-prefix: 'api-gateway-v'
# ... (same workflow settings as above)

ignore:
  paths:
    - '^src[\\/]+user-service[\\/].*'
    - '^src[\\/]+order-service[\\/].*'
    - '^docs[\\/].*'
    - '^\.github[\\/].*'

This approach explicitly ignores paths from other services, giving you fine-grained control over which commits affect each project's version.

Inverse Regex: Include Only Specific Paths

For more precise control, you can use a negative lookahead regex to ignore everything except specific paths. This is particularly useful in larger monorepos where listing all paths to ignore would be impractical:

# src/event-sync/GitVersion.yml
tag-prefix: event-sync-v
commit-message-incrementing: Enabled

ignore:
  paths:
    - '^(?!src[\\/]+event-sync(?:[\\/]|$))(?!terraform[\\/]+event-sync(?:[\\/]|$)).+'

This regex pattern breaks down as follows:

  • ^ - Start of the path
  • (?!src[\\/]+event-sync(?:[\\/]|$)) - Negative lookahead: don't match if path starts with src/event-sync/ or src\event-sync\
  • (?!terraform[\\/]+event-sync(?:[\\/]|$)) - Also don't match if path starts with terraform/event-sync/
  • .+ - Match any other path (which will be ignored)

The [\\/]+ handles both forward and backslashes for cross-platform compatibility. The (?:[\\/]|$) ensures we match the folder or end of string, preventing partial matches like event-sync-legacy.

For a project with multiple source directories:

ignore:
  paths:
    - '^(?!src[\\/]+my-service(?:[\\/]|$))(?!terraform[\\/]+my-service(?:[\\/]|$))(?!shared[\\/]+common-lib(?:[\\/]|$)).+'

Conventional Commits in Practice

With the configuration in place, your team can use conventional commits to drive versioning automatically.

Example Commit Messages

For a patch release fixing a bug in the user service:

fix(user-service): resolve null reference in authentication flow

The authentication middleware was not handling expired tokens correctly,
causing null reference exceptions for some users.

For a minor release adding a new feature:

feat(order-service): add bulk order cancellation endpoint

Introduces a new POST /orders/bulk-cancel endpoint that allows
cancelling multiple orders in a single request.

Closes #1234

For a major release with breaking changes:

feat(api-gateway)!: restructure authentication response format

BREAKING CHANGE: The authentication response now returns a structured
object instead of a flat token string. Clients must update their
token parsing logic.

Before: { "token": "eyJ..." }
After: { "auth": { "token": "eyJ...", "expiresAt": "..." } }

CI/CD Integration

Integrating GitVersion 6 into your CI/CD pipeline is straightforward. Here's an example for Azure DevOps:

Azure DevOps Pipeline

trigger:
  - main
  - feature/*
  - release/*

pool:
  vmImage: ubuntu-latest

stages:
  - stage: Version
    jobs:
      - job: CalculateVersions
        steps:
          - checkout: self
            fetchDepth: 0  # Required for GitVersion

          - task: gitversion/setup@3
            displayName: 'Install GitVersion'
            inputs:
              versionSpec: '6.x'
              preferLatestVersion: true

          - task: gitversion/execute@3
            displayName: 'Calculate API Gateway Version'
            inputs:
              useConfigFile: true
              configFilePath: 'src/api-gateway/GitVersion.yml'
              overrideConfig: |
                tag-prefix=api-gateway-v

          - bash: |
              echo "##vso[task.setvariable variable=ApiGatewaySemVer]$(GitVersion.SemVer)"
            displayName: 'Capture API Gateway SemVer'

          - task: gitversion/execute@3
            displayName: 'Calculate User Service Version'
            inputs:
              useConfigFile: true
              configFilePath: 'src/user-service/GitVersion.yml'
              overrideConfig: |
                tag-prefix=user-service-v

          - bash: |
              echo "##vso[task.setvariable variable=UserServiceSemVer]$(GitVersion.SemVer)"
            displayName: 'Capture User Service SemVer'

          - script: |
              echo "API Gateway: $(ApiGatewaySemVer)"
              echo "User Service: $(UserServiceSemVer)"
            displayName: 'Display Versions'

GitHub Actions

name: Version and Build

on:
  push:
    branches: [main, 'feature/**', 'release/**']

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      api-gateway-version: ${{ steps.api-gateway.outputs.semVer }}
      user-service-version: ${{ steps.user-service.outputs.semVer }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v3
        with:
          versionSpec: '6.x'

      - name: Calculate API Gateway Version
        id: api-gateway
        uses: gittools/actions/gitversion/execute@v3
        with:
          useConfigFile: true
          configFilePath: src/api-gateway/GitVersion.yml
          overrideConfig: |
            tag-prefix=api-gateway-v

      - name: Calculate User Service Version
        id: user-service
        uses: gittools/actions/gitversion/execute@v3
        with:
          useConfigFile: true
          configFilePath: src/user-service/GitVersion.yml
          overrideConfig: |
            tag-prefix=user-service-v
💡 Pro Tip

Use path filters in your CI/CD to only build and version projects that have actually changed. This significantly reduces build times in large monorepos.

Best Practices

Enforce Conventional Commits

Use tools like commitlint with Husky to enforce conventional commit format:

# Install dependencies
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

# Configure commitlint
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

# Set up Husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Include Scope for Clarity

In a monorepo, always include the scope to indicate which project the commit affects. This helps GitVersion filter commits correctly and makes the git history more readable.

Use Descriptive Messages

The commit body should explain why the change was made, not just what. This becomes invaluable when generating changelogs automatically.

Tag Releases Consistently

Use project-prefixed tags to identify releases. GitVersion 6 uses these tags as version anchors:

git tag user-service-v1.2.0
git tag api-gateway-v3.0.0
git push --tags

Troubleshooting Common Issues

Version Not Incrementing

If versions aren't incrementing as expected, verify your commit messages follow the conventional format exactly, check that your ignore.paths regex patterns are correct (use a regex tester to validate), and ensure you're fetching full git history in CI (fetchDepth: 0).

Wrong Project Getting Versioned

Ensure each project's GitVersion.yml has correct ignore.paths patterns and that your tag-prefix is unique per project. Test your regex patterns against actual file paths to ensure they match correctly.

Shared Libraries

When updating shared libraries, ensure they're not included in your ignore.paths patterns. This ensures downstream services get version bumps when shared code changes. For example, if shared/common-lib is a dependency, don't add it to the ignore list.

How We Can Help

Implementing GitVersion 6 with conventional commits in your monorepo can transform your release process. We can help with:

  • Monorepo strategy and structure design
  • GitVersion 6 configuration and optimisation
  • CI/CD pipeline integration (Azure DevOps, GitHub Actions)
  • Conventional commit enforcement and team training
  • Automated changelog generation

Get in touch to discuss how we can streamline your versioning workflow.

Want more insights?

Explore our other articles or subscribe to our newsletter for the latest cloud security guidance.