Setting Up Nerdbank.GitVersioning for .NET Projects

Versioning is one of those things that seems simple until you try to automate it consistently across a team. Manual version bumps get forgotten, build numbers drift, and tracing a deployed binary back

Introduction#

Versioning is one of those things that seems simple until you try to automate it consistently across a team. Manual version bumps get forgotten, build numbers drift, and tracing a deployed binary back to its source commit becomes guesswork.

Nerdbank.GitVersioning (NBGV) solves this by deriving version numbers directly from git history. Every commit produces a unique, deterministic SemVer-compliant version with no manual intervention. This guide covers how to set it up for .NET projects, configure it for your workflow, prepare releases, and integrate it into CI/CD pipelines.

How NBGV Computes Versions#

NBGV uses two inputs to calculate a version:

  1. The version field in a version.json file (e.g., 1.2-beta)
  2. The git height: the number of commits since the version field was last changed

The resulting version follows the pattern: {major}.{minor}.{height}[-{prerelease}][+{commit-id}]

Concrete Example#

Given "version": "1.0-alpha" in version.json and 24 commits since it was set:

Output Value
SemVer2 1.0.24-alpha+g9a7eb6c819
NuGetPackageVersion 1.0.24-alpha
AssemblyVersion 1.0.0.0
AssemblyFileVersion 1.0.24.27065

The 1.0 comes from version.json, 24 is the git height (used as PATCH), -alpha is the prerelease tag, and +g9a7eb6c819 is the git commit ID appended for non-public builds.

Why Git Height Instead of Commit Hashes#

Git commit hashes are not sortable in SemVer. An incrementing PATCH number ensures NuGet consumers always get the latest version when updating. The height resets automatically when you bump the major or minor version.

How It Differs from GitVersion#

NBGV does not depend on git tags or branch names for version calculation. Tags are optional markers, not inputs. This means clones that skip tag fetching still produce correct versions, and build reproducibility is maintained regardless of tag state.

Installation#

Install the CLI Tool#

1
dotnet tool install -g nbgv

For CI environments, a local install is preferred:

1
dotnet tool install --tool-path . nbgv

Initialize Your Repository#

Run nbgv install from the root of your git repository:

1
nbgv install

This creates two files:

  • version.json – the version configuration
  • Directory.Build.props – adds the NuGet package reference

You can specify an initial version:

1
nbgv install --version 1.0-beta

Manual Setup#

If you prefer manual configuration, create version.json at the repository root:

1
2
3
4
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "1.0-beta"
}

Then add the NuGet package reference in Directory.Build.props:

1
2
3
4
5
6
7
<Project>
  <ItemGroup>
    <PackageReference Include="Nerdbank.GitVersioning"
                      Version="3.9.50"
                      PrivateAssets="all" />
  </ItemGroup>
</Project>

The PrivateAssets="all" attribute prevents NBGV from becoming a transitive dependency of your packages.

Understanding version.json#

Minimal Configuration#

1
2
3
4
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "1.0-beta"
}

Full Configuration Reference#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "1.0-beta",
  "assemblyVersion": {
    "precision": "minor"
  },
  "nugetPackageVersion": {
    "semVer": 2
  },
  "publicReleaseRefSpec": [
    "^refs/heads/main$",
    "^refs/heads/release/.*$"
  ],
  "cloudBuild": {
    "setAllVariables": true,
    "buildNumber": {
      "enabled": true,
      "includeCommitId": {
        "when": "nonPublicReleaseOnly",
        "where": "buildMetadata"
      }
    }
  },
  "release": {
    "tagName": "v{version}",
    "branchName": "release/v{version}",
    "versionIncrement": "minor",
    "firstUnstableTag": "alpha"
  },
  "pathFilters": [
    "./src/",
    ":^./docs/"
  ]
}

Key Properties#

version (required): The base version in major.minor[-prerelease] format. This is the only required field.

