From 5818800168e6c4f3e06e8a1165c9344b7289c605 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 20:06:51 +0000 Subject: [PATCH 1/6] Tighten release version parsing and split suffix --- build.ps1 | 10 +++++++++- scripts/bumpVersion.ps1 | 23 +++++++++++++++++++++-- scripts/getReleaseNotes.ps1 | 29 +++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/build.ps1 b/build.ps1 index 221898a..49c7249 100644 --- a/build.ps1 +++ b/build.ps1 @@ -6,7 +6,15 @@ ###################################################################### $releaseNotes = Get-ReleaseNotes -MarkdownFile (Join-Path -Path $PSScriptRoot -ChildPath "RELEASE_NOTES.md") +Write-Output "Updating release metadata to $($releaseNotes.Version)" + +if ($releaseNotes.VersionSuffix.Length -gt 0) { + Write-Output "Release suffix: $($releaseNotes.VersionSuffix)" +} else { + Write-Output "Release is stable: no prerelease suffix" +} + # inject release notes into Directory.Buil UpdateVersionAndReleaseNotes -ReleaseNotesResult $releaseNotes -XmlFilePath (Join-Path -Path $PSScriptRoot -ChildPath "Directory.Build.props") -Write-Output "Added release notes $releaseNotes" \ No newline at end of file +Write-Output "Updated Directory.Build.props from $($releaseNotes.Version)" diff --git a/scripts/bumpVersion.ps1 b/scripts/bumpVersion.ps1 index cdc46ed..c7bde9d 100644 --- a/scripts/bumpVersion.ps1 +++ b/scripts/bumpVersion.ps1 @@ -11,9 +11,28 @@ function UpdateVersionAndReleaseNotes { $xmlContent = New-Object XML $xmlContent.Load($XmlFilePath) - # Update VersionPrefix and PackageReleaseNotes + if (-not $ReleaseNotesResult.VersionCore) { + throw "Get-ReleaseNotes did not return VersionCore for metadata update" + } + + # Update VersionPrefix/VersionSuffix and PackageReleaseNotes $versionPrefixElement = $xmlContent.SelectSingleNode("//VersionPrefix") - $versionPrefixElement.InnerText = $ReleaseNotesResult.Version + if (-not $versionPrefixElement) { + throw "Directory.Build.props is missing VersionPrefix" + } + + $versionPrefixElement.InnerText = $ReleaseNotesResult.VersionCore + + $versionSuffixElement = $xmlContent.SelectSingleNode("//VersionSuffix") + if (-not $versionSuffixElement) { + throw "Directory.Build.props is missing VersionSuffix" + } + + $versionSuffixElement.InnerText = $ReleaseNotesResult.VersionSuffix + + if ($ReleaseNotesResult.VersionSuffix.Length -eq 0) { + Write-Output "Updated release version suffix to empty (stable release)" + } $packageReleaseNotesElement = $xmlContent.SelectSingleNode("//PackageReleaseNotes") $packageReleaseNotesElement.InnerText = $ReleaseNotesResult.ReleaseNotes diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index 419d85f..9107308 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -10,11 +10,15 @@ function Get-ReleaseNotes { # Split content based on headers $sections = $content -split "####" + $versionPattern = '^(?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))(?:-(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$' + # Output object to store result $outputObject = [PSCustomObject]@{ - Version = $null - Date = $null - ReleaseNotes = $null + Version = $null + VersionCore = $null + VersionSuffix = $null + Date = $null + ReleaseNotes = $null } # Check if we have at least 3 sections (1. Before the header, 2. Header, 3. Release notes) @@ -24,12 +28,25 @@ function Get-ReleaseNotes { # Extract version and date from the header $headerParts = $header -split " ", 2 - if ($headerParts.Count -eq 2) { - $outputObject.Version = $headerParts[0] - $outputObject.Date = $headerParts[1] + if ($headerParts.Count -ge 1) { + $versionText = $headerParts[0] + + if ($versionText -notmatch $versionPattern) { + throw "Invalid release version '$versionText' in $MarkdownFile." + } + + $outputObject.Version = $versionText + $outputObject.VersionCore = $matches.core + $outputObject.VersionSuffix = if ($matches.suffix) { $matches.suffix } else { '' } + + if ($headerParts.Count -ge 2) { + $outputObject.Date = $headerParts[1] + } } $outputObject.ReleaseNotes = $releaseNotes + } else { + throw "Unable to parse release notes from $MarkdownFile." } # Return the output object From 71e7bd07f36372fe17413cefc457f4f3ef980129 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 20:14:49 +0000 Subject: [PATCH 2/6] Make release notes parsing line-based --- scripts/getReleaseNotes.ps1 | 81 ++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index 9107308..1bea887 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -5,10 +5,7 @@ function Get-ReleaseNotes { ) # Read markdown file content - $content = Get-Content -Path $MarkdownFile -Raw - - # Split content based on headers - $sections = $content -split "####" + $lines = Get-Content -Path $MarkdownFile $versionPattern = '^(?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))(?:-(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$' @@ -21,34 +18,70 @@ function Get-ReleaseNotes { ReleaseNotes = $null } - # Check if we have at least 3 sections (1. Before the header, 2. Header, 3. Release notes) - if ($sections.Count -ge 3) { - $header = $sections[1].Trim() - $releaseNotes = $sections[2].Trim() + if (-not $lines -or $lines.Count -eq 0) { + throw "Unable to parse release notes from $MarkdownFile." + } - # Extract version and date from the header - $headerParts = $header -split " ", 2 - if ($headerParts.Count -ge 1) { - $versionText = $headerParts[0] + # Find the first non-empty line to find the latest release header. + $headerLine = $null + $headerLineIndex = 0 + while ($headerLineIndex -lt $lines.Count) { + $candidate = $lines[$headerLineIndex].Trim() - if ($versionText -notmatch $versionPattern) { - throw "Invalid release version '$versionText' in $MarkdownFile." - } + if (-not [string]::IsNullOrWhiteSpace($candidate)) { + $headerLine = $candidate + break + } - $outputObject.Version = $versionText - $outputObject.VersionCore = $matches.core - $outputObject.VersionSuffix = if ($matches.suffix) { $matches.suffix } else { '' } + $headerLineIndex++ + } - if ($headerParts.Count -ge 2) { - $outputObject.Date = $headerParts[1] - } - } + if ($null -eq $headerLine) { + throw "Unable to parse release notes from $MarkdownFile." + } + + # Strip BOMs or other non-content leading chars. + $headerLine = $headerLine.TrimStart([char]0xFEFF, [char]0x200B) - $outputObject.ReleaseNotes = $releaseNotes - } else { + if (-not $headerLine.StartsWith("####")) { throw "Unable to parse release notes from $MarkdownFile." } + # Extract header text, then version/date. + $headerLine = $headerLine.Substring(4).Trim() + $headerLine = $headerLine -replace "\s*####\s*$", "" + + $headerParts = $headerLine -split " ", 2 + $versionText = $headerParts[0] + + if ($versionText -notmatch $versionPattern) { + throw "Invalid release version '$versionText' in $MarkdownFile." + } + + $outputObject.Version = $versionText + $outputObject.VersionCore = $matches.core + $outputObject.VersionSuffix = if ($matches.suffix) { $matches.suffix } else { '' } + + if ($headerParts.Count -ge 2) { + $outputObject.Date = $headerParts[1] + } + + # Grab release notes from this first section only. + $releaseNotesEndLine = $lines.Count + for ($i = $headerLineIndex + 1; $i -lt $lines.Count; $i++) { + if ($lines[$i].Trim().StartsWith("####")) { + $releaseNotesEndLine = $i + break + } + } + + if ($releaseNotesEndLine -gt $headerLineIndex + 1) { + $outputObject.ReleaseNotes = ($lines[($headerLineIndex + 1)..($releaseNotesEndLine - 1)] -join "`r`n").Trim() + } + else { + $outputObject.ReleaseNotes = "" + } + # Return the output object return $outputObject } From 148eb5f83613c4eccbfae314b02ecd9cc317bda9 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 20:17:26 +0000 Subject: [PATCH 3/6] Handle non-header lines in release parser --- scripts/getReleaseNotes.ps1 | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index 1bea887..e8f5771 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -22,28 +22,26 @@ function Get-ReleaseNotes { throw "Unable to parse release notes from $MarkdownFile." } - # Find the first non-empty line to find the latest release header. + # Find the first valid release header line. $headerLine = $null - $headerLineIndex = 0 - while ($headerLineIndex -lt $lines.Count) { - $candidate = $lines[$headerLineIndex].Trim() + $headerLineIndex = -1 + for ($i = 0; $i -lt $lines.Count; $i++) { + $candidate = $lines[$i].Trim() + $candidate = [regex]::Replace($candidate, '^[\uFEFF\u200B\u200C\u200D\u2060]+', '') - if (-not [string]::IsNullOrWhiteSpace($candidate)) { + if ($candidate.StartsWith('####')) { $headerLine = $candidate + $headerLineIndex = $i break } - - $headerLineIndex++ } if ($null -eq $headerLine) { throw "Unable to parse release notes from $MarkdownFile." } - # Strip BOMs or other non-content leading chars. - $headerLine = $headerLine.TrimStart([char]0xFEFF, [char]0x200B) - - if (-not $headerLine.StartsWith("####")) { + # Safety check after filtering out hidden leading characters. + if ($null -eq $headerLine -or -not $headerLine.StartsWith('####')) { throw "Unable to parse release notes from $MarkdownFile." } From 9e41614c11f38453c018ef82a55d56ad80f53027 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 20:21:25 +0000 Subject: [PATCH 4/6] Harden release note header matching --- scripts/getReleaseNotes.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index e8f5771..5c8a20b 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -22,14 +22,17 @@ function Get-ReleaseNotes { throw "Unable to parse release notes from $MarkdownFile." } + $leadingNoisePattern = '^[\p{Z}\p{Cf}\p{Cc}]+' + $headerPrefixPattern = '^([\p{Z}\p{Cf}\p{Cc}]*)####\s*' + # Find the first valid release header line. $headerLine = $null $headerLineIndex = -1 for ($i = 0; $i -lt $lines.Count; $i++) { - $candidate = $lines[$i].Trim() - $candidate = [regex]::Replace($candidate, '^[\uFEFF\u200B\u200C\u200D\u2060]+', '') + $candidate = [regex]::Replace($lines[$i], $leadingNoisePattern, '') - if ($candidate.StartsWith('####')) { + $headerMatch = [regex]::Match($candidate, $headerPrefixPattern) + if ($headerMatch.Success) { $headerLine = $candidate $headerLineIndex = $i break @@ -46,7 +49,7 @@ function Get-ReleaseNotes { } # Extract header text, then version/date. - $headerLine = $headerLine.Substring(4).Trim() + $headerLine = $headerLine -replace "^([\p{Z}\p{Cf}\p{Cc}]*)####\s*", "" $headerLine = $headerLine -replace "\s*####\s*$", "" $headerParts = $headerLine -split " ", 2 @@ -67,7 +70,9 @@ function Get-ReleaseNotes { # Grab release notes from this first section only. $releaseNotesEndLine = $lines.Count for ($i = $headerLineIndex + 1; $i -lt $lines.Count; $i++) { - if ($lines[$i].Trim().StartsWith("####")) { + $candidate = [regex]::Replace($lines[$i], $leadingNoisePattern, '') + + if ([regex]::IsMatch($candidate, $headerPrefixPattern)) { $releaseNotesEndLine = $i break } From 6da922250d63522f8f55f96d86007bfa0b99024b Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 20:25:23 +0000 Subject: [PATCH 5/6] Handle hidden header prefixes --- scripts/getReleaseNotes.ps1 | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index 5c8a20b..5b27153 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -22,17 +22,33 @@ function Get-ReleaseNotes { throw "Unable to parse release notes from $MarkdownFile." } - $leadingNoisePattern = '^[\p{Z}\p{Cf}\p{Cc}]+' - $headerPrefixPattern = '^([\p{Z}\p{Cf}\p{Cc}]*)####\s*' + function Get-LeadingNoiseFreeText { + param( + [Parameter()] + [AllowEmptyString()] + [string]$Line + ) + + while (-not [string]::IsNullOrEmpty($Line)) { + $firstCharacter = $Line[0] + if ([char]::IsWhiteSpace($firstCharacter) -or [char]::IsControl($firstCharacter) -or [System.Char]::GetUnicodeCategory($firstCharacter) -eq [System.Globalization.UnicodeCategory]::Format) { + $Line = $Line.Substring(1) + continue + } + + break + } + + return $Line + } # Find the first valid release header line. $headerLine = $null $headerLineIndex = -1 for ($i = 0; $i -lt $lines.Count; $i++) { - $candidate = [regex]::Replace($lines[$i], $leadingNoisePattern, '') + $candidate = Get-LeadingNoiseFreeText -Line $lines[$i] - $headerMatch = [regex]::Match($candidate, $headerPrefixPattern) - if ($headerMatch.Success) { + if ($candidate.StartsWith('####')) { $headerLine = $candidate $headerLineIndex = $i break @@ -43,13 +59,8 @@ function Get-ReleaseNotes { throw "Unable to parse release notes from $MarkdownFile." } - # Safety check after filtering out hidden leading characters. - if ($null -eq $headerLine -or -not $headerLine.StartsWith('####')) { - throw "Unable to parse release notes from $MarkdownFile." - } - # Extract header text, then version/date. - $headerLine = $headerLine -replace "^([\p{Z}\p{Cf}\p{Cc}]*)####\s*", "" + $headerLine = $headerLine -replace "^####\s*", "" $headerLine = $headerLine -replace "\s*####\s*$", "" $headerParts = $headerLine -split " ", 2 @@ -70,9 +81,9 @@ function Get-ReleaseNotes { # Grab release notes from this first section only. $releaseNotesEndLine = $lines.Count for ($i = $headerLineIndex + 1; $i -lt $lines.Count; $i++) { - $candidate = [regex]::Replace($lines[$i], $leadingNoisePattern, '') + $candidate = Get-LeadingNoiseFreeText -Line $lines[$i] - if ([regex]::IsMatch($candidate, $headerPrefixPattern)) { + if ($candidate.StartsWith('####')) { $releaseNotesEndLine = $i break } From fd458e73bc2d77430939e179a468e7fe00674c39 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 7 Jul 2026 21:18:01 +0000 Subject: [PATCH 6/6] Harden release note header parsing --- scripts/getReleaseNotes.ps1 | 85 +++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 index 5b27153..663112b 100644 --- a/scripts/getReleaseNotes.ps1 +++ b/scripts/getReleaseNotes.ps1 @@ -4,8 +4,12 @@ function Get-ReleaseNotes { [string]$MarkdownFile ) - # Read markdown file content - $lines = Get-Content -Path $MarkdownFile + # Read markdown file content explicitly as UTF-8 to avoid platform-dependent defaults. + # Fall back to UTF-16 only if the UTF-8 read returns no lines. + $lines = [System.IO.File]::ReadAllLines($MarkdownFile, [System.Text.Encoding]::UTF8) + if (-not $lines -or $lines.Count -eq 0) { + $lines = [System.IO.File]::ReadAllLines($MarkdownFile, [System.Text.Encoding]::Unicode) + } $versionPattern = '^(?(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))(?:-(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$' @@ -41,38 +45,77 @@ function Get-ReleaseNotes { return $Line } + + function Get-ReleaseHeaderCandidate { + param( + [Parameter()] + [AllowEmptyString()] + [string]$Line + ) + + $candidate = Get-LeadingNoiseFreeText -Line $Line + $headerIndex = $candidate.IndexOf('####') + if ($headerIndex -ge 0) { + return $candidate.Substring($headerIndex) + } + + return $null + } + + function Get-ReleaseHeaderData { + param( + [Parameter()] + [AllowEmptyString()] + [string]$Line + ) + + $candidate = Get-ReleaseHeaderCandidate -Line $Line + if (-not $candidate) { + return $null + } + + $normalizedHeaderLine = $candidate -replace "^####\s*", "" + $normalizedHeaderLine = $normalizedHeaderLine -replace "\s*####\s*$", "" + + $headerParts = $normalizedHeaderLine -split " ", 2 + $versionText = $headerParts[0] + + $versionMatch = [regex]::Match($versionText, $versionPattern) + if (-not $versionMatch.Success) { + return $null + } + + return [PSCustomObject]@{ + HeaderParts = $headerParts + VersionText = $versionText + VersionCore = $versionMatch.Groups['core'].Value + VersionSuffix = $versionMatch.Groups['suffix'].Value + } + } # Find the first valid release header line. - $headerLine = $null + $headerData = $null $headerLineIndex = -1 for ($i = 0; $i -lt $lines.Count; $i++) { - $candidate = Get-LeadingNoiseFreeText -Line $lines[$i] - - if ($candidate.StartsWith('####')) { - $headerLine = $candidate + $candidate = Get-ReleaseHeaderData -Line $lines[$i] + if ($candidate) { + $headerData = $candidate $headerLineIndex = $i break } } - if ($null -eq $headerLine) { + if ($null -eq $headerData) { throw "Unable to parse release notes from $MarkdownFile." } # Extract header text, then version/date. - $headerLine = $headerLine -replace "^####\s*", "" - $headerLine = $headerLine -replace "\s*####\s*$", "" - - $headerParts = $headerLine -split " ", 2 - $versionText = $headerParts[0] - - if ($versionText -notmatch $versionPattern) { - throw "Invalid release version '$versionText' in $MarkdownFile." - } + $headerParts = $headerData.HeaderParts + $versionText = $headerData.VersionText $outputObject.Version = $versionText - $outputObject.VersionCore = $matches.core - $outputObject.VersionSuffix = if ($matches.suffix) { $matches.suffix } else { '' } + $outputObject.VersionCore = $headerData.VersionCore + $outputObject.VersionSuffix = if ($headerData.VersionSuffix) { $headerData.VersionSuffix } else { '' } if ($headerParts.Count -ge 2) { $outputObject.Date = $headerParts[1] @@ -81,9 +124,7 @@ function Get-ReleaseNotes { # Grab release notes from this first section only. $releaseNotesEndLine = $lines.Count for ($i = $headerLineIndex + 1; $i -lt $lines.Count; $i++) { - $candidate = Get-LeadingNoiseFreeText -Line $lines[$i] - - if ($candidate.StartsWith('####')) { + if (Get-ReleaseHeaderData -Line $lines[$i]) { $releaseNotesEndLine = $i break }