@@ -30,11 +30,20 @@ class AzureEngSemanticVersion : IComparable {
3030 [bool ] $IsSemVerFormat
3131 [string ] $DefaultPrereleaseLabel
3232 [string ] $DefaultAlphaReleaseLabel
33+ # For Python PEP440 post-release support only
34+ [bool ] $IsPostRelease
35+ [int ] $PostReleaseNumber
36+ [string ] $PostReleaseSeparator
3337
3438 # Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
3539 # Validation: https://regex101.com/r/vkijKf/426
3640 static [string ] $SEMVER_REGEX = " (?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:(?<presep>-?)(?<prelabel>[a-zA-Z]+)(?:(?<prenumsep>\.?)(?<prenumber>[0-9]{1,8})(?:(?<buildnumsep>\.?)(?<buildnumber>\d{1,3}))?)?)?"
3741
42+ # Python PEP 440 post-release extension
43+ # Handles all PEP 440 alternate formats: .postN, -postN, _postN, postN, .post.N, .post (implicit 0) (case-insensitive)
44+ # Validation: https://regex101.com/r/rAdOg0/2
45+ static [string ] $PYTHON_SEMVER_REGEX = [AzureEngSemanticVersion ]::SEMVER_REGEX + " (?:(?<postsep>[.\-_]?)(?<postword>(?i:post))\.?(?<postnum>\d{1,8})?)?"
46+
3847 static [AzureEngSemanticVersion ] ParseVersionString([string ] $versionString )
3948 {
4049 $version = [AzureEngSemanticVersion ]::new($versionString )
@@ -47,36 +56,59 @@ class AzureEngSemanticVersion : IComparable {
4756
4857 static [AzureEngSemanticVersion ] ParsePythonVersionString([string ] $versionString )
4958 {
50- $version = [AzureEngSemanticVersion ]::ParseVersionString($versionString )
59+ $previousLanguage = (Get-Variable - Name " Language" - ValueOnly - ErrorAction " Ignore" )
60+ $global :Language = " python"
61+ $version = $null
62+ try {
63+ $version = [AzureEngSemanticVersion ]::new($versionString )
64+ }
65+ finally {
66+ $global :Language = $previousLanguage
67+ }
5168
52- if (! $version ) {
69+ if (! $version.IsSemVerFormat ) {
5370 return $null
5471 }
55-
56- $version.SetupPythonConventions ()
5772 return $version
5873 }
5974
6075 AzureEngSemanticVersion([string ] $versionString )
6176 {
62- if ($versionString -match " ^$ ( [AzureEngSemanticVersion ]::SEMVER_REGEX) $" )
77+ $parseLanguage = (Get-Variable - Name " Language" - ValueOnly - ErrorAction " Ignore" )
78+
79+ if ($parseLanguage -eq " python" ) {
80+ $parseRegex = $this.SetupPythonConventions ()
81+ }
82+ else {
83+ $parseRegex = $this.SetupDefaultConventions ()
84+ }
85+
86+ if ($versionString -match " ^${parseRegex} $" )
6387 {
6488 $this.IsSemVerFormat = $true
6589 $this.RawVersion = $versionString
6690 $this.Major = [int ]$matches.Major
6791 $this.Minor = [int ]$matches.Minor
6892 $this.Patch = [int ]$matches.Patch
6993
70- # If Language exists and is set to python setup the python conventions.
71- $parseLanguage = (Get-Variable - Name " Language" - ValueOnly - ErrorAction " Ignore" )
94+ $skipPrelabel = $false
7295 if ($parseLanguage -eq " python" ) {
73- $this.SetupPythonConventions ()
74- }
75- else {
76- $this.SetupDefaultConventions ()
96+ if ($matches [' postword' ]) {
97+ $this.IsPostRelease = $true
98+ $this.PostReleaseNumber = if ($matches [' postnum' ]) { [int ]$matches [' postnum' ] } else { 0 }
99+ $this.PostReleaseSeparator = " .post"
100+ }
101+ elseif ($matches [' prelabel' ] -and $matches [' prelabel' ] -ieq ' post' ) {
102+ # Alternate PEP 440 forms like "1.0.0-post1" or "1.0.0post1" where the regex
103+ # matched "post" as a prerelease label — reinterpret as post-release.
104+ $this.IsPostRelease = $true
105+ $this.PostReleaseNumber = [int ]$matches [' prenumber' ]
106+ $this.PostReleaseSeparator = " .post"
107+ $skipPrelabel = $true
108+ }
77109 }
78110
79- if ($null -eq $matches [' prelabel' ])
111+ if ($skipPrelabel -or $ null -eq $matches [' prelabel' ])
80112 {
81113 $this.IsPrerelease = $false
82114 $this.VersionType = " GA"
@@ -141,6 +173,9 @@ class AzureEngSemanticVersion : IComparable {
141173 $versionString += $this.BuildNumberSeparator + $this.BuildNumber
142174 }
143175 }
176+ if ($this.IsPostRelease ) {
177+ $versionString += $this.PostReleaseSeparator + $this.PostReleaseNumber
178+ }
144179 return $versionString ;
145180 }
146181
@@ -150,6 +185,13 @@ class AzureEngSemanticVersion : IComparable {
150185 throw " Cannot increment releases tagged with azure pipelines build numbers"
151186 }
152187
188+ # Clear post-release state before incrementing
189+ if ($this.IsPostRelease ) {
190+ $this.IsPostRelease = $false
191+ $this.PostReleaseNumber = 0
192+ $this.PostReleaseSeparator = " "
193+ }
194+
153195 if ($this.PrereleaseLabel )
154196 {
155197 $this.PrereleaseNumber ++
@@ -180,22 +222,55 @@ class AzureEngSemanticVersion : IComparable {
180222 $this.IncrementAndSetToPrerelease (" Minor" )
181223 }
182224
183- [void ] SetupPythonConventions()
225+ [void ] IncrementAndSetToPostRelease() {
226+ if ($this.BuildNumber )
227+ {
228+ throw " Cannot increment releases tagged with azure pipelines build numbers"
229+ }
230+
231+ if ($this.IsPostRelease ) {
232+ $this.PostReleaseNumber ++
233+ }
234+ else {
235+ $this.IsPostRelease = $true
236+ $this.PostReleaseNumber = 1
237+ $this.PostReleaseSeparator = " .post"
238+ }
239+ }
240+
241+ # Sets the version to a prerelease state with the specified label and number.
242+ # This clears any post-release state to ensure a clean prerelease version.
243+ [void ] SetPrerelease([string ] $Label , [int ] $Number ) {
244+ # Clear post-release state
245+ if ($this.IsPostRelease ) {
246+ $this.IsPostRelease = $false
247+ $this.PostReleaseNumber = 0
248+ $this.PostReleaseSeparator = " "
249+ }
250+
251+ $this.PrereleaseLabel = $Label
252+ $this.PrereleaseNumber = $Number
253+ $this.IsPrerelease = $true
254+ }
255+
256+ [string ] SetupPythonConventions()
184257 {
185258 # Python uses no separators and "b" for beta so this sets up the the object to work with those conventions
186259 $this.PrereleaseLabelSeparator = $this.PrereleaseNumberSeparator = $this.BuildNumberSeparator = " "
187260 $this.DefaultPrereleaseLabel = " b"
188261 $this.DefaultAlphaReleaseLabel = " a"
262+ return [AzureEngSemanticVersion ]::PYTHON_SEMVER_REGEX
189263 }
190264
191- [void ] SetupDefaultConventions()
265+ [string ] SetupDefaultConventions()
192266 {
193267 # Use the default common conventions
194268 $this.PrereleaseLabelSeparator = " -"
195269 $this.PrereleaseNumberSeparator = " ."
196270 $this.BuildNumberSeparator = " ."
197271 $this.DefaultPrereleaseLabel = " beta"
198272 $this.DefaultAlphaReleaseLabel = " alpha"
273+ return [AzureEngSemanticVersion ]::SEMVER_REGEX
199274 }
200275
201276 [int ] CompareTo($other )
@@ -239,12 +314,29 @@ class AzureEngSemanticVersion : IComparable {
239314 $ret = $thisPrereleaseNumber.CompareTo ($otherPrereleaseNumber )
240315 if ($ret ) { return $ret }
241316
242- return ([int ] $this.BuildNumber ).CompareTo([int ] $other.BuildNumber )
317+ $thisBuildNumber = if ($this.BuildNumber ) { [int ] $this.BuildNumber } else { 0 }
318+ $otherBuildNumber = if ($other.BuildNumber ) { [int ] $other.BuildNumber } else { 0 }
319+ $ret = $thisBuildNumber.CompareTo ($otherBuildNumber )
320+ if ($ret ) { return $ret }
321+
322+ # Post-release versions sort after their base version
323+ $thisPost = if ($this.IsPostRelease ) { 1 } else { 0 }
324+ $otherPost = if ($other.IsPostRelease ) { 1 } else { 0 }
325+ $ret = $thisPost.CompareTo ($otherPost )
326+ if ($ret ) { return $ret }
327+
328+ return $this.PostReleaseNumber.CompareTo ($other.PostReleaseNumber )
243329 }
244330
245331 static [string []] SortVersionStrings([string []] $versionStrings )
246332 {
247- $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion ]::ParseVersionString($_ ) }
333+ $parseLanguage = (Get-Variable - Name " Language" - ValueOnly - ErrorAction " Ignore" )
334+ if ($parseLanguage -eq " python" ) {
335+ $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion ]::ParsePythonVersionString($_ ) }
336+ }
337+ else {
338+ $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion ]::ParseVersionString($_ ) }
339+ }
248340 $sortedVersions = [AzureEngSemanticVersion ]::SortVersions($versions )
249341 return ($sortedVersions | ForEach-Object { $_.RawVersion })
250342 }
@@ -429,6 +521,80 @@ class AzureEngSemanticVersion : IComparable {
429521 Write-Host " Error: version string did not correctly increment. Expected: $expected , Actual: $version "
430522 }
431523
524+ # Python post-release parsing tests
525+ $postVerString = " 1.0.0.post1"
526+ $postVer = [AzureEngSemanticVersion ]::ParsePythonVersionString($postVerString )
527+ if ($postVer.Major -ne 1 -or $postVer.Minor -ne 0 -or $postVer.Patch -ne 0 -or `
528+ ! $postVer.IsPostRelease -or $postVer.PostReleaseNumber -ne 1 -or $postVer.IsPrerelease ) {
529+ Write-Host " Error: Didn't correctly parse python post-release string $postVerString "
530+ }
531+ if ($postVerString -ne $postVer.ToString ()) {
532+ Write-Host " Error: post-release string did not correctly round trip with ToString. Expected: $ ( $postVerString ) , Actual: $ ( $postVer ) "
533+ }
534+
535+ # Implicit post-release number (PEP 440: 1.0.0.post == 1.0.0.post0)
536+ $implicitPostVerString = " 1.0.0.post"
537+ $implicitPostVer = [AzureEngSemanticVersion ]::ParsePythonVersionString($implicitPostVerString )
538+ if ($null -eq $implicitPostVer -or ! $implicitPostVer.IsSemVerFormat ) {
539+ Write-Host " Error: Failed to parse implicit post-release string $implicitPostVerString "
540+ }
541+ elseif ($implicitPostVer.Major -ne 1 -or $implicitPostVer.Minor -ne 0 -or $implicitPostVer.Patch -ne 0 -or `
542+ ! $implicitPostVer.IsPostRelease -or $implicitPostVer.PostReleaseNumber -ne 0 ) {
543+ Write-Host " Error: Didn't correctly parse implicit post-release string $implicitPostVerString "
544+ }
545+ $expected = " 1.0.0.post0"
546+ if ($expected -ne $implicitPostVer.ToString ()) {
547+ Write-Host " Error: implicit post-release did not normalize. Expected: $expected , Actual: $ ( $implicitPostVer ) "
548+ }
549+
550+ # Prerelease + post-release
551+ $preBetaPostString = " 1.0.0b2.post1"
552+ $preBetaPost = [AzureEngSemanticVersion ]::ParsePythonVersionString($preBetaPostString )
553+ if ($preBetaPost.Major -ne 1 -or $preBetaPost.Minor -ne 0 -or $preBetaPost.Patch -ne 0 -or `
554+ $preBetaPost.PrereleaseLabel -ne " b" -or $preBetaPost.PrereleaseNumber -ne 2 -or `
555+ ! $preBetaPost.IsPostRelease -or $preBetaPost.PostReleaseNumber -ne 1 ) {
556+ Write-Host " Error: Didn't correctly parse python prerelease post-release string $preBetaPostString "
557+ }
558+ if ($preBetaPostString -ne $preBetaPost.ToString ()) {
559+ Write-Host " Error: prerelease post-release string did not correctly round trip with ToString. Expected: $ ( $preBetaPostString ) , Actual: $ ( $preBetaPost ) "
560+ }
561+
562+ # Post-release alternate separators normalize to canonical form
563+ $expectedNormalized = " 1.0.0.post1"
564+ foreach ($altVerString in @ (" 1.0.0-post1" , " 1.0.0_post1" , " 1.0.0post1" )) {
565+ $parsed = [AzureEngSemanticVersion ]::ParsePythonVersionString($altVerString )
566+ if ($null -eq $parsed -or ! $parsed.IsPostRelease -or $parsed.PostReleaseNumber -ne 1 ) {
567+ Write-Host " Error: Failed to parse alternate post-release format $altVerString "
568+ }
569+ if ($expectedNormalized -ne $parsed.ToString ()) {
570+ Write-Host " Error: Alternate post-release '$altVerString ' did not normalize. Expected: $expectedNormalized , Actual: $ ( $parsed ) "
571+ }
572+ }
573+
574+ # Post-release increment clears post state
575+ $postIncVer = [AzureEngSemanticVersion ]::ParsePythonVersionString(" 1.0.0.post1" )
576+ $postIncVer.IncrementAndSetToPrerelease ()
577+ $expected = " 1.1.0b1"
578+ if ($expected -ne $postIncVer.ToString ()) {
579+ Write-Host " Error: post-release increment did not produce expected result. Expected: $expected , Actual: $ ( $postIncVer ) "
580+ }
581+
582+ # Post-release increment stays in post state
583+ $postBumpVer = [AzureEngSemanticVersion ]::ParsePythonVersionString(" 1.0.0.post1" )
584+ $postBumpVer.IncrementAndSetToPostRelease ()
585+ $expected = " 1.0.0.post2"
586+ if ($expected -ne $postBumpVer.ToString ()) {
587+ Write-Host " Error: post-release bump did not produce expected result. Expected: $expected , Actual: $ ( $postBumpVer ) "
588+ }
589+
590+ # Non-post version enters post state
591+ $gaToPost = [AzureEngSemanticVersion ]::ParsePythonVersionString(" 1.0.0" )
592+ $gaToPost.IncrementAndSetToPostRelease ()
593+ $expected = " 1.0.0.post1"
594+ if ($expected -ne $gaToPost.ToString ()) {
595+ Write-Host " Error: GA to post-release did not produce expected result. Expected: $expected , Actual: $ ( $gaToPost ) "
596+ }
597+
432598 Write-Host " QuickTests done"
433599 }
434600}
0 commit comments