Skip to content

Implement native versioning (replace Nerdbank.GitVersioning) #229

Description

@rdeago

Reference version

1.0.204-preview

Background and motivation

Buildvana currently uses Nerdbank.GitVersioning (NBGV) for version-number management. It is wired in at two points:

  • In the MSBuild SDK, the NerdbankGitVersioning module (src/Buildvana.Sdk/Modules/NerdbankGitVersioning/) auto-detects version.json in a consumer repo and injects a Nerdbank.GitVersioning package reference so that NBGV's MSBuild task populates $(Version), $(AssemblyVersion), $(FileVersion), and $(AssemblyInformationalVersion) during build.
  • In the bv tool, src/Buildvana.Tool/Services/Versioning/VersionService.cs shells out to the nbgv CLI (declared in .config/dotnet-tools.json) via dotnet tool run nbgv get-version --format json, parses the JSON, and exposes CurrentStr, IsPublicRelease, and IsPrerelease to the release workflow.

This arrangement has several costs:

  • Consumer overhead. Every repo that uses Buildvana must keep NBGV installed as a local or global tool and keep its version in sync. A repo that cannot install a dotnet tool in CI cannot use Buildvana's versioning.
  • Feature mismatch. Buildvana uses a small subset of NBGV's capabilities: 3-part versions, nuGetPackageVersion.semVer = 2.0, a simple publicReleaseRefSpec, firstUnstableTag. pathFilters is empty, assemblyVersion.precision is fixed. We pay for a full dependency to exercise ~20% of its surface.
  • Subprocess + JSON at runtime. bv launches nbgv as a subprocess every time it needs the current version, parses the JSON output, and extracts a handful of properties. This is slower than a direct call, more failure-prone, and couples the tool to a third-party CLI contract.
  • No standalone version management. There is no Buildvana-native way to inspect or advance the current version outside of the release workflow. Operations that modify version.json (preview→stable, bump minor) are embedded inside ReleaseTask and cannot be invoked independently.

Proposed enhancement

Replace NBGV end-to-end with an in-house implementation. The end state:

  • A new shared library, Buildvana.Versioning, contains all version-related pure logic: parsing version.json, constructing SemVer2 strings from (VersionSpec, height, isPublicRelease), matching branches against publicReleaseRefSpec patterns. It has no git and no MSBuild dependencies — just NuGet.Versioning for SemanticVersion.
  • Buildvana.Sdk.Tasks gains a new MSBuild task that shells out to the git CLI for height and current-branch detection, then calls Buildvana.Versioning to produce the version string. The task sets $(Version), $(AssemblyVersion), $(FileVersion), $(AssemblyInformationalVersion); the .NET SDK's built-in GenerateAssemblyInfo target emits the corresponding [assembly: ...] attributes as before — no custom attribute generation required.
  • The SDK's NerdbankGitVersioning module is replaced by a Versioning module that wires the new task into the same GetBuildVersion extension point. Auto-detection of version.json in the repo tree is preserved.
  • Buildvana.Tool no longer shells out to nbgv. VersionService uses Buildvana.Versioning for pure computation and GitService (LibGit2Sharp, already present) for height and branch data. The nbgv entry is removed from .config/dotnet-tools.json.
  • A new top-level command bv version exposes version operations directly: bv version show prints current version info; bv version advance <major|minor|stable|unstable> applies a VersionSpecChange to version.json. The underlying logic already exists in VersionFile.ApplyVersionSpecChange and VersionService.ComputeVersionSpecChange; the command just wires it to the CLI.
  • Version semantics match NBGV's observable behavior for the feature set Buildvana actually uses:
    • Height = number of commits from HEAD back to, but not including, the last commit that modified version.json.
    • IsPublicRelease = HEAD's canonical ref matches any publicReleaseRefSpec regex.
    • Output format is the SemVer2 NpmPackageVersion equivalent (no git-commit-hash build metadata).
  • pathFilters is not supported — Buildvana has never used it. A non-empty pathFilters in a consumer's version.json is ignored (with a build warning).
  • version.json schema remains readable as-is. Consumers upgrading the SDK do not need to change their version file.

Implementation proposals

Four sequenced phases, each shippable on its own.

