From 8748b6c502d883aff41c1c1c56df42243bfbe5fe Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:42:56 +0200 Subject: [PATCH 1/8] Suppress false-positive ClassCannotBeInstantiated inspection in JsonSourceMap ReSharper's solution-wide analysis inspects the JsonSourceMap.Frame.cs partial part in isolation and misses the instantiation in JsonSourceMap.cs, so it claims the class (which has a private constructor and a static factory method) cannot be instantiated. Co-Authored-By: Claude Fable 5 --- src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs b/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs index ac2605ed..973ffd31 100644 --- a/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs +++ b/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs @@ -3,6 +3,9 @@ namespace Buildvana.Core.JsonSchema; +// ReSharper disable once ClassCannotBeInstantiated -- false positive: ReSharper analyzes this partial part +// in isolation, missing the instantiation in JsonSourceMap.cs (the class has a private constructor and is +// created by its static Build method). partial class JsonSourceMap { // Tracks one open container while walking the document, so a value's pointer can be built from its parent. From 1aa26b7dd16368635393c591dfbb38feb4c506ec Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:43:06 +0200 Subject: [PATCH 2/8] Add Buildvana.Core.Testing, a shared library of test harnesses It starts with the TempGitRepo harness (moved from the versioning tests, where the SDK task tests would otherwise have needed a copy) and a TempHome harness for tests exercising file-based code that resolves paths against a home directory. Co-Authored-By: Claude Fable 5 --- Buildvana.slnx | 1 + .../Buildvana.Core.Testing.csproj | 17 +++ src/Buildvana.Core.Testing/TempGitRepo.cs | 115 ++++++++++++++++++ src/Buildvana.Core.Testing/TempHome.cs | 44 +++++++ 4 files changed, 177 insertions(+) create mode 100644 src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj create mode 100644 src/Buildvana.Core.Testing/TempGitRepo.cs create mode 100644 src/Buildvana.Core.Testing/TempHome.cs diff --git a/Buildvana.slnx b/Buildvana.slnx index 1109e4fe..21087ba9 100644 --- a/Buildvana.slnx +++ b/Buildvana.slnx @@ -44,6 +44,7 @@ + diff --git a/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj b/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj new file mode 100644 index 00000000..d5b03336 --- /dev/null +++ b/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj @@ -0,0 +1,17 @@ + + + + Buildvana testing helpers + Shared test harnesses for Buildvana test projects, e.g. temporary Git repositories and home directories. + $(StandardTfm) + + + + + + + + + + + diff --git a/src/Buildvana.Core.Testing/TempGitRepo.cs b/src/Buildvana.Core.Testing/TempGitRepo.cs new file mode 100644 index 00000000..084a8398 --- /dev/null +++ b/src/Buildvana.Core.Testing/TempGitRepo.cs @@ -0,0 +1,115 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using LibGit2Sharp; + +namespace Buildvana.Core.Testing; + +/// +/// A disposable real Git repository in a temporary directory, for tests exercising Git-dependent code. +/// +public sealed class TempGitRepo : IDisposable +{ + private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); + + private readonly Repository _repository; + + /// + /// Initializes a new instance of the class, creating and initializing + /// a Git repository in a newly-created temporary directory. + /// + public TempGitRepo() + { + RootPath = Directory.CreateTempSubdirectory("bv-test-repo-").FullName; + _ = Repository.Init(RootPath); + _repository = new Repository(RootPath); + } + + /// + /// Gets the full path of the repository's root directory. + /// + public string RootPath { get; } + + /// + /// Gets the friendly name of the current branch. + /// + public string CurrentBranchName => _repository.Head.FriendlyName; + + /// + /// Gets the SHA of the current HEAD commit. + /// + public string HeadSha => _repository.Head.Tip.Sha; + + /// + /// Writes a file in the repository's root directory. + /// + /// The name of the file, relative to the root directory. + /// The content of the file. + /// The file encoding; UTF-8 without BOM if unspecified. + public void WriteFile(string name, string content, Encoding? encoding = null) + => File.WriteAllText(Path.Combine(RootPath, name), content, encoding ?? Utf8NoBom); + + /// + /// Stages all changes and creates a commit, which may be empty. + /// + /// The commit message. + public void CommitAll(string message = "Test commit") + { + Commands.Stage(_repository, "*"); + var signature = new Signature("Test", "test@example.com", DateTimeOffset.Now); + _ = _repository.Commit(message, signature, signature, new CommitOptions { AllowEmptyCommit = true }); + } + + /// + /// Creates a new branch at the current HEAD commit and checks it out. + /// + /// The name of the new branch. + public void CheckoutNewBranch(string name) + { + var branch = _repository.CreateBranch(name); + _ = Commands.Checkout(_repository, branch); + } + + /// + /// Checks out an existing branch. + /// + /// The name of the branch. + public void Checkout(string name) => _ = Commands.Checkout(_repository, _repository.Branches[name]); + + /// + /// Checks out the current HEAD commit directly, detaching HEAD from any branch. + /// + public void CheckoutDetached() => _ = Commands.Checkout(_repository, _repository.Head.Tip); + + /// + /// Merges a branch into the current branch, always creating a merge commit. + /// + /// The name of the branch to merge. + public void Merge(string branchName) + { + var signature = new Signature("Test", "test@example.com", DateTimeOffset.Now); + var options = new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }; + _ = _repository.Merge(_repository.Branches[branchName], signature, options); + } + + /// + public void Dispose() + { + _repository.Dispose(); + DeleteDirectory(RootPath); + } + + private static void DeleteDirectory(string path) + { + // Git object files are read-only; clear attributes so the recursive delete succeeds on Windows. + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + { + File.SetAttributes(file, FileAttributes.Normal); + } + + Directory.Delete(path, recursive: true); + } +} diff --git a/src/Buildvana.Core.Testing/TempHome.cs b/src/Buildvana.Core.Testing/TempHome.cs new file mode 100644 index 00000000..79218d6a --- /dev/null +++ b/src/Buildvana.Core.Testing/TempHome.cs @@ -0,0 +1,44 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Buildvana.Core.HomeDirectory; + +namespace Buildvana.Core.Testing; + +/// +/// A disposable temporary home directory (with no Git repository), for tests exercising file-based code +/// that resolves paths against a home directory. +/// +public sealed class TempHome : IDisposable +{ + private readonly DirectoryInfo _directory = Directory.CreateTempSubdirectory("bv-test-home-"); + + /// + /// Gets the full path of the home directory. + /// + public string RootPath => _directory.FullName; + + /// + /// Gets a home directory provider resolving to . + /// + public FixedHomeDirectoryProvider Provider => new(RootPath); + + /// + /// Writes a file in the home directory. + /// + /// The name of the file, relative to the home directory. + /// The content of the file. + public void WriteFile(string name, string content) => File.WriteAllText(Path.Combine(RootPath, name), content); + + /// + /// Reads a file in the home directory. + /// + /// The name of the file, relative to the home directory. + /// The content of the file. + public string ReadFile(string name) => File.ReadAllText(Path.Combine(RootPath, name)); + + /// + public void Dispose() => _directory.Delete(recursive: true); +} From 85060b6046784c3b87837e72db788eaca66de1fd Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:43:24 +0200 Subject: [PATCH 3/8] Add version advancing, file writing, and FileVersion to Buildvana.Core.Versioning In preparation for replacing the tool-side versioning types: - VersionSpec gains Stable/Unstable/NextMinor/NextMajor/ApplyChange extension methods, porting the advance semantics of the tool's version.json-based VersionSpec (new version lines start as prereleases). - A new VersionFile type becomes the single reader and writer of the VERSION file; VersioningService now reads the version specification through it. The writer only emits the configured prerelease tag as informational text when it fits the version file grammar, falling back to a bare dash. - VersioningService computes FileVersion (MAJOR.MINOR.HEIGHT.0). - GitHeightCalculator can describe the repository state Calculate depends on as an opaque token, so callers can cache computed versions; its static constructor points LibGit2Sharp at the runtimes/{rid}/native directory next to the managed assembly when present, for hosts without a dependency context (e.g. MSBuild task hosts). The versioning and task test projects now use the harnesses from Buildvana.Core.Testing instead of project-local copies. Co-Authored-By: Claude Fable 5 --- .../GitHeightCalculator-nativeLibrary.cs | 40 ++++++ .../GitHeightCalculator.cs | 24 +++- src/Buildvana.Core.Versioning/VersionFile.cs | 127 ++++++++++++++++++ .../VersionSpecExtensions-private.cs | 1 + .../VersionSpecExtensions.cs | 49 +++++++ .../VersioningService.cs | 40 ++---- .../Buildvana.Core.Versioning.Tests.csproj | 2 +- .../FixedHomeDirectoryProvider.cs | 9 -- .../GitHeightCalculatorTests.cs | 51 +++++++ .../TempGitRepo.cs | 69 ---------- .../VersionFileTests.cs | 110 +++++++++++++++ .../VersionSpecChangeTests.cs | 73 ++++++++++ .../VersioningServiceTests.cs | 14 +- 13 files changed, 498 insertions(+), 111 deletions(-) create mode 100644 src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs create mode 100644 src/Buildvana.Core.Versioning/VersionFile.cs delete mode 100644 tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs delete mode 100644 tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs create mode 100644 tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs create mode 100644 tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs diff --git a/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs b/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs new file mode 100644 index 00000000..b09ec6ad --- /dev/null +++ b/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs @@ -0,0 +1,40 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.IO; +using System.Runtime.InteropServices; +using LibGit2Sharp; + +namespace Buildvana.Core.Versioning; + +partial class GitHeightCalculator +{ + // LibGit2Sharp normally locates its native library through the host's dependency context (deps.json). + // MSBuild task hosts load task assemblies without one, so when the conventional runtimes/{rid}/native + // directory exists next to the managed assembly, point LibGit2Sharp at it explicitly. + // This must happen before the first native call in the process, hence the static constructor: it is + // guaranteed to run before any member of the library's only Git touchpoint is used. + static GitHeightCalculator() + { + var baseDirectory = Path.GetDirectoryName(typeof(Repository).Assembly.Location); + if (string.IsNullOrEmpty(baseDirectory)) + { + return; + } + + var nativeDirectory = Path.Combine(baseDirectory, "runtimes", RuntimeInformation.RuntimeIdentifier, "native"); + if (!Directory.Exists(nativeDirectory)) + { + return; + } + + try + { + GlobalSettings.NativeLibraryPath = nativeDirectory; + } + catch (LibGit2SharpException) + { + // The native library is already loaded, so resolution has evidently succeeded some other way. + } + } +} diff --git a/src/Buildvana.Core.Versioning/GitHeightCalculator.cs b/src/Buildvana.Core.Versioning/GitHeightCalculator.cs index d2fb2d33..1389503d 100644 --- a/src/Buildvana.Core.Versioning/GitHeightCalculator.cs +++ b/src/Buildvana.Core.Versioning/GitHeightCalculator.cs @@ -26,7 +26,7 @@ namespace Buildvana.Core.Versioning; /// This class is the library's only Git touchpoint; its public surface deliberately exposes no /// LibGit2Sharp types. /// -public sealed class GitHeightCalculator +public sealed partial class GitHeightCalculator { private readonly string _versionFileName; @@ -40,6 +40,28 @@ public GitHeightCalculator(string versionFileName) _versionFileName = versionFileName; } + /// + /// Gets an opaque token describing the repository state that depends on: + /// the current HEAD reference and the commit it points to. For a given version file name and + /// version specification, equal tokens guarantee equal results, because the + /// commit graph reachable from a commit is immutable. + /// + /// The directory of the Git repository. + /// A state token, or if there is no Git repository at + /// . + public static string? TryGetRepositoryStateToken(string repositoryDirectory) + { + Guard.IsNotNullOrEmpty(repositoryDirectory); + if (!Repository.IsValid(repositoryDirectory)) + { + return null; + } + + using var repository = new Repository(repositoryDirectory); + var head = repository.Head; + return FormattableString.Invariant($"{head.CanonicalName}\0{head.Tip?.Sha}"); + } + /// /// Calculates the Git height of the version line identified by in the /// repository at , along with the repository facts needed to diff --git a/src/Buildvana.Core.Versioning/VersionFile.cs b/src/Buildvana.Core.Versioning/VersionFile.cs new file mode 100644 index 00000000..70ebbb30 --- /dev/null +++ b/src/Buildvana.Core.Versioning/VersionFile.cs @@ -0,0 +1,127 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Security; +using Buildvana.Core.HomeDirectory; +using CommunityToolkit.Diagnostics; + +namespace Buildvana.Core.Versioning; + +/// +/// Represents the repository's version file, for reading the version specification and applying +/// version advances. +/// +/// +/// The version file (, in the home directory) is plain text holding a single +/// MAJOR.MINOR[-[tag]] version specification; see the Parse method in +/// for the accepted format. +/// +public sealed class VersionFile +{ + /// + /// The name of the version file, relative to the home directory. + /// + public const string FileName = "VERSION"; + + private VersionFile(string absolutePath, VersionSpec spec) + { + Path = absolutePath; + Spec = spec; + } + + /// + /// Gets the absolute path of the version file. + /// + public string Path { get; } + + /// + /// Gets the version specification read from the version file, as possibly modified by + /// . + /// + public VersionSpec Spec { get; private set; } + + /// + /// Constructs a instance by loading the repository's version file. + /// + /// The home directory provider used to locate the version file. + /// A newly-created , representing the loaded data. + /// The version file is absent, cannot be read, or does not + /// contain a valid version specification. + public static VersionFile Load(IHomeDirectoryProvider home) + { + Guard.IsNotNull(home); + var path = System.IO.Path.Combine(home.HomeDirectory, FileName); + return new(path, ReadSpec(path)); + } + + /// + /// Applies a version specification change to this instance. + /// + /// A constant representing the kind of change to apply. + /// If the property actually changed as a result of + /// , ; otherwise, . + /// + /// This method does not save the modified version file; call for that. + /// + public bool ApplyChange(VersionSpecChange change) + { + (Spec, var changed) = Spec.ApplyChange(change); + return changed; + } + + /// + /// Saves the version file, possibly with a modified , back to the repository. + /// + /// The informational tag text to write after the prerelease marker, + /// usually the configured . It is only written when + /// is a prerelease and the tag fits the version file grammar; otherwise a bare + /// - marks the prerelease. + /// The version file cannot be written. + public void Save(string? prereleaseTag = null) + { + var text = Format(Spec, prereleaseTag); + try + { + File.WriteAllText(Path, text); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException) + { + throw new BuildFailedException($"Could not write to {Path}: {e.Message}", e); + } + } + + private static VersionSpec ReadSpec(string path) + { + BuildFailedException.ThrowIfNot(File.Exists(path), $"Version file {path} not found."); + string text; + try + { + text = File.ReadAllText(path); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException) + { + throw new BuildFailedException($"Could not read from {path}: {e.Message}", e); + } + + text = text.Trim(); + BuildFailedException.ThrowIfNot( + VersionSpec.TryParse(text, out var spec), + $"{path} contains an invalid version specification '{text}'."); + return spec; + } + + private static string Format(VersionSpec spec, string? prereleaseTag) + { + var stable = FormattableString.Invariant($"{spec.Major}.{spec.Minor}"); + if (!spec.Prerelease) + { + return stable + "\n"; + } + + var tagged = stable + "-" + prereleaseTag; + var tagFits = prereleaseTag is not null && VersionSpec.TryParse(tagged, out _); + return (tagFits ? tagged : stable + "-") + "\n"; + } +} diff --git a/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs b/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs index 04ce94ce..34469d15 100644 --- a/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs +++ b/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs @@ -5,6 +5,7 @@ namespace Buildvana.Core.Versioning; +#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716 partial class VersionSpecExtensions { private static readonly Regex VersionSpecRegex = GetVersionSpecRegex(); diff --git a/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs b/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs index 7090e9e2..9e8a763b 100644 --- a/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs +++ b/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs @@ -75,4 +75,53 @@ public static bool TryParse([NotNullWhen(true)] string? str, [MaybeNullWhen(fals return true; } } + + extension(VersionSpec @this) + { + /// + /// Gets a that represents the same version as this instance and is not + /// a prerelease. + /// + /// This instance if it is not a prerelease; otherwise, a newly-created + /// . + public VersionSpec Stable() => @this.Prerelease ? @this with { Prerelease = false } : @this; + + /// + /// Gets a that represents the same version as this instance and is + /// a prerelease. + /// + /// This instance if it is a prerelease; otherwise, a newly-created + /// . + public VersionSpec Unstable() => @this.Prerelease ? @this : @this with { Prerelease = true }; + + /// + /// Gets a that represents the next minor version with respect to this + /// instance. The new version line starts as a prerelease. + /// + /// A newly-created . + public VersionSpec NextMinor() => new(@this.Major, @this.Minor + 1, true); + + /// + /// Gets a that represents the next major version with respect to this + /// instance. The new version line starts as a prerelease. + /// + /// A newly-created . + public VersionSpec NextMajor() => new(@this.Major + 1, 0, true); + + /// + /// Gets a that represents the result of applying the specified change + /// to this instance. + /// + /// A constant representing the kind of change to apply. + /// A tuple of the result of applying to this instance, and a + /// value indicating whether the result differs from this instance. + public (VersionSpec Result, bool Changed) ApplyChange(VersionSpecChange change) => change switch + { + VersionSpecChange.Unstable => @this.Prerelease ? (@this, false) : (@this.Unstable(), true), + VersionSpecChange.Stable => @this.Prerelease ? (@this.Stable(), true) : (@this, false), + VersionSpecChange.Minor => (@this.NextMinor(), true), + VersionSpecChange.Major => (@this.NextMajor(), true), + _ => (@this, false), + }; + } } diff --git a/src/Buildvana.Core.Versioning/VersioningService.cs b/src/Buildvana.Core.Versioning/VersioningService.cs index f6ef459e..f63677c1 100644 --- a/src/Buildvana.Core.Versioning/VersioningService.cs +++ b/src/Buildvana.Core.Versioning/VersioningService.cs @@ -2,8 +2,6 @@ // See the LICENSE file in the project root for full license information. using System; -using System.IO; -using System.Security; using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; using Buildvana.Core.HomeDirectory; @@ -17,17 +15,12 @@ namespace Buildvana.Core.Versioning; /// and exposes it in the string forms needed by builds and releases. /// /// -/// The version file (, in the home directory) holds a +/// The version file (, in the home directory) holds a /// MAJOR.MINOR[-[tag]] specification; the patch number is the Git height of the version line /// (see ). All values are computed once, on construction. /// public sealed class VersioningService { - /// - /// The name of the version file, relative to the home directory. - /// - public const string VersionFileName = "VERSION"; - private const int ShortCommitIdLength = 10; /// @@ -54,7 +47,7 @@ public VersioningService( Guard.IsNotNull(heightCalculator); var homeDirectory = home.HomeDirectory; - Spec = ReadVersionFile(Path.Combine(homeDirectory, VersionFileName)); + Spec = VersionFile.Load(home).Spec; var prereleaseTag = Spec.Prerelease ? GetPrereleaseTag(settings) : null; var facts = heightCalculator.Calculate(homeDirectory, Spec); Height = facts.Height; @@ -63,6 +56,7 @@ public VersioningService( SimpleVersion = FormattableString.Invariant($"{Spec.Major}.{Spec.Minor}.{Height}"); SemVer = prereleaseTag is null ? SimpleVersion : $"{SimpleVersion}-{prereleaseTag}"; AssemblyVersion = ComputeAssemblyVersion(Spec, Height, settings.AssemblyVersionPrecision); + FileVersion = FormattableString.Invariant($"{SimpleVersion}.0"); InformationalVersion = ComputeInformationalVersion(SemVer, Spec.Prerelease, IsPublicRelease, CommitId); var publicity = IsPublicRelease ? "public release" : "not a public release"; reporter.Info(FormattableString.Invariant($"Version {SemVer} (height {Height}, {publicity})")); @@ -112,38 +106,24 @@ public VersioningService( /// public string AssemblyVersion { get; } + /// + /// Gets the file version: at full precision, plus a fourth component + /// that is always 0. + /// + public string FileVersion { get; } + /// /// Gets the informational version: , plus a g-prefixed short commit ID /// appended to the prerelease part when the build is not a public release. /// public string InformationalVersion { get; } - private static VersionSpec ReadVersionFile(string path) - { - BuildFailedException.ThrowIfNot(File.Exists(path), $"Version file {path} not found."); - string text; - try - { - text = File.ReadAllText(path); - } - catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException) - { - throw new BuildFailedException($"Could not read from {path}: {e.Message}", e); - } - - text = text.Trim(); - BuildFailedException.ThrowIfNot( - VersionSpec.TryParse(text, out var spec), - $"{path} contains an invalid version specification '{text}'."); - return spec; - } - private static string GetPrereleaseTag(VersioningSettings settings) { var tag = settings.PrereleaseTag; BuildFailedException.ThrowIf( string.IsNullOrEmpty(tag), - $"{VersionFileName} specifies a prerelease version, but versioning.prereleaseTag is not set in the configuration file."); + $"{VersionFile.FileName} specifies a prerelease version, but versioning.prereleaseTag is not set in the configuration file."); var isValid = SemanticVersion.TryParse(FormattableString.Invariant($"0.0.0-{tag}"), out _); BuildFailedException.ThrowIfNot( isValid, diff --git a/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj b/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj index 6f81faf1..de7f516f 100644 --- a/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj +++ b/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj @@ -8,11 +8,11 @@ - + diff --git a/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs b/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs deleted file mode 100644 index b4221384..00000000 --- a/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using Buildvana.Core.HomeDirectory; - -internal sealed class FixedHomeDirectoryProvider(string homeDirectory) : IHomeDirectoryProvider -{ - public string HomeDirectory => homeDirectory; -} diff --git a/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs b/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs index a2eba6b3..8685362c 100644 --- a/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs @@ -3,6 +3,7 @@ using System.Text; using Buildvana.Core; +using Buildvana.Core.Testing; using Buildvana.Core.Versioning; internal sealed class GitHeightCalculatorTests @@ -173,6 +174,56 @@ public async Task Calculate_DetachedHead_ReportsEmptyBranchName() await Assert.That(result.Height).IsEqualTo(1); } + [Test] + public async Task TryGetRepositoryStateToken_WithoutRepository_ReturnsNull() + { + var directory = Directory.CreateTempSubdirectory("bv-test-"); + try + { + var token = GitHeightCalculator.TryGetRepositoryStateToken(directory.FullName); + await Assert.That(token).IsNull(); + } + finally + { + directory.Delete(recursive: true); + } + } + + [Test] + public async Task TryGetRepositoryStateToken_UnchangedState_ReturnsEqualTokens() + { + using var repo = new TempGitRepo(); + repo.CommitAll(); + var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + await Assert.That(first).IsNotNull(); + await Assert.That(second).IsEqualTo(first); + } + + [Test] + public async Task TryGetRepositoryStateToken_NewCommit_ChangesToken() + { + using var repo = new TempGitRepo(); + repo.CommitAll(); + var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + repo.CommitAll(); + var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + await Assert.That(second).IsNotNull(); + await Assert.That(string.Equals(second, first, StringComparison.Ordinal)).IsFalse(); + } + + [Test] + public async Task TryGetRepositoryStateToken_DetachedHead_ChangesToken() + { + using var repo = new TempGitRepo(); + repo.CommitAll(); + var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + repo.CheckoutDetached(); + var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath); + await Assert.That(second).IsNotNull(); + await Assert.That(string.Equals(second, first, StringComparison.Ordinal)).IsFalse(); + } + private static GitHeightResult Calculate(TempGitRepo repo, int major, int minor) => new GitHeightCalculator("VERSION").Calculate(repo.RootPath, new VersionSpec(major, minor, false)); } diff --git a/tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs b/tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs deleted file mode 100644 index 509c07e4..00000000 --- a/tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System.Text; -using LibGit2Sharp; - -internal sealed class TempGitRepo : IDisposable -{ - private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); - - private readonly Repository _repository; - - public TempGitRepo() - { - RootPath = Directory.CreateTempSubdirectory("bv-test-repo-").FullName; - _ = Repository.Init(RootPath); - _repository = new Repository(RootPath); - } - - public string RootPath { get; } - - public string CurrentBranchName => _repository.Head.FriendlyName; - - public string HeadSha => _repository.Head.Tip.Sha; - - public void WriteFile(string name, string content, Encoding? encoding = null) - => File.WriteAllText(Path.Combine(RootPath, name), content, encoding ?? Utf8NoBom); - - public void CommitAll(string message = "Test commit") - { - Commands.Stage(_repository, "*"); - var signature = new Signature("Test", "test@example.com", DateTimeOffset.Now); - _ = _repository.Commit(message, signature, signature, new CommitOptions { AllowEmptyCommit = true }); - } - - public void CheckoutNewBranch(string name) - { - var branch = _repository.CreateBranch(name); - _ = Commands.Checkout(_repository, branch); - } - - public void Checkout(string name) => _ = Commands.Checkout(_repository, _repository.Branches[name]); - - public void CheckoutDetached() => _ = Commands.Checkout(_repository, _repository.Head.Tip); - - public void Merge(string branchName) - { - var signature = new Signature("Test", "test@example.com", DateTimeOffset.Now); - var options = new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }; - _ = _repository.Merge(_repository.Branches[branchName], signature, options); - } - - public void Dispose() - { - _repository.Dispose(); - DeleteDirectory(RootPath); - } - - private static void DeleteDirectory(string path) - { - // Git object files are read-only; clear attributes so the recursive delete succeeds on Windows. - foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) - { - File.SetAttributes(file, FileAttributes.Normal); - } - - Directory.Delete(path, recursive: true); - } -} diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs new file mode 100644 index 00000000..f23efcd3 --- /dev/null +++ b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs @@ -0,0 +1,110 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Core; +using Buildvana.Core.Testing; +using Buildvana.Core.Versioning; + +internal sealed class VersionFileTests +{ + [Test] + public async Task Load_MissingFile_Throws() + { + using var home = new TempHome(); + var exception = CatchLoad(home); + await Assert.That(exception).IsNotNull(); + await Assert.That(exception!.Message).Contains("not found"); + } + + [Test] + public async Task Load_MalformedFile_Throws() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "banana\n"); + var exception = CatchLoad(home); + await Assert.That(exception).IsNotNull(); + await Assert.That(exception!.Message).Contains("banana"); + } + + [Test] + public async Task Load_ValidFile_ParsesSpecAndExposesPath() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3-preview\n"); + var versionFile = VersionFile.Load(home.Provider); + await Assert.That(versionFile.Spec).IsEqualTo(new VersionSpec(2, 3, true)); + await Assert.That(versionFile.Path).IsEqualTo(Path.Combine(home.RootPath, VersionFile.FileName)); + } + + [Test] + public async Task ApplyChange_UpdatesSpecAndReportsChange() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3-x\n"); + var versionFile = VersionFile.Load(home.Provider); + await Assert.That(versionFile.ApplyChange(VersionSpecChange.Stable)).IsTrue(); + await Assert.That(versionFile.Spec).IsEqualTo(new VersionSpec(2, 3, false)); + await Assert.That(versionFile.ApplyChange(VersionSpecChange.Stable)).IsFalse(); + } + + [Test] + public async Task Save_StableSpec_WritesCanonicalForm() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3\n"); + VersionFile.Load(home.Provider).Save("preview"); + await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3\n"); + } + + [Test] + public async Task Save_PrereleaseSpecWithConformingTag_WritesTag() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3-\n"); + VersionFile.Load(home.Provider).Save("preview"); + await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-preview\n"); + } + + [Test] + public async Task Save_PrereleaseSpecWithNoTag_WritesBareMarker() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3-x\n"); + VersionFile.Load(home.Provider).Save(); + await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-\n"); + } + + [Test] + public async Task Save_PrereleaseSpecWithNonConformingTag_WritesBareMarker() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3-x\n"); + VersionFile.Load(home.Provider).Save("beta.1"); + await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-\n"); + } + + [Test] + public async Task Save_AfterApplyChange_RoundTrips() + { + using var home = new TempHome(); + home.WriteFile(VersionFile.FileName, "2.3\n"); + var versionFile = VersionFile.Load(home.Provider); + _ = versionFile.ApplyChange(VersionSpecChange.Minor); + versionFile.Save("preview"); + var reloaded = VersionFile.Load(home.Provider); + await Assert.That(reloaded.Spec).IsEqualTo(new VersionSpec(2, 4, true)); + } + + private static BuildFailedException? CatchLoad(TempHome home) + { + try + { + _ = VersionFile.Load(home.Provider); + return null; + } + catch (BuildFailedException e) + { + return e; + } + } +} diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs new file mode 100644 index 00000000..7e51ea80 --- /dev/null +++ b/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs @@ -0,0 +1,73 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Core.Versioning; + +internal sealed class VersionSpecChangeTests +{ + [Test] + public async Task Stable_OnPrerelease_ClearsPrereleaseMarker() + { + var result = new VersionSpec(2, 3, true).Stable(); + await Assert.That(result).IsEqualTo(new VersionSpec(2, 3, false)); + } + + [Test] + public async Task Stable_OnStable_ReturnsEqualSpec() + { + var spec = new VersionSpec(2, 3, false); + await Assert.That(spec.Stable()).IsEqualTo(spec); + } + + [Test] + public async Task Unstable_OnStable_SetsPrereleaseMarker() + { + var result = new VersionSpec(2, 3, false).Unstable(); + await Assert.That(result).IsEqualTo(new VersionSpec(2, 3, true)); + } + + [Test] + public async Task Unstable_OnPrerelease_ReturnsEqualSpec() + { + var spec = new VersionSpec(2, 3, true); + await Assert.That(spec.Unstable()).IsEqualTo(spec); + } + + [Test] + public async Task NextMinor_IncrementsMinorAndStartsPrerelease() + { + var result = new VersionSpec(2, 3, false).NextMinor(); + await Assert.That(result).IsEqualTo(new VersionSpec(2, 4, true)); + } + + [Test] + public async Task NextMajor_IncrementsMajorResetsMinorAndStartsPrerelease() + { + var result = new VersionSpec(2, 3, false).NextMajor(); + await Assert.That(result).IsEqualTo(new VersionSpec(3, 0, true)); + } + + [Test] + [Arguments(false, VersionSpecChange.None, 2, 3, false, false)] + [Arguments(true, VersionSpecChange.None, 2, 3, true, false)] + [Arguments(false, VersionSpecChange.Unstable, 2, 3, true, true)] + [Arguments(true, VersionSpecChange.Unstable, 2, 3, true, false)] + [Arguments(false, VersionSpecChange.Stable, 2, 3, false, false)] + [Arguments(true, VersionSpecChange.Stable, 2, 3, false, true)] + [Arguments(false, VersionSpecChange.Minor, 2, 4, true, true)] + [Arguments(true, VersionSpecChange.Minor, 2, 4, true, true)] + [Arguments(false, VersionSpecChange.Major, 3, 0, true, true)] + [Arguments(true, VersionSpecChange.Major, 3, 0, true, true)] + public async Task ApplyChange_ComputesExpectedResult( + bool prerelease, + VersionSpecChange change, + int expectedMajor, + int expectedMinor, + bool expectedPrerelease, + bool expectedChanged) + { + var (result, changed) = new VersionSpec(2, 3, prerelease).ApplyChange(change); + await Assert.That(result).IsEqualTo(new VersionSpec(expectedMajor, expectedMinor, expectedPrerelease)); + await Assert.That(changed).IsEqualTo(expectedChanged); + } +} diff --git a/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs index 560d2149..425e1ed6 100644 --- a/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs +++ b/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs @@ -4,6 +4,8 @@ using Buildvana.Core; using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; +using Buildvana.Core.HomeDirectory; +using Buildvana.Core.Testing; using Buildvana.Core.Versioning; internal sealed class VersioningServiceTests @@ -138,12 +140,22 @@ public async Task Constructor_DefaultPrecision_IsMajor() await Assert.That(service.AssemblyVersion).IsEqualTo("2.0.0.0"); } + [Test] + public async Task Constructor_FileVersion_IsFullPrecisionRegardlessOfAssemblyVersionPrecision() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "2.3\n"); + repo.CommitAll(); + var service = CreateService(repo, new BuildvanaConfig()); + await Assert.That(service.FileVersion).IsEqualTo("2.3.1.0"); + } + private static VersioningService CreateService(TempGitRepo repo, BuildvanaConfig config) => new( NullReporter.Instance, new FixedHomeDirectoryProvider(repo.RootPath), new VersioningSettings(config), - new GitHeightCalculator(VersioningService.VersionFileName)); + new GitHeightCalculator(VersionFile.FileName)); private static BuildFailedException? CatchCreate(TempGitRepo repo, BuildvanaConfig config) { From 21dc4ce9469a197e68924c145d5e675e4a986069 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:43:42 +0200 Subject: [PATCH 4/8] Replace the NerdbankGitVersioning SDK module with native versioning The new Versioning module is auto-detected by the presence of a VERSION file in the home directory and computes Version, PackageVersion, AssemblyVersion, FileVersion, and InformationalVersion through the new ComputeVersion task, which runs VersioningService in-process using the engine-backed reporter. Computed versions are cached per home directory for the lifetime of the task host, keyed on a fingerprint of the VERSION file, the configuration file, and the repository state, so multi-project builds pay for a single Git graph walk. UseNerdbankGitVersioning remains as a deprecated alias of UseVersioning (warning BVSDK2001); the GetBuildVersion target name is kept, real or stub, for targets that depend on it. Versioned C# projects enable GenerateThisAssemblyClass by default, and the module contributes SimpleVersion, SemVer, IsPublicRelease, IsPrerelease, and GitCommitId constants to the generated class. LibGit2Sharp's managed assembly and native binaries ship in the package's bin directory; the Unix binaries are renamed at pack time (the lib prefix is stripped) because LibGit2Sharp composes the file name without it when probing the explicitly-set native library path, which is the only resolution path available without a deps.json. The SDK's own version props now read PackageVersion, which is set by both the old and the new versioning, instead of NBGV's NuGetPackageVersion. Co-Authored-By: Claude Fable 5 --- .../Buildvana.Sdk.Tasks.csproj | 3 + .../Tasks/ComputeVersion-caching.cs | 93 ++++++++++++++ .../Tasks/ComputeVersion.CachedVersion.cs | 23 ++++ .../Tasks/ComputeVersion.cs | 67 +++++++++++ src/Buildvana.Sdk/Buildvana.Sdk.csproj | 13 +- .../BeforeModules.targets | 57 --------- .../NerdbankGitVersioning/Module.Core.targets | 21 ---- .../NerdbankGitVersioning/Module.targets | 10 -- .../Modules/Versioning/BeforeModules.targets | 72 +++++++++++ .../Modules/Versioning/Module.Core.targets | 72 +++++++++++ .../Module.Stubs.targets | 2 +- .../Modules/Versioning/Module.targets | 10 ++ src/Buildvana.Sdk/Sdk/PackageVersions.props | 1 - src/Buildvana.Sdk/Sdk/Sdk.props | 1 + .../Buildvana.Sdk.Tasks.Tests.csproj | 1 + .../ComputeVersionTests.cs | 113 ++++++++++++++++++ 16 files changed, 468 insertions(+), 91 deletions(-) create mode 100644 src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs create mode 100644 src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs create mode 100644 src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs delete mode 100644 src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets delete mode 100644 src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets delete mode 100644 src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets create mode 100644 src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets create mode 100644 src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets rename src/Buildvana.Sdk/Modules/{NerdbankGitVersioning => Versioning}/Module.Stubs.targets (67%) create mode 100644 src/Buildvana.Sdk/Modules/Versioning/Module.targets create mode 100644 tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs diff --git a/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj b/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj index fe394ad0..302b4112 100644 --- a/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj +++ b/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj @@ -19,7 +19,10 @@ + + + diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs new file mode 100644 index 00000000..ab614521 --- /dev/null +++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs @@ -0,0 +1,93 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.IO; +using Buildvana.Core.Configuration; +using Buildvana.Core.ConsoleOutput; +using Buildvana.Core.HomeDirectory; +using Buildvana.Core.Versioning; +using Buildvana.Sdk.Internal; + +namespace Buildvana.Sdk.Tasks; + +partial class ComputeVersion +{ + private static readonly ConcurrentDictionary Cache = new(StringComparer.OrdinalIgnoreCase); + + private static CachedVersion GetOrComputeVersion(string homeDirectory, IReporter reporter) + { + homeDirectory = Path.GetFullPath(homeDirectory); + var fingerprint = ComputeFingerprint(homeDirectory); + if (fingerprint is null) + { + return ComputeCore(homeDirectory, reporter, null); + } + + if (Cache.TryGetValue(homeDirectory, out var cached) && cached.Fingerprint == fingerprint) + { + return cached; + } + + var computed = ComputeCore(homeDirectory, reporter, fingerprint); + Cache[homeDirectory] = computed; + return computed; + } + + private static CachedVersion ComputeCore(string homeDirectory, IReporter reporter, string? fingerprint) + { + var service = new VersioningService( + reporter, + new FixedHomeDirectoryProvider(homeDirectory), + new VersioningSettings(BuildvanaConfigLoader.Load(homeDirectory)), + new GitHeightCalculator(VersionFile.FileName)); + return new CachedVersion( + fingerprint, + service.SimpleVersion, + service.SemVer, + service.AssemblyVersion, + service.FileVersion, + service.InformationalVersion, + service.IsPublicRelease, + service.IsPrerelease, + service.CommitId ?? string.Empty, + service.Height); + } + + // The fingerprint captures everything the computed version depends on: the working-tree VERSION file, + // the configuration file, and the repository state (the height walk only sees commits reachable from HEAD). + // A null fingerprint disables caching, letting VersioningService surface the proper error. + private static string? ComputeFingerprint(string homeDirectory) + { + try + { + var versionPath = Path.Combine(homeDirectory, VersionFile.FileName); + if (!File.Exists(versionPath)) + { + return null; + } + + var repositoryStateToken = GitHeightCalculator.TryGetRepositoryStateToken(homeDirectory); + if (repositoryStateToken is null) + { + return null; + } + + string?[] parts = + [ + repositoryStateToken, + File.ReadAllText(versionPath), + ReadOptionalFile(Path.Combine(homeDirectory, "buildvana.json")), + ReadOptionalFile(Path.Combine(homeDirectory, "buildvana.jsonc")), + ]; + return string.Join('\0', parts); + } + catch (Exception e) when (!e.IsFatalException()) + { + return null; + } + } + + private static string? ReadOptionalFile(string path) => File.Exists(path) ? File.ReadAllText(path) : null; +} diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs new file mode 100644 index 00000000..041f8a1a --- /dev/null +++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs @@ -0,0 +1,23 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Buildvana.Sdk.Tasks; + +partial class ComputeVersion +{ + /// + /// The computed version values for a home directory, together with the repository-state fingerprint + /// they were computed from ( if the state could not be fingerprinted). + /// + private sealed record CachedVersion( + string? Fingerprint, + string SimpleVersion, + string SemVer, + string AssemblyVersion, + string FileVersion, + string InformationalVersion, + bool IsPublicRelease, + bool IsPrerelease, + string CommitId, + int Height); +} diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs new file mode 100644 index 00000000..9e3b458b --- /dev/null +++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs @@ -0,0 +1,67 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Globalization; +using Buildvana.Core; +using Buildvana.Sdk.Resources; +using Microsoft.Build.Framework; + +namespace Buildvana.Sdk.Tasks; + +/// +/// Computes the version being built from the repository's VERSION file, Git height, and versioning +/// settings, exposing it in the string forms consumed by builds. +/// Results are cached per home directory for the lifetime of the task host process, keyed on the repository +/// state, so that multi-project builds pay for a single Git graph walk. +/// +public sealed partial class ComputeVersion : BuildvanaSdkTask +{ + [Required] + public string HomeDirectory { get; set; } = string.Empty; + + [Output] + public string SimpleVersion { get; private set; } = string.Empty; + + [Output] + public string SemVer { get; private set; } = string.Empty; + + [Output] + public string AssemblyVersion { get; private set; } = string.Empty; + + [Output] + public string FileVersion { get; private set; } = string.Empty; + + [Output] + public string InformationalVersion { get; private set; } = string.Empty; + + [Output] + public bool IsPublicRelease { get; private set; } + + [Output] + public bool IsPrerelease { get; private set; } + + [Output] + public string CommitId { get; private set; } = string.Empty; + + [Output] + public int Height { get; private set; } + + protected override Undefined Run() + { + BuildFailedException.ThrowIfNot( + !string.IsNullOrEmpty(HomeDirectory), + string.Format(CultureInfo.InvariantCulture, Strings.MissingParameterFmt, nameof(HomeDirectory))); + + var version = GetOrComputeVersion(HomeDirectory, Reporter); + SimpleVersion = version.SimpleVersion; + SemVer = version.SemVer; + AssemblyVersion = version.AssemblyVersion; + FileVersion = version.FileVersion; + InformationalVersion = version.InformationalVersion; + IsPublicRelease = version.IsPublicRelease; + IsPrerelease = version.IsPrerelease; + CommitId = version.CommitId; + Height = version.Height; + return Undefined.Value; + } +} diff --git a/src/Buildvana.Sdk/Buildvana.Sdk.csproj b/src/Buildvana.Sdk/Buildvana.Sdk.csproj index ab27a01a..042efbfa 100644 --- a/src/Buildvana.Sdk/Buildvana.Sdk.csproj +++ b/src/Buildvana.Sdk/Buildvana.Sdk.csproj @@ -67,7 +67,7 @@ <_SdkVersionPropsXml> - $(NuGetPackageVersion) + $(PackageVersion) $(BV_MinRoslynVersion) $(BV_MinRoslynVersionHint) @@ -272,8 +272,19 @@ <_PublishedBinaries Remove="$(OutputPath)\**\*.pdb" Condition="'$(Configuration)' != 'Debug'" /> + + <_PublishedLibGit2NativeBinaries Include="$(OutputPath)\**\libgit2-*.so;$(OutputPath)\**\libgit2-*.dylib" /> + <_PublishedBinaries Remove="@(_PublishedLibGit2NativeBinaries)" /> + + <_PublishedBinaries Remove="@(_PublishedBinaries)" /> + <_PublishedLibGit2NativeBinaries Remove="@(_PublishedLibGit2NativeBinaries)" /> diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets deleted file mode 100644 index 025b2464..00000000 --- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - $([MSBuild]::GetPathOfFileAbove('version.json', '$(MSBuildProjectDirectory)')) - $([MSBuild]::GetPathOfFileAbove('.version.json', '$(MSBuildProjectDirectory)')) - - - - - - false - true - - - - - $([System.IO.Path]::GetDirectoryName('$(BV_VersionJsonPath)')) - - - - - - - - - - false - - - diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets deleted file mode 100644 index e15d8244..00000000 --- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - $(NpmPackageVersion) - - - - - diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets deleted file mode 100644 index 64a0dadd..00000000 --- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets b/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets new file mode 100644 index 00000000..79fc0863 --- /dev/null +++ b/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + true + false + + + + + $(HomeDirectory)VERSION + + + + + false + true + + + + + + + + + + false + + + + + true + + + diff --git a/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets new file mode 100644 index 00000000..be504a85 --- /dev/null +++ b/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets @@ -0,0 +1,72 @@ + + + + + false + + + + + + + + + + + + + + + + + + + $(BV_SemVer) + $(BV_SemVer) + $(BV_AssemblyVersion) + $(BV_FileVersion) + $(BV_InformationalVersion) + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets similarity index 67% rename from src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets rename to src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets index 409a7886..b6c735cb 100644 --- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets +++ b/src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets @@ -1,7 +1,7 @@ diff --git a/src/Buildvana.Sdk/Modules/Versioning/Module.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.targets new file mode 100644 index 00000000..3c7aa7f8 --- /dev/null +++ b/src/Buildvana.Sdk/Modules/Versioning/Module.targets @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/Buildvana.Sdk/Sdk/PackageVersions.props b/src/Buildvana.Sdk/Sdk/PackageVersions.props index 474bfc2e..51de1dc5 100644 --- a/src/Buildvana.Sdk/Sdk/PackageVersions.props +++ b/src/Buildvana.Sdk/Sdk/PackageVersions.props @@ -9,7 +9,6 @@ - diff --git a/src/Buildvana.Sdk/Sdk/Sdk.props b/src/Buildvana.Sdk/Sdk/Sdk.props index 54ee1c73..7116c677 100644 --- a/src/Buildvana.Sdk/Sdk/Sdk.props +++ b/src/Buildvana.Sdk/Sdk/Sdk.props @@ -113,6 +113,7 @@ $(BV_BinariesDir)Buildvana.Sdk.Tasks.dll + diff --git a/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj b/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj index 6409d9fb..d57601a8 100644 --- a/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj +++ b/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs new file mode 100644 index 00000000..e75397cb --- /dev/null +++ b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs @@ -0,0 +1,113 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Core.Testing; +using Buildvana.Sdk.Tasks; + +internal sealed class ComputeVersionTests +{ + [Test] + public async Task Execute_WithMissingHomeDirectory_LogsError() + { + var engine = new RecordingBuildEngine(); + var task = new ComputeVersion { BuildEngine = engine }; + await Assert.That(task.Execute()).IsFalse(); + await Assert.That(engine.Errors.Count).IsEqualTo(1); + await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050"); + } + + [Test] + public async Task Execute_StableNonPublic_ComputesVersionOutputs() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "1.2\n"); + repo.CommitAll(); + var task = RunTask(repo); + await Assert.That(task.SimpleVersion).IsEqualTo("1.2.1"); + await Assert.That(task.SemVer).IsEqualTo("1.2.1"); + await Assert.That(task.AssemblyVersion).IsEqualTo("1.0.0.0"); + await Assert.That(task.FileVersion).IsEqualTo("1.2.1.0"); + await Assert.That(task.InformationalVersion).IsEqualTo($"1.2.1-g{repo.HeadSha[..10]}"); + await Assert.That(task.IsPublicRelease).IsFalse(); + await Assert.That(task.IsPrerelease).IsFalse(); + await Assert.That(task.CommitId).IsEqualTo(repo.HeadSha); + await Assert.That(task.Height).IsEqualTo(1); + } + + [Test] + public async Task Execute_WithConfigurationFile_HonorsVersioningSettings() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "1.2-x\n"); + repo.WriteFile( + "buildvana.json", + """{ "release": { "branches": ["^rel$"] }, "versioning": { "prereleaseTag": "preview", "assemblyVersionPrecision": "minor" } }"""); + repo.CommitAll(); + repo.CheckoutNewBranch("rel"); + var task = RunTask(repo); + await Assert.That(task.SemVer).IsEqualTo("1.2.1-preview"); + await Assert.That(task.AssemblyVersion).IsEqualTo("1.2.0.0"); + await Assert.That(task.InformationalVersion).IsEqualTo("1.2.1-preview"); + await Assert.That(task.IsPublicRelease).IsTrue(); + await Assert.That(task.IsPrerelease).IsTrue(); + } + + [Test] + public async Task Execute_WithUnchangedRepositoryState_UsesCachedResult() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "1.2\n"); + repo.CommitAll(); + var firstEngine = new RecordingBuildEngine(); + _ = RunTask(repo, firstEngine); + var secondEngine = new RecordingBuildEngine(); + var second = RunTask(repo, secondEngine); + + // The versioning service logs the computed version; a cache hit computes nothing and stays silent. + await Assert.That(firstEngine.Messages.Count(m => m.Message!.Contains("Version 1.2.1", StringComparison.Ordinal))).IsEqualTo(1); + await Assert.That(secondEngine.Messages.Count(m => m.Message!.Contains("Version 1.2.1", StringComparison.Ordinal))).IsEqualTo(0); + await Assert.That(second.SemVer).IsEqualTo("1.2.1"); + await Assert.That(second.Height).IsEqualTo(1); + } + + [Test] + public async Task Execute_AfterNewCommit_RecomputesVersion() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "1.2\n"); + repo.CommitAll(); + var first = RunTask(repo); + await Assert.That(first.Height).IsEqualTo(1); + repo.CommitAll(); + var engine = new RecordingBuildEngine(); + var second = RunTask(repo, engine); + await Assert.That(second.Height).IsEqualTo(2); + await Assert.That(second.SemVer).IsEqualTo("1.2.2"); + await Assert.That(engine.Messages.Count(m => m.Message!.Contains("Version 1.2.2", StringComparison.Ordinal))).IsEqualTo(1); + } + + [Test] + public async Task Execute_AfterVersionFileChange_RecomputesVersion() + { + using var repo = new TempGitRepo(); + repo.WriteFile("VERSION", "1.2\n"); + repo.CommitAll(); + var first = RunTask(repo); + await Assert.That(first.SemVer).IsEqualTo("1.2.1"); + repo.WriteFile("VERSION", "1.3\n"); + var second = RunTask(repo); + await Assert.That(second.SemVer).IsEqualTo("1.3.0"); + await Assert.That(second.Height).IsEqualTo(0); + } + + private static ComputeVersion RunTask(TempGitRepo repo, RecordingBuildEngine? engine = null) + { + var task = new ComputeVersion + { + BuildEngine = engine ?? new RecordingBuildEngine(), + HomeDirectory = repo.RootPath, + }; + _ = task.Execute(); + return task; + } +} From 0fcbfbae39564c07e645aff8501adc82f7b3b8da Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:43:56 +0200 Subject: [PATCH 5/8] Route bv's release flow through Buildvana.Core.Versioning The tool's own versioning types (VersionSpec, VersionSpecChange, VersionFile, VersionIncrement) are deleted in favor of the library's, and the nbgv subprocess is gone: VersionService is now a thin release-flow facade over a recomputable VersioningService (ServerRelease refreshes the version after creating the release commit), keeping the tool-only policy methods (EnsureConsistency, ComputeVersionSpecChange) and the latest-version data from GitService. VersionIncrement survives as a private nested enum of the facade. The release flow reads and rewrites the VERSION file through the library's VersionFile, passing the configured versioning.prereleaseTag as the informational tag text. Co-Authored-By: Claude Fable 5 --- src/Buildvana.Tool/Buildvana.Tool.csproj | 1 + src/Buildvana.Tool/Program.cs | 2 + .../Services/Versioning/VersionFile.cs | 111 --------------- .../Services/Versioning/VersionIncrement.cs | 28 ---- .../VersionService.VersionIncrement.cs | 26 ++++ .../Services/Versioning/VersionService.cs | 82 ++++++----- .../Services/Versioning/VersionSpec.cs | 131 ------------------ .../Services/Versioning/VersionSpecChange.cs | 35 ----- .../Subcommands/ReleaseCommand.cs | 14 +- .../Subcommands/ReleaseSettings.cs | 2 +- .../ReleaseSettingsTests.cs | 2 +- 11 files changed, 78 insertions(+), 356 deletions(-) delete mode 100644 src/Buildvana.Tool/Services/Versioning/VersionFile.cs delete mode 100644 src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs create mode 100644 src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs delete mode 100644 src/Buildvana.Tool/Services/Versioning/VersionSpec.cs delete mode 100644 src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs diff --git a/src/Buildvana.Tool/Buildvana.Tool.csproj b/src/Buildvana.Tool/Buildvana.Tool.csproj index 0548890d..94355c2b 100644 --- a/src/Buildvana.Tool/Buildvana.Tool.csproj +++ b/src/Buildvana.Tool/Buildvana.Tool.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Buildvana.Tool/Program.cs b/src/Buildvana.Tool/Program.cs index 34fb76d0..0ef34ca6 100644 --- a/src/Buildvana.Tool/Program.cs +++ b/src/Buildvana.Tool/Program.cs @@ -10,6 +10,7 @@ using Buildvana.Core.HomeDirectory; using Buildvana.Core.Json; using Buildvana.Core.Process; +using Buildvana.Core.Versioning; using Buildvana.Tool.Build; using Buildvana.Tool.CommandLine; using Buildvana.Tool.Infrastructure.DependencyInjection; @@ -190,6 +191,7 @@ private static ServiceProvider BuildServiceProvider( .AddSingleton() .AddSingleton() .AddSingleton(ServerAdapter.Create) + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Buildvana.Tool/Services/Versioning/VersionFile.cs b/src/Buildvana.Tool/Services/Versioning/VersionFile.cs deleted file mode 100644 index 9a9a4af6..00000000 --- a/src/Buildvana.Tool/Services/Versioning/VersionFile.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System.Text.Json.Nodes; -using Buildvana.Core; -using Buildvana.Core.HomeDirectory; -using Buildvana.Core.Json; -using CommunityToolkit.Diagnostics; - -namespace Buildvana.Tool.Services.Versioning; - -/// -/// Represents the version.json file, for the purpose of applying version advances. -/// -internal sealed class VersionFile -{ - private const string VersionJsonPath = "version.json"; - private const string VersionPropertyName = "version"; - private const string DefaultFirstUnstableTag = "preview"; - - private readonly IJsonHelper _jsonHelper; - - private VersionFile( - IJsonHelper jsonHelper, - string absolutePath, - VersionSpec versionSpec, - string firstUnstableTag) - { - _jsonHelper = jsonHelper; - Path = absolutePath; - VersionSpec = versionSpec; - FirstUnstableTag = firstUnstableTag; - } - - /// - /// Gets the absolute path of the version.json file. - /// - public string Path { get; } - - /// - /// Gets a representing the "version" value in the version.json file. - /// - public VersionSpec VersionSpec { get; private set; } - - /// - /// Gets the unstable tag to use for version advances. - /// - /// Either the "release.firstUnstableTag" value read from version.json, or "preview" as a default value. - public string FirstUnstableTag { get; } - - /// - /// Constructs a instance by loading the repository's version.json file. - /// - /// The home directory provider used to resolve the path of version.json. - /// The JSON helper used to load and rewrite the file. - /// A newly-created , representing the loaded data. - public static VersionFile Load(IHomeDirectoryProvider home, IJsonHelper jsonHelper) - { - Guard.IsNotNull(home); - Guard.IsNotNull(jsonHelper); - - var path = System.IO.Path.GetFullPath(VersionJsonPath, home.HomeDirectory); - var json = jsonHelper.LoadObject(path); - var versionStr = jsonHelper.GetPropertyValue(json, VersionPropertyName, path + " file"); - BuildFailedException.ThrowIfNot(VersionSpec.TryParse(versionStr, out var versionSpec), $"{VersionJsonPath} contains invalid version specification '{versionStr}'."); - var firstUnstableTag = DefaultFirstUnstableTag; - var release = json["release"]; - if (release is not null) - { - var firstUnstableTagNode = release["firstUnstableTag"]; - if (firstUnstableTagNode is JsonValue firstUnstableTagValue && firstUnstableTagValue.TryGetValue(out var firstUnstableTagStr) && !string.IsNullOrEmpty(firstUnstableTagStr)) - { - firstUnstableTag = firstUnstableTagStr; - } - } - - return new(jsonHelper, path, versionSpec, firstUnstableTag); - } - - /// - /// Applies a version spec change to this instance. - /// - /// A constant representing the kind of change to apply. - /// If the property is actually changed as a result of , ; - /// otherwise, . - /// - /// This method does not save the modified version.json file; you will have to call - /// the method if this method returns . - /// - public bool ApplyVersionSpecChange(VersionSpecChange change) - { - (VersionSpec, var changed) = VersionSpec.ApplyChange(change, FirstUnstableTag); - return changed; - } - - /// - /// Saves the version.json file, possibly with a modified , back to the repository. - /// - public void Save() - { - var newVersion = VersionSpec.ToString(); - var rewritten = _jsonHelper.RewriteStringValues( - Path, - (propertyPath, _) => propertyPath is [VersionPropertyName] ? newVersion : null); - - // Load already validated that a top-level string "version" property exists, so a no-op here - // means either the on-disk file changed underneath us or VersionSpec.ToString() produced the - // same string the file already held — both cases would let the release flow stage stale data. - BuildFailedException.ThrowIfNot(rewritten, $"Could not update {VersionJsonPath}: expected a top-level string '{VersionPropertyName}' property to rewrite."); - } -} diff --git a/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs b/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs deleted file mode 100644 index 79dbc640..00000000 --- a/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -namespace Buildvana.Tool.Services.Versioning; - -/// -/// Specifies a kind of version increment. -/// -/// -/// The values of this enum are sorted in ascending order of importance, so that they may be compared. -/// -internal enum VersionIncrement -{ - /// - /// Represents no version advancement. - /// - None, - - /// - /// Represents an increment of minor version. - /// - Minor, - - /// - /// Represents an increment of major version and reset of minor version. - /// - Major, -} diff --git a/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs b/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs new file mode 100644 index 00000000..3974a692 --- /dev/null +++ b/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs @@ -0,0 +1,26 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Buildvana.Tool.Services.Versioning; + +partial class VersionService +{ + /// + /// Represents a version increment. + /// + /// + /// Constants are defined in ascending order of importance, so that they can be compared + /// with relational operators. + /// + private enum VersionIncrement + { + /// No version increment. + None, + + /// Increment of the minor version. + Minor, + + /// Increment of the major version. + Major, + } +} diff --git a/src/Buildvana.Tool/Services/Versioning/VersionService.cs b/src/Buildvana.Tool/Services/Versioning/VersionService.cs index 4228b6f7..b3d6af34 100644 --- a/src/Buildvana.Tool/Services/Versioning/VersionService.cs +++ b/src/Buildvana.Tool/Services/Versioning/VersionService.cs @@ -3,8 +3,8 @@ using Buildvana.Core; using Buildvana.Core.ConsoleOutput; -using Buildvana.Core.Json; -using Buildvana.Core.Process; +using Buildvana.Core.HomeDirectory; +using Buildvana.Core.Versioning; using Buildvana.Tool.Services.Git; using Buildvana.Tool.Services.PublicApiFiles; using CommunityToolkit.Diagnostics; @@ -13,42 +13,49 @@ namespace Buildvana.Tool.Services.Versioning; /// -/// Provides methods to manage project versioning. +/// Exposes the version being built, computed by , alongside +/// release-flow version policy: consistency checks against published versions and computation +/// of the version specification change to apply upon release. /// -internal sealed class VersionService +internal sealed partial class VersionService { private readonly IReporter _reporter; - private readonly IJsonHelper _jsonHelper; - private readonly IProcessRunner _processRunner; + private readonly IHomeDirectoryProvider _home; + private readonly VersioningSettings _settings; + private readonly GitHeightCalculator _heightCalculator; private readonly PublicApiFilesService _publicApiFiles; + private VersioningService _current; + /// /// Initializes a new instance of the class. /// public VersionService( IReporter reporter, - IJsonHelper jsonHelper, - IProcessRunner processRunner, + IHomeDirectoryProvider home, + VersioningSettings settings, GitService git, PublicApiFilesService publicApiFiles) { Guard.IsNotNull(reporter); - Guard.IsNotNull(jsonHelper); - Guard.IsNotNull(processRunner); + Guard.IsNotNull(home); + Guard.IsNotNull(settings); Guard.IsNotNull(git); Guard.IsNotNull(publicApiFiles); _reporter = reporter; - _jsonHelper = jsonHelper; - _processRunner = processRunner; + _home = home; + _settings = settings; _publicApiFiles = publicApiFiles; - (CurrentStr, Current, IsPublicRelease, IsPrerelease) = GetVersionInformationFromNbgv(); + _heightCalculator = new(VersionFile.FileName); + _current = new(reporter, home, settings, _heightCalculator); + Current = SemanticVersion.Parse(_current.SemVer); (Latest, LatestStable) = git.GetLatestVersions(); } /// - /// Gets the version to build, as a string computed by Nerdbank.GitVersioning. + /// Gets the version to build, as a string. /// - public string CurrentStr { get; private set; } + public string CurrentStr => _current.SemVer; /// /// Gets the version to build, as a SemanticVersion object. @@ -58,14 +65,14 @@ public VersionService( /// /// Gets a value indicating whether a public release can be built. /// - /// If Git's HEAD is on a public release branch, as indicated in version.json, ; - /// otherwise, . - public bool IsPublicRelease { get; private set; } + /// If Git's HEAD is on a public release branch, as configured in release.branches, + /// ; otherwise, . + public bool IsPublicRelease => _current.IsPublicRelease; /// /// Gets a value indicating whether the version to build is a prerelease. /// - public bool IsPrerelease { get; private set; } + public bool IsPrerelease => _current.IsPrerelease; /// /// Gets the latest published version, if any, as a SemanticVersion object. @@ -118,9 +125,9 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha { // Determine how we are currently already incrementing version var currentVersionIncrement = LatestStable == null ? VersionIncrement.None - : Current.Major > LatestStable.Major ? VersionIncrement.Major - : Current.Minor > LatestStable.Minor ? VersionIncrement.Minor - : VersionIncrement.None; + : Current.Major > LatestStable.Major ? VersionIncrement.Major + : Current.Minor > LatestStable.Minor ? VersionIncrement.Minor + : VersionIncrement.None; _reporter.Info($"Current version increment: {currentVersionIncrement}"); // Determine the kind of change in public API @@ -132,7 +139,8 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha // When the major version is 0, "anything MAY change" according to SemVer; // by convention, we increment the minor version for breaking changes (0.x -> 0.(x+1)) var isMajorVersionZero = LatestStable is { Major: 0 }; - var semanticVersionIncrement = publicApiChangeKind switch { + var semanticVersionIncrement = publicApiChangeKind switch + { ApiChangeKind.Breaking => isMajorVersionZero ? VersionIncrement.Minor : VersionIncrement.Major, ApiChangeKind.Additive => isMajorVersionZero ? VersionIncrement.None : VersionIncrement.Minor, _ => VersionIncrement.None, @@ -141,7 +149,8 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha // Determine the requested version increment, if any. _reporter.Info($"Requested version spec change: {requestedChange}"); - var requestedVersionIncrement = requestedChange switch { + var requestedVersionIncrement = requestedChange switch + { VersionSpecChange.Major => VersionIncrement.Major, VersionSpecChange.Minor => VersionIncrement.Minor, _ => VersionIncrement.None, @@ -161,11 +170,13 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha // Determine the actual version spec change to apply: // - forget any increment-related change (already accounted for via requestedVersionIncrement) // - set the change to the required increment if any, otherwise leave it as is (None, Unstable, Stable) - var actualChange = requestedChange switch { + var actualChange = requestedChange switch + { VersionSpecChange.Major or VersionSpecChange.Minor => VersionSpecChange.None, _ => requestedChange, }; - actualChange = actualVersionIncrement switch { + actualChange = actualVersionIncrement switch + { VersionIncrement.Major => VersionSpecChange.Major, VersionIncrement.Minor => VersionSpecChange.Minor, _ => actualChange, @@ -177,22 +188,9 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha /// /// Update version information, typically after a commit. /// - public void Update() => (CurrentStr, Current, IsPublicRelease, IsPrerelease) = GetVersionInformationFromNbgv(); - - private (string CurrentStr, SemanticVersion Current, bool IsPublicRelease, bool IsPrerelease) GetVersionInformationFromNbgv() + public void Update() { - // Block synchronously: this method runs from the constructor (and Update()), neither of which is async. - var result = _processRunner - .RunAsync("dotnet", ["nbgv", "get-version", "--format", "json"]) - .GetAwaiter() - .GetResult(); - - var json = _jsonHelper.ParseObject(result.StandardOutput, "The output of nbgv"); - var currentStr = _jsonHelper.GetPropertyValue(json, "SemVer2", "the output of nbgv"); - return ( - currentStr, - SemanticVersion.Parse(currentStr), - _jsonHelper.GetPropertyValue(json, "PublicRelease", "the output of nbgv"), - !string.IsNullOrEmpty(_jsonHelper.GetPropertyValue(json, "PrereleaseVersion", "the output of nbgv"))); + _current = new(_reporter, _home, _settings, _heightCalculator); + Current = SemanticVersion.Parse(_current.SemVer); } } diff --git a/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs b/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs deleted file mode 100644 index 31ff2e45..00000000 --- a/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Buildvana.Tool.Services.Versioning; - -/// -/// Represents a Major.Minor[-Tag] version as found in version.json. -/// -internal sealed partial record VersionSpec -{ - private static readonly Regex VersionSpecRegex = GetVersionSpecRegex(); - - private VersionSpec(int major, int minor, string tag) - { - Major = major; - Minor = minor; - Tag = tag; - } - - /// - /// Gets the major version. - /// - public int Major { get; } - - /// - /// Gets the minor version. - /// - public int Minor { get; } - - /// - /// Gets the current unstable tag. - /// - /// The current unstable tag, or the empty string if the current version is stable. - public string Tag { get; } - - /// - /// Gets a value indicating whether this instance has an unstable tag. - /// - public bool HasTag => !string.IsNullOrEmpty(Tag); - - /// - /// Attempts to parse a from the specified string. - /// - /// The string to parse. - /// When this method returns , a newly-created . - /// This parameter is passed uninitialized. - /// if successful, otherwise. - public static bool TryParse(string str, [MaybeNullWhen(false)] out VersionSpec result) - { - var match = VersionSpecRegex.Match(str); - if (!match.Success) - { - result = null; - return false; - } - - result = new( - int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), - int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture), - match.Groups["tag"].Value); - - return true; - } - - /// - public override string ToString() => $"{Major}.{Minor}{(HasTag ? "-" + Tag : null)}"; - - /// - /// Gets an instance of that represents the same version as the current instance and has no unstable tag. - /// - /// If this instance has no unstable tag, this instance; otherwise, a newly-created - /// that represents the same version as the current instance and has no unstable tag. - public VersionSpec Stable() => HasTag ? new(Major, Minor, string.Empty) : this; - - /// - /// Gets an instance of that represents the same version as the current instance and has the specified unstable tag. - /// - /// The unstable tag of the returned instance. - /// If this instance's property is equal to the given , this instance; - /// otherwise, a newly-created that represents the same version as the current instance and has the specified unstable tag. - public VersionSpec Unstable(string tag) => string.Equals(Tag, tag, StringComparison.Ordinal) ? this : new(Major, Minor, tag); - - /// - /// Gets an instance of that represents the next minor version with respect to the current instance and has the specified unstable tag. - /// - /// The unstable tag of the returned instance. - /// A newly-created . - public VersionSpec NextMinor(string tag) => new(Major, Minor + 1, tag); - - /// - /// Gets an instance of that represents the next major version with respect to the current instance and has the specified unstable tag. - /// - /// The unstable tag of the returned instance. - /// A newly-created . - public VersionSpec NextMajor(string tag) => new(Major + 1, 0, tag); - - /// - /// Gets an instance of that represents the result of applying the specified change to the current instance. - /// - /// A constant representing the kind of change to apply. - /// If the returned instance has an unstable tag, the unstable tag of the returned instance; otherwise, this parameter is ignored. - /// - /// A tuple of the following values: - /// - /// - /// Result - /// The result of applying to the current instance. - /// - /// - /// Changed - /// If Result is equal to the current instance, ; otherwise, . - /// - /// - /// - public (VersionSpec Result, bool Changed) ApplyChange(VersionSpecChange change, string tag) - => change switch { - VersionSpecChange.Unstable => HasTag ? (this, false) : (Unstable(tag), true), - VersionSpecChange.Stable => HasTag ? (Stable(), true) : (this, false), - VersionSpecChange.Minor => (NextMinor(tag), true), - VersionSpecChange.Major => (NextMajor(tag), true), - _ => (this, false), - }; - - [GeneratedRegex(@"(?-imsx)^v?(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(-(?.*))?$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant)] - private static partial Regex GetVersionSpecRegex(); -} diff --git a/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs b/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs deleted file mode 100644 index 8c3eeb35..00000000 --- a/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -namespace Buildvana.Tool.Services.Versioning; - -/// -/// Specifies how to modify the version specification upon publishing a release. -/// -internal enum VersionSpecChange -{ - /// - /// Do not force a version increment; do not modify the unstable tag. - /// - None, - - /// - /// Do not force a version increment; add an unstable tag if not present. - /// - Unstable, - - /// - /// Do not force a version increment; remove the unstable tag if present. - /// - Stable, - - /// - /// Force a minor version increment with respect to the latest stable version; add an unstable tag. - /// - Minor, - - /// - /// Force a major version increment and minor version reset with respect to the latest stable version; add an unstable tag. - /// - Major, -} diff --git a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs index 184d9390..acd393a9 100644 --- a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs +++ b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs @@ -12,7 +12,7 @@ using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; using Buildvana.Core.HomeDirectory; -using Buildvana.Core.Json; +using Buildvana.Core.Versioning; using Buildvana.Tool.Build; using Buildvana.Tool.Infrastructure; using Buildvana.Tool.Infrastructure.Execution; @@ -42,7 +42,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) await pipeline.RunThroughAsync(BuildStep.Test, configuration, cancellationToken).ConfigureAwait(false); var home = services.GetRequiredService(); - var jsonHelper = services.GetRequiredService(); + var versioningSettings = services.GetRequiredService(); var server = services.GetRequiredService(); var version = services.GetRequiredService(); var dotnet = services.GetRequiredService(); @@ -91,12 +91,12 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) // Modify version file if required. if (versionSpecChange != VersionSpecChange.None) { - var versionFile = VersionFile.Load(home, jsonHelper); - var previousVersionSpec = versionFile.VersionSpec; - if (versionFile.ApplyVersionSpecChange(versionSpecChange)) + var versionFile = VersionFile.Load(home); + var previousVersionSpec = versionFile.Spec; + if (versionFile.ApplyChange(versionSpecChange)) { - reporter.Info($"Version spec changed from {previousVersionSpec} to {versionFile.VersionSpec}."); - versionFile.Save(); + reporter.Info($"Version spec changed from {previousVersionSpec} to {versionFile.Spec}."); + versionFile.Save(versioningSettings.PrereleaseTag); release.UpdateRepository(versionFile.Path); } else diff --git a/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs b/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs index c19beb63..f0354ca5 100644 --- a/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs +++ b/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs @@ -7,9 +7,9 @@ using System.Text.RegularExpressions; using Buildvana.Core; using Buildvana.Core.Configuration; +using Buildvana.Core.Versioning; using Buildvana.Tool.CommandLine; using Buildvana.Tool.Services; -using Buildvana.Tool.Services.Versioning; using CommunityToolkit.Diagnostics; namespace Buildvana.Tool.Subcommands; diff --git a/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs b/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs index b36770f1..be023511 100644 --- a/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs +++ b/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs @@ -3,8 +3,8 @@ using Buildvana.Core; using Buildvana.Core.Configuration; +using Buildvana.Core.Versioning; using Buildvana.Tool.Services; -using Buildvana.Tool.Services.Versioning; using Buildvana.Tool.Subcommands; internal sealed class ReleaseSettingsTests From 0c46a2776064e5c4bddd26a58fa546ae0c0692df Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 03:44:08 +0200 Subject: [PATCH 6/8] Migrate the repository to native versioning and document the swap The repository gets its own VERSION file and buildvana.json, mirroring the policy in the retained version.json. version.json and the nbgv tool entry stay for now: the repository still self-hosts on the last published (NBGV-based) SDK, and they will be removed after the new SDK is published and the pins rolled (the post-publish tail of the migration). Documentation follows: the Versioning module's diagnostics replace the NerdbankGitVersioning section (BVSDK2000 reworded, BVSDK2001 added), the VERSION file is documented in the directory structure, and the changelog gains the native-versioning feature entries and the breaking changes for the NBGV removal, the UseNerdbankGitVersioning rename, the version.json migration, and the dropped version.json-only features. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- VERSION | 1 + buildvana.json | 9 +++++++++ docs/DirectoryStructure.md | 10 +++++++++- docs/SdkDiagnostics.md | 11 ++++++----- 6 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 VERSION create mode 100644 buildvana.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7d32c8..129c2a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - the GitHub token (`github.tokenEnv`, default `GITHUB_TOKEN`): names the environment variable that holds the token used for release operations. - `buildvana.json` accepts a `git.identity` (`{ name, email }`) section describing the author/committer for automated commits. It is validated and exposed, but not yet wired to release commits. - The `ThisAssemblyClass` SDK module, removed along with code generation tasks after v1.0.0-alpha.20, has been reintroduced. Setting the `GenerateThisAssemblyClass` property to `true` (default: `false`) in a C# project generates a `ThisAssembly` static class containing constants defined via `ThisAssemblyConstant` items, using the syntax documented in [docs/ConstantsSyntax.md](docs/ConstantsSyntax.md). A set of default constants (assembly version, company, product, etc.) is defined unless the `EnableDefaultThisAssemblyConstants` property is set to `false`; the class name and namespace can be customized via the `ThisAssemblyClassName` and `ThisAssemblyClassNamespace` properties. Unlike its previous incarnation, the feature is implemented as a Roslyn incremental source generator, and supports C# projects only: setting `GenerateThisAssemblyClass` to `true` in a project in any other language raises warning BVSDK2300. +- Buildvana now computes project versions natively, replacing Nerdbank.GitVersioning end-to-end (see the corresponding breaking change below). The single source of truth for the version is a plain-text `VERSION` file at the repository root, holding a `MAJOR.MINOR[-[tag]]` specification; the patch number is the Git height of the version line, i.e. the number of commits since the last change of `MAJOR.MINOR`, computed with the same rules as Nerdbank.GitVersioning (counting from 1 at the bump commit, longest path across merges; prerelease-only edits to `VERSION` do not reset the height). Versioning policy lives in `buildvana.json`: + - `release.branches`: regular expressions (matched against the short branch name) identifying branches that produce public releases. On other branches, and in detached-HEAD state, the build is a non-public release, and the informational version carries a short commit ID (`1.2.3-preview.g0123456789` on prereleases, `1.2.3-g0123456789` on stable versions); + - `versioning.prereleaseTag`: the effective prerelease tag (e.g. `preview`), required when `VERSION` marks a prerelease line (the tag text in `VERSION` itself is informational only); + - `versioning.assemblyVersionPrecision` (`major` | `minor` | `build`, default `major`): how much of the computed version goes into `AssemblyVersion`. + + With a `VERSION` file present, every project built with Buildvana SDK gets `$(Version)`, `$(PackageVersion)`, `$(AssemblyVersion)` (precision-controlled), `$(FileVersion)` (`MAJOR.MINOR.HEIGHT.0`), and `$(InformationalVersion)` computed at build time by a compiled task using LibGit2Sharp — no `git` executable and no external package required — under plain `dotnet build` as well as `MSBuild.exe` and `bv`. The `UseVersioning` property overrides the automatic module activation in both directions. `bv release` now computes versions the same way, in-process. +- Versioned C# projects get the generated `ThisAssembly` class by default: when the `Versioning` module is active, `GenerateThisAssemblyClass` defaults to `true`, and the module contributes versioning constants alongside the default assembly-attribute constants: `SimpleVersion`, `SemVer`, `IsPublicRelease`, `IsPrerelease`, and `GitCommitId`. Compared with Nerdbank.GitVersioning's `ThisAssembly`, the `GitCommitDate`/`GitCommitAuthorDate` and `PublicKey`/`PublicKeyToken` constants are not provided, while `AssemblyDescription`, `SimpleVersion`, and `SemVer` are new. ### Changes to existing features @@ -69,6 +76,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING CHANGE**: The `--main-branch` global option has been removed, along with `bv`'s main-branch discovery. Documentation generation on release is now gated by `release.generateDocsFrom` in `buildvana.json`: a list of regular expressions matched against the current short branch name (default `["^main$", "^master$"]`, reproducing the previous main/master discovery). The human-curated changelog permalink in generated release notes now points at the release branch itself rather than the discovered main branch. - **BREAKING CHANGE**: The `--unstable-changelog` and `--require-changelog` options of `bv release` have been removed with no CLI replacement; changelog policy is repository-stable, not per-invocation. Configure it in `buildvana.json` instead: `release.changelogUpdates` (`none` | `stable` | `all`, default `stable`) selects which releases update the changelog, and `release.emptyChangelog` provides substitute text for an empty "Unreleased changes" section (when unset, an empty section fails the release, matching the previous `--require-changelog` default of `true`). - `bv`'s build commands (`clean`, `restore`, `build`, `test`, `pack`) and `release` now observe cancellation. Pressing Ctrl-C (or a host cancelling the operation) stops the pipeline promptly: it stops launching further steps and terminates the running `dotnet` child process instead of waiting for it to finish, then `bv` exits with code 130. Partial build output may be left behind on cancellation; `bv clean` recovers. +- **BREAKING CHANGE**: Nerdbank.GitVersioning has been removed from Buildvana SDK and `bv`. Versions are now computed natively from a `VERSION` file and `buildvana.json` keys (see _New features_ above): the `NerdbankGitVersioning` SDK module is gone, the `Nerdbank.GitVersioning` package is no longer injected into projects, `version.json` is no longer read, and `bv` no longer invokes the `nbgv` CLI. The `GetBuildVersion` target name is retained (as a real target or a stub) for targets that depend on it. To migrate a repository: + - create a `VERSION` file at the repository root holding the `version` value from `version.json` (e.g. `2.0-preview`); Git heights remain comparable across the migration, so the computed version does not regress within a `MAJOR.MINOR` line; + - move `publicReleaseRefSpec` to `release.branches` in `buildvana.json`, converting refspec patterns to short branch-name regular expressions (e.g. `^refs/heads/main$` → `^main$`); + - move `release.firstUnstableTag` to `versioning.prereleaseTag`, and `assemblyVersion.precision` to `versioning.assemblyVersionPrecision`; + - delete `version.json` and remove `nbgv` from your tool manifest, if present. +- **BREAKING CHANGE**: The `UseNerdbankGitVersioning` property has been renamed to `UseVersioning`. The old name still works as an alias (when `UseVersioning` itself is not set), but raises warning BVSDK2001 and will be removed in a future version. +- **BREAKING CHANGE**: `version.json` features without a native equivalent are dropped: `pathFilters` is not supported (the version height is always computed from the whole history of the version line), nested `version.json` files are not supported (`VERSION` lives at the repository root only), and the NuGet package version scheme is fixed at SemVer 2.0. ### Bugs fixed in this release diff --git a/README.md b/README.md index 6b0bb143..6405d64a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ However, Buildvana SDK has already been used successfully in production, for bot ### Benefits - Helps you keep your project files clean and concise - even better than "plain" MSBuild SDKs -- Single source of truth for assembly versions (via [`Nerdbank.GitVersioning`(https://github.com/dotnet/Nerdbank.GitVersioning)]) +- Single source of truth for assembly versions (a plain-text `VERSION` file, with the patch number computed from Git height) - Single source of truth for package licenses and copyright notices - More auto-generated assembly information (`ClsCompliant`, `COMVisible`) - Automatic configuration of commonly-used code analyzers diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..6fc53064 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.0-preview diff --git a/buildvana.json b/buildvana.json new file mode 100644 index 00000000..e6f12306 --- /dev/null +++ b/buildvana.json @@ -0,0 +1,9 @@ +{ + "release": { + "branches": ["^main$", "^v\\d+\\.\\d+$"] + }, + "versioning": { + "prereleaseTag": "preview", + "assemblyVersionPrecision": "major" + } +} diff --git a/docs/DirectoryStructure.md b/docs/DirectoryStructure.md index d2ebef87..bb106520 100644 --- a/docs/DirectoryStructure.md +++ b/docs/DirectoryStructure.md @@ -268,4 +268,12 @@ TODO ## `VERSION` -TODO +The single source of truth for the version of your product: a plain-text file, in the home directory, holding a single `MAJOR.MINOR[-[tag]]` version specification, for example: + +```text +2.0-preview +``` + +The presence of `-` after the minor version marks a prerelease line; the tag text after it is optional and informational (the effective prerelease tag comes from the `versioning.prereleaseTag` key of `buildvana.json`). The patch number is not stored in the file: it is the Git height of the version line, i.e. the number of commits since the last change of `MAJOR.MINOR`. + +When a `VERSION` file is present, the Buildvana SDK computes `Version`, `AssemblyVersion`, `FileVersion`, and `InformationalVersion` for all projects in the repository; `bv` uses the same computation for releases and rewrites the file when advancing the version. diff --git a/docs/SdkDiagnostics.md b/docs/SdkDiagnostics.md index c746df99..da07ec70 100644 --- a/docs/SdkDiagnostics.md +++ b/docs/SdkDiagnostics.md @@ -16,7 +16,7 @@ - [StandardAnalyzers module (1700-1799)](#standardanalyzers-module-1700-1799) - [XmlDocumentation module (1800-1899)](#xmldocumentation-module-1800-1899) - [AlternatePack module (1900-1999)](#alternatepack-module-1900-1999) -- [NerdbankGitVersioning module (2000-2099)](#nerdbankgitversioning-module-2000-2099) +- [Versioning module (2000-2099)](#versioning-module-2000-2099) - [ReleaseAssetList module (2100-2199)](#releaseassetlist-module-2100-2199) - [Wine module (2200-2299)](#wine-module-2200-2299) - [ThisAssemblyClass module (2300-2399)](#thisassemblyclass-module-2300-2399) @@ -109,11 +109,12 @@ This module has no associated diagnostics. | BVSDK1901 | Error | InnoSetup script '...' referenced by '...' does not exist. | An `InnoSetup` item's `Script` metadata refers to a non-existing file. | | BVSDK1902 | Error | InnoSetup item '...' refers to non-existent PublishFolder '...'. | An `InnoSetup` item has a `SourcePublishFolder` metadata, but no `PublishFolder` item exists with the specified name. | -## NerdbankGitVersioning module (2000-2099) +## Versioning module (2000-2099) -| Code | Severity | Message | Description | -| --------- | :------: | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | -| BVSDK2000 | Error | Version specification JSON file not found. | A `version.json` or `.version.json` file for the project was not found within the repository root. | +| Code | Severity | Message | Description | +| --------- | :------: | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| BVSDK2000 | Error | Version file (VERSION) not found in home directory. | The `UseVersioning` property was explicitly set to `true`, but no `VERSION` file was found in the home directory. | +| BVSDK2001 | Warning | The UseNerdbankGitVersioning property is deprecated; use UseVersioning instead. | The project sets the `UseNerdbankGitVersioning` property, which is a deprecated alias for `UseVersioning` kept for compatibility with older projects. | ## ReleaseAssetList module (2100-2199) From b3609e937500099056a7dc2e2b3497b58be089ff Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 04:47:14 +0200 Subject: [PATCH 7/8] Drop a stale Nerdbank.GitVersioning mention from a release-flow comment The full-history requirement for the tag-existence check still holds, but it now comes from the native Git height computation, not from NBGV. Co-Authored-By: Claude Fable 5 --- src/Buildvana.Tool/Subcommands/ReleaseCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs index acd393a9..f235ca6e 100644 --- a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs +++ b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs @@ -180,7 +180,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) // Ensure that the release tag doesn't already exist. // This assumes that full repo history has been checked out; - // however, that is already a prerequisite for using Nerdbank.GitVersioning. + // however, that is already a prerequisite for computing the Git height. BuildFailedException.ThrowIfNot(!git.TagExists(version.CurrentStr), $"Tag '{version.CurrentStr}' already exists in repository."); // Artifact pass (Restore→Pack, no Clean): rebuild against the resolved version and make artifacts. From c5519904e66e914f3180da641359a44913f4ea02 Mon Sep 17 00:00:00 2001 From: Riccardo De Agostini Date: Fri, 31 Jul 2026 04:47:29 +0200 Subject: [PATCH 8/8] Key the ComputeVersion cache with an ordinal comparer Case-insensitive keying could make two case-differing home directories share a cache slot on case-sensitive filesystems. The fingerprint check already keeps that from ever serving stale data, but the collision would thrash the shared entry; ordinal keying gives each directory its own. Co-Authored-By: Claude Fable 5 --- src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs index ab614521..f1e8fc36 100644 --- a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs +++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs @@ -14,7 +14,7 @@ namespace Buildvana.Sdk.Tasks; partial class ComputeVersion { - private static readonly ConcurrentDictionary Cache = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal); private static CachedVersion GetOrComputeVersion(string homeDirectory, IReporter reporter) {