1
2$ReleaseDevOpsOrgParameters = @("--organization", "https://dev.azure.com/azure-sdk")
3$ReleaseDevOpsCommonParameters = $ReleaseDevOpsOrgParameters + @("--output", "json")
4$ReleaseDevOpsCommonParametersWithProject = $ReleaseDevOpsCommonParameters + @("--project", "Release")
5
6function Get-DevOpsRestHeaders()
7{
8 $headers = $null
9 if (Get-Variable -Name "devops_pat" -ValueOnly -ErrorAction "Ignore")
10 {
11 $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes([string]::Format("{0}:{1}", "", $devops_pat)))
12 $headers = @{ Authorization = "Basic $encodedToken" }
13 }
14 else
15 {
16 # Get a temp access token from the logged in az cli user for azure devops resource
17 $jwt_accessToken = (az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query "accessToken" --output tsv)
18 $headers = @{ Authorization = "Bearer $jwt_accessToken" }
19 }
20
21 return $headers
22}
23
24function CheckDevOpsAccess()
25{
26 # Dummy test query to validate permissions
27 $query = "SELECT [System.ID] FROM WorkItems WHERE [Work Item Type] = 'Package' AND [Package] = 'azure-sdk-template'"
28
29 $response = Invoke-RestMethod -Method POST `
30 -Uri "https://dev.azure.com/azure-sdk/Release/_apis/wit/wiql/?api-version=6.0" `
31 -Headers (Get-DevOpsRestHeaders) -Body "{ ""query"": ""$query"" }" -ContentType "application/json" | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashTable
32
33 if ($response -isnot [HashTable] -or !$response.ContainsKey("workItems")) {
34 throw "Failed to run test query against Azure DevOps. Please ensure you are logged into the public azure cloud. Consider running 'az logout' and then 'az login'."
35 }
36}
37
38function Invoke-AzBoardsCmd($subCmd, $parameters, $output = $true)
39{
40 $azCmdStr = "az boards ${subCmd} $($parameters -join ' ')"
41 if ($output) {
42 Write-Host $azCmdStr
43 }
44 return Invoke-Expression "$azCmdStr" | ConvertFrom-Json -AsHashTable
45}
46
47function Invoke-Query($fields, $wiql, $output = $true)
48{
49 #POST https://dev.azure.com/{organization}/{project}/{team}/_apis/wit/wiql?timePrecision={timePrecision}&$top={$top}&api-version=6.1-preview.2
50
51 $body = @"
52{
53 "query": "$wiql"
54}
55"@
56
57 if ($output) {
58 Write-Host "Executing query $wiql"
59 }
60
61 $response = Invoke-RestMethod -Method POST `
62 -Uri "https://dev.azure.com/azure-sdk/Release/_apis/wit/wiql/?`$top=10000&api-version=6.0" `
63 -Headers (Get-DevOpsRestHeaders) -Body $body -ContentType "application/json" | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashTable
64
65 if ($response -isnot [HashTable] -or !$response.ContainsKey("workItems") -or $response.workItems.Count -eq 0) {
66 Write-Verbose "Query returned no items. $wiql"
67 return ,@()
68 }
69
70 $workItems = @()
71 $i = 0
72 do
73 {
74 $idBatch = @()
75 while ($idBatch.Count -lt 200 -and $i -lt $response.workItems.Count)
76 {
77 $idBatch += $response.workItems[$i].id
78 $i++
79 }
80
81 $uri = "https://dev.azure.com/azure-sdk/Release/_apis/wit/workitems?ids=$($idBatch -join ',')&fields=$($fields -join ',')&api-version=6.0"
82
83 Write-Verbose "Pulling work items $uri "
84
85 $batchResponse = Invoke-RestMethod -Method GET -Uri $uri `
86 -Headers (Get-DevOpsRestHeaders) -ContentType "application/json" -MaximumRetryCount 3 | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashTable
87
88 if ($batchResponse.value)
89 {
90 $batchResponse.value | ForEach-Object { $workItems += $_ }
91 }
92 else
93 {
94 Write-Warning "Batch return no items from $uri"
95 }
96 }
97 while ($i -lt $response.workItems.Count)
98
99 if ($output) {
100 Write-Host "Query return $($workItems.Count) items"
101 }
102
103 return $workItems
104}
105
106function LoginToAzureDevops([string]$devops_pat)
107{
108 if (!$devops_pat) {
109 return
110 }
111 $azCmdStr = "'$devops_pat' | az devops login $($ReleaseDevOpsOrgParameters -join ' ')"
112 Invoke-Expression $azCmdStr
113}
114
115function BuildHashKeyNoNull()
116{
117 $filterNulls = $args | Where-Object { $_ }
118 # if we had any nulls then return null
119 if (!$filterNulls -or $args.Count -ne $filterNulls.Count) {
120 return $null
121 }
122 return BuildHashKey $args
123}
124
125function BuildHashKey()
126{
127 # if no args or the first arg is null return null
128 if ($args.Count -lt 1 -or !$args[0]) {
129 return $null
130 }
131
132 # exclude null values
133 $keys = $args | Where-Object { $_ }
134 return $keys -join "|"
135}
136
137$parentWorkItems = @{}
138function FindParentWorkItem($serviceName, $packageDisplayName, $outputCommand = $false)
139{
140 $key = BuildHashKey $serviceName $packageDisplayName
141 if ($key -and $parentWorkItems.ContainsKey($key)) {
142 return $parentWorkItems[$key]
143 }
144
145 if ($serviceName) {
146 $serviceCondition = "[ServiceName] = '${serviceName}'"
147 if ($packageDisplayName) {
148 $serviceCondition += " AND [PackageDisplayName] = '${packageDisplayName}'"
149 }
150 else {
151 $serviceCondition += " AND [PackageDisplayName] = ''"
152 }
153 }
154 else {
155 $serviceCondition = "[ServiceName] <> ''"
156 }
157
158 $query = "SELECT [ID], [ServiceName], [PackageDisplayName], [Parent] FROM WorkItems WHERE [Work Item Type] = 'Epic' AND ${serviceCondition}"
159
160 $fields = @("System.Id", "Custom.ServiceName", "Custom.PackageDisplayName", "System.Parent")
161
162 $workItems = Invoke-Query $fields $query $outputCommand
163
164 foreach ($wi in $workItems)
165 {
166 $localKey = BuildHashKey $wi.fields["Custom.ServiceName"] $wi.fields["Custom.PackageDisplayName"]
167 if (!$localKey) { continue }
168 if ($parentWorkItems.ContainsKey($localKey) -and $parentWorkItems[$localKey].id -ne $wi.id) {
169 Write-Warning "Already found parent [$($parentWorkItems[$localKey].id)] with key [$localKey], using that one instead of [$($wi.id)]."
170 }
171 else {
172 Write-Verbose "[$($wi.id)]$localKey - Cached"
173 $parentWorkItems[$localKey] = $wi
174 }
175 }
176
177 if ($key -and $parentWorkItems.ContainsKey($key)) {
178 return $parentWorkItems[$key]
179 }
180 return $null
181}
182
183$packageWorkItems = @{}
184$packageWorkItemWithoutKeyFields = @{}
185
186function FindLatestPackageWorkItem($lang, $packageName, $outputCommand = $true)
187{
188 # Cache all the versions of this package and language work items
189 $null = FindPackageWorkItem $lang $packageName -includeClosed $true -outputCommand $outputCommand
190
191 $latestWI = $null
192 foreach ($wi in $packageWorkItems.Values)
193 {
194 if ($wi.fields["Custom.Language"] -ne $lang) { continue }
195 if ($wi.fields["Custom.Package"] -ne $packageName) { continue }
196
197 if (!$latestWI) {
198 $latestWI = $wi
199 continue
200 }
201
202 if (($wi.fields["Custom.PackageVersionMajorMinor"] -as [Version]) -gt ($latestWI.fields["Custom.PackageVersionMajorMinor"] -as [Version])) {
203 $latestWI = $wi
204 }
205 }
206 return $latestWI
207}
208
209function FindPackageWorkItem($lang, $packageName, $version, $outputCommand = $true, $includeClosed = $false)
210{
211 $key = BuildHashKeyNoNull $lang $packageName $version
212 if ($key -and $packageWorkItems.ContainsKey($key)) {
213 return $packageWorkItems[$key]
214 }
215
216 $fields = @()
217 $fields += "System.ID"
218 $fields += "System.State"
219 $fields += "System.AssignedTo"
220 $fields += "System.Parent"
221 $fields += "Custom.Language"
222 $fields += "Custom.Package"
223 $fields += "Custom.PackageDisplayName"
224 $fields += "System.Title"
225 $fields += "Custom.PackageType"
226 $fields += "Custom.PackageTypeNewLibrary"
227 $fields += "Custom.PackageVersionMajorMinor"
228 $fields += "Custom.PackageRepoPath"
229 $fields += "Custom.ServiceName"
230 $fields += "Custom.PlannedPackages"
231 $fields += "Custom.ShippedPackages"
232 $fields += "Custom.PackageBetaVersions"
233 $fields += "Custom.PackageGAVersion"
234 $fields += "Custom.PackagePatchVersions"
235 $fields += "Custom.Generated"
236 $fields += "Custom.RoadmapState"
237
238 $fieldList = ($fields | ForEach-Object { "[$_]"}) -join ", "
239 $query = "SELECT ${fieldList} FROM WorkItems WHERE [Work Item Type] = 'Package'"
240
241 if (!$includeClosed -and !$lang) {
242 $query += " AND [State] <> 'No Active Development' AND [PackageTypeNewLibrary] = true"
243 }
244 if ($lang) {
245 $query += " AND [Language] = '${lang}'"
246 }
247 if ($packageName) {
248 $query += " AND [Package] = '${packageName}'"
249 }
250 if ($version) {
251 $query += " AND [PackageVersionMajorMinor] = '${version}'"
252 }
253
254 $workItems = Invoke-Query $fields $query $outputCommand
255
256 foreach ($wi in $workItems)
257 {
258 $localKey = BuildHashKeyNoNull $wi.fields["Custom.Language"] $wi.fields["Custom.Package"] $wi.fields["Custom.PackageVersionMajorMinor"]
259 if (!$localKey) {
260 $packageWorkItemWithoutKeyFields[$wi.id] = $wi
261 Write-Host "Skipping package [$($wi.id)]$($wi.fields['System.Title']) which is missing required fields language, package, or version."
262 continue
263 }
264 if ($packageWorkItems.ContainsKey($localKey) -and $packageWorkItems[$localKey].id -ne $wi.id) {
265 Write-Warning "Already found package [$($packageWorkItems[$localKey].id)] with key [$localKey], using that one instead of [$($wi.id)]."
266 }
267 else {
268 Write-Verbose "Caching package [$($wi.id)] for [$localKey]"
269 $packageWorkItems[$localKey] = $wi
270 }
271 }
272
273 if ($key -and $packageWorkItems.ContainsKey($key)) {
274 return $packageWorkItems[$key]
275 }
276 return $null
277}
278
279function InitializeWorkItemCache($outputCommand = $true, $includeClosed = $false)
280{
281 # Pass null to cache all service parents
282 $null = FindParentWorkItem -serviceName $null -packageDisplayName $null -outputCommand $outputCommand
283
284 # Pass null to cache all the package items
285 $null = FindPackageWorkItem -lang $null -packageName $null -version $null -outputCommand $outputCommand -includeClosed $includeClosed
286}
287
288function GetCachedPackageWorkItems()
289{
290 return $packageWorkItems.Values
291}
292
293function UpdateWorkItemParent($childWorkItem, $parentWorkItem, $outputCommand = $true)
294{
295 $childId = $childWorkItem.id
296 $existingParentId = $childWorkItem.fields["System.Parent"]
297 $newParentId = $parentWorkItem.id
298
299 if ($existingParentId -eq $newParentId) {
300 return
301 }
302
303 CreateWorkItemParent $childId $newParentId $existingParentId -outputCommand $outputCommand
304 $childWorkItem.fields["System.Parent"] = $newParentId
305}
306
307function CreateWorkItemParent($id, $parentId, $oldParentId, $outputCommand = $true)
308{
309 # Have to remove old parent first if you want to add a new parent.
310 if ($oldParentId)
311 {
312 $parameters = $ReleaseDevOpsCommonParameters
313 $parameters += "--yes"
314 $parameters += "--id", $id
315 $parameters += "--relation-type", "parent"
316 $parameters += "--target-id", $oldParentId
317
318 Invoke-AzBoardsCmd "work-item relation remove" $parameters $outputCommand | Out-Null
319 }
320
321 $parameters = $ReleaseDevOpsCommonParameters
322 $parameters += "--id", $id
323 $parameters += "--relation-type", "parent"
324 $parameters += "--target-id", $parentId
325
326 Invoke-AzBoardsCmd "work-item relation add" $parameters $outputCommand | Out-Null
327}
328function CreateWorkItem($title, $type, $iteration, $area, $fields, $assignedTo, $parentId, $outputCommand = $true)
329{
330 $parameters = $ReleaseDevOpsCommonParametersWithProject
331 $parameters += "--title", "`"${title}`""
332 $parameters += "--type", "`"${type}`""
333 $parameters += "--iteration", "`"${iteration}`""
334 $parameters += "--area", "`"${area}`""
335 if ($assignedTo) {
336 $parameters += "--assigned-to", "`"${assignedTo}`""
337 }
338 if ($fields) {
339 $parameters += "--fields"
340 $parameters += $fields
341 }
342
343 $workItem = Invoke-AzBoardsCmd "work-item create" $parameters $outputCommand
344
345 if ($parentId) {
346 $parameters = $ReleaseDevOpsCommonParameters
347 $parameters += "--id", $workItem.id
348 $parameters += "--relation-type", "parent"
349 $parameters += "--target-id", $parentId
350
351 Invoke-AzBoardsCmd "work-item relation add" $parameters $outputCommand | Out-Null
352 }
353
354 return $workItem
355}
356
357function UpdateWorkItem($id, $fields, $title, $state, $assignedTo, $outputCommand = $true)
358{
359 $parameters = $ReleaseDevOpsCommonParameters
360 $parameters += "--id", $id
361 if ($title) {
362 $parameters += "--title", "`"${title}`""
363 }
364 if ($state) {
365 $parameters += "--state", "`"${state}`""
366 }
367 if ($assignedTo) {
368 $parameters += "--assigned-to", "`"${assignedTo}`""
369 }
370 if ($fields) {
371 $parameters += "--fields"
372 $parameters += $fields
373 }
374
375 return Invoke-AzBoardsCmd "work-item update" $parameters $outputCommand
376}
377
378function UpdatePackageWorkItemReleaseState($id, $state, $releaseType, $outputCommand = $true)
379{
380 $fields = "`"Custom.ReleaseType=${releaseType}`""
381 return UpdateWorkItem -id $id -state $state -fields $fields -outputCommand $outputCommand
382}
383
384function FindOrCreateClonePackageWorkItem($lang, $pkg, $verMajorMinor, $allowPrompt = $false, $outputCommand = $false)
385{
386 $workItem = FindPackageWorkItem -lang $lang -packageName $pkg.Package -version $verMajorMinor -includeClosed $true -outputCommand $outputCommand
387
388 if (!$workItem) {
389 $latestVersionItem = FindLatestPackageWorkItem -lang $lang -packageName $pkg.Package -outputCommand $outputCommand
390 $assignedTo = "me"
391 $extraFields = @()
392 if ($latestVersionItem) {
393 Write-Verbose "Copying data from latest matching [$($latestVersionItem.id)] with version $($latestVersionItem.fields["Custom.PackageVersionMajorMinor"])"
394 if ($latestVersionItem.fields["System.AssignedTo"]) {
395 $assignedTo = $latestVersionItem.fields["System.AssignedTo"]["uniqueName"]
396 }
397 $pkg.DisplayName = $latestVersionItem.fields["Custom.PackageDisplayName"]
398 $pkg.ServiceName = $latestVersionItem.fields["Custom.ServiceName"]
399 if (!$pkg.RepoPath -and $pkg.RepoPath -ne "NA" -and $pkg.fields["Custom.PackageRepoPath"]) {
400 $pkg.RepoPath = $pkg.fields["Custom.PackageRepoPath"]
401 }
402
403 if ($latestVersionItem.fields["Custom.Generated"]) {
404 $extraFields += "`"Generated=" + $latestVersionItem.fields["Custom.Generated"] + "`""
405 }
406
407 if ($latestVersionItem.fields["Custom.RoadmapState"]) {
408 $extraFields += "`"RoadmapState=" + $latestVersionItem.fields["Custom.RoadmapState"] + "`""
409 }
410 }
411
412 if ($allowPrompt) {
413 if (!$pkg.DisplayName) {
414 Write-Host "Display name is used to identify this package across languages and is usually the friendly name (i.e. For 'Azure Anomaly Detector' it would be 'Anomaly Detector'. For 'Azure Cognitive Search' it would be 'Search'.). See https://aka.ms/azsdk/mark-release-status for more info."
415 while (($readInput = Read-Host -Prompt "Input the display name") -eq "") { }
416 $packageInfo.DisplayName = $readInput
417 }
418
419 if (!$pkg.ServiceName) {
420 Write-Host "This is the friendly service name for this package that is used to align it with other packages and languages (i.e., no need to include 'Azure' or 'Microsoft' in the title). The service name is sometimes the same as the `Package Display Name` if there is only one package for a service. (i.e. For 'Azure Anomaly Detector' it would be 'Anomaly Detector'). For services that ship multiple packages be sure to list the service only. (i.e. For 'Schema Registry Avro', the service name is just 'Schema Registry'; For 'Key Vault Certificates', the service name is simply Key Vault.). See https://aka.ms/azsdk/mark-release-status for more info."
421 while (($readInput = Read-Host -Prompt "Input the service name") -eq "") { }
422 $packageInfo.ServiceName = $readInput
423 }
424 }
425
426
427 $workItem = CreateOrUpdatePackageWorkItem $lang $pkg $verMajorMinor -existingItem $null -assignedTo $assignedTo -extraFields $extraFields -outputCommand $outputCommand
428 }
429
430 return $workItem
431}
432
433function CreateOrUpdatePackageWorkItem($lang, $pkg, $verMajorMinor, $existingItem, $assignedTo = $null, $extraFields = $null, $outputCommand = $true)
434{
435 if (!$lang -or !$pkg -or !$verMajorMinor) {
436 Write-Host "Cannot create or update because one of lang, pkg or verMajorMinor aren't set. [$lang|$($pkg.Package)|$verMajorMinor]"
437 return
438 }
439 $pkgName = $pkg.Package
440 $pkgDisplayName = $pkg.DisplayName
441 $pkgType = $pkg.Type
442 $pkgNewLibrary = $pkg.New
443 $pkgRepoPath = $pkg.RepoPath
444 $serviceName = $pkg.ServiceName
445 $title = $lang + " - " + $pkg.DisplayName + " - " + $verMajorMinor
446
447 $fields = @()
448 $fields += "`"Language=${lang}`""
449 $fields += "`"Package=${pkgName}`""
450 $fields += "`"PackageDisplayName=${pkgDisplayName}`""
451 $fields += "`"PackageType=${pkgType}`""
452 $fields += "`"PackageTypeNewLibrary=${pkgNewLibrary}`""
453 $fields += "`"PackageVersionMajorMinor=${verMajorMinor}`""
454 $fields += "`"ServiceName=${serviceName}`""
455 $fields += "`"PackageRepoPath=${pkgRepoPath}`""
456
457 if ($extraFields) {
458 $fields += $extraFields
459 }
460
461 if ($existingItem)
462 {
463 $changedField = $null
464
465 if ($lang -ne $existingItem.fields["Custom.Language"]) { $changedField = "Custom.Language" }
466 if ($pkgName -ne $existingItem.fields["Custom.Package"]) { $changedField = "Custom.Package" }
467 if ($verMajorMinor -ne $existingItem.fields["Custom.PackageVersionMajorMinor"]) { $changedField = "Custom.PackageVersionMajorMinor" }
468 if ($pkgDisplayName -ne $existingItem.fields["Custom.PackageDisplayName"]) { $changedField = "Custom.PackageDisplayName" }
469 if ($pkgType -ne $existingItem.fields["Custom.PackageType"]) { $changedField = "Custom.PackageType" }
470 if ($pkgNewLibrary -ne $existingItem.fields["Custom.PackageTypeNewLibrary"]) { $changedField = "Custom.PackageTypeNewLibrary" }
471 if ($pkgRepoPath -ne $existingItem.fields["Custom.PackageRepoPath"]) { $changedField = "Custom.PackageRepoPath" }
472 if ($serviceName -ne $existingItem.fields["Custom.ServiceName"]) { $changedField = "Custom.ServiceName" }
473 if ($title -ne $existingItem.fields["System.Title"]) { $changedField = "System.Title" }
474
475 if ($changedField) {
476 Write-Host "At least field $changedField ($($existingItem.fields[$changedField])) changed so updating."
477 }
478
479 if ($changedField) {
480 $beforeState = $existingItem.fields["System.State"]
481
482 # Need to set to New to be able to update
483 $existingItem = UpdateWorkItem -id $existingItem.id -fields $fields -title $title -state "New" -assignedTo $assignedTo -outputCommand $outputCommand
484 Write-Host "[$($existingItem.id)]$lang - $pkgName($verMajorMinor) - Updated"
485
486 if ($beforeState -ne $existingItem.fields['System.State']) {
487 Write-Verbose "Resetting state for [$($existingItem.id)] from '$($existingItem.fields['System.State'])' to '$beforeState'"
488 $existingItem = UpdateWorkItem $existingItem.id -state $beforeState -outputCommand $outputCommand
489 }
490 }
491
492 $newparentItem = FindOrCreatePackageGroupParent $serviceName $pkgDisplayName -outputCommand $false
493 UpdateWorkItemParent $existingItem $newParentItem -outputCommand $outputCommand
494 return $existingItem
495 }
496
497 $parentItem = FindOrCreatePackageGroupParent $serviceName $pkgDisplayName -outputCommand $false
498 $workItem = CreateWorkItem $title "Package" "Release" "Release" $fields $assignedTo $parentItem.id -outputCommand $outputCommand
499 Write-Host "[$($workItem.id)]$lang - $pkgName($verMajorMinor) - Created"
500 return $workItem
501}
502
503function FindOrCreatePackageGroupParent($serviceName, $packageDisplayName, $outputCommand = $true)
504{
505 $existingItem = FindParentWorkItem $serviceName $packageDisplayName -outputCommand $outputCommand
506 if ($existingItem) {
507 $newparentItem = FindOrCreateServiceParent $serviceName -outputCommand $outputCommand
508 UpdateWorkItemParent $existingItem $newParentItem
509 return $existingItem
510 }
511
512 $fields = @()
513 $fields += "`"PackageDisplayName=${packageDisplayName}`""
514 $fields += "`"ServiceName=${serviceName}`""
515 $serviceParentItem = FindOrCreateServiceParent $serviceName -outputCommand $outputCommand
516 $workItem = CreateWorkItem $packageDisplayName "Epic" "Release" "Release" $fields $null $serviceParentItem.id
517
518 $localKey = BuildHashKey $serviceName $packageDisplayName
519 Write-Host "[$($workItem.id)]$localKey - Created Parent"
520 $parentWorkItems[$localKey] = $workItem
521 return $workItem
522}
523
524function FindOrCreateServiceParent($serviceName, $outputCommand = $true)
525{
526 $serviceParent = FindParentWorkItem $serviceName -outputCommand $outputCommand
527 if ($serviceParent) {
528 return $serviceParent
529 }
530
531 $fields = @()
532 $fields += "`"PackageDisplayName=`""
533 $fields += "`"ServiceName=${serviceName}`""
534 $parentId = $null
535 $workItem = CreateWorkItem $serviceName "Epic" "Release" "Release" $fields $null $parentId -outputCommand $outputCommand
536
537 $localKey = BuildHashKey $serviceName
538 Write-Host "[$($workItem.id)]$localKey - Created"
539 $parentWorkItems[$localKey] = $workItem
540 return $workItem
541}
542
543function ParseVersionSetFromMDField([string]$field)
544{
545 $MDTableRegex = "\|\s*(?<t>\S*)\s*\|\s*(?<v>\S*)\s*\|\s*(?<d>\S*)\s*\|"
546 $versionSet = @{}
547 $tableMatches = [Regex]::Matches($field, $MDTableRegex)
548
549 foreach ($match in $tableMatches)
550 {
551 if ($match.Groups["t"].Value -eq "Type" -or $match.Groups["t"].Value -eq "-") {
552 continue
553 }
554 $version = New-Object PSObject -Property @{
555 Type = $match.Groups["t"].Value
556 Version = $match.Groups["v"].Value
557 Date = $match.Groups["d"].Value
558 }
559 if (!$versionSet.ContainsKey($version.Version)) {
560 $versionSet[$version.Version] = $version
561 }
562 }
563 return $versionSet
564}
565
566function GetTextVersionFields($versionList, $pkgWorkItem)
567{
568 $betaVersions = $gaVersions = $patchVersions = ""
569 foreach ($v in $versionList) {
570 $vstr = "$($v.Version),$($v.Date)"
571 if ($v.Type -eq "Beta") {
572 if ($betaVersions.Length + $vstr.Length -lt 255) {
573 if ($betaVersions.Length -gt 0) { $betaVersions += "|" }
574 $betaVersions += $vstr
575 }
576 }
577 elseif ($v.Type -eq "GA") {
578 if ($gaVersions.Length + $vstr.Length -lt 255) {
579 if ($gaVersions.Length -gt 0) { $gaVersions += "|" }
580 $gaVersions += $vstr
581 }
582 }
583 elseif ($v.Type -eq "Patch") {
584 if ($patchVersions.Length + $vstr.Length -lt 255) {
585 if ($patchVersions.Length -gt 0) { $patchVersions += "|" }
586 $patchVersions += $vstr
587 }
588 }
589 }
590
591 $fieldUpdates = @()
592 if ("$($pkgWorkItem.fields["Custom.PackageBetaVersions"])" -ne $betaVersions)
593 {
594 $fieldUpdates += @"
595{
596 "op": "replace",
597 "path": "/fields/PackageBetaVersions",
598 "value": "$betaVersions"
599}
600"@
601 }
602
603 if ("$($pkgWorkItem.fields["Custom.PackageGAVersion"])" -ne $gaVersions)
604 {
605 $fieldUpdates += @"
606{
607 "op": "replace",
608 "path": "/fields/PackageGAVersion",
609 "value": "$gaVersions"
610}
611"@
612 }
613
614 if ("$($pkgWorkItem.fields["Custom.PackagePatchVersions"])" -ne $patchVersions)
615 {
616 $fieldUpdates += @"
617{
618 "op": "replace",
619 "path": "/fields/PackagePatchVersions",
620 "value": "$patchVersions"
621}
622"@
623 }
624 return ,$fieldUpdates
625}
626
627function GetMDVersionValue($versionlist)
628{
629 $mdVersions = ""
630 $mdFormat = "| {0} | {1} | {2} |`n"
631
632 $htmlVersions = ""
633 $htmlFormat = @"
634<tr>
635<td>{0}</td>
636<td>{1}</td>
637<td>{2}</td>
638</tr>
639
640"@
641
642 foreach ($version in $versionList) {
643 $mdVersions += ($mdFormat -f $version.Type, $version.Version, $version.Date)
644 $htmlVersions += ($htmlFormat -f $version.Type, $version.Version, $version.Date)
645 }
646
647 $htmlTemplate = @"
648<div style='display:none;width:0;height:0;overflow:hidden;position:absolute;font-size:0;' id=__md>| Type | Version | Date |
649| - | - | - |
650mdVersions
651</div><style id=__mdStyle>
652.rendered-markdown img {
653cursor:pointer;
654}
655
656.rendered-markdown h1, .rendered-markdown h2, .rendered-markdown h3, .rendered-markdown h4, .rendered-markdown h5, .rendered-markdown h6 {
657color:#007acc;
658font-weight:400;
659}
660
661.rendered-markdown h1 {
662border-bottom:1px solid #e6e6e6;
663font-size:26px;
664font-weight:600;
665margin-bottom:20px;
666}
667
668.rendered-markdown h2 {
669font-size:18px;
670border-bottom:1px solid #e6e6e6;
671font-weight:600;
672color:#303030;
673margin-bottom:10px;
674margin-top:20px;
675}
676
677.rendered-markdown h3 {
678font-size:16px;
679font-weight:600;
680margin-bottom:10px;
681}
682
683.rendered-markdown h4 {
684font-size:14px;
685margin-bottom:10px;
686}
687
688.rendered-markdown h5 {
689font-size:12px;
690margin-bottom:10px;
691}
692
693.rendered-markdown h6 {
694font-size:12px;
695font-weight:300;
696margin-bottom:10px;
697}
698
699.rendered-markdown.metaitem {
700font-size:12px;
701padding-top:15px;
702}
703
704.rendered-markdown.metavalue {
705font-size:12px;
706padding-left:4px;
707}
708
709.rendered-markdown.metavalue>img {
710height:32px;
711width:32px;
712margin-bottom:3px;
713padding-left:1px;
714}
715
716.rendered-markdown li.metavaluelink {
717list-style-type:disc;
718list-style-position:inside;
719}
720
721.rendered-markdown li.metavalue>a {
722border:none;
723padding:0;
724display:inline;
725}
726
727.rendered-markdown li.metavalue>a:hover {
728background-color:inherit;
729text-decoration:underline;
730}
731
732.rendered-markdown code, .rendered-markdown pre, .rendered-markdown samp {
733font-family:Monaco,Menlo,Consolas,'Droid Sans Mono','Inconsolata','Courier New',monospace;
734}
735
736.rendered-markdown code {
737color:#333;
738background-color:#f8f8f8;
739border:1px solid #ccc;
740border-radius:3px;
741padding:2px 4px;
742font-size:90%;
743line-height:2;
744white-space:nowrap;
745}
746
747.rendered-markdown pre {
748color:#333;
749background-color:#f8f8f8;
750border:1px solid #ccc;
751display:block;
752padding:6px;
753font-size:13px;
754word-break:break-all;
755word-wrap:break-word;
756}
757
758.rendered-markdown pre code {
759padding:0;
760font-size:inherit;
761color:inherit;
762white-space:pre-wrap;
763background-color:transparent;
764line-height:1.428571429;
765border:none;
766}
767
768.rendered-markdown.pre-scrollable {
769max-height:340px;
770overflow-y:scroll;
771}
772
773.rendered-markdown table {
774border-collapse:collapse;
775}
776
777.rendered-markdown table {
778width:auto;
779}
780
781.rendered-markdown table, .rendered-markdown th, .rendered-markdown td {
782border:1px solid #ccc;
783padding:4px;
784}
785
786.rendered-markdown th {
787font-weight:bold;
788background-color:#f8f8f8;
789}
790</style><div class=rendered-markdown><table>
791<thead>
792<tr>
793<th>Type</th>
794<th>Version</th>
795<th>Date</th>
796</tr>
797</thead>
798<tbody>htmlVersions</tbody>
799</table>
800</div>
801"@ -replace "'", '\"'
802
803 return $htmlTemplate.Replace("mdVersions", $mdVersions).Replace("htmlVersions", "`n$htmlVersions");
804}
805
806function UpdatePackageVersions($pkgWorkItem, $plannedVersions, $shippedVersions)
807{
808 # Create the planned and shipped versions, adding the new ones if any
809 $updatePlanned = $false
810 $plannedVersionSet = ParseVersionSetFromMDField $pkgWorkItem.fields["Custom.PlannedPackages"]
811 foreach ($version in $plannedVersions)
812 {
813 if (!$plannedVersionSet.ContainsKey($version.Version))
814 {
815 $plannedVersionSet[$version.Version] = $version
816 $updatePlanned = $true
817 }
818 else
819 {
820 # Lets check to see if someone wanted to update a date
821 $existingVersion = $plannedVersionSet[$version.Version]
822 if ($existingVersion.Date -ne $version.Date) {
823 $existingVersion.Date = $version.Date
824 $updatePlanned = $true
825 }
826 }
827 }
828
829 $updateShipped = $false
830 $shippedVersionSet = ParseVersionSetFromMDField $pkgWorkItem.fields["Custom.ShippedPackages"]
831 foreach ($version in $shippedVersions)
832 {
833 if (!$shippedVersionSet.ContainsKey($version.Version))
834 {
835 $shippedVersionSet[$version.Version] = $version
836 $updateShipped = $true
837 }
838 }
839
840 $versionSet = @{}
841 foreach ($version in $shippedVersionSet.Keys)
842 {
843 if (!$versionSet.ContainsKey($version))
844 {
845 $versionSet[$version] = $shippedVersionSet[$version]
846 }
847 }
848
849 foreach ($version in @($plannedVersionSet.Keys))
850 {
851 if (!$versionSet.ContainsKey($version))
852 {
853 $versionSet[$version] = $plannedVersionSet[$version]
854 }
855 else
856 {
857 # Looks like we shipped this version so remove it from the planned set
858 $plannedVersionSet.Remove($version)
859 $updatePlanned = $true
860 }
861 }
862
863 $fieldUpdates = @()
864 if ($updatePlanned)
865 {
866 $plannedPackages = GetMDVersionValue ($plannedVersionSet.Values | Sort-Object {$_.Date -as [DateTime]}, Version -Descending)
867 $fieldUpdates += @"
868{
869 "op": "replace",
870 "path": "/fields/Planned Packages",
871 "value": "$plannedPackages"
872}
873"@
874 }
875
876 if ($updateShipped)
877 {
878 $newShippedVersions = $shippedVersionSet.Values | Sort-Object {$_.Date -as [DateTime]}, Version -Descending
879 $shippedPackages = GetMDVersionValue $newShippedVersions
880 $fieldUpdates += @"
881{
882 "op": "replace",
883 "path": "/fields/Shipped Packages",
884 "value": "$shippedPackages"
885}
886"@
887 }
888
889 # Full merged version set
890 $versionList = $versionSet.Values | Sort-Object {$_.Date -as [DateTime]}, Version -Descending
891
892 $versionFieldUpdates = GetTextVersionFields $versionList $pkgWorkItem
893 if ($versionFieldUpdates.Count -gt 0)
894 {
895 $fieldUpdates += $versionFieldUpdates
896 }
897
898 # If no version files to update do nothing
899 if ($fieldUpdates.Count -eq 0) {
900 return $pkgWorkItem
901 }
902
903 $versionsForDebug = ($versionList | Foreach-Object { $_.Version }) -join ","
904 $id = $pkgWorkItem.id
905 $loggingString = "[$($pkgWorkItem.id)]"
906 $loggingString += "$($pkgWorkItem.fields['Custom.Language'])"
907 $loggingString += " - $($pkgWorkItem.fields['Custom.Package'])"
908 $loggingString += "($($pkgWorkItem.fields['Custom.PackageVersionMajorMinor']))"
909 $loggingString += " - Updating versions $versionsForDebug"
910 Write-Host $loggingString
911
912 $body = "[" + ($fieldUpdates -join ',') + "]"
913
914 $response = Invoke-RestMethod -Method PATCH `
915 -Uri "https://dev.azure.com/azure-sdk/_apis/wit/workitems/${id}?api-version=6.0" `
916 -Headers (Get-DevOpsRestHeaders) -Body $body -ContentType "application/json-patch+json" | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashTable
917 return $response
918}
View as plain text