Build identity: version + git hash synced to releases #24

Merged
KofTwentyTwo merged 1 commits from dev into main 2026-07-04 19:58:30 +00:00
7 changed files with 100 additions and 37 deletions

No files matched your search

+10
View File
@@ -10,4 +10,14 @@
<Deterministic>true</Deterministic>
</PropertyGroup>
<!-- Single version source. Local builds are 0.1.0-dev; release builds pass an
explicit -p:Version=<tag-version>, which overrides both parts. The SDK's
built-in SourceLink appends the git commit as "+<sha>" build metadata on
AssemblyInformationalVersion, which the About dialog and the CLI's
version output surface as the short hash. -->
<PropertyGroup>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -234,5 +234,5 @@ done
```console
$ gclo --version
0.1.0
0.1.0-beta.6 (9ac6c2bde)
```
+4 -13
View File
@@ -1,4 +1,4 @@
using System.Reflection;
using gclo.Engine;
namespace gclo.Cli;
@@ -102,16 +102,7 @@ internal static class Program
}
private static string Version()
{
Assembly assembly = typeof(Program).Assembly;
string version =
assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "unknown";
// Strip the '+<metadata>' suffix (e.g. a SourceLink commit hash); it is
// noise for a scripted version check.
int plus = version.IndexOf('+');
return plus >= 0 ? version[..plus] : version;
}
// Shared with the GUI's About dialog: "0.1.0-beta.6 (9ac6c2b)" for
// releases, "0.1.0-dev (<hash>)" for local builds.
=> BuildVersion.Describe(typeof(Program).Assembly);
}
-1
View File
@@ -8,7 +8,6 @@
<AssemblyName>gclo</AssemblyName>
<RootNamespace>gclo.Cli</RootNamespace>
<InvariantGlobalization>true</InvariantGlobalization>
<Version>0.1.0</Version>
</PropertyGroup>
<ItemGroup>
+30
View File
@@ -0,0 +1,30 @@
using gclo.Engine;
namespace gclo.Engine.Tests;
/// <summary>Formatting of the version + git-hash build identity string.</summary>
public sealed class BuildVersionTests
{
[Theory]
[InlineData(null, "unknown")]
[InlineData("", "unknown")]
[InlineData(" ", "unknown")]
[InlineData("1.2.3", "1.2.3")]
[InlineData("0.1.0-dev", "0.1.0-dev")]
[InlineData("0.1.0-beta.6+9ac6c2b1234567890abcdef", "0.1.0-beta.6 (9ac6c2b12)")]
[InlineData("1.2.3+abc", "1.2.3 (abc)")]
[InlineData("1.2.3+9ac6c2b1234567890.dirty", "1.2.3 (9ac6c2b12)")]
[InlineData("1.2.3+", "1.2.3")]
public void Format_ProducesReadableIdentity(string? informational, string expected)
{
Assert.Equal(expected, BuildVersion.Format(informational));
}
[Fact]
public void Describe_UsesThisAssembly_AndNeverThrows()
{
string described = BuildVersion.Describe(typeof(BuildVersionTests).Assembly);
Assert.False(string.IsNullOrWhiteSpace(described));
}
}
+49
View File
@@ -0,0 +1,49 @@
using System.Reflection;
namespace gclo.Engine;
/// <summary>
/// Human-readable build identity shared by the app's About dialog and the CLI's
/// version output: the semantic version plus the short git commit the build came
/// from, e.g. "0.1.0-beta.6 (9ac6c2b)" for releases or "0.1.0-dev (3a87f9e)" for
/// local builds.
/// </summary>
public static class BuildVersion
{
private const int ShortHashLength = 9;
/// <summary>Describes <paramref name="assembly"/>'s version and commit.</summary>
public static string Describe(Assembly assembly)
=> Format(assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString());
/// <summary>
/// Formats an informational version: the "+&lt;metadata&gt;" suffix that SourceLink
/// appends becomes a parenthesized short commit hash.
/// </summary>
public static string Format(string? informationalVersion)
{
if (string.IsNullOrWhiteSpace(informationalVersion))
{
return "unknown";
}
int plus = informationalVersion.IndexOf('+');
if (plus < 0)
{
return informationalVersion;
}
string version = informationalVersion[..plus];
string metadata = informationalVersion[(plus + 1)..];
// Metadata may carry more than the sha (dot-separated); the hash is first.
int dot = metadata.IndexOf('.');
string hash = dot >= 0 ? metadata[..dot] : metadata;
if (hash.Length > ShortHashLength)
{
hash = hash[..ShortHashLength];
}
return hash.Length == 0 ? version : $"{version} ({hash})";
}
}
+6 -22
View File
@@ -18,29 +18,13 @@ namespace gclo
}
/// <summary>
/// Returns the app version formatted as "major.minor.build". Prefers the MSIX
/// package version; Package.Current throws when the app runs unpackaged
/// (no package identity), in which case the assembly version is used instead.
/// Returns the build identity synced to the release pipeline: the semantic
/// version CI injects for the tag (0.1.0-dev on local builds) plus the short
/// git commit, e.g. "0.1.0-beta.6 (9ac6c2b)". The MSIX package version is
/// deliberately not used — its numeric quad cannot carry prerelease tags or
/// commit hashes, so it cannot distinguish a local build from a release.
/// </summary>
private static string GetAppVersion()
{
try
{
var packageVersion = Windows.ApplicationModel.Package.Current.Id.Version;
return $"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}";
}
catch (Exception)
{
Version? assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
if (assemblyVersion is null)
{
return "1.0.0";
}
// Version.Build is -1 when the assembly version has fewer than three parts.
int build = assemblyVersion.Build < 0 ? 0 : assemblyVersion.Build;
return $"{assemblyVersion.Major}.{assemblyVersion.Minor}.{build}";
}
}
=> gclo.Engine.BuildVersion.Describe(Assembly.GetExecutingAssembly());
}
}