Skip to content

Commit 6f6a779

Browse files
authored
Resolve differences on computation of CiBuildIndex between C# and PowerShell (#25)
1 parent 7a40003 commit 6f6a779

File tree

10 files changed

+153
-100
lines changed

10 files changed

+153
-100
lines changed

New-GeneratedVersionProps.ps1

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ using module "PSModules/RepoBuild/RepoBuild.psd1"
2222
[cmdletbinding()]
2323
Param(
2424
[hashtable]$buildInfo,
25-
[string]$Configuration="Release",
25+
[string]$Configuration='Release',
2626
[switch]$ForceClean
2727
)
2828

@@ -42,23 +42,34 @@ function ConvertTo-BuildIndex
4242
[DateTime]$timeStamp
4343
)
4444

45+
# This is VERY EXPLICIT on type conversions and truncation as there's a difference in the implicit conversion
46+
# between the C# for the tasks and library vs. PowerShell.
47+
# PowerShell:
48+
#> [UInt16]1.5
49+
# 2 <== Rounded UP!
50+
# But...
51+
# C#
52+
#> (ushort)1.5
53+
# 1 <== Truncated!
54+
# This needs to match the C# exactly so explicit behavior is used to unify the variances
55+
4556
$commonBaseDate = [DateTime]::new(2000, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)
4657

4758
$timeStamp = $timeStamp.ToUniversalTime()
4859
$midnightTodayUtc = [DateTime]::new($timeStamp.Year, $timeStamp.Month, $timeStamp.Day, 0, 0, 0, [DateTimeKind]::Utc)
4960

5061
$buildNumber = ([Uint32]($timeStamp - $commonBaseDate).Days) -shl 16
51-
$buildNUmber += [UInt16](($timeStamp - $midnightTodayUtc).TotalSeconds / 2)
62+
$buildNumber += [UInt16]([Math]::Truncate(($timeStamp - $midnightTodayUtc).TotalSeconds / 2))
5263

5364
return $buildNumber
5465
}
5566

5667
class PreReleaseVersion
5768
{
58-
[ValidateSet("alpha", "beta", "delta", "epsilon", "gamma", "kappa", "prerelease", "rc")]
69+
[ValidateSet('alpha', 'beta', 'delta', 'epsilon', 'gamma', 'kappa', 'prerelease', 'rc')]
5970
[string] $Name;
6071

61-
[ValidateSet("a", "b", "d", "e", "g", "k", "p", "r")]
72+
[ValidateSet('a', 'b', 'd', 'e', 'g', 'k', 'p', 'r')]
6273
[string] $ShortName;
6374

6475
[ValidateRange(-1,7)]
@@ -114,8 +125,8 @@ class PreReleaseVersion
114125
return $bldr.ToString()
115126
}
116127

117-
hidden static [string[]] $PreReleaseNames = @("alpha", "beta", "delta", "epsilon", "gamma", "kappa", "prerelease", "rc" );
118-
hidden static [string[]] $PreReleaseShortNames = @("a", "b", "d", "e", "g", "k", "p", "r");
128+
hidden static [string[]] $PreReleaseNames = @('alpha', 'beta', 'delta', 'epsilon', 'gamma', 'kappa', 'prerelease', 'rc' );
129+
hidden static [string[]] $PreReleaseShortNames = @('a', 'b', 'd', 'e', 'g', 'k', 'p', 'r');
119130