publicReleaseRefSpec: An array of regex patterns matching branches or tags that produce public release builds. Public builds omit the git commit ID suffix. Non-public builds (e.g., feature branches) include it for traceability.

assemblyVersion.precision: Controls how much of the version populates AssemblyVersion. The default minor produces 1.0.0.0, avoiding binding redirect churn on .NET Framework when shipping patches.

nugetPackageVersion.semVer: Set to 2 to include build metadata in NuGet package versions. SemVer 1 is the default for backward compatibility with older NuGet clients.

cloudBuild: Configures CI/CD integration. When buildNumber.enabled is true, NBGV sets the build number on supported CI systems automatically.

release: Controls the nbgv prepare-release command behavior including branch naming, version increment strategy, and the prerelease tag applied to the development branch after branching.

pathFilters: In mono-repos, this limits which file changes count toward version height. Prefix paths with :^ to exclude them.

Setting Up for Different Project Structures#

Single Solution (Unified Versioning)#

Place version.json at the repository root. All projects share the same version.

1
2
3
4
5
6
repo-root/
  version.json
  Directory.Build.props
  src/
    ProjectA/ProjectA.csproj
    ProjectB/ProjectB.csproj

For solutions with many projects, add a performance optimization to Directory.Build.props:

1
2
3
4
5
6
7
8
9
10
<Project>
  <ItemGroup>
    <PackageReference Include="Nerdbank.GitVersioning"
                      Version="3.9.50"
                      PrivateAssets="all" />
  </ItemGroup>
  <PropertyGroup>
    <GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
  </PropertyGroup>
</Project>

The GitVersionBaseDirectory property tells MSBuild to compute the version once and share it across all projects, rather than running the version calculation per project.

Mono-Repo (Per-Component Versioning)#

Use version.json inheritance. Place a root file with shared settings, then override per component:

1
2
3
4
5
6
7
8
9
repo-root/
  version.json              # base version and shared settings
  Directory.Build.props
  src/
    LibraryA/
      version.json          # "inherit": true, "version": "2.0-beta"
    LibraryB/
      version.json          # "inherit": true, "version": "3.1"
    SharedLib/              # inherits root version.json

Root version.json:

1
2
3
4
5
6
7
8
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "1.0-beta",
  "publicReleaseRefSpec": ["^refs/heads/main$"],
  "cloudBuild": {
    "buildNumber": { "enabled": true }
  }
}

Per-component version.json (e.g., src/LibraryA/version.json):

1
2
3
4
5
6
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "inherit": true,
  "version": "2.0-beta",
  "pathFilters": ["./"]
}

This inherits publicReleaseRefSpec, cloudBuild, and other settings from the root, but uses its own version. The pathFilters setting ensures version height only increments when files within this component’s directory change.

Verifying Your Setup#

After installation, build your project and check the generated version:

1
nbgv get-version

For specific version properties:

1
2
3
nbgv get-version -v NuGetPackageVersion
nbgv get-version -v SemVer2
nbgv get-version -v AssemblyVersion

For full JSON output:

1
nbgv get-version -f json

Version Properties Explained#

Given "version": "1.2-beta" with 24 commits of git height:

Property Value Purpose
AssemblyVersion 1.2.0.0 .NET assembly identity and binding. Low precision avoids binding redirect churn.
AssemblyFileVersion 1.2.24.27065 Win32 file properties. Includes git height.
AssemblyInformationalVersion 1.2.24-beta+g9a7eb6c819 Human-readable version with full SemVer and commit ID.
NuGetPackageVersion (SemVer1) 1.2.24-beta Package version for older NuGet clients.
NuGetPackageVersion (SemVer2) 1.2.24-beta+g9a7eb6c819 Package version with build metadata for NuGet 4.3+.

Preparing Releases#

The nbgv prepare-release command automates release branching and version bumping.

What It Does#

  1. Creates a release branch from the current branch
  2. On the release branch: removes (or replaces) the prerelease tag
  3. On the original branch: increments the version and applies a new prerelease tag

Basic Release Workflow#

