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