Comparing .NET Versioning Tools for a Microsoft-Style SemVer Release Strategy

Mehdi Hadeli
@mehdihadeli
On this page
Table of contents
Introduction
The previous article defines a GitHub Flow strategy with continuous previews and tag-controlled RC and stable releases. Its sample implements the policy with Nerdbank.GitVersioning (NBGV).
This continuation keeps that release contract and changes the implementation. The same .NET 10 Web API is versioned with GitVersion, MinVer, NBGV, Arcade, and semantic-release so their configuration, CI integration, and tradeoffs can be compared directly. The first article explains why ordinary merges create previews, why RC and stable releases need reviewed version intent, and how Docker, runtime metadata, and Release Drafter carry the selected version. This article keeps that operating model and asks a narrower question: what changes when the version calculator changes?
All code is available in the Microsoft-style .NET versioning samples repository. The examples retain protected main, squash-merged pull requests, explicit release preparation, and tags on approved commits. They focus on version calculation and publishing the Web API, rather than repeating the container and /version implementation from the first article. Production deployment and environment approvals still need to be connected to your deployment platform.
This versioning strategy treats a version as a release contract, not only as an assembly number. It answers four questions for every artifact: which source commit produced it, whether it is safe for a particular audience, what compatibility promise it makes, and whether it has been approved for release. The same contract works for a deployed .NET application and for a NuGet library, although applications usually map prerelease stages to environments while libraries expose them directly to package consumers.
The lifecycle has three audiences. Development and early adopters receive continuous preview artifacts from accepted changes on main. A wider validation group receives a release candidate in staging after release intent has been reviewed. Production consumers receive a stable artifact only after the approved commit is tagged. This keeps development frequent while making the promotion boundary explicit: a tag approves one exact commit, and CI builds that selected source rather than rebuilding an unreviewed workstation state.
The strategy uses two related version forms. The short SemVer identity is easy to review and is used for release intent and tags, such as 1.0.0-preview.2, 1.0.0-rc.1, and 1.0.0. Published preview and RC artifacts can add the UTC date and CI run revision used by Microsoft-style package versions, such as 1.0.0-rc.1.YYDDD.RUN_NUMBER. Stable artifacts remain the clean version 1.0.0. The suffix makes CI builds unique and traceable without changing the meaning of the prerelease stage.
That contract maps to several .NET version fields, each with a different job:
| Field | Consumer | Policy in this article |
|---|---|---|
PackageVersion | NuGet and package consumers | Public SemVer, including preview and rc identifiers. |
AssemblyVersion | CLR binding for strong-named assemblies | A compatibility-oriented value, normally kept less volatile than CI build identity. |
FileVersion | Windows file properties and operational inspection | A numeric Major.Minor.Build.Revision value that can carry a CI revision. |
InformationalVersion | Diagnostics and support tooling | The full version plus commit or repository metadata when useful. |
Keeping these fields separate prevents a CI run number from changing the public compatibility promise. Microsoft’s .NET library guidance recommends SemVer for the package version, prerelease suffixes for nonstable packages, and a CI build number in the file-version revision. The exact MSBuild properties vary by tool, but the ownership boundary should remain the same: release policy decides meaning, while build metadata helps identify the produced file.
SemVer precedence also gives the environments a predictable promotion path. 1.0.0-preview.2 is older than 1.0.0-rc.1, and every prerelease is lower precedence than 1.0.0. Numeric identifiers compare numerically, so rc.10 sorts after rc.2; avoid zero-padding or embedding an opaque value where consumers need to compare release maturity. A date and CI run identifier can improve traceability, but they must not replace the preview or rc stage and its ordinal.
The five tools are therefore versioning adapters around one policy, not interchangeable sources of truth. GitVersion interprets branch and Git-graph rules, MinVer starts from the nearest matching tag and adds Git height, NBGV combines committed version.json intent with Git height, Arcade supplies the .NET build infrastructure and MSBuild stamping conventions, and semantic-release derives release intent from Conventional Commits. The workflows normalize their outputs at the CI boundary: validate one release identity, format the artifact version once, then pass that exact value to dotnet publish, Docker, runtime metadata, and release tooling. There is also an important formatting difference: the NBGV sample retains the first article's CI artifact format, X.Y.Z-preview.N.YYDDD.RUN_NUMBER and X.Y.Z-rc.N.YYDDD.RUN_NUMBER, while the other samples currently publish shorter versions. Sharing the release policy does not mean that every sample already produces identical artifact names or implements the policy entirely inside its native tool.
The Microsoft-style SemVer strategy
Before comparing tools, establish the policy they must implement. The version calculator is replaceable; the release contract is not. This article uses the same strategy as part 1: GitHub Flow, SemVer, continuous previews, reviewed release intent, and tag-gated RC and stable publication.
SemVer identifies release maturity
The stable version follows MAJOR.MINOR.PATCH. A breaking API or behavior change increments MAJOR, a backward-compatible feature increments MINOR, and a backward-compatible fix increments PATCH. Before stable publication, the prerelease identifier communicates maturity:
1.0.0-preview.1
1.0.0-preview.2
1.0.0-rc.1
1.0.0-rc.2
1.0.0preview means active development and early-adopter feedback. rc means feature-complete validation with only release-blocking fixes expected. The version without a prerelease label is stable. The v prefix belongs to the Git tag (v1.0.0-rc.1), not to the SemVer value passed to the .NET SDK or package metadata.
Microsoft-style CI artifact versions
The release identity and the published CI artifact have different jobs. The short SemVer value makes release intent easy to review and tag. Preview and RC artifacts may add a UTC date and CI run revision so every build is unique and traceable, following the style used by Microsoft packages:
Release identity: 1.0.0-preview.2
Published artifact: 1.0.0-preview.2.YYDDD.RUN_NUMBER
Release identity: 1.0.0-rc.1
Published artifact: 1.0.0-rc.1.YYDDD.RUN_NUMBER
Stable artifact: 1.0.0The date and run number are additional prerelease identifiers for the CI artifact, not SemVer build metadata in the strict sense and not a replacement for the stage or ordinal. Stable artifacts stay clean so 1.0.0 remains the public release version. When a project does not need this Microsoft-style suffix, it can publish the short prerelease value instead; the release policy remains the same.
Release flow and control points
The policy has two kinds of changes:
- Ordinary feature and fix merges do not edit version intent. Each accepted merge to protected
mainadvances the preview sequence. - Release-preparation changes explicitly select the RC or stable phase. They are reviewed and merged before an operator creates the matching tag.
CI then calculates one version, validates the ref, and passes that value to dotnet publish, Docker, runtime metadata, and Release Drafter. A tag approves the exact commit; it does not silently rewrite a version calculated from a different commit. The version calculator may use Git history, committed files, MSBuild properties, or Conventional Commits, but it must honor the same boundaries.
The release contract
Use GitHub Flow. main is the only long-lived branch. Feature branches merge through pull requests, and a release is created by tagging the commit on main.
The example release has two automatically calculated previews followed by three tagged releases:
1.0.0-preview.1
1.0.0-preview.2
v1.0.0-rc.1
v1.0.0-rc.2
v1.0.0The five versions represent one release line:
| Version | Intended audience | Allowed change |
|---|---|---|
1.0.0-preview.1 | Maintainers and early adopters | Features, API changes, fixes |
1.0.0-preview.2 | Early adopters | Features, API changes, fixes |
1.0.0-rc.1 | Wider validation group | Release blockers and documentation |
1.0.0-rc.2 | Final validation group | Fixes required for stable release |
1.0.0 | All consumers | Stable baseline |
How each tool represents the five versions
The release events are the same for every sample. Preview calculation differs, while RC and stable publication is tag-gated. Each sample records release intent before tagging: NBGV uses version.json, GitVersion uses its configuration and release.env, MinVer uses version.env, Arcade uses MSBuild properties, and semantic-release uses release.env.
| Tool | Preview 1 and preview 2 after PR merges | RC1: v1.0.0-rc.1 | RC2: v1.0.0-rc.2 | Stable: v1.0.0 |
|---|---|---|---|---|
| GitVersion | Calculates preview versions from the Git graph and main rules. | Uses the tag value directly. | Uses the tag value directly. | Uses the tag value directly. |
| MinVer | Derives unique preview versions from the nearest tag and Git height. | Reads the tag exactly. | Reads the tag exactly. | Reads the tag exactly. |
| NBGV | Uses committed version intent and Git height for exact 1.0.0-preview.N versions. | Validates a matching tag, then publishes 1.0.0-rc.1. | Validates a matching tag, then publishes 1.0.0-rc.2. | Validates a matching tag, then publishes 1.0.0. |
| Arcade | Uses 1.0.0-preview.${GITHUB_RUN_NUMBER}. | Passes the tag value and sets OfficialBuild=true. | Passes the tag value and sets OfficialBuild=true. | Passes the tag value and sets OfficialBuild=true. |
| semantic-release | Uses Conventional Commits for the base and appends preview.${GITHUB_RUN_NUMBER}. | Workflow bypasses calculation and uses the tag value. | Workflow bypasses calculation and uses the tag value. | Workflow bypasses calculation and uses the tag value. |
All five samples use the previous article's promotion model as their reference. Their calculations and some validation-only rules differ, as the sections below explain. GitVersion and MinVer derive versions from Git history, NBGV combines committed intent with history, Arcade stamps adapter-supplied metadata, and semantic-release derives a stable base from Conventional Commits.
Shared implementation constraints
Each workflow fetches complete Git history, calculates the version once, and passes that exact value to dotnet publish with -p:Version. The versioning tools need tags and commit history, so every checkout uses fetch-depth: 0.
The three jobs keep the same boundaries as the earlier sample: build-test validates source, calculate-version produces one SemVer output, and publish builds the artifact and invokes Release Drafter. The sections below focus on what changes inside calculate-version for each tool.
Base versions and published versions
Keep the version used to test the release sequence separate from the version attached to a CI artifact. A local scenario should be able to assert 1.0.0-preview.1 or 1.0.0-rc.2 without knowing the current date or supplying a workflow run number.
| Sample | Local calculation | CI artifact formatting |
|---|---|---|
| GitVersion | Native preview calculation; the adapter applies committed RC/stable intent. | Short preview, RC, or stable version. |
| MinVer | Native tag and commit-height version, including 1.0.0-rc.1.1 between RC tags. | The same short native version. |
| NBGV | Committed intent and Git height, with the non-public commit suffix removed by the adapter. | Preview and tagged RC versions add YYDDD.RUN_NUMBER; stable stays clean. |
| Arcade | The adapter derives local preview height and RC intent; MSBuild stamps its inputs. | Preview intent uses the CI run number; RC and stable stay short. |
| semantic-release | Native analysis selects the stable base; the adapter supplies local preview height and RC intent. | Preview intent uses the CI run number; RC and stable stay short. |
The initialization version also differs: native MinVer returns 1.0.0-preview, while the other scenario fixtures start at 1.0.0-preview.0. That difference is documented rather than hidden behind an assertion that the native CLI cannot satisfy.
If your application needs the same Microsoft-style artifact format across all tools, add a consistent formatting step after base-version calculation. Preserve the stage ordinal, append the date and workflow revision only for published previews and RCs, and leave stable versions unsuffixed. That is an extension to these alternative samples, not their current behavior. The first article remains the reference for carrying the selected artifact version into container and runtime metadata.
The same release-helper interface
The first article uses a helper to make reviewed version changes explicit. Each alternative sample exposes the same commands: prepare-rc 1.0.0, prepare-stable 1.0.0, prepare-train 1.1.0, and tag. Run them from the selected sample directory. Preparation edits files; it does not commit, merge, or push. Tagging creates a local tag that you push separately after validation.
| Sample helper | What preparation changes | How tag selects a release |
|---|---|---|
| GitVersion helper | next-version in GitVersion configuration and the version/phase in release intent. | Next RC tag ordinal, or the committed stable core. |
| MinVer helper | Minimum major/minor, target release version, and phase in version intent. | Next RC tag ordinal, or the committed stable core. |
| NBGV helper | Preview/RC height template or stable version; removes the bootstrap offset for a new train or stable. | Calls nbgv tag using the calculated version. |
| Arcade helper | Version prefix and release phase in MSBuild properties. | Next RC tag ordinal, or the committed stable core. |
| semantic-release helper | Target release version and phase in release intent. | Next RC tag ordinal, or the committed stable core. |
For example, GitVersion preparation updates both its native base-version setting and the adapter's phase:
Source: gitversion-versioning-sample/release-version.sh
prepare() {
set_next_version "$2"
set_value "$intent" GITVERSION_RELEASE_VERSION "$2"
set_value "$intent" GITVERSION_RELEASE_PHASE "$1"
}Those lines record the target version and phase in the same pull request. Native GitVersion supplies preview calculation, while the adapter needs the phase to distinguish an RC fix from ordinary preview development. Review both changes before merging.
The protected-branch flow remains the one established in the first article:
git switch main
git pull --ff-only
git switch -c chore/prepare-1.0.0-rc
./release-version.sh prepare-rc 1.0.0
git add -A
git commit -m "chore: prepare 1.0.0 RC train"
git push -u origin chore/prepare-1.0.0-rc
# Open and squash-merge the PR; wait for validation.
git switch main
git pull --ff-only
./release-version.sh tag
git push origin v1.0.0-rc.1
# Merge an RC fix through a PR, update main, and validate it.
./release-version.sh tag
git push origin v1.0.0-rc.2
# Prepare stable on a new branch; review and squash-merge before tagging.
git switch -c chore/prepare-1.0.0
./release-version.sh prepare-stable 1.0.0The preparation commands belong on short-lived branches; tag belongs on the approved merged commit. The RC fix needs no second prepare-rc call. Stable requires a separate intent change, so do not skip preparation and tag the RC2 commit as stable. After stable publication, prepare 1.1.0 on another branch to start the next feature train.
The counters are not interchangeable. NBGV advances the RC height for each accepted fix even without an intervening tag. The GitVersion, Arcade, and semantic-release adapters select the next ordinal from reachable RC tags, and their helpers select the next existing RC tag ordinal. Until RC2 is tagged, multiple fixes can therefore share the prospective rc.2 version. MinVer keeps its native intermediate version, such as rc.1.1, until an explicit RC2 tag exists. The tests verify the one-fix-between-tags scenario; they do not prove that all tools have NBGV's per-commit RC numbering.
GitVersion
GitVersion derives a version by interpreting the Git graph. Its official GitHubFlow/v1 template uses Continuous Delivery, patch increments on main, labels for other branch classes, tagged-commit discovery, and optional commit-message increments. This makes it the strongest choice here when branch topology and merge history are part of the version policy.
GitVersion setup
- Add
GitVersion.ymlat the repository root. - Select
GitHubFlow/v1and label untaggedmainbuilds aspreview. - Fetch complete history and tags in CI.
- Install the pinned
GitVersion.ToolCLI and exposeSemVeras a job output. - Pass the output into
dotnet publishwith-p:Version.
Official guides: configuration reference, GitHub Flow examples, and version increments.
The important settings map to this strategy as follows:
| Setting | Why this sample sets it |
|---|---|
workflow: GitHubFlow/v1 | Uses main plus short-lived branches without requiring a permanent release branch. |
mode: ContinuousDelivery | Gives every untagged accepted commit a prerelease version that CI can build safely. |
next-version: 1.0.0 | Establishes the first development line before any stable version tag exists. |
main.label: preview | Produces the -preview.N suffix on untagged main commits. |
main.increment: Patch | Controls GitVersion's default increment; the shared scenario overrides the next train explicitly with next-version: 1.1.0. |
tag-prefix: "[vV]?" | Lets GitVersion parse both 1.0.0 and repository tags prefixed with v. |
commit-message-incrementing: Enabled | Allows explicit +semver: directives to alter the next core version; disable it if only configuration may do that. |
Source: gitversion-versioning-sample/GitVersion.yml
workflow: GitHubFlow/v1
mode: ContinuousDelivery
next-version: 1.0.0
tag-prefix: '[vV]?'
semantic-version-format: Strict
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
commit-message-incrementing: Enabled
branches:
main:
label: preview
increment: Patch
is-main-branch: trueThe highlighted settings tell GitVersion to use GitHub Flow, accept tags such as v1.0.0-rc.1, and emit preview versions for untagged commits on main. The workflow reads explicit release versions from GITHUB_REF_NAME because GitVersion's continuous-delivery calculation can otherwise keep its preview label when an rc tag points at a later commit.
Source: .github/workflows/gitversion.yml
- name: Install GitVersion
run: dotnet tool install --tool-path "$RUNNER_TEMP/gitversion" GitVersion.Tool --version 6.8.2
- name: Calculate version
id: version
shell: bash
env:
GITVERSION_CLI: ${{ runner.temp }}/gitversion/dotnet-gitversion
run: |
./scripts/calculate-version.sh github-output | tee -a "$GITHUB_OUTPUT"The CLI owns preview calculation. The wrapper honors committed RC or stable intent on untagged preparation commits and treats an explicit release tag as the release identity. During RC intent, it selects the next ordinal from reachable RC tags, so a fix after RC1 validates as rc.2, not preview.4. This is adapter policy, not a claim that native GitVersion calculates NBGV's RC sequence. The .NET SDK receives the resulting value through -p:Version.
GitVersion's preview number is based on Git height, not only on the number of merged pull requests. If next-version: 1.0.0 is used without a version baseline tag, the initialization commit contributes to that height: the first merged pull request can therefore produce 1.0.0-preview.2, followed by 1.0.0-preview.3. To make the first merged pull request produce 1.0.0-preview.1, establish the initialization commit as v1.0.0-preview.0. The scenario test performs this exact setup with repository.Tag("v1.0.0-preview.0") immediately after its initialization commit and before either feature merge.
Create this baseline tag once, immediately after the initialization commit and before merging feature pull requests:
git tag -a v1.0.0-preview.0 -m "1.0.0 preview baseline"
git push origin v1.0.0-preview.0This tag establishes GitVersion's starting point; it is not a published preview release. The first feature merge can then produce 1.0.0-preview.1, followed by 1.0.0-preview.2.
The baseline is specific to the GitVersion sample. Subsequent previews remain untagged; only the one-time baseline, RCs, and stable release have Git tags.
How GitVersion handles the five phases
- Untagged
maincommits with preview intent receive calculatedpreviewversions from Git history. - RC intent keeps preparation and fixes in the RC phase; stable intent produces the stable core for validation before tagging.
- The workflow also preserves
v1.0.0-rc.1andv1.0.0-rc.2instead of asking GitVersion to reinterpret those tags. v1.0.0removes the prerelease label and becomes stable.- Full history is mandatory because GitVersion evaluates version sources and the commit graph.
GitVersion is a good fit when branch context matters. It is also the most configuration-heavy option in this set. A team should agree on its branch model before using its calculated versions as public package versions.
Sample files: GitVersion.csproj, GitVersion.yml, and scenario tests.
MinVer
MinVer follows a smaller rule: release versions come from Git tags. Its documentation calls this “tag first.” On an exactly tagged commit, the tag is the version. On an untagged commit, MinVer derives an interim prerelease from the nearest version tag and can append Git height.
MinVer setup
- Install the pinned
minver-clitool in CI. - Pass
--tag-prefix v,--minimum-major-minor 1.0, and--default-pre-release-identifiers preview. - Keep Git height enabled so successive untagged commits are unique; at initial height zero MinVer emits
1.0.0-preview, then appends.1,.2, and later heights. - Calculate once, then pass the result to
dotnet publishwith-p:Version.
Official guides: MinVer options and MinVer CLI reference.
The CLI exposes the controls needed by this strategy:
| CLI option | Purpose in this strategy |
|---|---|
--tag-prefix v / -t v | Recognizes RC and stable tags such as v1.0.0-rc.1 and v1.0.0. |
--minimum-major-minor 1.0 | Keeps builds before the first release on the intended 1.0 line instead of 0.0. |
--default-pre-release-identifiers preview | Names untagged builds preview rather than MinVer's default prerelease identifier. |
Omit --ignore-height | Makes successive merges unique and ordered. |
These CLI options make the tag vocabulary match the release contract. Git height provides both uniqueness across merged commits and the numbered preview suffix used after a stable tag.
Source: .github/workflows/minver.yml
- name: Install MinVer CLI
run: dotnet tool install --tool-path "$RUNNER_TEMP/minver" minver-cli --version 8.0.0
- name: Calculate version
id: version
shell: bash
env:
MINVER_CLI: ${{ runner.temp }}/minver/minver
run: |
./scripts/calculate-version.sh github-output | tee -a "$GITHUB_OUTPUT"How MinVer handles the five phases
- Each explicit tag is used exactly on its tagged commit.
- Before the first tag, untagged merges produce
previewversions with increasing Git height. Afterv1.0.0-rc.1, MinVer produces an internal build such as1.0.0-rc.1.1until the next explicit tag. - The stable tag
v1.0.0produces1.0.0. - A later untagged commit defaults to the next patch prerelease line.
- Shallow history can hide the nearest tag, so use
fetch-depth: 0.
MinVer does not need a release branch. That matches GitHub Flow well, because the tag, rather than a branch name, is the release decision. Its smaller rule set is an advantage when a team wants predictable tag behavior and little automation magic.
Sample files: MinVer.csproj, MinVer README, and scenario tests.
Nerdbank.GitVersioning
Nerdbank.GitVersioning, usually called NBGV, starts with a committed version.json. Unlike MinVer, it does not require tags to calculate a unique build identity. It combines an author-controlled base version with Git height and commit information, which makes local and CI builds reproducible from the same commit.
NBGV setup
- Commit
version.jsonwith the initial development line, here1.0.0-preview.{height}. - Declare release refs through
publicReleaseRefSpec. - Install the pinned
nbgvCLI in CI. - Run the shared
calculate-version.shpolicy adapter for every main or release-tag build. - Let the adapter validate that a release tag matches NBGV's calculated base version.
- Pass the adapter's selected version and metadata to
dotnet publishwith-p:Versionand-p:InformationalVersion.
Official guides: version.json reference, versioning workflow, and cloud-build integration.
NBGV deliberately keeps more policy in source control than MinVer:
| Setting | Purpose in this strategy |
|---|---|
version: 1.0.0-preview.{height} | Declares the initial development line and exposes Git height. |
versionHeightOffset: -1 | Reserves preview.0 for validation of the initialization commit. |
versionHeightOffsetAppliesTo | Limits the bootstrap offset to the initial preview train. |
publicReleaseRefSpec | Identifies supported vMAJOR.MINOR.PATCH and -rc.N release refs. |
calculate-version.sh | Converts NBGV output into the shared CI version and rejects mismatched release tags. |
After stable, the next preview train is an explicit version-intent change. A release-preparation branch runs release-version.sh prepare-train 1.1.0, which changes version.json to 1.1.0-preview.{height} and removes the initial bootstrap offset. This keeps the release decision in reviewed source control instead of silently inferring a new patch line from the nearest stable tag.
Source: nerdbank-versioning-sample/version.json
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/Nerdbank.GitVersioning.Schema/version.schema.json",
"version": "1.0.0-preview.{height}",
"versionHeightOffset": -1,
"versionHeightOffsetAppliesTo": "1.0.0-preview.{height}",
"publicReleaseRefSpec": ["^refs/tags/v\\d+\\.\\d+\\.\\d+(?:-rc\\.\\d+)?$"]
}The committed base makes the CI calculation deterministic. NBGV's official workflow also supports nbgv set-version and nbgv tag; this sample uses the local release-version.sh helper for those release-intent changes. The sample does not add the NBGV MSBuild package because CI supplies the final version to the .NET SDK.
Source: .github/workflows/nerdbank.yml
nbgv="$RUNNER_TEMP/nbgv/nbgv"
dotnet tool install --tool-path "$RUNNER_TEMP/nbgv" nbgv --version 3.10.94
bash scripts/calculate-version.sh . "$nbgv" github-output | tee -a "$GITHUB_OUTPUT"The script has two modes. With the default mode it prints one base version for local validation and tests. With github-output, it emits version, assembly_version, informational_version, environment, and commit lines for the workflow. It calculates NBGV's SemVer2 value, removes only the non-public .g<commit> suffix, and checks any release tag at HEAD against that base version. A mismatched tag fails the job rather than silently changing the artifact version.
For an untagged main build, preview.0 selects validation only. Later previews receive the NBGV preview ordinal plus the UTC YYDDD.RUN_NUMBER suffix and target dev. A matching RC tag receives the same date and run suffix and targets staging; a matching stable tag remains the clean NBGV version and targets production.
How NBGV handles the five phases
- The initialization commit calculates
1.0.0-preview.0and is validation-only. - Ordinary feature merges calculate
1.0.0-preview.1,1.0.0-preview.2, and so on from Git height. - A release-intent merge changes
version.jsonto1.0.0-rc.{height}; the first RC merge calculates1.0.0-rc.1. - Matching RC and stable tags approve already-calculated versions; they do not override an unrelated NBGV result.
- Stable promotion changes version intent to
1.0.0; the next preview train requires another explicit base-version change.
NBGV is useful when every commit needs a reproducible identity and release intent belongs in source control. It is different from MinVer: MinVer is deliberately tag-first, while NBGV can calculate versions without waiting for a tag.
Sample files: version.json, NBGV project, and scenario tests.
Arcade
Arcade is shared .NET engineering infrastructure rather than a small standalone tag calculator. It provides common build targets, official-build concepts, dependency flow, and conventions used by many dotnet repositories. The Arcade SDK defaults to SemVer 2 output, but it does not decide our five-stage policy by itself.
Arcade setup
- Add
Microsoft.DotNet.Arcade.Sdk. - Configure the Microsoft
dotnet-engNuGet feed. - Put shared version properties in
Directory.Build.props. - Give local builds a valid fallback such as
1.0.0-preview.0. - Calculate
1.0.0-preview.${GITHUB_RUN_NUMBER}on untaggedmainbuilds with preview intent; keep RC and stable intent in their selected phase. - Use explicit tags only for RC and stable events.
- Set
OfficialBuild=trueonly for selected tagged builds.
Official guides: Arcade SDK, especially its eng/Versions.props, repository layout, and official-build sections.
Arcade provides the build infrastructure, while this sample supplies the release policy:
| Property or input | Purpose in this strategy |
|---|---|
VersionPrefix | Holds the current stable core, initially 1.0.0; prepare-train 1.1.0 advances the shared feature train. |
GITHUB_RUN_NUMBER | Supplies a monotonically increasing preview suffix for accepted main builds. |
Version | Carries the complete SemVer passed to the SDK and application build. |
PackageVersion | Keeps package identity synchronized with Version. |
OfficialBuild | Distinguishes tagged RC/stable artifacts from ordinary local or preview builds. |
Full Arcade repositories usually keep shared versions in eng/Versions.props and invoke common eng build scripts. This compact sample uses Directory.Build.props so the versioning behavior remains visible without reproducing the entire Arcade repository layout.
Source: arcade-versioning-sample/Directory.Build.props
<PropertyGroup>
<VersionPrefix Condition="'$(VersionPrefix)' == ''">1.0.0</VersionPrefix>
<ReleasePhase Condition="'$(ReleasePhase)' == ''">preview</ReleasePhase>
<ReleaseCandidateNumber Condition="'$(ReleaseCandidateNumber)' == ''">1</ReleaseCandidateNumber>
<Version Condition="'$(Version)' == '' and '$(ReleasePhase)' == 'rc'">$(VersionPrefix)-rc.$(ReleaseCandidateNumber)</Version>
<Version Condition="'$(Version)' == '' and '$(ReleasePhase)' == 'stable'">$(VersionPrefix)</Version>
<Version Condition="'$(Version)' == '' and '$(GITHUB_RUN_NUMBER)' != ''">$(VersionPrefix)-preview.$(GITHUB_RUN_NUMBER)</Version>
<Version Condition="'$(Version)' == ''">$(VersionPrefix)-preview.0</Version>
<PackageVersion Condition="'$(PackageVersion)' == ''">$(Version)</PackageVersion>
<OfficialBuild Condition="'$(OfficialBuild)' == '' and '$(GITHUB_REF_TYPE)' == 'tag'">true</OfficialBuild>
</PropertyGroup>The phase-specific properties prevent RC and stable preparation builds from falling back to preview. The shell adapter derives local preview ordinals from Git history when no CI run number is supplied, and calculates the next RC ordinal from reachable tags. Arcade stamps the supplied metadata; it is not the Git-history calculator.
Source: .github/workflows/arcade.yml
- name: Calculate version
id: version
shell: bash
run: |
./scripts/calculate-version.sh github-output | tee -a "$GITHUB_OUTPUT"How Arcade handles the five phases
- Arcade stamps the version supplied through MSBuild properties.
- The workflow, not Arcade, maps untagged builds to numbered previews.
- Untagged preview-intent builds provide preview values; RC preparation and fixes remain in the RC phase until stable intent is committed. Tags authorize publication.
OfficialBuildseparates selected release artifacts from local builds.- Stable publication removes the prerelease suffix by supplying
1.0.0.
Arcade is the right choice when a repository wants to follow .NET engineering infrastructure and can accept the additional feed and build-system setup. It is not the best first choice when the only need is a version from a tag.
Sample files: Arcade project, Directory.Build.props, and scenario tests.
semantic-release
semantic-release is a Node.js release orchestrator. It analyzes Conventional Commits, chooses a SemVer increment, generates notes, creates tags, and can publish through plugins. It is not a .NET MSBuild version provider.
The sample places a v0.0.0 bootstrap tag on the initialization commit. Its shell calculator recognizes this as a history anchor rather than a real release and emits the committed first-train intent, 1.0.0-preview.0.
Its default release mapping is consumer-impact based: fix produces a patch, feat produces a minor, and BREAKING CHANGE produces a major. Official semantic-release prerelease support normally uses dedicated prerelease branches such as beta. Our no-release-branch policy uses semantic-release to calculate the next stable base on main, then CI appends the preview sequence or accepts an explicit tag. That adaptation should be understood rather than hidden.
semantic-release setup
- Add
semantic-releaseand the commit analyzer plugins topackage.json. - Configure
branches: ['main']andtagFormat: 'v${version}'. - Enforce Conventional Commit pull-request titles or commit messages.
- Call the JavaScript API with
dryRun: trueto retrievenextRelease.version. - Append
-preview.${GITHUB_RUN_NUMBER}for preview-intent main builds; let the adapter honor RC and stable intent separately. - Use explicit SemVer tags only for RC and stable events.
- Pass the final value into
dotnet publish. - Keep Release Drafter as the human-reviewed GitHub release-note source.
Official guides: configuration, workflow configuration, and JavaScript API.
Source: semantic-release-versioning-sample/release.config.cjs
module.exports = {
branches: ['main'],
tagFormat: 'v${version}',
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/exec',
{
publishCmd:
'dotnet publish src/semantic-release-versioning-sample.csproj --configuration Release --output artifacts/${nextRelease.version} -p:Version=${nextRelease.version}',
},
],
'@semantic-release/github',
],
}The configuration has separate responsibilities:
| Setting or plugin | Purpose in this strategy |
|---|---|
branches: ["main"] | Restricts version analysis to the protected release branch. |
tagFormat: "v${version}" | Makes existing RC/stable tags discoverable in the same format used by the other samples. |
@semantic-release/commit-analyzer | Maps Conventional Commits to the next stable core version. |
@semantic-release/release-notes-generator | Calculates notes during dry run; Release Drafter remains the reviewed GitHub release-note source. |
@semantic-release/exec | Demonstrates how semantic-release can pass ${nextRelease.version} into dotnet publish. |
@semantic-release/github | Supplies GitHub integration when semantic-release owns publication; our workflow keeps final control. |
Source: semantic-release-versioning-sample/scripts/calculate-version.mjs
import semanticRelease from 'semantic-release'
const result = await semanticRelease(
{
ci: false,
dryRun: true,
},
{
cwd: process.cwd(),
env: process.env,
stderr: process.stderr,
stdout: process.stderr,
}
)
if (result?.nextRelease?.version) {
process.stdout.write(result.nextRelease.version)
}Source: .github/workflows/semantic-release.yml
- name: Calculate version
id: version
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./scripts/calculate-version.sh github-output | tee -a "$GITHUB_OUTPUT"How semantic-release handles the five phases
- Conventional Commits determine the next stable base, such as
1.0.0. - Preview intent creates numbered preview builds; RC intent keeps preparation and fixes in the RC phase before tagging.
- Preview numbers come from CI runs; explicit
rc.1andrc.2tags are preserved. - The stable tag supplies
1.0.0directly to the .NET build. @semantic-release/execcan rundotnet publish, but the workflow shown here keeps build/publish steps visible in GitHub Actions.
This division avoids a false promise that semantic-release understands .csproj metadata. The sample uses semantic-release for orchestration and the .NET CLI for the actual application build.
Local scenario tests supply no run numbers: the shell adapter derives preview ordinals from Git history since the release-intent change. CI can still supply GITHUB_RUN_NUMBER for preview artifacts. RC fixes use the next reachable RC-tag ordinal, so the local sequence remains rc.1, rc.2, stable, then the new preview train.
Sample files: package.json and release.config.cjs.
Release Drafter keeps the same responsibility
Release Drafter keeps the same responsibility described in the previous article: previews remain drafts, RC tags publish prereleases, and stable tags publish the latest release. Each workflow supplies its calculated version to the same action inputs, so Release Drafter never recalculates the artifact version.
The workflows share release-drafter.yml and the same action inputs, but their publish-job conditions are not identical. The GitVersion workflow skips untagged RC/stable intent because those commits are validation-only. The MinVer workflow accepts any untagged prerelease for draft artifacts, including an intermediate 1.0.0-rc.1.1. That draft is not a public staging release: public RC/stable publication still requires a tag.
If you require the first article's exact rule that every untagged RC-preparation or RC-fix commit is validation-only, narrow MinVer's publish condition before adopting it. Likewise, do not assume every sample excludes its initial preview from draft publication just because NBGV excludes preview.0. The first article's deployment approvals and container publication must be implemented separately; a release name or an environment output is not a deployment.
Moving from NBGV to another tool
Keep the release operator's workflow stable while replacing the version provider:
- Commit the chosen tool's version intent and configure the initial baseline. GitVersion's preview baseline and semantic-release's bootstrap anchor are sample-specific; they are not public releases.
- Run both the helper-driven and direct-input scenarios. Inspect calculated versions and tags separately, including the RC fix and the first three previews of the next train.
- Preserve the CI output contract:
version,assembly_version,informational_version,environment, andcommit. Decide explicitly whether artifact formatting stays short or retains the first article's date/revision suffix. - Review publication conditions. Confirm which main-branch commits create drafts, which are validation-only, and which tags publish RC or stable artifacts. Reject unsupported and mismatched tags before publishing.
- Carry the selected metadata into your container and runtime implementation from the first article. Keep production approval and deployment wiring separate from calculation and GitHub release publication.
Switch at the start of a reviewed release train, and retain immutable tags and already-published artifact versions. Recalculating an old commit with a different tool can produce a different version; it does not justify replacing the release that consumers already received.
Which tool should you choose?
| Tool | Primary source of truth | Best fit | Main tradeoff |
|---|---|---|---|
| GitVersion | Git graph, branch rules, tags | Branch-aware repositories | Most policy and configuration |
| MinVer | Version tags | Simple tag-driven libraries/apps | Untagged build uniqueness needs care |
| NBGV | version.json plus commit identity | Reproducible version per commit | Release promotion differs from pure tag-first flow |
| Arcade | MSBuild/repository engineering properties | Repositories adopting dotnet engineering infrastructure | Much broader than version calculation |
| semantic-release | Conventional Commits and release history | Multi-ecosystem automated releases | Node orchestration, not native MSBuild versioning |
For a small .NET application using GitHub Flow, MinVer or NBGV is usually the simplest starting point. The correct choice depends on whether tags or every commit is the primary source of truth.
Validation checklist
Each sample has a repository fixture that creates a temporary Git repository and exercises the release process, rather than calculating expected versions from the current checkout. The scenario suite has three responsibilities:
Release_helper_commands_follow_release_scenarioprepares intent and creates tags through the helper.- A direct-tool/input scenario prepares the same history without using the release helper, then verifies the tool or adapter appropriate to that sample.
Mismatched_manual_tag_is_rejectedverifies that an unrelated tag such asv9.9.9cannot silently replace the intended release version.
The complete local sequence includes the next train, not only the stable release:
initialize -> 1.0.0-preview.0
feature merge -> 1.0.0-preview.1
feature merge -> 1.0.0-preview.2
prepare RC intent and tag -> 1.0.0-rc.1
merge RC fix and tag -> 1.0.0-rc.2
prepare stable intent and tag -> 1.0.0
prepare next train -> 1.1.0-preview.1
feature merge -> 1.1.0-preview.2
feature merge -> 1.1.0-preview.3MinVer's initialization and untagged RC-fix versions are the native exceptions described earlier. Scenario assertions supply no run numbers. They check the calculated version separately from the tag at HEAD, and compare native output where the native tool owns that calculation. For GitVersion's RC policy, Arcade's Git-history adapter, and semantic-release's prerelease formatting, the wrapper owns behavior that the native tool does not provide. A passing wrapper assertion is not evidence of native-tool equivalence.
Run the complete suite from the sample repository root:
dotnet test microsoft-style-dotnet-versioning-samples.slnx --configuration ReleaseThese tests cover local version calculation, preparation commands, and tag selection. They do not exercise the dated CI artifact suffix, GitHub permissions, Release Drafter publication, artifact upload, or deployment approvals. Verify those separately in the real workflow before adopting the samples for production.
Tool references
- GitVersion GitHub Flow
- MinVer
- Nerdbank.GitVersioning workflow
- Arcade SDK
- semantic-release workflow configuration
Conclusion
The release policy does not require one specific tool. GitVersion and MinVer use Git history directly, NBGV combines committed intent with Git identity, Arcade relies on MSBuild and CI inputs, and semantic-release uses commit semantics. Keeping the contract fixed makes those differences visible and lets a team choose a tool without redesigning its release process.


