1 <# 2 .DESCRIPTION 3 Parses a semver version string into its components and supports operations around it that we use for versioning our packages. 4 5 See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning 6 7 Example: 1.2.3-beta.4 8 Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber 9 10 Example: 1.2.3-alpha.20200828.4 11 Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber.BuildNumber 12 13 Note: A builtin Powershell version of SemVer exists in 'System.Management.Automation'. At this time, it does not parsing of PrereleaseNumber. It's name is also type accelerated to 'SemVer'. 14 #> 15 16 class AzureEngSemanticVersion : IComparable { 17 [int] $Major 18 [int] $Minor 19 [int] $Patch 20 [string] $PrereleaseLabelSeparator 21 [string] $PrereleaseLabel 22 [string] $PrereleaseNumberSeparator 23 [string] $BuildNumberSeparator 24 # BuildNumber is string to preserve zero-padding where applicable 25 [string] $BuildNumber 26 [int] $PrereleaseNumber 27 [bool] $IsPrerelease 28 [string] $VersionType 29 [string] $RawVersion 30 [bool] $IsSemVerFormat 31 [string] $DefaultPrereleaseLabel 32 [string] $DefaultAlphaReleaseLabel 33 34 # Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 35 # Validation: https://regex101.com/r/vkijKf/426 36 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}))?)?)?" 37 38 static [AzureEngSemanticVersion] ParseVersionString([string] $versionString) 39 { 40 $version = [AzureEngSemanticVersion]::new($versionString) 41 42 if (!$version.IsSemVerFormat) { 43 return $null 44 } 45 return $version 46 } 47 48 static [AzureEngSemanticVersion] ParsePythonVersionString([string] $versionString) 49 { 50 $version = [AzureEngSemanticVersion]::ParseVersionString($versionString) 51 52 if (!$version) { 53 return $null 54 } 55 56 $version.SetupPythonConventions() 57 return $version 58 } 59 60 AzureEngSemanticVersion([string] $versionString) 61 { 62 if ($versionString -match "^$([AzureEngSemanticVersion]::SEMVER_REGEX)$") 63 { 64 $this.IsSemVerFormat = $true 65 $this.RawVersion = $versionString 66 $this.Major = [int]$matches.Major 67 $this.Minor = [int]$matches.Minor 68 $this.Patch = [int]$matches.Patch 69 70 # If Language exists and is set to python setup the python conventions. 71 $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore") 72 if ($parseLanguage -eq "python") { 73 $this.SetupPythonConventions() 74 } 75 else { 76 $this.SetupDefaultConventions() 77 } 78 79 if ($null -eq $matches['prelabel']) 80 { 81 # artifically provide these values for non-prereleases to enable easy sorting of them later than prereleases. 82 $this.PrereleaseLabel = "zzz" 83 $this.PrereleaseNumber = 99999999 84 $this.IsPrerelease = $false 85 $this.VersionType = "GA" 86 if ($this.Patch -ne 0) { 87 $this.VersionType = "Patch" 88 } 89 } 90 else 91 { 92 $this.PrereleaseLabel = $matches["prelabel"] 93 $this.PrereleaseLabelSeparator = $matches["presep"] 94 $this.PrereleaseNumber = [int]$matches["prenumber"] 95 $this.PrereleaseNumberSeparator = $matches["prenumsep"] 96 $this.IsPrerelease = $true 97 $this.VersionType = "Beta" 98 99 $this.BuildNumberSeparator = $matches["buildnumsep"] 100 $this.BuildNumber = $matches["buildnumber"] 101 } 102 } 103 else 104 { 105 $this.RawVersion = $versionString 106 $this.IsSemVerFormat = $false 107 } 108 } 109 110 # If a prerelease label exists, it must be 'beta', and similar semantics used in our release guidelines 111 # See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning 112 [bool] HasValidPrereleaseLabel() 113 { 114 if ($this.IsPrerelease -eq $true) { 115 if ($this.PrereleaseLabel -ne $this.DefaultPrereleaseLabel -and $this.PrereleaseLabel -ne $this.DefaultAlphaReleaseLabel) { 116 Write-Host "Unexpected pre-release identifier '$($this.PrereleaseLabel)', "` 117 "should be '$($this.DefaultPrereleaseLabel)' or '$($this.DefaultAlphaReleaseLabel)'" 118 return $false; 119 } 120 if ($this.PrereleaseNumber -lt 1) 121 { 122 Write-Host "Unexpected pre-release version '$($this.PrereleaseNumber)', should be >= '1'" 123 return $false; 124 } 125 } 126 127 return $true; 128 } 129 130 [string] ToString() 131 { 132 $versionString = "{0}.{1}.{2}" -F $this.Major, $this.Minor, $this.Patch 133 134 if ($this.IsPrerelease) 135 { 136 $versionString += $this.PrereleaseLabelSeparator + $this.PrereleaseLabel + ` 137 $this.PrereleaseNumberSeparator + $this.PrereleaseNumber 138 if ($this.BuildNumber) { 139 $versionString += $this.BuildNumberSeparator + $this.BuildNumber 140 } 141 } 142 return $versionString; 143 } 144 145 [void] IncrementAndSetToPrerelease() { 146 if ($this.IsPrerelease -eq $false) 147 { 148 $this.PrereleaseLabel = $this.DefaultPrereleaseLabel 149 $this.PrereleaseNumber = 1 150 $this.Minor++ 151 $this.Patch = 0 152 $this.IsPrerelease = $true 153 } 154 else 155 { 156 if ($this.BuildNumber) { 157 throw "Cannot increment releases tagged with azure pipelines build numbers" 158 } 159 $this.PrereleaseNumber++ 160 } 161 } 162 163 [void] SetupPythonConventions() 164 { 165 # Python uses no separators and "b" for beta so this sets up the the object to work with those conventions 166 $this.PrereleaseLabelSeparator = $this.PrereleaseNumberSeparator = $this.BuildNumberSeparator = "" 167 $this.DefaultPrereleaseLabel = "b" 168 $this.DefaultAlphaReleaseLabel = "a" 169 } 170 171 [void] SetupDefaultConventions() 172 { 173 # Use the default common conventions 174 $this.PrereleaseLabelSeparator = "-" 175 $this.PrereleaseNumberSeparator = "." 176 $this.BuildNumberSeparator = "." 177 $this.DefaultPrereleaseLabel = "beta" 178 $this.DefaultAlphaReleaseLabel = "alpha" 179 } 180 181 [int] CompareTo($other) 182 { 183 if ($other -isnot [AzureEngSemanticVersion]) { 184 throw "Cannot compare $other with $this" 185 } 186 187 $ret = $this.Major.CompareTo($other.Major) 188 if ($ret) { return $ret } 189 190 $ret = $this.Minor.CompareTo($other.Minor) 191 if ($ret) { return $ret } 192 193 $ret = $this.Patch.CompareTo($other.Patch) 194 if ($ret) { return $ret } 195 196 # Mimic PowerShell that uses case-insensitive comparisons by default. 197 $ret = [string]::Compare($this.PrereleaseLabel, $other.PrereleaseLabel, $true) 198 if ($ret) { return $ret } 199 200 $ret = $this.PrereleaseNumber.CompareTo($other.PrereleaseNumber) 201 if ($ret) { return $ret } 202 203 return ([int] $this.BuildNumber).CompareTo([int] $other.BuildNumber) 204 } 205 206 static [string[]] SortVersionStrings([string[]] $versionStrings) 207 { 208 $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) } 209 $sortedVersions = [AzureEngSemanticVersion]::SortVersions($versions) 210 return ($sortedVersions | ForEach-Object { $_.RawVersion }) 211 } 212 213 static [AzureEngSemanticVersion[]] SortVersions([AzureEngSemanticVersion[]] $versions) 214 { 215 return $versions | Sort-Object -Descending 216 } 217 218 static [void] QuickTests() 219 { 220 $global:Language = "" 221 $versions = @( 222 "1.0.1", 223 "2.0.0", 224 "2.0.0-alpha.20200920", 225 "2.0.0-alpha.20200920.1", 226 "2.0.0-beta.2", 227 "1.0.10", 228 "2.0.0-alpha.20201221.03", 229 "2.0.0-alpha.20201221.1", 230 "2.0.0-alpha.20201221.5", 231 "2.0.0-alpha.20201221.2", 232 "2.0.0-alpha.20201221.10", 233 "2.0.0-beta.1", 234 "2.0.0-beta.10", 235 "1.0.0", 236 "1.0.0b2", 237 "1.0.2") 238 239 $expectedSort = @( 240 "2.0.0", 241 "2.0.0-beta.10", 242 "2.0.0-beta.2", 243 "2.0.0-beta.1", 244 "2.0.0-alpha.20201221.10", 245 "2.0.0-alpha.20201221.5", 246 "2.0.0-alpha.20201221.03", 247 "2.0.0-alpha.20201221.2", 248 "2.0.0-alpha.20201221.1", 249 "2.0.0-alpha.20200920.1", 250 "2.0.0-alpha.20200920", 251 "1.0.10", 252 "1.0.2", 253 "1.0.1", 254 "1.0.0", 255 "1.0.0b2") 256 257 $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) 258 259 for ($i = 0; $i -lt $expectedSort.Count; $i++) 260 { 261 if ($sort[$i] -ne $expectedSort[$i]) { 262 Write-Host "Error: Incorrect version sort:" 263 Write-Host "Expected: " 264 Write-Host $expectedSort 265 Write-Host "Actual:" 266 Write-Host $sort 267 break 268 } 269 } 270 271 $alphaVerString = "1.2.3-alpha.20200828.9" 272 $alphaVer = [AzureEngSemanticVersion]::new($alphaVerString) 273 if (!$alphaVer.IsPrerelease) { 274 Write-Host "Expected alpha version to be marked as prerelease" 275 } 276 if ($alphaVer.Major -ne 1 -or $alphaVer.Minor -ne 2 -or $alphaVer.Patch -ne 3 -or ` 277 $alphaVer.PrereleaseLabel -ne "alpha" -or $alphaVer.PrereleaseNumber -ne 20200828 -or $alphaVer.BuildNumber -ne 9) { 278 Write-Host "Error: Didn't correctly parse alpha version string $alphaVerString" 279 } 280 if ($alphaVerString -ne $alphaVer.ToString()) { 281 Write-Host "Error: alpha string did not correctly round trip with ToString. Expected: $($alphaVerString), Actual: $($alphaVer)" 282 } 283 284 $global:Language = "python" 285 $pythonAlphaVerString = "1.2.3a20200828009" 286 $pythonAlphaVer = [AzureEngSemanticVersion]::new($pythonAlphaVerString) 287 if (!$pythonAlphaVer.IsPrerelease) { 288 Write-Host "Expected python alpha version to be marked as prerelease" 289 } 290 # Note: For python we lump build number into prerelease number, since it simplifies the code and regex, and is behaviorally the same 291 if ($pythonAlphaVer.Major -ne 1 -or $pythonAlphaVer.Minor -ne 2 -or $pythonAlphaVer.Patch -ne 3 ` 292 -or $pythonAlphaVer.PrereleaseLabel -ne "a" -or $pythonAlphaVer.PrereleaseNumber -ne 20200828 ` 293 -or $pythonAlphaVer.BuildNumber -ne "009") { 294 Write-Host "Error: Didn't correctly parse python alpha version string $pythonAlphaVerString" 295 } 296 if ($pythonAlphaVerString -ne $pythonAlphaVer.ToString()) { 297 Write-Host "Error: python alpha string did not correctly round trip with ToString. Expected: $($pythonAlphaVerString), Actual: $($pythonAlphaVer)" 298 } 299 300 $versions = @("1.0.1", "2.0.0", "2.0.0a20201208001", "2.0.0a20201105020", "2.0.0a20201208012", ` 301 "2.0.0b2", "1.0.10", "2.0.0b1", "2.0.0b10", "1.0.0", "1.0.0b2", "1.0.2") 302 $expectedSort = @("2.0.0", "2.0.0b10", "2.0.0b2", "2.0.0b1", "2.0.0a20201208012", "2.0.0a20201208001", ` 303 "2.0.0a20201105020", "1.0.10", "1.0.2", "1.0.1", "1.0.0", "1.0.0b2") 304 $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions) 305 for ($i = 0; $i -lt $expectedSort.Count; $i++) 306 { 307 if ($sort[$i] -ne $expectedSort[$i]) { 308 Write-Host "Error: Incorrect python version sort:" 309 Write-Host "Expected: " 310 Write-Host $expectedSort 311 Write-Host "Actual:" 312 Write-Host $sort 313 break 314 } 315 } 316 317 $global:Language = "" 318 319 $gaVerString = "1.2.3" 320 $gaVer = [AzureEngSemanticVersion]::ParseVersionString($gaVerString) 321 if ($gaVer.Major -ne 1 -or $gaVer.Minor -ne 2 -or $gaVer.Patch -ne 3) { 322 Write-Host "Error: Didn't correctly parse ga version string $gaVerString" 323 } 324 if ($gaVerString -ne $gaVer.ToString()) { 325 Write-Host "Error: Ga string did not correctly round trip with ToString. Expected: $($gaVerString), Actual: $($gaVer)" 326 } 327 $gaVer.IncrementAndSetToPrerelease() 328 if ("1.3.0-beta.1" -ne $gaVer.ToString()) { 329 Write-Host "Error: Ga string did not correctly increment" 330 } 331 332 $betaVerString = "1.2.3-beta.4" 333 $betaVer = [AzureEngSemanticVersion]::ParseVersionString($betaVerString) 334 if ($betaVer.Major -ne 1 -or $betaVer.Minor -ne 2 -or $betaVer.Patch -ne 3 -or $betaVer.PrereleaseLabel -ne "beta" -or $betaVer.PrereleaseNumber -ne 4) { 335 Write-Host "Error: Didn't correctly parse beta version string $betaVerString" 336 } 337 if ($betaVerString -ne $betaVer.ToString()) { 338 Write-Host "Error: beta string did not correctly round trip with ToString. Expected: $($betaVerString), Actual: $($betaVer)" 339 } 340 $betaVer.IncrementAndSetToPrerelease() 341 if ("1.2.3-beta.5" -ne $betaVer.ToString()) { 342 Write-Host "Error: Beta string did not correctly increment" 343 } 344 345 $pythonBetaVerString = "1.2.3b4" 346 $pbetaVer = [AzureEngSemanticVersion]::ParsePythonVersionString($pythonBetaVerString) 347 if ($pbetaVer.Major -ne 1 -or $pbetaVer.Minor -ne 2 -or $pbetaVer.Patch -ne 3 -or $pbetaVer.PrereleaseLabel -ne "b" -or $pbetaVer.PrereleaseNumber -ne 4) { 348 Write-Host "Error: Didn't correctly parse python beta string $pythonBetaVerString" 349 } 350 if ($pythonBetaVerString -ne $pbetaVer.ToString()) { 351 Write-Host "Error: python beta string did not correctly round trip with ToString" 352 } 353 $pbetaVer.IncrementAndSetToPrerelease() 354 if ("1.2.3b5" -ne $pbetaVer.ToString()) { 355 Write-Host "Error: Python beta string did not correctly increment" 356 } 357 358 Write-Host "QuickTests done" 359 } 360 } 361