120131
hidden static [int] GetPrerelIndex([string] $preRelName)
121132
{
@@ -124,17 +135,17 @@ class PreReleaseVersion
124135
{
125136
$preRelIndex = [PreReleaseVersion]::PreReleaseNames |
126137
ForEach-Object {$index=0} {@{Name = $_; Index = $index++}} |
127-
Where-Object {$_["Name"] -ieq $preRelName} |
128-
ForEach-Object {$_["Index"]} |
138+
Where-Object {$_['Name'] -ieq $preRelName} |
139+
ForEach-Object {$_['Index']} |
129140
Select-Object -First 1
130141

131142
# if not found in long names, test against the short names
132143
if($preRelIndex -lt 0)
133144
{
134145
$preRelIndex = [PreReleaseVersion]::PreReleaseShortNames |
135146
ForEach-Object {$index=0} {@{Name = $_; Index = $index++}} |
136-
Where-Object {$_["Name"] -ieq $preRelName} |
137-
ForEach-Object {$_["Index"]} |
147+
Where-Object {$_['Name'] -ieq $preRelName} |
148+
ForEach-Object {$_['Index']} |
138149
Select-Object -First 1
139150
}
140151
}
@@ -171,31 +182,31 @@ class CSemVer
171182

172183
CSemVer([hashtable]$buildVersionData)
173184
{
174-
$this.Major = $buildVersionData["BuildMajor"]
175-
$this.Minor = $buildVersionData["BuildMinor"]
176-
$this.Patch = $buildVersionData["BuildPatch"]
177-
if($buildVersionData["PreReleaseName"])
185+
$this.Major = $buildVersionData['BuildMajor']
186+
$this.Minor = $buildVersionData['BuildMinor']
187+
$this.Patch = $buildVersionData['BuildPatch']
188+
if($buildVersionData['PreReleaseName'])
178189
{
179190
$this.PreReleaseVersion = [PreReleaseVersion]::new($buildVersionData)
180191
if(!$this.PreReleaseVersion)
181192
{
182-
throw "Internal ERROR: PreReleaseVersion version is NULL!"
193+
throw 'Internal ERROR: PreReleaseVersion version is NULL!'
183194
}
184195
}
185196

186-
$this.BuildMetadata = $buildVersionData["BuildMetadata"]
197+
$this.BuildMetadata = $buildVersionData['BuildMetadata']
187198

188-
$this.CiBuildName = $buildVersionData["CiBuildName"];
189-
$this.CiBuildIndex = $buildVersionData["CiBuildIndex"];
199+
$this.CiBuildName = $buildVersionData['CiBuildName'];
200+
$this.CiBuildIndex = $buildVersionData['CiBuildIndex'];
190201

191202
if( (![string]::IsNullOrEmpty( $this.CiBuildName )) -and [string]::IsNullOrEmpty( $this.CiBuildIndex ) )
192203
{
193-
throw "CiBuildIndex is required if CiBuildName is provided";
204+
throw 'CiBuildIndex is required if CiBuildName is provided';
194205
}
195206

196207
if( (![string]::IsNullOrEmpty( $this.CiBuildIndex )) -and [string]::IsNullOrEmpty( $this.CiBuildName ) )
197208
{
198-
throw "CiBuildName is required if CiBuildIndex is provided";
209+
throw 'CiBuildName is required if CiBuildIndex is provided';
199210
}
200211

201212
$this.OrderedVersion = [CSemVer]::GetOrderedVersion($this.Major, $this.Minor, $this.Patch, $this.PreReleaseVersion)
@@ -286,7 +297,7 @@ try
286297
$buildInfo = Initialize-BuildEnvironment -FullInit
287298
if(!$buildInfo -or $buildInfo -isnot [hashtable])
288299
{
289-
throw "build scripts BUSTED; Got null buildinfo hashtable..."
300+
throw 'build scripts BUSTED; Got null buildinfo hashtable...'
290301
}
291302
}
292303

@@ -296,18 +307,18 @@ try
296307
[string] $buildKind = Get-CurrentBuildKind
297308

298309
$verInfo = Get-ParsedBuildVersionXML -BuildInfo $buildInfo
299-
if($buildKind -ne "ReleaseBuild")
310+
if($buildKind -ne 'ReleaseBuild')
300311
{
301312
$verInfo['CiBuildIndex'] = ConvertTo-BuildIndex $env:BuildTime
302313
}
303314

304315
switch($buildKind)
305316
{
306-
"LocalBuild" { $verInfo['CiBuildName'] = "ZZZ" }
307-
"PullRequestBuild" { $verInfo['CiBuildName'] = "PRQ" }
308-
"CiBuild" { $verInfo['CiBuildName'] = "BLD" }
309-
"ReleaseBuild" { }
310-
default {throw "unknown build kind" }
317+
'LocalBuild' { $verInfo['CiBuildName'] = 'ZZZ' }
318+
'PullRequestBuild' { $verInfo['CiBuildName'] = 'PRQ' }
319+
'CiBuild' { $verInfo['CiBuildName'] = 'BLD' }
320+
'ReleaseBuild' { }
321+
default {throw 'unknown build kind' }
311322
}
312323

313324
# Generate props file with the version information for this build.
@@ -317,47 +328,51 @@ try
317328
# [Been there, done that, worn out the bloody T-Shirt...]
318329
$csemVer = [CSemVer]::New($verInfo)
319330
$xmlDoc = [System.Xml.XmlDocument]::new()
320-
$projectElement = $xmlDoc.CreateElement("Project")
331+
$projectElement = $xmlDoc.CreateElement('Project')
321332
$xmlDoc.AppendChild($projectElement) | Out-Null
322333

323-
$propGroupElement = $xmlDoc.CreateElement("PropertyGroup")
334+
$propGroupElement = $xmlDoc.CreateElement('PropertyGroup')
324335
$projectElement.AppendChild($propGroupElement) | Out-Null
325336

326-
$fileVersionElement = $xmlDoc.CreateElement("FileVersion")
337+
$fileVersionElement = $xmlDoc.CreateElement('FileVersion')
327338
$fileVersionElement.InnerText = $csemVer.FileVersion.ToString()
328339
$propGroupElement.AppendChild($fileVersionElement) | Out-Null
329340

330-
$packageVersionElement = $xmlDoc.CreateElement("PackageVersion")
341+
$packageVersionElement = $xmlDoc.CreateElement('PackageVersion')
331342
$packageVersionElement.InnerText = $csemVer.ToString($false,$true) # short form of version
332343
$propGroupElement.AppendChild($packageVersionElement) | Out-Null
333344

334-
$productVersionElement = $xmlDoc.CreateElement("ProductVersion")
345+
$productVersionElement = $xmlDoc.CreateElement('ProductVersion')
335346
$productVersionElement.InnerText = $csemVer.ToString($true, $false) # long form of version
336347
$propGroupElement.AppendChild($productVersionElement) | Out-Null
337348

338-
$assemblyVersionElement = $xmlDoc.CreateElement("AssemblyVersion")
349+
$assemblyVersionElement = $xmlDoc.CreateElement('AssemblyVersion')
339350
$assemblyVersionElement.InnerText = $csemVer.FileVersion.ToString()
340351
$propGroupElement.AppendChild($assemblyVersionElement) | Out-Null
341352

342-
$informationalVersionElement = $xmlDoc.CreateElement("InformationalVersion")
353+
$informationalVersionElement = $xmlDoc.CreateElement('InformationalVersion')
343354
$informationalVersionElement.InnerText = $csemVer.ToString($true, $false) # long form of version
344355
$propGroupElement.AppendChild($informationalVersionElement) | Out-Null
345356

346357
# Unit tests need to see the CI build info as it isn't something they can determine on their own.
347358
# The Build index is based on a timestamp and the build name depends on the runtime environment
348359
# to set some env vars etc...
349-
if($buildKind -ne "ReleaseBuild")
360+
if($buildKind -ne 'ReleaseBuild')
350361
{
351-
$ciBuildIndexElement = $xmlDoc.CreateElement("CiBuildIndex")
362+
$buildTimeElement = $xmlDoc.CreateElement('BuildTime')
363+
$buildTimeElement.InnerText = $env:BuildTime
364+
$propGroupElement.AppendChild($buildTimeElement) | Out-Null
365+
366+
$ciBuildIndexElement = $xmlDoc.CreateElement('CiBuildIndex')
352367
$ciBuildIndexElement.InnerText = $verInfo['CiBuildIndex']
353368
$propGroupElement.AppendChild($ciBuildIndexElement) | Out-Null
354369

355-
$ciBuildNameElement = $xmlDoc.CreateElement("CiBuildName")
370+
$ciBuildNameElement = $xmlDoc.CreateElement('CiBuildName')
356371
$ciBuildNameElement.InnerText = $verInfo['CiBuildName']
357372
$propGroupElement.AppendChild($ciBuildNameElement) | Out-Null
358373
}
359374

360-
$buildGeneratedPropsPath = Join-Path $buildInfo["RepoRootPath"] "GeneratedVersion.props"
375+
$buildGeneratedPropsPath = Join-Path $buildInfo['RepoRootPath'] 'GeneratedVersion.props'
361376
$xmlDoc.Save($buildGeneratedPropsPath)
362377
}
363378
catch
@@ -375,4 +390,4 @@ finally
375390
$env:Path = $oldPath
376391
}
377392

