From 19af99e1308eda1dc1468313cfc71573fbc34fa3 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:54:44 -0700 Subject: [PATCH 01/15] add initial tests for post-release --- eng/common-tests/SemVer.Tests.ps1 | 304 ++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 eng/common-tests/SemVer.Tests.ps1 diff --git a/eng/common-tests/SemVer.Tests.ps1 b/eng/common-tests/SemVer.Tests.ps1 new file mode 100644 index 00000000000..5a58358cd42 --- /dev/null +++ b/eng/common-tests/SemVer.Tests.ps1 @@ -0,0 +1,304 @@ +Import-Module Pester + +BeforeAll { + . $PSScriptRoot/../common/scripts/SemVer.ps1 +} + +Describe "Post-release version parsing - Default convention (negative tests for non-Python languages)" { + BeforeEach { + $global:Language = "" + } + + It "Should parse '1.0.0-post.1' as prerelease label 'post', NOT as post-release" { + $ver = [AzureEngSemanticVersion]::ParseVersionString("1.0.0-post.1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.PrereleaseLabel | Should -Be "post" + $ver.PrereleaseNumber | Should -Be 1 + $ver.IsPrerelease | Should -BeTrue + $ver.IsPostRelease | Should -BeFalse + } + + It "Should fail to parse '2.0.0-beta.1-post.1' (regex doesn't match)" { + $ver = [AzureEngSemanticVersion]::ParseVersionString("2.0.0-beta.1-post.1") + $ver | Should -BeNullOrEmpty + } + + It "Should fail to parse '1.2.3-alpha.20200828.9-post.3' (regex doesn't match)" { + $ver = [AzureEngSemanticVersion]::ParseVersionString("1.2.3-alpha.20200828.9-post.3") + $ver | Should -BeNullOrEmpty + } +} + +Describe "Post-release version parsing - Python convention" { + It "Should parse GA post-release '1.0.0.post1'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 1 + $ver.Minor | Should -Be 0 + $ver.Patch | Should -Be 0 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.PrereleaseLabel | Should -BeNullOrEmpty + $ver.IsPrerelease | Should -BeFalse + $ver.VersionType | Should -Be "GA" + } + + It "Should parse patch version post-release '1.2.3.post5'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.2.3.post5") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 1 + $ver.Minor | Should -Be 2 + $ver.Patch | Should -Be 3 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 5 + $ver.PrereleaseLabel | Should -BeNullOrEmpty + $ver.IsPrerelease | Should -BeFalse + $ver.VersionType | Should -Be "Patch" + } + + It "Should parse beta prerelease post-release '1.0.0b2.post1'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2.post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 1 + $ver.Minor | Should -Be 0 + $ver.Patch | Should -Be 0 + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.IsPrerelease | Should -BeTrue + $ver.VersionType | Should -Be "Beta" + } + + It "Should parse alpha prerelease post-release '2.0.0a20201208001.post2'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("2.0.0a20201208001.post2") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 2 + $ver.Minor | Should -Be 0 + $ver.Patch | Should -Be 0 + $ver.PrereleaseLabel | Should -Be "a" + $ver.PrereleaseNumber | Should -Be 20201208 + $ver.BuildNumber | Should -Be "001" + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 2 + $ver.IsPrerelease | Should -BeTrue + } + + It "Should parse zero-major post-release '0.1.0.post1'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("0.1.0.post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 0 + $ver.Minor | Should -Be 1 + $ver.Patch | Should -Be 0 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.IsPrerelease | Should -BeTrue + $ver.VersionType | Should -Be "Beta" + } +} + +Describe "Post-release version ToString round-trip - Default convention (non-Python languages don't support post-release, so should round-trip as prerelease)" { + BeforeEach { + $global:Language = "" + } + + It "'1.0.0-post.1' round-trips as prerelease (label 'post'), not post-release" { + $ver = [AzureEngSemanticVersion]::ParseVersionString("1.0.0-post.1") + $ver.ToString() | Should -Be "1.0.0-post.1" + $ver.IsPostRelease | Should -BeFalse + $ver.PrereleaseLabel | Should -Be "post" + } +} + +Describe "PEP 440 alternate post-release format normalization - Python convention" { + It "Should normalize hyphen separator '1.0.0-post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0-post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should normalize underscore separator '1.0.0_post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0_post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should normalize no-separator '1.0.0post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0post1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should normalize dot-number separator '1.0.0.post.1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post.1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should normalize uppercase '1.0.0.POST1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.POST1") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should normalize prerelease hyphen separator '1.0.0b2-post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2-post1") + $ver | Should -Not -BeNullOrEmpty + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0b2.post1" + } + + It "Should normalize prerelease underscore separator '1.0.0b2_post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2_post1") + $ver | Should -Not -BeNullOrEmpty + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0b2.post1" + } + + It "Should normalize prerelease no-separator '1.0.0b2post1' to canonical form" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2post1") + $ver | Should -Not -BeNullOrEmpty + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 1 + $ver.ToString() | Should -Be "1.0.0b2.post1" + } +} + +Describe "Post-release version ToString round-trip - Python convention" { + It "Should round-trip GA post-release '1.0.0.post1'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post1") + $ver.ToString() | Should -Be "1.0.0.post1" + } + + It "Should round-trip patch version post-release '1.2.3.post5'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.2.3.post5") + $ver.ToString() | Should -Be "1.2.3.post5" + } + + It "Should round-trip beta prerelease post-release '1.0.0b2.post1'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2.post1") + $ver.ToString() | Should -Be "1.0.0b2.post1" + } + + It "Should round-trip alpha prerelease post-release '2.0.0a20201208001.post2'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("2.0.0a20201208001.post2") + $ver.ToString() | Should -Be "2.0.0a20201208001.post2" + } +} + +Describe "Post-release version sorting - Python convention" { + BeforeAll { + $global:Language = "python" + } + + AfterAll { + $global:Language = "" + } + + It "Should sort GA post-releases after GA and before next patch" { + $versions = @( + "1.0.1", + "1.0.0.post2", + "1.0.0", + "1.0.0.post1" + ) + $expectedSort = @( + "1.0.1", + "1.0.0.post2", + "1.0.0.post1", + "1.0.0" + ) + $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) + for ($i = 0; $i -lt $expectedSort.Count; $i++) { + $sort[$i] | Should -Be $expectedSort[$i] + } + } + + It "Should sort prerelease post-releases after prerelease and before next prerelease" { + $versions = @( + "1.0.0", + "1.0.0b2", + "1.0.0b1.post1", + "1.0.0b1" + ) + $expectedSort = @( + "1.0.0", + "1.0.0b2", + "1.0.0b1.post1", + "1.0.0b1" + ) + $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) + for ($i = 0; $i -lt $expectedSort.Count; $i++) { + $sort[$i] | Should -Be $expectedSort[$i] + } + } + + It "Should sort mixed versions with post-releases correctly" { + $versions = @( + "2.0.0", + "1.0.0.post1", + "2.0.0b1", + "1.0.0", + "2.0.0b1.post1", + "2.0.0.post1", + "1.0.1" + ) + $expectedSort = @( + "2.0.0.post1", + "2.0.0", + "2.0.0b1.post1", + "2.0.0b1", + "1.0.1", + "1.0.0.post1", + "1.0.0" + ) + $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) + for ($i = 0; $i -lt $expectedSort.Count; $i++) { + $sort[$i] | Should -Be $expectedSort[$i] + } + } +} + +Describe "Post-release version increment - Python convention" { + It "Should increment GA post-release '1.0.0.post1' to next prerelease" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post1") + $ver.IncrementAndSetToPrerelease() + $ver.ToString() | Should -Be "1.1.0b1" + } + + It "Should increment beta post-release '1.0.0b2.post1' to next prerelease number" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2.post1") + $ver.IncrementAndSetToPrerelease() + $ver.ToString() | Should -Be "1.0.0b3" + } + + It "Should increment zero-major post-release '0.1.0.post1' to next minor" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("0.1.0.post1") + $ver.IncrementAndSetToPrerelease() + $ver.ToString() | Should -Be "0.2.0" + } +} From 6ab8c1e08a9209aca59790d37795ff0f9460095e Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:15:20 -0700 Subject: [PATCH 02/15] support post-release with new python semver regex --- eng/common/scripts/ChangeLog-Operations.ps1 | 5 +- eng/common/scripts/SemVer.ps1 | 82 ++++++++++++++++--- .../scripts/artifact-metadata-parsing.ps1 | 2 +- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/eng/common/scripts/ChangeLog-Operations.ps1 b/eng/common/scripts/ChangeLog-Operations.ps1 index 170157ff511..98126a695c2 100644 --- a/eng/common/scripts/ChangeLog-Operations.ps1 +++ b/eng/common/scripts/ChangeLog-Operations.ps1 @@ -3,6 +3,7 @@ . "${PSScriptRoot}\SemVer.ps1" $RELEASE_TITLE_REGEX = "(?^\#+\s+(?$([AzureEngSemanticVersion]::SEMVER_REGEX))(\s+(?\(.+\))))" +$PYTHON_RELEASE_TITLE_REGEX = "(?^\#+\s+(?$([AzureEngSemanticVersion]::PYTHON_SEMVER_REGEX))(\s+(?\(.+\))))" $SECTION_HEADER_REGEX_SUFFIX = "##\s(?.*)" $CHANGELOG_UNRELEASED_STATUS = "(Unreleased)" $CHANGELOG_DATE_FORMAT = "yyyy-MM-dd" @@ -62,11 +63,13 @@ function Get-ChangeLogEntriesFromContent { $changeLogEntries | Add-Member -NotePropertyName "InitialAtxHeader" -NotePropertyValue $initialAtxHeader $releaseTitleAtxHeader = $initialAtxHeader + "#" $headerLines = @() + $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") + $titleRegex = if ($parseLanguage -eq "python") { $PYTHON_RELEASE_TITLE_REGEX } else { $RELEASE_TITLE_REGEX } try { # walk the document, finding where the version specifiers are and creating lists foreach ($line in $changeLogContent) { - if ($line -match $RELEASE_TITLE_REGEX) { + if ($line -match $titleRegex) { $changeLogEntry = [pscustomobject]@{ ReleaseVersion = $matches["version"] ReleaseStatus = $matches["releaseStatus"] diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index 689a70e778c..cccb7d180a0 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -30,10 +30,18 @@ class AzureEngSemanticVersion : IComparable { [bool] $IsSemVerFormat [string] $DefaultPrereleaseLabel [string] $DefaultAlphaReleaseLabel + # For Python PEP440 post-release support only + [bool] $IsPostRelease + [int] $PostReleaseNumber + [string] $PostReleaseSeparator # Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string # Validation: https://regex101.com/r/vkijKf/426 - static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" + static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" + + # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. + # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N (case-insensitive) + static [string] $PYTHON_SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?(?:(?[.\-_]?)(?i:post)\.?(?\d+))?" static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) { @@ -47,19 +55,34 @@ class AzureEngSemanticVersion : IComparable { static [AzureEngSemanticVersion] ParsePythonVersionString([string] $versionString) { - $version = [AzureEngSemanticVersion]::ParseVersionString($versionString) + $previousLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") + $global:Language = "python" + $version = $null + try { + $version = [AzureEngSemanticVersion]::new($versionString) + } + finally { + $global:Language = $previousLanguage + } - if (!$version) { + if (!$version.IsSemVerFormat) { return $null } - - $version.SetupPythonConventions() return $version } AzureEngSemanticVersion([string] $versionString) { - if ($versionString -match "^$([AzureEngSemanticVersion]::SEMVER_REGEX)$") + $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") + + if ($parseLanguage -eq "python") { + $parseRegex = [AzureEngSemanticVersion]::PYTHON_SEMVER_REGEX + } + else { + $parseRegex = [AzureEngSemanticVersion]::SEMVER_REGEX + } + + if ($versionString -match "^${parseRegex}$") { $this.IsSemVerFormat = $true $this.RawVersion = $versionString @@ -67,16 +90,28 @@ class AzureEngSemanticVersion : IComparable { $this.Minor = [int]$matches.Minor $this.Patch = [int]$matches.Patch - # If Language exists and is set to python setup the python conventions. - $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") + $skipPrelabel = $false if ($parseLanguage -eq "python") { $this.SetupPythonConventions() + if ($matches['postnum']) { + $this.IsPostRelease = $true + $this.PostReleaseNumber = [int]$matches['postnum'] + $this.PostReleaseSeparator = ".post" + } + elseif ($matches['prelabel'] -and $matches['prelabel'] -ieq 'post') { + # Alternate PEP 440 forms like "1.0.0-post1" or "1.0.0post1" where the regex + # matched "post" as a prerelease label — reinterpret as post-release. + $this.IsPostRelease = $true + $this.PostReleaseNumber = [int]$matches['prenumber'] + $this.PostReleaseSeparator = ".post" + $skipPrelabel = $true + } } else { $this.SetupDefaultConventions() } - if ($null -eq $matches['prelabel']) + if ($skipPrelabel -or $null -eq $matches['prelabel']) { $this.IsPrerelease = $false $this.VersionType = "GA" @@ -141,6 +176,9 @@ class AzureEngSemanticVersion : IComparable { $versionString += $this.BuildNumberSeparator + $this.BuildNumber } } + if ($this.IsPostRelease) { + $versionString += $this.PostReleaseSeparator + $this.PostReleaseNumber + } return $versionString; } @@ -150,6 +188,13 @@ class AzureEngSemanticVersion : IComparable { throw "Cannot increment releases tagged with azure pipelines build numbers" } + # Clear post-release state before incrementing + if ($this.IsPostRelease) { + $this.IsPostRelease = $false + $this.PostReleaseNumber = 0 + $this.PostReleaseSeparator = "" + } + if ($this.PrereleaseLabel) { $this.PrereleaseNumber++ @@ -239,12 +284,27 @@ class AzureEngSemanticVersion : IComparable { $ret = $thisPrereleaseNumber.CompareTo($otherPrereleaseNumber) if ($ret) { return $ret } - return ([int] $this.BuildNumber).CompareTo([int] $other.BuildNumber) + $ret = ([int] $this.BuildNumber).CompareTo([int] $other.BuildNumber) + if ($ret) { return $ret } + + # Post-release versions sort after their base version + $thisPost = if ($this.IsPostRelease) { 1 } else { 0 } + $otherPost = if ($other.IsPostRelease) { 1 } else { 0 } + $ret = $thisPost.CompareTo($otherPost) + if ($ret) { return $ret } + + return $this.PostReleaseNumber.CompareTo($other.PostReleaseNumber) } static [string[]] SortVersionStrings([string[]] $versionStrings) { - $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } + $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") + if ($parseLanguage -eq "python") { + $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParsePythonVersionString($_) } + } + else { + $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } + } $sortedVersions = [AzureEngSemanticVersion]::SortVersions($versions) return ($sortedVersions | ForEach-Object { $_.RawVersion }) } diff --git a/eng/common/scripts/artifact-metadata-parsing.ps1 b/eng/common/scripts/artifact-metadata-parsing.ps1 index 9a45855c0ec..2bc1713e191 100644 --- a/eng/common/scripts/artifact-metadata-parsing.ps1 +++ b/eng/common/scripts/artifact-metadata-parsing.ps1 @@ -1,6 +1,6 @@ . (Join-Path $EngCommonScriptsDir SemVer.ps1) -$SDIST_PACKAGE_REGEX = "^(?.*)\-(?$([AzureEngSemanticVersion]::SEMVER_REGEX))" +$SDIST_PACKAGE_REGEX = "^(?.*)\-(?$([AzureEngSemanticVersion]::PYTHON_SEMVER_REGEX))" # Posts a github release for each item of the pkgList variable. Silently continue function CreateReleases($pkgList, $releaseApiUrl, $releaseSha) { From 705ccb12ac9f1d19ed5e80630474188e9c44203e Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:12:20 -0700 Subject: [PATCH 03/15] add regex validation --- eng/common/scripts/SemVer.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index cccb7d180a0..ee32cad455e 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -41,6 +41,7 @@ class AzureEngSemanticVersion : IComparable { # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N (case-insensitive) + # Validation: https://regex101.com/r/rAdOg0/1 static [string] $PYTHON_SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?(?:(?[.\-_]?)(?i:post)\.?(?\d+))?" static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) From b21cba4cddf376f59e342bd3136f1227402ea310 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:18:17 -0700 Subject: [PATCH 04/15] minor --- eng/common/scripts/SemVer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index ee32cad455e..5d6e83b93ef 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -42,7 +42,7 @@ class AzureEngSemanticVersion : IComparable { # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N (case-insensitive) # Validation: https://regex101.com/r/rAdOg0/1 - static [string] $PYTHON_SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?(?:(?[.\-_]?)(?i:post)\.?(?\d+))?" + static [string] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion]::SEMVER_REGEX + "(?:(?[.\-_]?)(?i:post)\.?(?\d+))?" static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) { From 3d7b14679e2e5ed373fcaa09ce9d5fb265cf52f1 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:45:07 -0700 Subject: [PATCH 05/15] add changelog pester tests --- .../ChangeLog-Operations.Tests.ps1 | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/eng/common-tests/ChangeLog-Operations.Tests.ps1 b/eng/common-tests/ChangeLog-Operations.Tests.ps1 index 2a406e80aff..6c76d9bd29c 100644 --- a/eng/common-tests/ChangeLog-Operations.Tests.ps1 +++ b/eng/common-tests/ChangeLog-Operations.Tests.ps1 @@ -225,3 +225,299 @@ Describe "Integration: Update Changelog Entry and Write Back" { $updatedEntries["0.9.0"] | Should -Not -BeNullOrEmpty } } + +Describe "Python post-release changelog parsing" { + BeforeEach { + $global:Language = "python" + } + + AfterEach { + $global:Language = "" + } + + It "Should parse changelog with GA post-release version '1.0.0.post1'" { + $changelogContent = @" +# Release History + +## 1.0.0.post1 (2025-03-01) + +### Other Changes + +- Updated package metadata for distribution + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial GA release +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $entries | Should -Not -BeNullOrEmpty + $entries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $entries["1.0.0.post1"].ReleaseVersion | Should -Be "1.0.0.post1" + $entries["1.0.0.post1"].ReleaseStatus | Should -Be "(2025-03-01)" + $entries["1.0.0.post1"].Sections.ContainsKey("Other Changes") | Should -BeTrue + $entries["1.0.0"] | Should -Not -BeNullOrEmpty + } + + It "Should parse changelog with beta post-release version '1.0.0b2.post1'" { + $changelogContent = @" +# Release History + +## 1.0.0b2.post1 (2025-03-01) + +### Other Changes + +- Updated classifier metadata for beta release + +## 1.0.0b2 (2025-02-15) + +### Features Added + +- Beta feature +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $entries | Should -Not -BeNullOrEmpty + $entries["1.0.0b2.post1"] | Should -Not -BeNullOrEmpty + $entries["1.0.0b2.post1"].ReleaseVersion | Should -Be "1.0.0b2.post1" + $entries["1.0.0b2.post1"].Sections.ContainsKey("Other Changes") | Should -BeTrue + $entries["1.0.0b2"] | Should -Not -BeNullOrEmpty + } + + It "Should parse changelog with alpha post-release version '2.0.0a20201208001.post2'" { + $changelogContent = @" +# Release History + +## 2.0.0a20201208001.post2 (2025-03-01) + +### Other Changes + +- Updated alpha package metadata + +## 2.0.0a20201208001 (2025-02-15) + +### Features Added + +- Alpha feature +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $entries | Should -Not -BeNullOrEmpty + $entries["2.0.0a20201208001.post2"] | Should -Not -BeNullOrEmpty + $entries["2.0.0a20201208001.post2"].ReleaseVersion | Should -Be "2.0.0a20201208001.post2" + $entries["2.0.0a20201208001"] | Should -Not -BeNullOrEmpty + } + + It "Should parse changelog with unreleased post-release version" { + $changelogContent = @" +# Release History + +## 1.0.0.post2 (Unreleased) + +### Other Changes + +## 1.0.0.post1 (2025-02-15) + +### Other Changes + +- Updated package metadata +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $entries | Should -Not -BeNullOrEmpty + $entries["1.0.0.post2"] | Should -Not -BeNullOrEmpty + $entries["1.0.0.post2"].ReleaseStatus | Should -Be "(Unreleased)" + $entries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + } + + It "Should parse changelog with multiple post-release versions" { + $changelogContent = @" +# Release History + +## 1.0.0.post3 (2025-04-01) + +### Other Changes + +- Updated package classifiers + +## 1.0.0.post2 (2025-03-15) + +### Other Changes + +- Updated package description + +## 1.0.0.post1 (2025-03-01) + +### Other Changes + +- Updated package metadata + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial release +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $entries | Should -Not -BeNullOrEmpty + $entries["1.0.0.post3"] | Should -Not -BeNullOrEmpty + $entries["1.0.0.post2"] | Should -Not -BeNullOrEmpty + $entries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $entries["1.0.0"] | Should -Not -BeNullOrEmpty + } +} + +Describe "Python post-release changelog sorting" { + BeforeEach { + $global:Language = "python" + } + + AfterEach { + $global:Language = "" + } + + It "Should sort post-release versions after their base version" { + $changelogContent = @" +# Release History + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial release + +## 1.0.0.post1 (2025-03-01) + +### Other Changes + +- Updated package metadata +"@ + $entries = Get-ChangeLogEntriesFromContent $changelogContent + $sorted = Sort-ChangeLogEntries -changeLogEntries $entries + + # post1 should come before (sort higher than) base 1.0.0 in descending sort + $sortedVersions = @($sorted | ForEach-Object { $_.ReleaseVersion }) + $postIndex = [array]::IndexOf($sortedVersions, "1.0.0.post1") + $baseIndex = [array]::IndexOf($sortedVersions, "1.0.0") + $postIndex | Should -BeLessThan $baseIndex + } +} + +Describe "Python post-release changelog integration" { + BeforeEach { + $global:Language = "python" + $script:tempChangelogPath = Join-Path ([System.IO.Path]::GetTempPath()) "CHANGELOG_$([System.Guid]::NewGuid().ToString()).md" + } + + AfterEach { + $global:Language = "" + if (Test-Path $script:tempChangelogPath) { + Remove-Item -Path $script:tempChangelogPath -Force -ErrorAction SilentlyContinue + } + } + + It "Should round-trip a changelog with post-release versions" { + $initialChangelog = @" +# Release History + +## 1.0.0.post1 (2025-03-01) + +### Other Changes + +- Updated package metadata + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial release +"@ + Set-Content -Path $script:tempChangelogPath -Value $initialChangelog + + $entries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + $entries | Should -Not -BeNullOrEmpty + $entries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $entries["1.0.0"] | Should -Not -BeNullOrEmpty + + # Write back and re-read + Set-ChangeLogContent -ChangeLogLocation $script:tempChangelogPath -ChangeLogEntries $entries + + $reReadEntries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + $reReadEntries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $reReadEntries["1.0.0.post1"].ReleaseVersion | Should -Be "1.0.0.post1" + $reReadEntries["1.0.0.post1"].ReleaseStatus | Should -Be "(2025-03-01)" + $reReadEntries["1.0.0"] | Should -Not -BeNullOrEmpty + } + + It "Should update a post-release changelog entry content" { + $initialChangelog = @" +# Release History + +## 1.0.0.post1 (Unreleased) + +### Other Changes + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial release +"@ + Set-Content -Path $script:tempChangelogPath -Value $initialChangelog + + $entries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + $postEntry = $entries["1.0.0.post1"] + $postEntry | Should -Not -BeNullOrEmpty + + $newContent = "### Other Changes`n`n- Updated package metadata`n- Updated readme formatting" + Set-ChangeLogEntryContent -ChangeLogEntry $postEntry -NewContent $newContent -InitialAtxHeader $entries.InitialAtxHeader + Set-ChangeLogContent -ChangeLogLocation $script:tempChangelogPath -ChangeLogEntries $entries + + $updatedEntries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + $updatedEntries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $updatedEntries["1.0.0.post1"].Sections.ContainsKey("Other Changes") | Should -BeTrue + + $updatedContent = Get-Content -Path $script:tempChangelogPath -Raw + $updatedContent | Should -Match "Updated package metadata" + $updatedContent | Should -Match "Updated readme formatting" + + # Verify the base version is preserved + $updatedEntries["1.0.0"] | Should -Not -BeNullOrEmpty + } + + It "Should preserve post-release entries when adding a new version" { + $initialChangelog = @" +# Release History + +## 1.0.0.post1 (2025-03-01) + +### Other Changes + +- Updated package metadata + +## 1.0.0 (2025-02-15) + +### Features Added + +- Initial release +"@ + Set-Content -Path $script:tempChangelogPath -Value $initialChangelog + + $entries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + + # Add a new version entry + $newEntry = [pscustomobject]@{ + ReleaseVersion = "1.1.0" + ReleaseStatus = "(Unreleased)" + ReleaseTitle = "## 1.1.0 (Unreleased)" + ReleaseContent = @("", "### Features Added", "", "- New feature for 1.1.0") + Sections = @{ "Features Added" = @("", "- New feature for 1.1.0") } + } + $entries["1.1.0"] = $newEntry + + Set-ChangeLogContent -ChangeLogLocation $script:tempChangelogPath -ChangeLogEntries $entries + + $updatedEntries = Get-ChangeLogEntries -ChangeLogLocation $script:tempChangelogPath + $updatedEntries["1.1.0"] | Should -Not -BeNullOrEmpty + $updatedEntries["1.0.0.post1"] | Should -Not -BeNullOrEmpty + $updatedEntries["1.0.0"] | Should -Not -BeNullOrEmpty + } +} From 6bdb3e857df68a6dcf89ba7a0d36440019fbd364 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:48:03 -0700 Subject: [PATCH 06/15] use SemVer in 'copy-docs-to-blobstorage' and add tests --- .../copy-docs-to-blobstorage.Tests.ps1 | 181 ++++++++++++++++++ .../scripts/copy-docs-to-blobstorage.ps1 | 50 ++--- 2 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 eng/common-tests/copy-docs-to-blobstorage.Tests.ps1 diff --git a/eng/common-tests/copy-docs-to-blobstorage.Tests.ps1 b/eng/common-tests/copy-docs-to-blobstorage.Tests.ps1 new file mode 100644 index 00000000000..d7f16ace2f3 --- /dev/null +++ b/eng/common-tests/copy-docs-to-blobstorage.Tests.ps1 @@ -0,0 +1,181 @@ +Import-Module Pester + +BeforeAll { + $ExitOnError = 0 + . $PSScriptRoot/../common/scripts/copy-docs-to-blobstorage.ps1 -DocLocation "/tmp" -ExitOnError 0 + # common.ps1 sets $Language = "Unknown" in this scope. Override it to "python" + # so AzureEngSemanticVersion uses the Python regex (with post-release support). + $Language = "python" +} + +Describe "ToSemVer - standard versions" { + It "Should parse GA version '1.0.0'" { + $v = ToSemVer "1.0.0" + $v | Should -Not -BeNullOrEmpty + $v.Major | Should -Be 1 + $v.Minor | Should -Be 0 + $v.Patch | Should -Be 0 + $v.IsPrerelease | Should -BeFalse + $v.IsPostRelease | Should -BeFalse + } + + It "Should parse prerelease version '1.0.0-beta.1'" { + $v = ToSemVer "1.0.0-beta.1" + $v | Should -Not -BeNullOrEmpty + $v.Major | Should -Be 1 + $v.PrereleaseLabel | Should -Be "beta" + $v.PrereleaseNumber | Should -Be 1 + $v.IsPrerelease | Should -BeTrue + $v.IsPostRelease | Should -BeFalse + } + + It "Should return null for invalid version" { + $v = ToSemVer "notaversion" + $v | Should -BeNullOrEmpty + } +} + +Describe "ToSemVer - Python post-release versions" { + It "Should parse Python beta version '1.0.0b2'" { + + $v = ToSemVer "1.0.0b2" + $v | Should -Not -BeNullOrEmpty + $v.PrereleaseLabel | Should -Be "b" + $v.PrereleaseNumber | Should -Be 2 + $v.IsPrerelease | Should -BeTrue + $v.IsPostRelease | Should -BeFalse + } + + It "Should parse GA post-release '1.0.0.post1' as non-prerelease" { + + $v = ToSemVer "1.0.0.post1" + $v | Should -Not -BeNullOrEmpty + $v.Major | Should -Be 1 + $v.Minor | Should -Be 0 + $v.Patch | Should -Be 0 + $v.IsPrerelease | Should -BeFalse + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 1 + } + + It "Should parse beta post-release '1.0.0b2.post1' as prerelease" { + + $v = ToSemVer "1.0.0b2.post1" + $v | Should -Not -BeNullOrEmpty + $v.PrereleaseLabel | Should -Be "b" + $v.PrereleaseNumber | Should -Be 2 + $v.IsPrerelease | Should -BeTrue + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 1 + } + + It "Should normalize hyphen-separated '1.0.0-post1' as post-release" { + + $v = ToSemVer "1.0.0-post1" + $v | Should -Not -BeNullOrEmpty + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 1 + $v.IsPrerelease | Should -BeFalse + } + + It "Should normalize underscore-separated '1.0.0_post1' as post-release" { + + $v = ToSemVer "1.0.0_post1" + $v | Should -Not -BeNullOrEmpty + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 1 + $v.IsPrerelease | Should -BeFalse + } + + It "Should normalize no-separator '1.0.0post1' as post-release" { + + $v = ToSemVer "1.0.0post1" + $v | Should -Not -BeNullOrEmpty + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 1 + $v.IsPrerelease | Should -BeFalse + } + + It "Should parse post-release with higher number '1.0.0.post15'" { + + $v = ToSemVer "1.0.0.post15" + $v | Should -Not -BeNullOrEmpty + $v.IsPostRelease | Should -BeTrue + $v.PostReleaseNumber | Should -Be 15 + } +} + +Describe "ToSemVer - non-Python '1.0.0-post1' is prerelease, not post-release" { + It "Should treat '-post' as a prerelease label for non-Python languages" { + $Language = "" + $v = ToSemVer "1.0.0-post1" + $v | Should -Not -BeNullOrEmpty + $v.PrereleaseLabel | Should -Be "post" + $v.IsPrerelease | Should -BeTrue + $v.IsPostRelease | Should -BeFalse + } +} + +Describe "Sort-Versions - Python post-release sorting" { + It "Should sort post-release after base GA version" { + + $sorted = Sort-Versions -VersionArray @("1.0.0", "1.0.0.post1") + $sorted.RawVersionsList[0] | Should -Be "1.0.0.post1" + $sorted.RawVersionsList[1] | Should -Be "1.0.0" + } + + It "Should sort multiple post-releases in descending order" { + + $sorted = Sort-Versions -VersionArray @("1.0.0", "1.0.0.post1", "1.0.0.post2") + $sorted.RawVersionsList[0] | Should -Be "1.0.0.post2" + $sorted.RawVersionsList[1] | Should -Be "1.0.0.post1" + $sorted.RawVersionsList[2] | Should -Be "1.0.0" + } + + It "Should sort post-release between base version and next version" { + + $sorted = Sort-Versions -VersionArray @("2.0.0", "1.0.0.post1", "1.0.0") + $sorted.RawVersionsList[0] | Should -Be "2.0.0" + $sorted.RawVersionsList[1] | Should -Be "1.0.0.post1" + $sorted.RawVersionsList[2] | Should -Be "1.0.0" + } + + It "Should sort mixed versions with post-releases and prereleases" { + + $sorted = Sort-Versions -VersionArray @("1.0.0", "1.0.0.post1", "1.0.0b1", "2.0.0") + $sorted.RawVersionsList[0] | Should -Be "2.0.0" + $sorted.RawVersionsList[1] | Should -Be "1.0.0.post1" + $sorted.RawVersionsList[2] | Should -Be "1.0.0" + $sorted.RawVersionsList[3] | Should -Be "1.0.0b1" + } +} + +Describe "Sort-Versions - Python post-release LatestGA/LatestPreview classification" { + It "Should classify GA post-release as LatestGA, not LatestPreview" { + + $sorted = Sort-Versions -VersionArray @("1.0.0", "1.0.0.post1") + $sorted.LatestGAPackage | Should -Be "1.0.0.post1" + $sorted.LatestPreviewPackage | Should -Be "" + } + + It "Should not set LatestPreview for GA post-release when newer GA exists" { + + $sorted = Sort-Versions -VersionArray @("2.0.0", "1.0.0.post1", "1.0.0") + $sorted.LatestGAPackage | Should -Be "2.0.0" + $sorted.LatestPreviewPackage | Should -Be "" + } + + It "Should set LatestPreview when prerelease is newer than GA post-release" { + + $sorted = Sort-Versions -VersionArray @("1.0.0", "1.0.0.post1", "2.0.0b1") + $sorted.LatestGAPackage | Should -Be "1.0.0.post1" + $sorted.LatestPreviewPackage | Should -Be "2.0.0b1" + } + + It "Should treat beta post-release as prerelease for LatestGA/LatestPreview" { + + $sorted = Sort-Versions -VersionArray @("1.0.0b2", "1.0.0b2.post1") + $sorted.LatestGAPackage | Should -Be "" + $sorted.LatestPreviewPackage | Should -Be "1.0.0b2.post1" + } +} diff --git a/eng/common/scripts/copy-docs-to-blobstorage.ps1 b/eng/common/scripts/copy-docs-to-blobstorage.ps1 index 852945338e9..aa33a93f33c 100644 --- a/eng/common/scripts/copy-docs-to-blobstorage.ps1 +++ b/eng/common/scripts/copy-docs-to-blobstorage.ps1 @@ -12,56 +12,28 @@ param ( . (Join-Path $PSScriptRoot common.ps1) -# Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string -$SEMVER_REGEX = "^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-?(?[a-zA-Z-]*)(?:\.?(?0|[1-9]\d*))?)?$" - function ToSemVer($version){ - if ($version -match $SEMVER_REGEX) - { - if(-not $matches['prelabel']) { - # artifically provide these values for non-prereleases to enable easy sorting of them later than prereleases. - $prelabel = "zzz" - $prenumber = 999; - $isPre = $false; - } - else { - $prelabel = $matches["prelabel"] - $prenumber = 0 - - # some older packages don't have a prenumber, should handle this - if($matches["prenumber"]){ - $prenumber = [int]$matches["prenumber"] + try { + $sv = [AzureEngSemanticVersion]::new($version) + if (!$sv.IsSemVerFormat) { + if ($ExitOnError) { + throw "Unable to convert $version to valid semver and hard exit on error is enabled. Exiting." } - - $isPre = $true; - } - - New-Object PSObject -Property @{ - Major = [int]$matches['major'] - Minor = [int]$matches['minor'] - Patch = [int]$matches['patch'] - PrereleaseLabel = $prelabel - PrereleaseNumber = $prenumber - IsPrerelease = $isPre - RawVersion = $version + return $null } + return $sv } - else - { - if ($ExitOnError) - { + catch { + if ($ExitOnError) { throw "Unable to convert $version to valid semver and hard exit on error is enabled. Exiting." } - else - { - return $null - } + return $null } } function SortSemVersions($versions) { - return $versions | Sort-Object -Property Major, Minor, Patch, PrereleaseLabel, PrereleaseNumber -Descending + return $versions | Sort-Object -Descending } function Sort-Versions From 727380c5aefd03b110f8d699c32d38e9d73a1eae Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:13:14 -0700 Subject: [PATCH 07/15] support implicit postrelease --- eng/common-tests/SemVer.Tests.ps1 | 89 +++++++++++++++++++++++++++++++ eng/common/scripts/SemVer.ps1 | 8 +-- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/eng/common-tests/SemVer.Tests.ps1 b/eng/common-tests/SemVer.Tests.ps1 index 5a58358cd42..1044813e105 100644 --- a/eng/common-tests/SemVer.Tests.ps1 +++ b/eng/common-tests/SemVer.Tests.ps1 @@ -101,6 +101,31 @@ Describe "Post-release version parsing - Python convention" { $ver.IsPrerelease | Should -BeTrue $ver.VersionType | Should -Be "Beta" } + + It "Should parse implicit post-release number '1.0.0.post' as post0" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.Major | Should -Be 1 + $ver.Minor | Should -Be 0 + $ver.Patch | Should -Be 0 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.PrereleaseLabel | Should -BeNullOrEmpty + $ver.IsPrerelease | Should -BeFalse + $ver.VersionType | Should -Be "GA" + } + + It "Should parse implicit prerelease post-release '1.0.0b2.post' as post0" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2.post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsSemVerFormat | Should -BeTrue + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.IsPrerelease | Should -BeTrue + } } Describe "Post-release version ToString round-trip - Default convention (non-Python languages don't support post-release, so should round-trip as prerelease)" { @@ -157,6 +182,48 @@ Describe "PEP 440 alternate post-release format normalization - Python conventio $ver.ToString() | Should -Be "1.0.0.post1" } + It "Should normalize implicit post number '1.0.0.post' to '1.0.0.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.ToString() | Should -Be "1.0.0.post0" + } + + It "Should normalize implicit post number with hyphen '1.0.0-post' to '1.0.0.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0-post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.ToString() | Should -Be "1.0.0.post0" + } + + It "Should normalize implicit post number with underscore '1.0.0_post' to '1.0.0.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0_post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.ToString() | Should -Be "1.0.0.post0" + } + + It "Should normalize implicit post number with no separator '1.0.0post' to '1.0.0.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0post") + $ver | Should -Not -BeNullOrEmpty + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.ToString() | Should -Be "1.0.0.post0" + } + + It "Should normalize implicit prerelease post number '1.0.0b2.post' to '1.0.0b2.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2.post") + $ver | Should -Not -BeNullOrEmpty + $ver.PrereleaseLabel | Should -Be "b" + $ver.PrereleaseNumber | Should -Be 2 + $ver.IsPostRelease | Should -BeTrue + $ver.PostReleaseNumber | Should -Be 0 + $ver.ToString() | Should -Be "1.0.0b2.post0" + } + It "Should normalize prerelease hyphen separator '1.0.0b2-post1' to canonical form" { $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0b2-post1") $ver | Should -Not -BeNullOrEmpty @@ -208,6 +275,11 @@ Describe "Post-release version ToString round-trip - Python convention" { $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("2.0.0a20201208001.post2") $ver.ToString() | Should -Be "2.0.0a20201208001.post2" } + + It "Should normalize implicit post-release '1.0.0.post' to '1.0.0.post0'" { + $ver = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post") + $ver.ToString() | Should -Be "1.0.0.post0" + } } Describe "Post-release version sorting - Python convention" { @@ -281,6 +353,23 @@ Describe "Post-release version sorting - Python convention" { $sort[$i] | Should -Be $expectedSort[$i] } } + + It "Should sort implicit post-release (post0) equivalently to explicit post0" { + $versions = @( + "1.0.0.post1", + "1.0.0", + "1.0.0.post0" + ) + $expectedSort = @( + "1.0.0.post1", + "1.0.0.post0", + "1.0.0" + ) + $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) + for ($i = 0; $i -lt $expectedSort.Count; $i++) { + $sort[$i] | Should -Be $expectedSort[$i] + } + } } Describe "Post-release version increment - Python convention" { diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index 5d6e83b93ef..ab73268e513 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -40,9 +40,9 @@ class AzureEngSemanticVersion : IComparable { static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. - # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N (case-insensitive) + # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive) # Validation: https://regex101.com/r/rAdOg0/1 - static [string] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion]::SEMVER_REGEX + "(?:(?[.\-_]?)(?i:post)\.?(?\d+))?" + static [string] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion]::SEMVER_REGEX + "(?:(?[.\-_]?)(?(?i:post))\.?(?\d+)?)?" static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) { @@ -94,9 +94,9 @@ class AzureEngSemanticVersion : IComparable { $skipPrelabel = $false if ($parseLanguage -eq "python") { $this.SetupPythonConventions() - if ($matches['postnum']) { + if ($matches['postword']) { $this.IsPostRelease = $true - $this.PostReleaseNumber = [int]$matches['postnum'] + $this.PostReleaseNumber = if ($matches['postnum']) { [int]$matches['postnum'] } else { 0 } $this.PostReleaseSeparator = ".post" } elseif ($matches['prelabel'] -and $matches['prelabel'] -ieq 'post') { From 81c8ad6afecb2d600d00595a404f71ad5e53abc9 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:16:35 -0700 Subject: [PATCH 08/15] add post-release quicktests --- eng/common/scripts/SemVer.ps1 | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index ab73268e513..1fab70f87da 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -490,6 +490,64 @@ class AzureEngSemanticVersion : IComparable { Write-Host "Error: version string did not correctly increment. Expected: $expected, Actual: $version" } + # Python post-release parsing tests + $postVerString = "1.0.0.post1" + $postVer = [AzureEngSemanticVersion]::ParsePythonVersionString($postVerString) + if ($postVer.Major -ne 1 -or $postVer.Minor -ne 0 -or $postVer.Patch -ne 0 -or ` + !$postVer.IsPostRelease -or $postVer.PostReleaseNumber -ne 1 -or $postVer.IsPrerelease) { + Write-Host "Error: Didn't correctly parse python post-release string $postVerString" + } + if ($postVerString -ne $postVer.ToString()) { + Write-Host "Error: post-release string did not correctly round trip with ToString. Expected: $($postVerString), Actual: $($postVer)" + } + + # Implicit post-release number (PEP 440: 1.0.0.post == 1.0.0.post0) + $implicitPostVerString = "1.0.0.post" + $implicitPostVer = [AzureEngSemanticVersion]::ParsePythonVersionString($implicitPostVerString) + if ($null -eq $implicitPostVer -or !$implicitPostVer.IsSemVerFormat) { + Write-Host "Error: Failed to parse implicit post-release string $implicitPostVerString" + } + elseif ($implicitPostVer.Major -ne 1 -or $implicitPostVer.Minor -ne 0 -or $implicitPostVer.Patch -ne 0 -or ` + !$implicitPostVer.IsPostRelease -or $implicitPostVer.PostReleaseNumber -ne 0) { + Write-Host "Error: Didn't correctly parse implicit post-release string $implicitPostVerString" + } + $expected = "1.0.0.post0" + if ($expected -ne $implicitPostVer.ToString()) { + Write-Host "Error: implicit post-release did not normalize. Expected: $expected, Actual: $($implicitPostVer)" + } + + # Prerelease + post-release + $preBetaPostString = "1.0.0b2.post1" + $preBetaPost = [AzureEngSemanticVersion]::ParsePythonVersionString($preBetaPostString) + if ($preBetaPost.Major -ne 1 -or $preBetaPost.Minor -ne 0 -or $preBetaPost.Patch -ne 0 -or ` + $preBetaPost.PrereleaseLabel -ne "b" -or $preBetaPost.PrereleaseNumber -ne 2 -or ` + !$preBetaPost.IsPostRelease -or $preBetaPost.PostReleaseNumber -ne 1) { + Write-Host "Error: Didn't correctly parse python prerelease post-release string $preBetaPostString" + } + if ($preBetaPostString -ne $preBetaPost.ToString()) { + Write-Host "Error: prerelease post-release string did not correctly round trip with ToString. Expected: $($preBetaPostString), Actual: $($preBetaPost)" + } + + # Post-release alternate separators normalize to canonical form + $expectedNormalized = "1.0.0.post1" + foreach ($altVerString in @("1.0.0-post1", "1.0.0_post1", "1.0.0post1")) { + $parsed = [AzureEngSemanticVersion]::ParsePythonVersionString($altVerString) + if ($null -eq $parsed -or !$parsed.IsPostRelease -or $parsed.PostReleaseNumber -ne 1) { + Write-Host "Error: Failed to parse alternate post-release format $altVerString" + } + if ($expectedNormalized -ne $parsed.ToString()) { + Write-Host "Error: Alternate post-release '$altVerString' did not normalize. Expected: $expectedNormalized, Actual: $($parsed)" + } + } + + # Post-release increment clears post state + $postIncVer = [AzureEngSemanticVersion]::ParsePythonVersionString("1.0.0.post1") + $postIncVer.IncrementAndSetToPrerelease() + $expected = "1.1.0b1" + if ($expected -ne $postIncVer.ToString()) { + Write-Host "Error: post-release increment did not produce expected result. Expected: $expected, Actual: $($postIncVer)" + } + Write-Host "QuickTests done" } } \ No newline at end of file From b6133a996841931e7b6e19bf4af170e7046da55c Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:06:22 -0700 Subject: [PATCH 09/15] update parseSemverVersionString --- eng/scripts/python_version_check.py | 4 +- tools/spec-gen-sdk/package-lock.json | 9 ++++ .../src/utils/parseSemverVersionString.ts | 45 +++++++++++++++-- .../utils/parseSemverVersionString.test.ts | 49 +++++++++++++++++++ 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/eng/scripts/python_version_check.py b/eng/scripts/python_version_check.py index 938d0e1fb2c..cd5717ed620 100644 --- a/eng/scripts/python_version_check.py +++ b/eng/scripts/python_version_check.py @@ -25,10 +25,10 @@ version_path = os.path.join(stub_gen_path, 'apistub', '_version.py') with open(changelog_path, 'r') as changelog_file: - latest = re.findall(r'## Version (\d+.\d+.\d+)', changelog_file.read())[0] + latest = re.findall(r'## Version (\d+\.\d+\.\d+(?:\.post\d*)?)', changelog_file.read())[0] with open(version_path, 'r') as version_file: - version = re.findall(r'VERSION = "(\d+.\d+.\d+)"', version_file.read())[0] + version = re.findall(r'VERSION = "(\d+\.\d+\.\d+(?:\.post\d*)?)"', version_file.read())[0] if version != latest: msg = f"Latest changelog version {latest} does not match _version.py version {version}." diff --git a/tools/spec-gen-sdk/package-lock.json b/tools/spec-gen-sdk/package-lock.json index 6da0bbf28b5..41fd813c2f2 100644 --- a/tools/spec-gen-sdk/package-lock.json +++ b/tools/spec-gen-sdk/package-lock.json @@ -1407,6 +1407,7 @@ "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.19.1", "@typescript-eslint/types": "8.19.1", @@ -1716,6 +1717,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2163,6 +2165,7 @@ "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3956,6 +3959,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4260,6 +4264,7 @@ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4341,6 +4346,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4454,6 +4460,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4467,6 +4474,7 @@ "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", @@ -4648,6 +4656,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/tools/spec-gen-sdk/src/utils/parseSemverVersionString.ts b/tools/spec-gen-sdk/src/utils/parseSemverVersionString.ts index 936b1ec5be7..d3608bdfeca 100644 --- a/tools/spec-gen-sdk/src/utils/parseSemverVersionString.ts +++ b/tools/spec-gen-sdk/src/utils/parseSemverVersionString.ts @@ -29,6 +29,13 @@ export function parseSemverVersionString( // Copied from https://github.com/Azure/azure-sdk-tools/blob/efa8a15c81e4614f2071b82dd8ca4f6ce6076f7b/eng/common/scripts/SemVer.ps1#L36 const SEMVER_REGEX = /(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?/im; + // Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. + // Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive) + const PYTHON_SEMVER_REGEX = + /(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?(?:(?[.\-_]?)(?[pP][oO][sS][tT])\.?(?\d+)?)?/im; + + const isPython = language.toLowerCase() === 'python'; + const parseRegex = isPython ? PYTHON_SEMVER_REGEX : SEMVER_REGEX; let prereleaseLabelSeparator: string | undefined; let prereleaseNumberSeparator: string | undefined; let buildNumberSeparator: string | undefined; @@ -45,7 +52,10 @@ export function parseSemverVersionString( let prelabel: string | undefined; let isSemVerFormat: boolean | undefined; let rawVersion: string | undefined; - const matches = versionString.match(SEMVER_REGEX); + let isPostRelease: boolean | undefined; + let postReleaseNumber: string | undefined; + let postReleaseSeparator: string | undefined; + const matches = versionString.match(parseRegex); if (matches) { isSemVerFormat = true; rawVersion = versionString; @@ -53,8 +63,7 @@ export function parseSemverVersionString( minor = matches && matches.groups && matches.groups.minor; patch = matches && matches.groups && matches.groups.patch; // If Language exists and is set to python setup the python conventions. - const parseLanguage = 'python'; - if (parseLanguage === language.toLowerCase()) { + if (isPython) { // Python uses no separators and 'b' for beta so this sets up the the object to work with those conventions prereleaseLabelSeparator = prereleaseNumberSeparator = buildNumberSeparator = ''; defaultPrereleaseLabel = 'b'; @@ -68,8 +77,28 @@ export function parseSemverVersionString( defaultAlphaReleaseLabel = 'alpha'; } + let skipPrelabel = false; + + // Python PEP 440 post-release detection + if (isPython) { + const postword = matches?.groups?.postword; + if (postword) { + // Case A: explicit post-release suffix (e.g., "1.0.0.post1", "1.0.0b2.post1") + isPostRelease = true; + postReleaseNumber = matches?.groups?.postnum ?? '0'; + postReleaseSeparator = '.post'; + } else if (matches?.groups?.prelabel && matches.groups.prelabel.toLowerCase() === 'post') { + // Case B: "post" captured as prelabel (e.g., "1.0.0-post1", "1.0.0post1") + // Reinterpret as post-release, not prerelease + isPostRelease = true; + postReleaseNumber = matches?.groups?.prenumber ?? '0'; + postReleaseSeparator = '.post'; + skipPrelabel = true; + } + } + prelabel = matches && matches.groups && matches.groups.prelabel; - if (!prelabel) { + if (skipPrelabel || !prelabel) { prereleaseLabel = 'zzz'; prereleaseNumber = '99999999'; isPrerelease = false; @@ -111,7 +140,10 @@ export function parseSemverVersionString( rawVersion, isSemVerFormat, defaultPrereleaseLabel, - defaultAlphaReleaseLabel + defaultAlphaReleaseLabel, + isPostRelease, + postReleaseNumber, + postReleaseSeparator }; } @@ -132,4 +164,7 @@ type ParseVersion = { prelabel: string | undefined; isSemVerFormat: boolean | undefined; rawVersion: string | undefined; + isPostRelease: boolean | undefined; + postReleaseNumber: string | undefined; + postReleaseSeparator: string | undefined; }; diff --git a/tools/spec-gen-sdk/test/utils/parseSemverVersionString.test.ts b/tools/spec-gen-sdk/test/utils/parseSemverVersionString.test.ts index 297e5517535..40dc48553cd 100644 --- a/tools/spec-gen-sdk/test/utils/parseSemverVersionString.test.ts +++ b/tools/spec-gen-sdk/test/utils/parseSemverVersionString.test.ts @@ -95,4 +95,53 @@ describe('parseSemverVersionString', () => { }), ).toBeTruthy(); }); + + it('Parse a standard Python post-release version', () => { + const parsedVersion = parseSemverVersionString('1.0.0.post1', 'Python'); + expect(parsedVersion?.isSemVerFormat).toEqual(true); + expect(parsedVersion?.major).toEqual('1'); + expect(parsedVersion?.minor).toEqual('0'); + expect(parsedVersion?.patch).toEqual('0'); + expect(parsedVersion?.isPostRelease).toEqual(true); + expect(parsedVersion?.postReleaseNumber).toEqual('1'); + expect(parsedVersion?.postReleaseSeparator).toEqual('.post'); + expect(parsedVersion?.isPrerelease).toEqual(false); + expect(parsedVersion?.versionType).toEqual('GA'); + }); + + it('Parse a Python post-release with implicit number', () => { + const parsedVersion = parseSemverVersionString('1.0.0.post', 'Python'); + expect(parsedVersion?.isPostRelease).toEqual(true); + expect(parsedVersion?.postReleaseNumber).toEqual('0'); + expect(parsedVersion?.isPrerelease).toEqual(false); + expect(parsedVersion?.versionType).toEqual('GA'); + }); + + it('Parse a Python prerelease + post-release version', () => { + const parsedVersion = parseSemverVersionString('1.0.0b2.post1', 'Python'); + expect(parsedVersion?.isPrerelease).toEqual(true); + expect(parsedVersion?.prereleaseLabel).toEqual('b'); + expect(parsedVersion?.prereleaseNumber).toEqual('2'); + expect(parsedVersion?.isPostRelease).toEqual(true); + expect(parsedVersion?.postReleaseNumber).toEqual('1'); + expect(parsedVersion?.versionType).toEqual('Beta'); + }); + + it('Parse Python post-release with alternate separators', () => { + for (const ver of ['1.0.0-post1', '1.0.0_post1', '1.0.0post1']) { + const parsedVersion = parseSemverVersionString(ver, 'Python'); + expect(parsedVersion?.isPostRelease).toEqual(true); + expect(parsedVersion?.postReleaseNumber).toEqual('1'); + expect(parsedVersion?.isPrerelease).toEqual(false); + expect(parsedVersion?.versionType).toEqual('GA'); + } + }); + + it('Non-Python language treats "post" as prerelease label, not post-release', () => { + const parsedVersion = parseSemverVersionString('1.0.0-post1', 'JavaScript'); + expect(parsedVersion?.isPostRelease).toBeUndefined(); + expect(parsedVersion?.isPrerelease).toEqual(true); + expect(parsedVersion?.versionType).toEqual('Beta'); + expect(parsedVersion?.prereleaseLabel).toEqual('post'); + }); }); From e5d5f916704b9d74a6904dcfcf83630c99974a78 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:09:18 -0700 Subject: [PATCH 10/15] undo package log changes --- tools/spec-gen-sdk/package-lock.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tools/spec-gen-sdk/package-lock.json b/tools/spec-gen-sdk/package-lock.json index 41fd813c2f2..6da0bbf28b5 100644 --- a/tools/spec-gen-sdk/package-lock.json +++ b/tools/spec-gen-sdk/package-lock.json @@ -1407,7 +1407,6 @@ "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.19.1", "@typescript-eslint/types": "8.19.1", @@ -1717,7 +1716,6 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2165,7 +2163,6 @@ "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3959,7 +3956,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4264,7 +4260,6 @@ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4346,7 +4341,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4460,7 +4454,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4474,7 +4467,6 @@ "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", @@ -4656,7 +4648,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, From bd7ed37cd10f53d72996cc6d2ff07bc8f91e6be3 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:48:57 -0700 Subject: [PATCH 11/15] undo python_version_check change --- eng/scripts/python_version_check.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/scripts/python_version_check.py b/eng/scripts/python_version_check.py index cd5717ed620..4a68d10ab65 100644 --- a/eng/scripts/python_version_check.py +++ b/eng/scripts/python_version_check.py @@ -25,10 +25,10 @@ version_path = os.path.join(stub_gen_path, 'apistub', '_version.py') with open(changelog_path, 'r') as changelog_file: - latest = re.findall(r'## Version (\d+\.\d+\.\d+(?:\.post\d*)?)', changelog_file.read())[0] + latest = re.findall(r'## Version (\d+.\d+.\d+)', changelog_file.read())[0] with open(version_path, 'r') as version_file: - version = re.findall(r'VERSION = "(\d+\.\d+\.\d+(?:\.post\d*)?)"', version_file.read())[0] + version = re.findall(r'VERSION = "(\d+.\d+.\d+)"', version_file.read())[0] if version != latest: msg = f"Latest changelog version {latest} does not match _version.py version {version}." @@ -36,4 +36,4 @@ sys.exit(1) else: print(f"Version {latest} is consistent.") - sys.exit(0) + sys.exit(0) \ No newline at end of file From 339283dfeb061bea9fe8c32cbef4223e30a52c5d Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:54:24 -0700 Subject: [PATCH 12/15] whitespace clean --- eng/common/scripts/SemVer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index 1fab70f87da..aa4fe6f4243 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -37,7 +37,7 @@ class AzureEngSemanticVersion : IComparable { # Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string # Validation: https://regex101.com/r/vkijKf/426 - static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" + static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive) From 1b28909bfcca59769a4bbd0e6bc657cc1afd43e5 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:55:48 -0700 Subject: [PATCH 13/15] minor --- eng/scripts/python_version_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/python_version_check.py b/eng/scripts/python_version_check.py index 4a68d10ab65..938d0e1fb2c 100644 --- a/eng/scripts/python_version_check.py +++ b/eng/scripts/python_version_check.py @@ -36,4 +36,4 @@ sys.exit(1) else: print(f"Version {latest} is consistent.") - sys.exit(0) \ No newline at end of file + sys.exit(0) From 86e82e861c3d2317cc649c61523ec9cea570418a Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:13:36 -0700 Subject: [PATCH 14/15] update regex validation link --- eng/common/scripts/SemVer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index aa4fe6f4243..35b2220bbce 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -41,7 +41,7 @@ class AzureEngSemanticVersion : IComparable { # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive) - # Validation: https://regex101.com/r/rAdOg0/1 + # Validation: https://regex101.com/r/rAdOg0/2 static [string] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion]::SEMVER_REGEX + "(?:(?[.\-_]?)(?(?i:post))\.?(?\d+)?)?" static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) From 8947e8151da4f76fd8e78f4b71b672b80aa86013 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:56:14 -0700 Subject: [PATCH 15/15] minor --- eng/common/scripts/SemVer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/SemVer.ps1 b/eng/common/scripts/SemVer.ps1 index 35b2220bbce..eeb7ab5f4ac 100644 --- a/eng/common/scripts/SemVer.ps1 +++ b/eng/common/scripts/SemVer.ps1 @@ -39,7 +39,7 @@ class AzureEngSemanticVersion : IComparable { # Validation: https://regex101.com/r/vkijKf/426 static [string] $SEMVER_REGEX = "(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:(?-?)(?[a-zA-Z]+)(?:(?\.?)(?[0-9]{1,8})(?:(?\.?)(?\d{1,3}))?)?)?" - # Python PEP 440 post-release extension: SEMVER_REGEX + optional post-release suffix. + # Python PEP 440 post-release extension # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive) # Validation: https://regex101.com/r/rAdOg0/2 static [string] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion]::SEMVER_REGEX + "(?:(?[.\-_]?)(?(?i:post))\.?(?\d+)?)?"