...

Text file src/github.com/Azure/azure-sdk-for-go/eng/common/scripts/Helpers/DevOps-WorkItem-Helpers.ps1

Documentation: github.com/Azure/azure-sdk-for-go/eng/common/scripts/Helpers

     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