Phase 0 — new shared library.

  • Create src/Buildvana.Versioning/Buildvana.Versioning.csproj targeting $(StandardTfm).
  • Move VersionSpec verbatim from src/Buildvana.Tool/Services/Versioning/ to the new project.
  • Add VersionFileData — an immutable record holding parsed version.json contents (version spec, first-unstable tag, public-release ref patterns). Built from a JsonDocument, no Cake or MSBuild dependencies.
  • Add VersionCalculator with a pure method approximately:
    public static VersionInfo Compute(
        VersionFileData fileData,
        int height,
        string canonicalBranchRef);
    where VersionInfo carries the SemanticVersion, CurrentStr, IsPublicRelease, IsPrerelease.
  • Add a SemanticVersion-based public API. NuGet.Versioning was reviewed: SemanticVersion at v10.0.201 is strictly SemVer 2.0 (3-part only, full metadata support, spec-correct comparison); NuGet-specific quirks live in NuGetVersion, which we do not use.
  • Reference from Buildvana.Tool. VersionFile delegates parsing to VersionFileData; its Save/ApplyVersionSpecChange methods stay in the tool (Cake-dependent I/O).

Phase 1 — in-tool replacement of the nbgv subprocess.

  • GitService gains methods to compute height (via LibGit2Sharp: find the last commit that modified version.json, count commits on the path from HEAD back to it, exclusive) and to return HEAD's canonical ref (already available internally).
  • VersionService.GetVersionInformationFromNbgv is rewritten as GetVersionInformationFromGit, calling GitService + Buildvana.Versioning.VersionCalculator. The nbgv invocation is deleted.
  • .config/dotnet-tools.json drops the nbgv entry.
  • At this point bv release works without NBGV, but the SDK still uses NBGV for build-time versioning.

Phase 2 — MSBuild task and SDK module.

  • Buildvana.Sdk.Tasks references Buildvana.Versioning.
  • New task, e.g. Buildvana.Sdk.Tasks.Versioning.GetBuildVersion: takes version-file path and repo root as inputs; shells out to git via System.Diagnostics.Process for height (git rev-list -n 1 HEAD -- <version-file> for the base SHA, git rev-list --count <sha>..HEAD for the height) and for branch (git symbolic-ref --short HEAD, fallback to detached-HEAD handling); calls VersionCalculator; returns version components as task outputs.
  • New SDK module: src/Buildvana.Sdk/Modules/Versioning/. BeforeModules.targets performs the same version.json auto-detection as today. Module.targets defines a GetBuildVersion target that invokes the new task and sets $(Version), $(AssemblyVersion), $(FileVersion), $(AssemblyInformationalVersion). The existing BV_AdjustAssemblyInformationalVersion logic folds into the task's outputs.
  • The old NerdbankGitVersioning module is removed. Decision to resolve during review: keep the UseNerdbankGitVersioning property name for consumer compatibility (mildly misleading), rename to UseVersioning with a deprecated alias, or rename outright. Auto-detection from version.json presence makes the property rarely set explicitly, which limits the blast radius.

Phase 3 — bv version command.

  • Add bv version show and bv version advance <major|minor|stable|unstable>.
    • show prints: current version, latest version, latest stable, IsPublicRelease, IsPrerelease, current branch.
    • advance loads VersionFile, applies ApplyVersionSpecChange, saves, and optionally commits (gated by a flag).
  • CLI shape depends on the state of the Cake-removal proposal. If this lands first (still on Cake.Frosting), use flat task names (version-show, version-advance) since the current parser does not support subcommands; post-Cake, use Spectre.Console.Cli subcommand syntax (bv version show). Either way, the underlying logic is identical and the rename is cosmetic.

Self-hosting: each phase must leave dotnet bv build functional on the repo. Buildvana builds itself with the last published bv, so CI is insulated during the transition; the risk is local-dev only.

Usage examples

Consumer version.json is unchanged:

{
  "version": "1.0-preview",
  "publicReleaseRefSpec": [
    "^refs/heads/main$",
    "^refs/heads/v\\d+\\.\\d+$"
  ],
  "release": {
    "firstUnstableTag": "preview"
  }
}

Consumer MSBuild integration is unchanged — the SDK auto-detects version.json and wires versioning on:

<Project Sdk="Buildvana.Sdk/1.x.y">
  <!-- nothing version-specific to declare; auto-detected from version.json -->
</Project>

New CLI operations exposed by bv:

dotnet bv version show
# current:        1.0.0-preview.42
# latest:         1.0.0-preview.41
# latest stable:  0.9.3
# public release: true
# prerelease:     true
# branch:         main

dotnet bv version advance minor
# version.json: 1.0-preview -> 1.1-preview

dotnet bv version advance stable
# version.json: 1.1-preview -> 1.1

Build-time behavior is identical to today: dotnet build, direct msbuild, and dotnet bv build all produce the same $(Version), $(AssemblyVersion), $(FileVersion), $(AssemblyInformationalVersion) as the current NBGV-based flow.

Risks

  • Version-semantics drift. The new height computation must yield the same number as NBGV for equivalent inputs. Off-by-one errors (inclusive/exclusive endpoints, boundary commits) would cause version jumps or stalls. Mitigation: a test suite that compares the new output against NBGV's get-version across a representative set of commits and branches before NBGV is removed.
  • publicReleaseRefSpec semantics. NBGV uses .NET's System.Text.RegularExpressions, same as us, so patterns should match identically. However, anchoring and implicit flags must be preserved exactly — this is a direct porting concern, not a redesign.
  • pathFilters drop. Any consumer with a non-empty pathFilters will see height diverge from NBGV. Buildvana does not use this feature; a build warning and documentation note will cover the discrepancy.
  • Git CLI availability. The new MSBuild task shells out to git. In virtually every environment where MSBuild runs (dev machine, CI agent, container), git is present — but a stripped-down image could lack it. Mitigation: emit a clear diagnostic error pointing at the missing tool.
  • Detached HEAD. git symbolic-ref --short HEAD fails on a detached HEAD. GitService already returns an empty string for CurrentBranch in that case; the new task must do the same and treat detached HEAD as non-public-release.
  • SDK property rename. If UseNerdbankGitVersioning is renamed, explicit uses in consumer projects break. Since the property is auto-set from version.json presence, explicit uses should be rare; a backward-compat alias is low-cost if needed.
  • Self-hosting during the transition. Each phase must leave dotnet bv build working locally; CI is insulated because it builds with the last published SDK/tool.
  • Dependency changes.
    • Added (internal only): Buildvana.Versioning project in the solution.
    • Removed from consumer surface: Nerdbank.GitVersioning package injection; nbgv from .config/dotnet-tools.json.
    • NuGet.Versioning stays (used in the new shared library; reviewed and confirmed strictly SemVer 2.0 compliant).
    • LibGit2Sharp stays (already used by GitService).
    • Net consumer footprint is smaller: one fewer package reference injected, one fewer dotnet tool to install.
  • No breaking changes for consumer version files. version.json as currently used by Buildvana is fully supported; only the NBGV-specific pathFilters feature is dropped (Buildvana consumers don't use it).

Additional information

  • The existing VersionSpec, VersionFile, VersionSpecChange, and VersionIncrement types in src/Buildvana.Tool/Services/Versioning/ already model most of what's needed. The new code is primarily the height-and-branch calculator, the MSBuild task shell, and the shared-library extraction.
  • NBGV is MIT-licensed. Where concrete implementation details are reproduced (the height algorithm for pathFilters: [], publicReleaseRefSpec matching semantics), source comments will cite the NBGV source and preserve the copyright notice.
  • This proposal is independent of the Cake.Frosting removal proposal. Either can ship first; the only coordination point is the CLI shape for bv version's subcommand syntax, which is a cosmetic difference between the two landing orders.
  • Follow-ups worth tracking separately once this lands: bv version tag (create a git tag from the current version), bv version check (expose the consistency checks currently embedded in ReleaseTask.EnsureConsistency), and a richer bv version show output with JSON/verbose modes.

Metadata

Metadata

Assignees

Labels

area:code[issue/PR] affects project code (excluding tests).area:deps[issue/PR] adds, updates, or removes dependencies.enhancement[issue/PR] requests / implements new or improved functionality.refactor[issue/PR] requests / implements a refactor of existing code without affecting functionality.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions