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 }