378-
Write-Information "Done build"
393+
Write-Information 'Done build'

src/Ubiquity.NET.Versioning.Build.Tasks/GetBuildIndexFromTime.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ public override bool Execute( )
2828
// establish an increasing build index based on the number of seconds from a common UTC date
2929
var timeStamp = TimeStamp.ToUniversalTime( );
3030
Log.LogMessage(MessageImportance.Low, $"Time Stamp(UTC; ISO-8601): {timeStamp:o}");
31+
var midnightUtc = new DateTime( timeStamp.Year, timeStamp.Month, timeStamp.Day, 0, 0, 0, DateTimeKind.Utc );
32+
Log.LogMessage(MessageImportance.Low, $"Midnight (UTC; ISO-8601): {midnightUtc:o}");
3133

3234
// Upper 16 bits of the build number is the number of days since the common base value
35+
// Lower 16 bits is the number of seconds (divided by 2) since midnight (on the date of the time stamp)
3336
uint buildNumber = ((uint)(timeStamp - CommonBaseDate).Days) << 16;
34-
Log.LogMessage(MessageImportance.Low, $"BuildNumber (upper): 0x{buildNumber:X04}");
37+
buildNumber += (ushort)((timeStamp - midnightUtc).TotalSeconds / 2);
3538

36-
// Lower 16 bits is the number of seconds (divided by 2) since midnight (on the date of the time stamp)
37-
var midnightTodayUtc = new DateTime( timeStamp.Year, timeStamp.Month, timeStamp.Day, 0, 0, 0, DateTimeKind.Utc );
38-
buildNumber += (ushort)((timeStamp - midnightTodayUtc).TotalSeconds / 2);
3939
Log.LogMessage(MessageImportance.Low, $"BuildNumber (full): 0x{buildNumber:X04}");
4040

