1<#
2.DESCRIPTION
3Parses a semver version string into its components and supports operations around it that we use for versioning our packages.
4
5See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning
6
7Example: 1.2.3-beta.4
8Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber
9
10Example: 1.2.3-alpha.20200828.4
11Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber.BuildNumber
12
13Note: 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
16class 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.Major -eq 0) {
87 # Treat initial 0 versions as a prerelease beta's
88 $this.VersionType = "Beta"
89 $this.IsPrerelease = $true
90 }
91 elseif ($this.Patch -ne 0) {
92 $this.VersionType = "Patch"
93 }
94 }
95 else
96 {
97 $this.PrereleaseLabel = $matches["prelabel"]
98 $this.PrereleaseLabelSeparator = $matches["presep"]
99 $this.PrereleaseNumber = [int]$matches["prenumber"]
100 $this.PrereleaseNumberSeparator = $matches["prenumsep"]
101 $this.IsPrerelease = $true
102 $this.VersionType = "Beta"
103
104 $this.BuildNumberSeparator = $matches["buildnumsep"]
105 $this.BuildNumber = $matches["buildnumber"]
106 }
107 }
108 else
109 {
110 $this.RawVersion = $versionString
111 $this.IsSemVerFormat = $false
112 }
113 }
114
115 # If a prerelease label exists, it must be 'beta', and similar semantics used in our release guidelines
116 # See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning
117 [bool] HasValidPrereleaseLabel()
118 {
119 if ($this.IsPrerelease -eq $true) {
120 if ($this.PrereleaseLabel -ne $this.DefaultPrereleaseLabel -and $this.PrereleaseLabel -ne $this.DefaultAlphaReleaseLabel) {
121 Write-Host "Unexpected pre-release identifier '$($this.PrereleaseLabel)', "`
122 "should be '$($this.DefaultPrereleaseLabel)' or '$($this.DefaultAlphaReleaseLabel)'"
123 return $false;
124 }
125 if ($this.PrereleaseNumber -lt 1)
126 {
127 Write-Host "Unexpected pre-release version '$($this.PrereleaseNumber)', should be >= '1'"
128 return $false;
129 }
130 }
131
132 return $true;
133 }
134
135 [string] ToString()
136 {
137 $versionString = "{0}.{1}.{2}" -F $this.Major, $this.Minor, $this.Patch
138
139 if ($this.IsPrerelease -and $this.PrereleaseLabel -ne "zzz")
140 {
141 $versionString += $this.PrereleaseLabelSeparator + $this.PrereleaseLabel + `
142 $this.PrereleaseNumberSeparator + $this.PrereleaseNumber
143 if ($this.BuildNumber) {
144 $versionString += $this.BuildNumberSeparator + $this.BuildNumber
145 }
146 }
147 return $versionString;
148 }
149
150 [void] IncrementAndSetToPrerelease() {
151 if ($this.IsPrerelease -eq $false)
152 {
153 $this.PrereleaseLabel = $this.DefaultPrereleaseLabel
154 $this.PrereleaseNumber = 1
155 $this.Minor++
156 $this.Patch = 0
157 $this.IsPrerelease = $true
158 }
159 else
160 {
161 if ($this.BuildNumber) {
162 throw "Cannot increment releases tagged with azure pipelines build numbers"
163 }
164 $this.PrereleaseNumber++
165 }
166 }
167
168 [void] SetupPythonConventions()
169 {
170 # Python uses no separators and "b" for beta so this sets up the the object to work with those conventions
171 $this.PrereleaseLabelSeparator = $this.PrereleaseNumberSeparator = $this.BuildNumberSeparator = ""
172 $this.DefaultPrereleaseLabel = "b"
173 $this.DefaultAlphaReleaseLabel = "a"
174 }
175
176 [void] SetupDefaultConventions()
177 {
178 # Use the default common conventions
179 $this.PrereleaseLabelSeparator = "-"
180 $this.PrereleaseNumberSeparator = "."
181 $this.BuildNumberSeparator = "."
182 $this.DefaultPrereleaseLabel = "beta"
183 $this.DefaultAlphaReleaseLabel = "alpha"
184 }
185
186 [int] CompareTo($other)
187 {
188 if ($other -isnot [AzureEngSemanticVersion]) {
189 throw "Cannot compare $other with $this"
190 }
191
192 $ret = $this.Major.CompareTo($other.Major)
193 if ($ret) { return $ret }
194
195 $ret = $this.Minor.CompareTo($other.Minor)
196 if ($ret) { return $ret }
197
198 $ret = $this.Patch.CompareTo($other.Patch)
199 if ($ret) { return $ret }
200
201 # Mimic PowerShell that uses case-insensitive comparisons by default.
202 $ret = [string]::Compare($this.PrereleaseLabel, $other.PrereleaseLabel, $true)
203 if ($ret) { return $ret }
204
205 $ret = $this.PrereleaseNumber.CompareTo($other.PrereleaseNumber)
206 if ($ret) { return $ret }
207
208 return ([int] $this.BuildNumber).CompareTo([int] $other.BuildNumber)
209 }
210
211 static [string[]] SortVersionStrings([string[]] $versionStrings)
212 {
213 $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) }
214 $sortedVersions = [AzureEngSemanticVersion]::SortVersions($versions)
215 return ($sortedVersions | ForEach-Object { $_.RawVersion })
216 }
217
218 static [AzureEngSemanticVersion[]] SortVersions([AzureEngSemanticVersion[]] $versions)
219 {
220 return $versions | Sort-Object -Descending
221 }
222
223 static [void] QuickTests()
224 {
225 $global:Language = ""
226 $versions = @(
227 "1.0.1",
228 "2.0.0",
229 "2.0.0-alpha.20200920",
230 "2.0.0-alpha.20200920.1",
231 "2.0.0-beta.2",
232 "1.0.10",
233 "2.0.0-alpha.20201221.03",
234 "2.0.0-alpha.20201221.1",
235 "2.0.0-alpha.20201221.5",
236 "2.0.0-alpha.20201221.2",
237 "2.0.0-alpha.20201221.10",
238 "2.0.0-beta.1",
239 "2.0.0-beta.10",
240 "1.0.0",
241 "1.0.0b2",
242 "1.0.2")
243
244 $expectedSort = @(
245 "2.0.0",
246 "2.0.0-beta.10",
247 "2.0.0-beta.2",
248 "2.0.0-beta.1",
249 "2.0.0-alpha.20201221.10",
250 "2.0.0-alpha.20201221.5",
251 "2.0.0-alpha.20201221.03",
252 "2.0.0-alpha.20201221.2",
253 "2.0.0-alpha.20201221.1",
254 "2.0.0-alpha.20200920.1",
255 "2.0.0-alpha.20200920",
256 "1.0.10",
257 "1.0.2",
258 "1.0.1",
259 "1.0.0",
260 "1.0.0b2")
261
262 $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions)
263
264 for ($i = 0; $i -lt $expectedSort.Count; $i++)
265 {
266 if ($sort[$i] -ne $expectedSort[$i]) {
267 Write-Host "Error: Incorrect version sort:"
268 Write-Host "Expected: "
269 Write-Host $expectedSort
270 Write-Host "Actual:"
271 Write-Host $sort
272 break
273 }
274 }
275
276 $alphaVerString = "1.2.3-alpha.20200828.9"
277 $alphaVer = [AzureEngSemanticVersion]::new($alphaVerString)
278 if (!$alphaVer.IsPrerelease) {
279 Write-Host "Expected alpha version to be marked as prerelease"
280 }
281 if ($alphaVer.Major -ne 1 -or $alphaVer.Minor -ne 2 -or $alphaVer.Patch -ne 3 -or `
282 $alphaVer.PrereleaseLabel -ne "alpha" -or $alphaVer.PrereleaseNumber -ne 20200828 -or $alphaVer.BuildNumber -ne 9) {
283 Write-Host "Error: Didn't correctly parse alpha version string $alphaVerString"
284 }
285 if ($alphaVerString -ne $alphaVer.ToString()) {
286 Write-Host "Error: alpha string did not correctly round trip with ToString. Expected: $($alphaVerString), Actual: $($alphaVer)"
287 }
288
289 $global:Language = "python"
290 $pythonAlphaVerString = "1.2.3a20200828009"
291 $pythonAlphaVer = [AzureEngSemanticVersion]::new($pythonAlphaVerString)
292 if (!$pythonAlphaVer.IsPrerelease) {
293 Write-Host "Expected python alpha version to be marked as prerelease"
294 }
295 # Note: For python we lump build number into prerelease number, since it simplifies the code and regex, and is behaviorally the same
296 if ($pythonAlphaVer.Major -ne 1 -or $pythonAlphaVer.Minor -ne 2 -or $pythonAlphaVer.Patch -ne 3 `
297 -or $pythonAlphaVer.PrereleaseLabel -ne "a" -or $pythonAlphaVer.PrereleaseNumber -ne 20200828 `
298 -or $pythonAlphaVer.BuildNumber -ne "009") {
299 Write-Host "Error: Didn't correctly parse python alpha version string $pythonAlphaVerString"
300 }
301 if ($pythonAlphaVerString -ne $pythonAlphaVer.ToString()) {
302 Write-Host "Error: python alpha string did not correctly round trip with ToString. Expected: $($pythonAlphaVerString), Actual: $($pythonAlphaVer)"
303 }
304
305 $versions = @("1.0.1", "2.0.0", "2.0.0a20201208001", "2.0.0a20201105020", "2.0.0a20201208012", `
306 "2.0.0b2", "1.0.10", "2.0.0b1", "2.0.0b10", "1.0.0", "1.0.0b2", "1.0.2")
307 $expectedSort = @("2.0.0", "2.0.0b10", "2.0.0b2", "2.0.0b1", "2.0.0a20201208012", "2.0.0a20201208001", `
308 "2.0.0a20201105020", "1.0.10", "1.0.2", "1.0.1", "1.0.0", "1.0.0b2")
309 $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions)
310 for ($i = 0; $i -lt $expectedSort.Count; $i++)
311 {
312 if ($sort[$i] -ne $expectedSort[$i]) {
313 Write-Host "Error: Incorrect python version sort:"
314 Write-Host "Expected: "
315 Write-Host $expectedSort
316 Write-Host "Actual:"
317 Write-Host $sort
318 break
319 }
320 }
321
322 $global:Language = ""
323
324 $gaVerString = "1.2.3"
325 $gaVer = [AzureEngSemanticVersion]::ParseVersionString($gaVerString)
326 if ($gaVer.Major -ne 1 -or $gaVer.Minor -ne 2 -or $gaVer.Patch -ne 3) {
327 Write-Host "Error: Didn't correctly parse ga version string $gaVerString"
328 }
329 if ($gaVerString -ne $gaVer.ToString()) {
330 Write-Host "Error: Ga string did not correctly round trip with ToString. Expected: $($gaVerString), Actual: $($gaVer)"
331 }
332 $gaVer.IncrementAndSetToPrerelease()
333 if ("1.3.0-beta.1" -ne $gaVer.ToString()) {
334 Write-Host "Error: Ga string did not correctly increment"
335 }
336
337 $betaVerString = "1.2.3-beta.4"
338 $betaVer = [AzureEngSemanticVersion]::ParseVersionString($betaVerString)
339 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) {
340 Write-Host "Error: Didn't correctly parse beta version string $betaVerString"
341 }
342 if ($betaVerString -ne $betaVer.ToString()) {
343 Write-Host "Error: beta string did not correctly round trip with ToString. Expected: $($betaVerString), Actual: $($betaVer)"
344 }
345 $betaVer.IncrementAndSetToPrerelease()
346 if ("1.2.3-beta.5" -ne $betaVer.ToString()) {
347 Write-Host "Error: Beta string did not correctly increment"
348 }
349
350 $pythonBetaVerString = "1.2.3b4"
351 $pbetaVer = [AzureEngSemanticVersion]::ParsePythonVersionString($pythonBetaVerString)
352 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) {
353 Write-Host "Error: Didn't correctly parse python beta string $pythonBetaVerString"
354 }
355 if ($pythonBetaVerString -ne $pbetaVer.ToString()) {
356 Write-Host "Error: python beta string did not correctly round trip with ToString"
357 }
358 $pbetaVer.IncrementAndSetToPrerelease()
359 if ("1.2.3b5" -ne $pbetaVer.ToString()) {
360 Write-Host "Error: Python beta string did not correctly increment"
361 }
362
363 Write-Host "QuickTests done"
364 }
365}
View as plain text