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.
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.
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 withsrc/event-sync/orsrc\event-sync\(?!terraform[\\/]+event-sync(?:[\\/]|$))- Also don't match if path starts withterraform/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 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.