azure-devops-migration-tools: MigrationTools.FieldMaps.NodePathNotAnchoredException: This path is not anchored in the source project name

I’ve been using this migration tool without issue to consolidate many individual projects into a single project. For example, I’ve been migrating all work items from ORG/bar to ORG/consolidated.

It’s been working well for many projects up until now. Here is the error, where it gets stuck on a particular work item number:

 [15:38:16 ERR] MigrationTools.FieldMaps.NodePathNotAnchoredException: This path is not anchored in the source project name: bar-old
   at MigrationTools.Enrichers.TfsNodeStructure.GetNewNodeName(String sourceNodePath, TfsNodeStructureType nodeStructureType) in D:\a\1\s\src\MigrationTools.Clients.AzureDevops.ObjectModel\ProcessorEnrichers\TfsNodeStructure.cs:line 109
   at VstsSyncMigrator.Engine.WorkItemMigrationContext.PopulateWorkItem(WorkItemData oldWi, WorkItemData newwit, String destType) in D:\a\1\s\src\VstsSyncMigrator.Core\Execution\MigrationContext\WorkItemMigrationContext.cs:line 421
   at VstsSyncMigrator.Engine.WorkItemMigrationContext.ReplayRevisions(List`1 revisionsToMigrate, WorkItemData sourceWorkItem, WorkItemData targetWorkItem) in D:\a\1\s\src\VstsSyncMigrator.Core\Execution\MigrationContext\WorkItemMigrationContext.cs:line 661 

In this particular situation, the source is ORG/bar and destination is ORG/consolidated.

The only reference I can find to ‘bar-old’ (from the error) is in the history of the work item at the source, when at some point in the past, a user moved the work item from project “bar-old” over to project “bar”.

How might I work around this?

For reference / if helpful, here is my config file:

 {
  "ChangeSetMappingFile": null,
  "Source": {
    "$type": "TfsTeamProjectConfig",
    "Name": "Source",
    "Collection": "https://dev.azure.com/ORG/",
    "Project": "bar",
    "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
    "AllowCrossProjectLinking": false,
    "AuthenticationMode": "AccessToken",
    "PersonalAccessToken": "xxxx",
    "PersonalAccessTokenVariableName": "",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "Target": {
    "$type": "TfsTeamProjectConfig",
    "Name": "Target",
    "Collection": "https://dev.azure.com/ORG/",
    "Project": "consolidated",
    "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
    "AllowCrossProjectLinking": false,
    "AuthenticationMode": "AccessToken",
    "PersonalAccessToken": "xxxx",
    "PersonalAccessTokenVariableName": "",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "Processors": [
    {
      "$type": "WorkItemMigrationConfig",
      "Enabled": true,
      "ReplayRevisions": true,
      "PrefixProjectToNodes": false,
      "UpdateCreatedDate": true,
      "UpdateCreatedBy": true,
      "WIQLQueryBit": "AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request')",
      "WIQLOrderBit": "[System.ChangedDate] desc",
      "LinkMigration": true,
      "AttachmentMigration": true,
      "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\",
      "FixHtmlAttachmentLinks": false,
      "SkipToFinalRevisedWorkItemType": true,
      "WorkItemCreateRetryLimit": 5,
      "FilterWorkItemsThatAlreadyExistInTarget": false,
      "PauseAfterEachWorkItem": false,
      "AttachmentMaxSize": 480000000,
      "AttachRevisionHistory": false,
      "LinkMigrationSaveEachAsAdded": false,
      "GenerateMigrationComment": true,
      "WorkItemIDs": null,
      "MaxRevisions": 0,
      "NodeStructureEnricherEnabled": null,
      "UseCommonNodeStructureEnricherConfig": false,
      "StopMigrationOnMissingAreaIterationNodes": false,
      "NodeBasePaths": [
      ],
      "AreaMaps": {},
      "IterationMaps": {},
      "MaxGracefulFailures": 0,
      "SkipRevisionWithInvalidIterationPath": false
    },

  ],
  "Version": "12.8",
  "workaroundForQuerySOAPBugEnabled": false,
  "WorkItemTypeDefinition": {
    "sourceWorkItemTypeName": "targetWorkItemTypeName"
  },
  "Endpoints": {
    "InMemoryWorkItemEndpoints": [
      {
        "Name": "Source",
        "EndpointEnrichers": null
      },
      {
        "Name": "Target",
        "EndpointEnrichers": null
      }
    ]
  }
} 

Thank you

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 21 (12 by maintainers)

Most upvoted comments

Ive just added some of what @RehabAbotalep did directly to the project itself in #1669

I was able to migrate work items that had been moved between projects successfully, and thus had the not anchored work items present.

The tool now lists more metadata, especially in debug… but also is more explicit with the areas an iterations. The new log will look like:

[15:11:06 WRN] !! There are MISSING Area or Iteration Paths
[15:11:06 WRN] !! There are 1 Nodes (Area or Iteration) found in the history of the Source that are missing from the Target! These MUST be added or mapped before we can continue using the instructions on https://nkdagility.com/learn/azure-devops-migration-tools/Reference/v1/Processors/WorkItemMigrationContext/#iteration-maps-and-area-maps
[15:11:06 WRN] MISSING Iteration: sourcePath=migrationSource1\Sprint 4, targetPath=migration target 1\Sprint 4, anchored=true
[15:11:06 WRN] MISSING Iteration: sourcePath=Skypoint Cloud\Sprint 1, targetPath=null, anchored=False, IDs=414

Note that it includes the IDs for non-anchored nodes in the entry.

Debug is also much more informative and full data for the missing items being checked:

[15:13:58 DBG] TfsNodeStructure:CheckForMissingPaths:CheckTarget::{"anchored": true, "sourcePath": "migrationSource1\\Iteration 5", "targetPath": "migration Target 1\\Iteration 5", "systemPath": "\\migration Target 1\\Iteration\\Iteration 5", "nodeType": "Iteration", "workItems": null, "$type": "NodeStructureMissingItem"}

This was my config:

  {
    "$type": "WorkItemMigrationConfig",
    "Enabled": true,
    "ReplayRevisions": true,
    "PrefixProjectToNodes": false,
    "UpdateCreatedDate": true,
    "UpdateCreatedBy": true,
    "WIQLQueryBit": "AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan')",
    "WIQLOrderBit": "[System.ChangedDate] desc",
    "LinkMigration": true,
    "AttachmentMigration": true,
    "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\",
    "FixHtmlAttachmentLinks": false,
    "SkipToFinalRevisedWorkItemType": false,
    "WorkItemCreateRetryLimit": 5,
    "FilterWorkItemsThatAlreadyExistInTarget": false,
    "PauseAfterEachWorkItem": false,
    "AttachmentMaxSize": 480000000,
    "AttachRevisionHistory": false,
    "LinkMigrationSaveEachAsAdded": false,
    "GenerateMigrationComment": true,
    "WorkItemIDs": null,
    "MaxRevisions": 0,
    "NodeStructureEnricherEnabled": false,
    "UseCommonNodeStructureEnricherConfig": false,
    "StopMigrationOnMissingAreaIterationNodes": true,
    "ShouldCreateMissingRevisionPaths": true,
    "NodeBasePaths": [],
    "AreaMaps": {},
    "IterationMaps": {},
    "MaxGracefulFailures": 0
  }

To map the non-anchored node I used the following mapping, and the NewArea was created by the tool, and no missing area paths were detected.

 "IterationMaps": {
      "^Skypoint Cloud\\\\Sprint 1": "migration Target 1\\NewArea"
    },

Supports #1643, #1629, #1589, #1548, #1667

@MrHinsh @ejmarmonti

Since the migration process pauses when it encounters items moved from another project, I’ve created a PowerShell script. It helps me list the IDs of these moved items before migration, so I can exclude them initially. Later, I migrate them separately just to save time.

The output of running the script is a txt file that includes the IDs of moved work items.

image

image

image

Here’s the script; feel free to use it if it helps, kindly find more details in my latest blog post.

param (

    [Parameter(Position = 0, mandatory = $false)]
    [string]  $PAT = "PAT" ,

    [Parameter(Position = 1, mandatory = $false)]
    [string]  $orgURL = "ORGURL",

    [Parameter(Position = 2, mandatory = $false)]
    [string]  $projectName = "PROJECT",

    [Parameter(Position = 3, mandatory = $false)]
    [string]  $workItemType = "WORKITEMTYPE",

    [Parameter(Position = 4, mandatory = $false)]
    [string]  $filePath = "PATH"
)

$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($PAT)"))
$header = @{authorization = "Basic $token" }
$query = "Select [System.Id], [System.Title] From WorkItems Where [System.WorkItemType] = '$workItemType' and [System.TeamProject] = '$projectName'"
$body = @{query = $query} | ConvertTo-Json

$url = "$orgURL/$projectName/_apis/wit/wiql?api-version=7.0"

$response = Invoke-RestMethod -Uri $url -Method POST -ContentType "application/json" -Headers $header -Body $body

# Loop through each item and check revisions for changes in team project

$movedItems = @()

foreach ($item in $response.workItems) {
    
    $itemUrl = $item.url
    $itemResponse = Invoke-RestMethod -Uri $itemUrl -Method Get -Headers $header

    $currentTeamProject = $itemResponse.fields."System.TeamProject"

    $id = $item.id

    $revisionsUrl = "$orgURL/$projectName/_apis/wit/workitems/$id/revisions?api-version=7.0"
    $revisions = Invoke-RestMethod -Uri $revisionsUrl -Method Get -Headers $header

    foreach ($revision in $revisions.value) {
        $oldTeamProject = $revision.fields."System.TeamProject"
        if ($oldTeamProject -ne $currentTeamProject) {
            $movedItems += $revision.id
            $movedItems = "$movedItems, "
            Write-Host "This item: $id has been moved from $oldTeamProject to $currentTeamProject"
            break
        }
    }
}


# Check if any user stories have changed team projects
if ($movedItems.Count -eq 0) {
    Write-Host "No items have been moved from another project."
}
else {
    # Trim the , in the end
    $movedItems = $movedItems.TrimEnd(', ')
    # Output array of changed user story IDs to a file
    $outputFileName = "Moved-$workItemType.txt"
    $outputFilePath = Join-Path -Path $filePath -ChildPath $outputFileName
    $movedItems | Out-File -FilePath $outputFilePath -Encoding utf8

    # Display a message indicating where the output file was written
    Write-Host "The moved items have been written to '$outputFilePath'."
}

@MrHinsh good news! I read your post here and it put me in the right direction. I was able to get past this by adding an IterationMap. In my situation, the apparent 5 missing iterationPaths were on the target, but for whatever reason it wanted me to add the additional IterationMap. Is this part of the bug, where the iterationPath exists, but it still wants you to setup the mapping to it?

This is an ongoing issue with Work Items that have been “moved” from one project to another. There are not only invalid Area/Iteration paths in history, but they have different project name!

We are considering options… but you can add a mapping from “[OriginalOldProject][OriginalOldAreaPath” to a new path.

@RehabAbotalep could you add this as a PR to https://github.com/nkdAgility/azure-devops-automation-tools?

It would be great to add it as Find-WorkItemsMovedToThisProject.ps1 to /src/migrationTools/

Sure, I’d be happy to contribute! I’ll open it as soon as possible.

@RehabAbotalep could you add this as a PR to https://github.com/nkdAgility/azure-devops-automation-tools?

It would be great to add it as Find-WorkItemsMovedToThisProject.ps1 to /src/migrationTools/

@RehabAbotalep sure thing, here is the config I used with some of the identifying bits masked out:

{
  "ChangeSetMappingFile": null,
  "Source": {
    "$type": "TfsTeamProjectConfig",
    "Name": "Source",
    "Collection": "https://dev.azure.com/ORG/",
    "Project": "SOURCE",
    "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
    "AllowCrossProjectLinking": false,
    "AuthenticationMode": "AccessToken",
    "PersonalAccessToken": "xxx",
    "PersonalAccessTokenVariableName": "",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "Target": {
    "$type": "TfsTeamProjectConfig",
    "Name": "Target",
    "Collection": "https://dev.azure.com/ORG/",
    "Project": "DEST",
    "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
    "AllowCrossProjectLinking": false,
    "AuthenticationMode": "AccessToken",
    "PersonalAccessToken": "xxx",
    "PersonalAccessTokenVariableName": "",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "LogLevel": "Debug",
  //// use this query for debugging to isolate a given problematic system.Id:   "WIQLQueryBit": "AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.Id] = '132043'",
  "Processors": [
    {
      "$type": "WorkItemMigrationConfig",
      "Enabled": true,
      "ReplayRevisions": true,
      "PrefixProjectToNodes": false,
      "UpdateCreatedDate": true,
      "UpdateCreatedBy": true,
      "WIQLQueryBit": "AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request')",
      "WIQLOrderBit": "[System.ChangedDate] desc",
      "LinkMigration": true,
      "AttachmentMigration": true,
      "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\",
      "FixHtmlAttachmentLinks": false,
      "SkipToFinalRevisedWorkItemType": true,
      "WorkItemCreateRetryLimit": 5,
      "FilterWorkItemsThatAlreadyExistInTarget": false,
      "PauseAfterEachWorkItem": false,
      "AttachmentMaxSize": 480000000,
      "AttachRevisionHistory": false,
      "LinkMigrationSaveEachAsAdded": false,
      "GenerateMigrationComment": true,
      "WorkItemIDs": null,
      "MaxRevisions": 0,
      "NodeStructureEnricherEnabled": null,
      "UseCommonNodeStructureEnricherConfig": false,
      "StopMigrationOnMissingAreaIterationNodes": false,
      "NodeBasePaths": [
      ],
///// api is the name of an old project (prior to the source being named SOURCE -- someone moved it manually into the SOURCE project)
      "AreaMaps": {
        "SOURCE": "DEST",
        "api": "DEST",
        "api\\\\June 20_2022": "DEST\\\\June 20_2022",
        "api\\\\July 11_2022": "DEST\\\\July 11_2022",
        "api\\\\August 1_2022": "DEST\\\\August 1_2022",
        "api\\\\August 22_2022": "DEST\\\\August 22_2022",
        "api\\\\September 12_2022": "DEST\\\\September 12_2022",
        "api\\\\October 3_2022": "DEST\\\\October 3_2022",
        "api\\\\October 24_2022": "DEST\\\\October 24_2022",
        "api\\\\November 14_2022": "DEST\\\\November 14_2022",
        "api\\\\December 5_2022": "DEST\\\\December 5_2022",
        "api\\\\December 26_2022": "DEST\\\\December 26_2022",
        "api\\\\December_26_2023": "DEST\\\\December_26_2023"
      },
      "IterationMaps": {
        "api\\\\June 20_2022": "DEST\\\\June 20_2022",
        "api\\\\July 11_2022": "DEST\\\\July 11_2022",
        "api\\\\August 1_2022": "DEST\\\\August 1_2022",
        "api\\\\August 22_2022": "DEST\\\\August 22_2022",
        "api\\\\September 12_2022": "DEST\\\\September 12_2022",
        "api\\\\October 3_2022": "DEST\\\\October 3_2022",
        "api\\\\October 24_2022": "DEST\\\\October 24_2022",
        "api\\\\November 14_2022": "DEST\\\\November 14_2022",
        "api\\\\December 5_2022": "DEST\\\\December 5_2022",
        "api\\\\December 26_2022": "DEST\\\\December 26_2022",
        "api\\\\December_26_2023": "DEST\\\\December_26_2023"
      },
      "MaxGracefulFailures": 0,
      "SkipRevisionWithInvalidIterationPath": true,
      "SkipRevisionWithInvalidAreaPath": true
    },

  ],
  "Version": "13.1",
  "workaroundForQuerySOAPBugEnabled": false,
  "WorkItemTypeDefinition": {
    "sourceWorkItemTypeName": "targetWorkItemTypeName"
  },
  "Endpoints": {
    "InMemoryWorkItemEndpoints": [
      {
        "Name": "Source",
        "EndpointEnrichers": null
      },
      {
        "Name": "Target",
        "EndpointEnrichers": null
      }
    ]
  }
}

It’s still failing once in a while as it gets stumped on some iterationPaths. But as I encounter them, I just add them to the list in the config. So far, so good.

Question: Is this “API” the name of the source project or was the work item moved from a previous project called “API” into the Source?

This particular work item was moved from a previous project called “API” into the source project.