1 
2 $ReleaseDevOpsOrgParameters =  @("--organization", "https://dev.azure.com/azure-sdk")
3 $ReleaseDevOpsCommonParameters =  $ReleaseDevOpsOrgParameters + @("--output", "json")
4 $ReleaseDevOpsCommonParametersWithProject = $ReleaseDevOpsCommonParameters + @("--project", "Release")
5 
Get-DevOpsRestHeaders()6 function 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 
CheckDevOpsAccess()24 function 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 
Invoke-AzBoardsCmd($subCmd, $parameters, $output = $true)38 function 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 
Invoke-Query($fields, $wiql, $output = $true)47 function 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 
LoginToAzureDevops([string]$devops_pat)106 function 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 
BuildHashKeyNoNull()115 function 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 
BuildHashKey()125 function 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 = @{}
FindParentWorkItem($serviceName, $packageDisplayName, $outputCommand = $false)138 function 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 
FindLatestPackageWorkItem($lang, $packageName, $outputCommand = $true)186 function 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 
FindPackageWorkItem($lang, $packageName, $version, $outputCommand = $true, $includeClosed = $false)209 function 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 
InitializeWorkItemCache($outputCommand = $true, $includeClosed = $false)279 function 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 
GetCachedPackageWorkItems()288 function GetCachedPackageWorkItems()
289 {
290   return $packageWorkItems.Values
291 }
292 
UpdateWorkItemParent($childWorkItem, $parentWorkItem, $outputCommand = $true)293 function 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 
CreateWorkItemParent($id, $parentId, $oldParentId, $outputCommand = $true)307 function 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 }
CreateWorkItem($title, $type, $iteration, $area, $fields, $assignedTo, $parentId, $outputCommand = $true)328 function 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 
UpdateWorkItem($id, $fields, $title, $state, $assignedTo, $outputCommand = $true)357 function 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 
UpdatePackageWorkItemReleaseState($id, $state, $releaseType, $outputCommand = $true)378 function 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 
FindOrCreateClonePackageWorkItem($lang, $pkg, $verMajorMinor, $allowPrompt = $false, $outputCommand = $false)384 function 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 "We need a package display name to be used in various places and it should be consistent across languages for similar packages."
415         while (($readInput = Read-Host -Prompt "Input the display name") -eq "") { }
416         $packageInfo.DisplayName = $readInput
417       }
418 
419       if (!$pkg.ServiceName) {
420         Write-Host "We need a package service name to be used in various places and it should be consistent across languages for similar packages."
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 
CreateOrUpdatePackageWorkItem($lang, $pkg, $verMajorMinor, $existingItem, $assignedTo = $null, $extraFields = $null, $outputCommand = $true)433 function 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 
FindOrCreatePackageGroupParent($serviceName, $packageDisplayName, $outputCommand = $true)503 function 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 
FindOrCreateServiceParent($serviceName, $outputCommand = $true)524 function 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 
ParseVersionSetFromMDField([string]$field)543 function 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 
GetTextVersionFields($versionList, $pkgWorkItem)566 function 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 
GetMDVersionValue($versionlist)627 function 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 | - | - | - |
650 mdVersions
651 </div><style id=__mdStyle>
652 .rendered-markdown img {
653 cursor:pointer;
654 }
655 
656 .rendered-markdown h1, .rendered-markdown h2, .rendered-markdown h3, .rendered-markdown h4, .rendered-markdown h5, .rendered-markdown h6 {
657 color:#007acc;
658 font-weight:400;
659 }
660 
661 .rendered-markdown h1 {
662 border-bottom:1px solid #e6e6e6;
663 font-size:26px;
664 font-weight:600;
665 margin-bottom:20px;
666 }
667 
668 .rendered-markdown h2 {
669 font-size:18px;
670 border-bottom:1px solid #e6e6e6;
671 font-weight:600;
672 color:#303030;
673 margin-bottom:10px;
674 margin-top:20px;
675 }
676 
677 .rendered-markdown h3 {
678 font-size:16px;
679 font-weight:600;
680 margin-bottom:10px;
681 }
682 
683 .rendered-markdown h4 {
684 font-size:14px;
685 margin-bottom:10px;
686 }
687 
688 .rendered-markdown h5 {
689 font-size:12px;
690 margin-bottom:10px;
691 }
692 
693 .rendered-markdown h6 {
694 font-size:12px;
695 font-weight:300;
696 margin-bottom:10px;
697 }
698 
699 .rendered-markdown.metaitem {
700 font-size:12px;
701 padding-top:15px;
702 }
703 
704 .rendered-markdown.metavalue {
705 font-size:12px;
706 padding-left:4px;
707 }
708 
709 .rendered-markdown.metavalue>img {
710 height:32px;
711 width:32px;
712 margin-bottom:3px;
713 padding-left:1px;
714 }
715 
716 .rendered-markdown li.metavaluelink {
717 list-style-type:disc;
718 list-style-position:inside;
719 }
720 
721 .rendered-markdown li.metavalue>a {
722 border:none;
723 padding:0;
724 display:inline;
725 }
726 
727 .rendered-markdown li.metavalue>a:hover {
728 background-color:inherit;
729 text-decoration:underline;
730 }
731 
732 .rendered-markdown code, .rendered-markdown pre, .rendered-markdown samp {
733 font-family:Monaco,Menlo,Consolas,'Droid Sans Mono','Inconsolata','Courier New',monospace;
734 }
735 
736 .rendered-markdown code {
737 color:#333;
738 background-color:#f8f8f8;
739 border:1px solid #ccc;
740 border-radius:3px;
741 padding:2px 4px;
742 font-size:90%;
743 line-height:2;
744 white-space:nowrap;
745 }
746 
747 .rendered-markdown pre {
748 color:#333;
749 background-color:#f8f8f8;
750 border:1px solid #ccc;
751 display:block;
752 padding:6px;
753 font-size:13px;
754 word-break:break-all;
755 word-wrap:break-word;
756 }
757 
758 .rendered-markdown pre code {
759 padding:0;
760 font-size:inherit;
761 color:inherit;
762 white-space:pre-wrap;
763 background-color:transparent;
764 line-height:1.428571429;
765 border:none;
766 }
767 
768 .rendered-markdown.pre-scrollable {
769 max-height:340px;
770 overflow-y:scroll;
771 }
772 
773 .rendered-markdown table {
774 border-collapse:collapse;
775 }
776 
777 .rendered-markdown table {
778 width:auto;
779 }
780 
781 .rendered-markdown table, .rendered-markdown th, .rendered-markdown td {
782 border:1px solid #ccc;
783 padding:4px;
784 }
785 
786 .rendered-markdown th {
787 font-weight:bold;
788 background-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 
UpdatePackageVersions($pkgWorkItem, $plannedVersions, $shippedVersions)806 function 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 }