4141
BuildIndex = buildNumber.ToString( CultureInfo.InvariantCulture );
42-
Log.LogMessage(MessageImportance.Low, $"BuildIndex set to: {BuildIndex}");
42+
Log.LogMessage(MessageImportance.Low, $"BuildIndex (string) set to: {BuildIndex}");
4343
Log.LogMessage(MessageImportance.Low, $"-{nameof(GetBuildIndexFromTime)} Task");
4444
return true;
4545
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="DateTimeExtensionsTests.cs" company="Ubiquity.NET Contributors">
3+
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
4+
// </copyright>
5+
// -----------------------------------------------------------------------
6+
7+
using System;
8+
using System.Globalization;
9+
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
12+
namespace Ubiquity.NET.Versioning.UT
13+
{
14+
[TestClass]
15+
public class DateTimeExtensionsTests
16+
{
17+
[TestMethod]
18+
public void ToBuildIndexTest( )
19+
{
20+
var timeStamp = new DateTime(2025, 5, 19, 17, 9, 0, DateTimeKind.Local);
21+
string index = timeStamp.ToBuildIndex();
22+
timeStamp = timeStamp.AddSeconds(1);
23+
string index2 = timeStamp.ToBuildIndex();
24+
Assert.AreEqual(index, index2, "Increment of only 1 second, results in same index value");
25+
26+
timeStamp = timeStamp.AddSeconds(1);
27+
index2 = timeStamp.ToBuildIndex();
28+
Assert.AreNotEqual(index, index2, "Increment of 2 seconds, results in different index value");
29+
}
30+
31+
[TestMethod]
32+
public void RoundTrippingProducesExpectedValue( )
33+
{
34+
// validate assumptions for framework consumers will use
35+
string testIso8601 = "2025-06-02T15:16:05.6936163Z";
36+
var testDt = DateTime.Parse(testIso8601, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
37+
string roundTrippedIso = testDt.ToString("o", CultureInfo.InvariantCulture);
38+
Assert.AreEqual(testIso8601, roundTrippedIso);
39+
40+
// Validate that a well known value is the result
41+
string actualIndex = testDt.ToBuildIndex();
42+
Assert.AreEqual("608463706", actualIndex);
43+
}
44+
}
45+
}

src/Ubiquity.NET.Versioning.UT/DateTimeOffsetExtensionsTests.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Ubiquity.NET.Versioning/CSemVer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public static CSemVer FromOrderedVersion(UInt64 orderedVersion, bool isCIBuild =
291291
/// <param name="ciBuildName">CI Build name for the build</param>
292292
/// <param name="buildMeta">Additional Build meta data for the build</param>
293293
/// <returns><see cref="CSemVer"/></returns>
294-
public static CSemVer From( string buildVersionXmlPath, DateTimeOffset timeStamp, string ciBuildName, string buildMeta )
294+
public static CSemVer From( string buildVersionXmlPath, DateTime timeStamp, string ciBuildName, string buildMeta )
295295
{
296296
string ciBuildIndex = timeStamp.ToBuildIndex( );
297297
var ciBuildInfo = new CiBuildInfo(ciBuildIndex, ciBuildName);

src/Ubiquity.NET.Versioning/CiBuildInfo.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public string ToString( string? format, IFormatProvider? formatProvider )
8282
/// <summary>Gets the Build name for this build</summary>
8383
public string BuildName { get; } = string.Empty;
8484

85-
private static readonly Regex CiBuildIdRegEx = new(@"\A[0-9a-zA-Z\-]+\Z");
85+
private static readonly Regex CiBuildIdRegEx = GetGeneratedBuildIdRegex();
86+
87+
[GeneratedRegex( @"\A[0-9a-zA-Z\-]+\Z" )]
88+
private static partial Regex GetGeneratedBuildIdRegex( );
8689
}
8790
}

src/Ubiquity.NET.Versioning/DateTimeOffsetExtensions.cs renamed to src/Ubiquity.NET.Versioning/DateTimeExtensions.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// -----------------------------------------------------------------------
2-
// <copyright file="DateTimeOffsetExtensions.cs" company="Ubiquity.NET Contributors">
2+
// <copyright file="DateTimeExtensions.cs" company="Ubiquity.NET Contributors">
33
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
44
// </copyright>
55
// -----------------------------------------------------------------------
@@ -9,8 +9,8 @@
99

1010
namespace Ubiquity.NET.Versioning
1111
{
12-
/// <summary>Utility Class for <see cref="DateTimeOffset"/> extensions</summary>
13-
public static class DateTimeOffsetExtensions
12+
/// <summary>Utility Class for <see cref="DateTime"/> extensions</summary>
13+
public static class DateTimeExtensions
1414
{
1515
/// <summary>Gets a build index based on a time stamp</summary>
1616
/// <param name="timeStamp">Time stamp to use to create the build index</param>
@@ -25,18 +25,18 @@ public static class DateTimeOffsetExtensions
2525
/// is only used for local builds that's not realistically a problem. (Automated builds use the
2626
/// commit hash of the repo as the build index)
2727
/// </remarks>
28-
public static string ToBuildIndex( this DateTimeOffset timeStamp )
28+
public static string ToBuildIndex( this DateTime timeStamp )
2929
{
3030
// establish an increasing build index based on the number of seconds from a common UTC date
3131
timeStamp = timeStamp.ToUniversalTime( );
32+
var midnightUtc = new DateTime( timeStamp.Year, timeStamp.Month, timeStamp.Day, 0, 0, 0, DateTimeKind.Utc );
3233

33-
// Upper 16 bits of the build number is the number of days since the common base value
34-
uint buildNumber = ((uint)(timeStamp - CommonBaseDate).Days) << 16;
35-
34+
// Upper 16 bits of the build index is the number of days since the common base value
3635
// Lower 16 bits is the number of seconds (divided by 2) since midnight (on the date of the time stamp)
37-
var midnightTodayUtc = new DateTime( timeStamp.Year, timeStamp.Month, timeStamp.Day, 0, 0, 0, DateTimeKind.Utc );
38-
buildNumber += (ushort)((timeStamp - midnightTodayUtc).TotalSeconds / 2);
39-
return buildNumber.ToString( CultureInfo.InvariantCulture );
36+
uint buildIndex = ((uint)(timeStamp - CommonBaseDate).Days) << 16;
37+
buildIndex += (ushort)((timeStamp - midnightUtc).TotalSeconds / 2);
38+
39+
return buildIndex.ToString( CultureInfo.InvariantCulture );
4040
}
4141

4242
// Fixed point in time to use as reference for a build index.

0 commit comments

Comments
 (0)