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 $_
}
}
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"
}
}
Remove ConvertTo-Json
as mentioned by @Mathias R. Jessen.
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 $_
}
}