Documentation

adrs/052-semantic-versioning-strategy.md

ADR-052: Semantic Versioning Strategy with Nerdbank.GitVersioning

Status

Proposed

Context

As the Dynaplex platform matures toward production readiness, we need a formal versioning strategy that:

  1. Provides platform-level versioning: A unified version that represents the overall Acsis Dynaplex release (e.g., 9.0.0-alpha)
  2. Supports component-level versioning: Each component should track its own changes independently, so API consumers see version increments only when that specific API changes
  3. Auto-increments build numbers: Avoid manual version bumping for every commit
  4. Integrates with CI/CD: Works seamlessly with GitLab pipelines
  5. Exposes version to consumers: API responses should include version information for debugging and compatibility checking

Tool Evaluation

Tool Approach Pros Cons
Nerdbank.GitVersioning version.json + git height Monorepo support, path filters, ThisAssembly generation Medium complexity
MinVer Git tags only Simple, no config files No path filtering, tag-dependent
GitVersion Branch names + YAML config Feature-rich Complex, opinionated branching

Nerdbank.GitVersioning is the best fit because:

  • Supports hierarchical version.json with inheritance for monorepos
  • Path filters enable per-component version tracking
  • Generates ThisAssembly class for runtime version access
  • Well-maintained (.NET Foundation project)

Decision

We will use Nerdbank.GitVersioning with a dual-level versioning strategy:

  1. Platform Version: Root version.json defines the overall Acsis Dynaplex version
  2. Component Versions: Each component inherits the platform version but uses path filters so its git height only increments when that component changes
  3. Version Header: All API responses include an X-Acsis-Version header showing the component's version

Version Format

{major}.{minor}.{patch}-{prerelease}.{height}+{commit}

Examples:

  • 9.0.0-alpha.47+a1b2c3d4 - Alpha build, 47th commit to this component
  • 9.0.0-beta.12+e5f6g7h8 - Beta build
  • 9.0.0+i9j0k1l2 - Stable release

Starting Configuration

Setting Value
Platform Version 9.0-alpha
Pre-release Label alpha (graduating to beta, then stable)
Release Strategy Tag releases from main branch
Version Exposure X-Acsis-Version HTTP response header

Consequences

Positive

  • Accurate API versioning: Component versions only change when that component changes
  • Automatic build numbers: Git height eliminates manual version management
  • Platform coherence: All components share the same base version (9.0)
  • Runtime access: ThisAssembly class provides version info for logging, health checks
  • CI/CD integration: Works with GitLab pipelines out of the box
  • Debugging: Full version with commit hash aids troubleshooting