Starting state on main with version.json:

1
{ "version": "1.2-beta" }

Run the prepare-release command:

1
nbgv prepare-release

This produces two changes:

Release branch (release/v1.2) gets:

1
{ "version": "1.2" }

Main branch is updated to:

1
{ "version": "1.3-alpha" }

The branch name and version increment behavior are controlled by the release section in version.json.

Adding a Prerelease Tag to the Release Branch#

If you want the release branch to start with a release candidate tag:

1
nbgv prepare-release rc

This creates the release branch with "version": "1.2-rc" instead of "version": "1.2".

Controlling Version Increment#

Override which version component gets bumped on the development branch:

1
nbgv prepare-release --versionIncrement major

Or set a specific next version:

1
nbgv prepare-release --nextVersion 2.0

Configuring Release Behavior in version.json#

1
2
3
4
5
6
7
8
{
  "release": {
    "branchName": "release/v{version}",
    "tagName": "v{version}",
    "versionIncrement": "minor",
    "firstUnstableTag": "alpha"
  }
}
Property Description
branchName Template for the release branch name. {version} is replaced with the version.
tagName Template for the git tag name.
versionIncrement Which part to increment on the dev branch: major, minor, or build.
firstUnstableTag Prerelease tag applied to the incremented version on the dev branch.

Complete Release Workflow Example#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Start on main branch with version 2.1-beta
git checkout main

# Step 1: Prepare the release
nbgv prepare-release
# Creates release/v2.1 branch with version "2.1"
# Updates main to version "2.2-alpha"

# Step 2: Switch to the release branch for stabilization
git checkout release/v2.1

# Step 3: Apply bug fixes if needed, then build and publish
dotnet build --configuration Release
dotnet pack --configuration Release

# Step 4: Tag the release commit
nbgv tag
# Creates tag v2.1.0 (or whatever the computed version is)

# Step 5: Push the tag
git push origin v2.1.0

# Step 6: For hotfixes, continue committing on release/v2.1
# Each commit increments the patch version: 2.1.1, 2.1.2, etc.

Tagging Releases#

Using the nbgv tag Command#

1
nbgv tag

This creates a git tag on the current commit using the version computed by NBGV. The tag name follows the release.tagName pattern from version.json (default: v{version}).

Tags Do Not Drive Versioning#

This is a fundamental design choice. NBGV does not use tags as inputs for version calculation. Tags are convenience markers. This means:

  • Clones that do not fetch tags still produce correct versions
  • Tags added retroactively do not change historical versions
  • Forgotten tags do not break the build

Manual Tagging#

If you prefer custom tag names:

1
2
git tag "v$(nbgv get-version -v SimpleVersion)"
git push origin "v$(nbgv get-version -v SimpleVersion)"

Finding the Commit for a Version#

To trace a version back to its source commit:

1
nbgv get-commits 1.2.24

CI/CD Integration#

GitHub Actions#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
name: Build and Package

on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history required for version calculation

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - uses: dotnet/nbgv@master
        id: nbgv
        with:
          setAllVars: true

      - name: Display version
        run: |
          echo "SemVer2: ${{ steps.nbgv.outputs.SemVer2 }}"
          echo "NuGetPackageVersion: ${{ steps.nbgv.outputs.NuGetPackageVersion }}"

      - name: Build
        run: dotnet build --configuration Release

      - name: Pack
        run: dotnet pack --configuration Release --no-build

      - name: Publish NuGet
        if: github.ref == 'refs/heads/main'
        run: dotnet nuget push **/*.nupkg
              --source https://api.nuget.org/v3/index.json
              --api-key ${{ secrets.NUGET_API_KEY }}

The fetch-depth: 0 setting is critical. Without full git history, NBGV cannot calculate the version height.

The dotnet/nbgv@master action exposes version properties as step outputs, including SemVer2, NuGetPackageVersion, AssemblyVersion, GitCommitId, PublicRelease, and more.

Azure DevOps Pipelines#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trigger:
  branches:
    include:
      - main
      - release/*

pool:
  vmImage: 'ubuntu-latest'

steps:
  - checkout: self
    fetchDepth: 0  # Full history required

  - task: UseDotNet@2
    inputs:
      packageType: 'sdk'
      version: '8.0.x'

  - script: dotnet tool install --tool-path . nbgv
    displayName: 'Install NBGV'

  - script: ./nbgv cloud
    displayName: 'Set version variables'

  - script: dotnet build --configuration Release
    displayName: 'Build'

  - script: dotnet pack --configuration Release --no-build
    displayName: 'Pack'

The nbgv cloud command auto-detects the CI system and sets build numbers and environment variables accordingly.

CI/CD Requirements#

  • Full git clone: The .git folder must be present. Source-only downloads will not work.
  • No shallow clones: NBGV needs the full commit history. Set fetch-depth: 0 in GitHub Actions or fetchDepth: 0 in Azure Pipelines.
  • Detached HEAD: Some CI systems check out a detached HEAD. NBGV handles this via environment variables provided by the CI system.

Open Source Library#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "2.1-preview",
  "publicReleaseRefSpec": [
    "^refs/heads/main$",
    "^refs/tags/v\\d+\\.\\d+"
  ],
  "nugetPackageVersion": {
    "semVer": 2
  },
  "cloudBuild": {
    "buildNumber": {
      "enabled": true
    }
  },
  "release": {
    "branchName": "release/v{version}",
    "versionIncrement": "minor",
    "firstUnstableTag": "preview"
  }
}

Enterprise Application#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "3.0",
  "assemblyVersion": {
    "precision": "major"
  },
  "publicReleaseRefSpec": [
    "^refs/heads/main$",
    "^refs/heads/release/.*$"
  ],
  "cloudBuild": {
    "setAllVariables": true,
    "buildNumber": {
      "enabled": true,
      "includeCommitId": {
        "when": "nonPublicReleaseOnly",
        "where": "buildMetadata"
      }
    }
  },
  "release": {
    "branchName": "release/v{version}",
    "versionIncrement": "minor",
    "firstUnstableTag": "alpha"
  }
}

Troubleshooting#

Version Height Is Zero or Unexpected#

Ensure your CI pipeline fetches full git history. Shallow clones are the most common cause.

1
2
3
4
# GitHub Actions
- uses: actions/checkout@v4
  with:
    fetch-depth: 0

Version Does Not Change After Commits#

If using pathFilters, only commits that modify files matching the filter patterns increment the version height. Verify your path patterns are correct.

Build Fails with “version.json Not Found”#

NBGV walks up the directory tree to find version.json. Ensure the file exists at or above your project directory and is committed to the repository (not just present on disk).

NuGet Package Version Includes Commit Hash#

This is expected for non-public builds. Add your branch to publicReleaseRefSpec to produce clean versions without the commit ID suffix.

Best Practices#

  • Always include the $schema field in version.json for editor auto-completion and validation.
  • Use publicReleaseRefSpec to define which branches produce clean release versions.
  • Keep assemblyVersion precision low (default minor) to avoid binding redirect issues on .NET Framework.
  • Use pathFilters in mono-repos to prevent unrelated changes from incrementing component versions.
  • Use nbgv prepare-release instead of manually managing release branches and version bumps.
  • Do not manually set version properties in .csproj files. Let NBGV handle all version assignment.
  • Verify versions locally with nbgv get-version before pushing changes to version.json.
  • Do not depend on tags for versioning. Use nbgv tag for convenience, but understand that tags are outputs, not inputs.

Conclusion#

Nerdbank.GitVersioning removes the manual effort from version management. By deriving versions from git history and a declarative version.json configuration, every commit produces a unique, traceable, SemVer-compliant version. The prepare-release workflow automates release branching and version bumping, while CI/CD integration ensures consistent versioning across all environments. Once configured, versioning becomes something you configure once and rarely think about again.

References#

Contents