powershellazure-devopsazure-devops-rest-api

How do I migrate Azure DevOPs Iterations through the API and PS


I am trying to migrate iterations between two ADOs. I can retrieve the list of iterations just fine but I get an error when doing the Invoke-RestMethod POST. Here is the error I receive from the POST:

Invoke-RestMethod : {"$id":"1","innerException":null,"message":"You must provide a value for the iteration parameter.","typeName":"Microsoft.VisualStudio.Services.Common.VssPropertyValidationException, Microsoft.VisualStudio.Services.Common","typeKey":"VssPropertyValidationException","errorCode":0,"eventId":3000} At C:\Users\Chris\Downloads\MigrateIterations.ps1:70 char:25

  • ... $response = Invoke-RestMethod -Uri $TargetURL -Headers $Header -Metho ...
  •             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    • FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

As I read the error I assume this is the salient part:

Invoke-RestMethod : {"$id":"1","innerException":null,"message":"You must provide a value for the iteration  parameter.","typeName"

But there is no iteration parameter "typeName", or at least none I could find in any documentation. I even asked ChatGPT to write the script for me and it was essentially identical. The error must not be literal but I can't find anything close online.

I tested my auth to the Target by retrieving a list of Iterations in the project and that worked fine, so my token is good.

Here is my script

$UserName = 'user@test.com'
$SourceToken = <PAT>
$TargetToken = <PAT>
$SourceOrg = 'FirstADO'
$SourceProject = 'ProjectA'
$TargetOrg = 'SecondADO'
$TargetProject = 'ProjectB'

$URLBase = "https`://dev.azure.com/"

#log into source ADO
$sourceBase64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $SourceToken)))
$Header = @{
    Authorization = ("Basic {0}" -f $SourceBase64AuthInfo)
}

$SourceURL = "$URLBase/$SourceOrg/$SourceProject/_apis/work/teamsettings/iterations?api-version=6.0"
Write-Output $SourceURL

Try {
    $SourceIterations = (Invoke-RestMethod $SourceURL -Headers $Header).value
}
Catch {
    if ($_ -match "Access Denied") {
        Throw "Access has been denied, please check your token"
    }
    else {
        Throw $_
    }
}

#log into target ADO
$TargetBase64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $TargetToken)))
$Header = @{
    Authorization = ("Basic {0}" -f $TargetBase64AuthInfo)
}
$TargetURL = "$URLBase/$TargetOrg/$TargetProject/_apis/work/teamsettings/iterations?api-version=6.0"
Write-Output $TargetURL

Try {
    $TargetIterations = (Invoke-RestMethod $TargetURL -Headers $Header).value
}
Catch {
    if ($_ -match "Access Denied") {
        Throw "Access has been denied, please check your token"
    }
    else {
        Throw $_
    }
}

foreach ($item in $SourceIterations) {
    Write-Output $item

    $iterationPath = "{0}\{1}" -f $TargetProject, $item.name
    $jsonData = '{{
      "name": "{0}",
      "attributes": {{
        "startDate": {1},
        "finishDate": {2} 
        }}
    }}' -f $item.name, $item.attributes.startDate, $item.attributes.finishDate

    $JSON = $jsonData | ConvertTo-Json

    try {
            $response = Invoke-RestMethod -Uri $TargetURL -Headers $Header -Method Post -Body $JSON -ContentType application/json
    Write-Output $response.value
    }
    catch {
        Write-Output $_
    }

}

Solution

    1. To create a new iteration, you should use REST API Classification Nodes - Create Or Update.

      Sample request:

    POST https://dev.azure.com/fabrikam/Fabrikam-Fiber-Git/_apis/wit/classificationnodes/Iterations?api-version=5.0
    
    {
      "name": "Final Iteration",
      "attributes": {
        "startDate": "2014-10-27T00:00:00Z",
        "finishDate": "2014-10-31T00:00:00Z"
      }
    }
    
    
    1. Remove ConvertTo-Json as mentioned by @Mathias R. Jessen.

    2. Add " to startDate and finishDate.

    Below are the modified scripts for your reference:

    $UserName = 'user@test.com'
    $SourceToken = <PAT>
    $TargetToken = <PAT>
    $SourceOrg = 'FirstADO'
    $SourceProject = 'ProjectA'
    $TargetOrg = 'SecondADO'
    $TargetProject = 'ProjectB'
    
    $URLBase = "https`://dev.azure.com/"
    
    #log into source ADO
    $sourceBase64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $SourceToken)))
    $Header = @{
        Authorization = ("Basic {0}" -f $SourceBase64AuthInfo)
    }
    
    $SourceURL = "$URLBase/$SourceOrg/$SourceProject/_apis/work/teamsettings/iterations?api-version=6.0"
    Write-Output $SourceURL
    
    Try {
        $SourceIterations = (Invoke-RestMethod $SourceURL -Headers $Header).value
    }
    Catch {
        if ($_ -match "Access Denied") {
            Throw "Access has been denied, please check your token"
        }
        else {
            Throw $_
        }
    }
    
    #log into target ADO
    $TargetBase64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $TargetToken)))
    $Header = @{
        Authorization = ("Basic {0}" -f $TargetBase64AuthInfo)
    }
    $TargetURL = "$URLBase/$TargetOrg/$TargetProject/_apis/work/teamsettings/iterations?api-version=6.0"
    Write-Output $TargetURL
    
    Try {
        $TargetIterations = (Invoke-RestMethod $TargetURL -Headers $Header).value
    }
    Catch {
        if ($_ -match "Access Denied") {
            Throw "Access has been denied, please check your token"
        }
        else {
            Throw $_
        }
    }
    
    $newTargetUrl = "$URLBase/$TargetOrg/$TargetProject/_apis/wit/classificationnodes/Iterations?api-version=5.0"
    
    foreach ($item in $SourceIterations) {
        Write-Output $item
    
        $iterationPath = "{0}\{1}" -f $TargetProject, $item.name
        $jsonData = '{{
          "name": "{0}",
          "attributes": {{
            "startDate": "{1}",
            "finishDate": "{2}" 
            }}
        }}' -f $item.name, $item.attributes.startDate, $item.attributes.finishDate
    
        $jsonData
    
        try {
                $response = Invoke-RestMethod -Uri $newTargetUrl -Headers $Header -Method Post -Body $jsonData -ContentType application/json
        Write-Output $response.value
        }
        catch {
            Write-Output $_
        }
    
    }