Negative

  • Multiple version.json files: Each component needs its own file (though they're simple)
  • Git depth requirement: CI/CD must fetch full history (GIT_DEPTH: 0)
  • Build dependency: Adds NuGet package to all projects
  • Learning curve: Team must understand path filters and inheritance

Neutral

  • Height resets: When base version changes (e.g., alpha → beta), heights reset to 1
  • Commit-based: Version changes on every commit (to that component), not on release

Implementation Notes

Package Setup

Directory.Packages.props (central package management):

<!-- Semantic versioning from git -->
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />

Directory.Build.props (applies to all projects):

<ItemGroup>
  <PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

Root version.json (Platform Version)

File: version.json (repository root)

{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "version": "9.0-alpha",
  "publicReleaseRefSpec": [
    "^refs/heads/main$",
    "^refs/tags/v\\d+\\.\\d+"
  ],
  "cloudBuild": {
    "buildNumber": {
      "enabled": true
    }
  },
  "release": {
    "tagName": "v{version}",
    "versionIncrement": "minor",
    "firstUnstableTag": "alpha"
  }
}

Component version.json (Per-Component)

Template (same for each component):

{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
  "inherit": true,
  "pathFilters": ["."]
}

Files to create:

  • engines/catalog/version.json
  • engines/identity/version.json
  • engines/prism/version.json
  • engines/spatial/version.json
  • engines/workflow/version.json
  • engines/transport/version.json
  • engines/iot/version.json
  • engines/events/version.json
  • engines/printing/version.json
  • engines/intelligence/version.json
  • engines/file-handling/version.json
  • engines/core-data/version.json
  • engines/system-environment/version.json
  • engines/bbu/version.json

How Path-Filtered Versioning Works

Each component's version.json with "pathFilters": ["."] means:

  • Inherits the base version 9.0-alpha from the root
  • Git height only counts commits that touch files in that component's directory
  • Different components will have different heights based on their change frequency
Initial state (after setup):
  - Catalog:  9.0.0-alpha.1
  - Identity: 9.0.0-alpha.1
  - Spatial:  9.0.0-alpha.1

After 5 commits touching only Catalog:
  - Catalog:  9.0.0-alpha.6
  - Identity: 9.0.0-alpha.1  (unchanged)
  - Spatial:  9.0.0-alpha.1  (unchanged)

After 3 commits touching Identity:
  - Catalog:  9.0.0-alpha.6
  - Identity: 9.0.0-alpha.4
  - Spatial:  9.0.0-alpha.1  (still unchanged)

GitLab CI/CD Configuration

File: .gitlab-ci.yml

variables:
  SECRET_DETECTION_ENABLED: 'true'
  GIT_DEPTH: 0  # Required for Nerdbank.GitVersioning git height calculation

Version Header Middleware

File: strata/service-defaults/src/Acsis.Dynaplex.Strata.ServiceDefaults/Extensions.cs

/// <summary>
/// Adds the X-Acsis-Version header to all HTTP responses.
/// Uses ThisAssembly.AssemblyInformationalVersion which reflects
/// the component's path-filtered version.
/// </summary>
public static IApplicationBuilder UseAcsisVersionHeader(this IApplicationBuilder app)
{
    return app.Use(async (context, next) =>
    {
        context.Response.OnStarting(() =>
        {
            context.Response.Headers["X-Acsis-Version"] = ThisAssembly.AssemblyInformationalVersion;
            return Task.CompletedTask;
        });
        await next();
    });
}

Register in MapAcsisEndpoints():

app.UseCors();
app.UseAcsisVersionHeader();  // Add version header to all responses
app.UseExceptionHandler();

Version in HTTP Responses

When calling /api/catalog/items:

HTTP/1.1 200 OK
X-Acsis-Version: 9.0.0-alpha.6+abc1234
Content-Type: application/json

When calling /api/identity/users:

HTTP/1.1 200 OK
X-Acsis-Version: 9.0.0-alpha.4+def5678
Content-Type: application/json

Runtime Version Access

Nerdbank.GitVersioning generates a ThisAssembly class:

// Available in any component
public class HealthService
{
    public HealthInfo GetHealth()
    {
        return new HealthInfo
        {
            Version = ThisAssembly.AssemblyInformationalVersion,
            // "9.0.0-alpha.47+a1b2c3d4"

            SemanticVersion = ThisAssembly.AssemblyFileVersion,
            // "9.0.0.47"

            GitCommitId = ThisAssembly.GitCommitId,
            // "a1b2c3d4e5f6..."
        };
    }
}

CLI Tool (Optional)

Install for local version management:

dotnet tool install -g nbgv

Commands:

# Show current version
nbgv get-version

# Show specific component's version
nbgv get-version --project engines/catalog

# Prepare a release (bumps version, optionally creates tag)
nbgv prepare-release

Version Lifecycle

Development Phase (Current)

// version.json
"version": "9.0-alpha"

Results in: 9.0.0-alpha.{height}

Beta Phase

// version.json
"version": "9.0-beta"

Results in: 9.0.0-beta.{height} (heights reset)

Stable Release

// version.json
"version": "9.0"

Results in: 9.0.0 for tagged commits, 9.0.0+{commit} for non-release builds

Next Development Cycle

// version.json
"version": "9.1-alpha"

Results in: 9.1.0-alpha.{height}

Best Practices

  1. Bump version deliberately: Only update root version.json when graduating phases (alpha → beta → stable)
  2. Don't edit component version.json: They should remain as simple inheritance files
  3. Tag releases: Use git tag v9.0.0 for official releases
  4. Full git history in CI: Always use GIT_DEPTH: 0 in pipelines
  5. Use version in logs: Include ThisAssembly.AssemblyInformationalVersion in startup logs
  6. Document breaking changes: When incrementing major version, document in release notes
  • ADR-007: Evolution to .NET Aspire Microservices (service architecture)
  • ADR-011: Flexible API Versioning Strategy (API path versioning)
  • ADR-019: OpenTelemetry Integration (version in telemetry)
  • ADR-021: Microsoft Kiota for API Client Generation (client versioning)

References