TFS Node Structure Tool
Migrates area path and iteration path structures between source and target systems, creating missing nodes and managing hierarchical project structures during work item migration.
Overview
The TFS Node Structure Tool manages the migration of area path and iteration path structures between source and target systems. This tool is essential for ensuring that the hierarchical organization of projects is properly maintained during work item migration.
The tool creates missing area and iteration nodes in the target project and provides mapping capabilities to handle structural differences between source and target environments.
topHow It Works
The TFS Node Structure Tool operates during project structure migration:
- Structure Discovery: Analyzes area and iteration path structures in the source project
- Node Creation: Creates missing area and iteration nodes in the target project
- Hierarchy Preservation: Maintains parent-child relationships in node structures
- Mapping Application: Applies configured mappings for node path transformations
- Validation: Ensures all required nodes exist before work item migration
The tool is typically run before work item migration to ensure target project structure compatibility.
topCritical Migration Requirement
IMPORTANT: It is NOT possible to migrate a work item if the Area or Iteration path does not exist in the target project. Work items are created with the same Area and Iteration paths as the source work item. If the path does not exist, the work item creation will fail.
You have two options to solve this problem:
- Manual Creation: Manually create the missing nodes in the target project (suitable for small numbers of missing nodes)
- Path Mapping: Use
Areas.Mappings
andIterations.Mappings
to remap nodes to existing paths in the target project
Use Cases
Common scenarios where the TFS Node Structure Tool is essential:
- Project Structure Migration: Replicating area and iteration hierarchies in target projects
- Path Standardization: Mapping different path structures between source and target
- Missing Node Creation: Automatically creating required nodes for work item migration
- Cross-Project Migration: Handling path differences when moving work items between projects
- Structure Consolidation: Combining multiple source structures into unified target hierarchies
Configuration Structure
topOptions
topSample
{
"MigrationTools": {
"Version": "16.0",
"CommonTools": {
"TfsNodeStructureTool": {
"Areas": {
"Filters": null,
"Mappings": [
{
"Match": "^Skypoint Cloud$",
"Replacement": "MigrationTest5"
}
]
},
"Enabled": "True",
"Iterations": {
"Filters": null,
"Mappings": [
{
"Match": "^Skypoint Cloud\\\\Sprint 1$",
"Replacement": "MigrationTest5\\Sprint 1"
}
]
},
"ReplicateAllExistingNodes": "True",
"ShouldCreateMissingRevisionPaths": "True"
}
}
}
}
Defaults
{
"MigrationTools": {
"Version": "16.0",
"CommonTools": {
"TfsNodeStructureTool": {
"Areas": {
"Filters": null,
"Mappings": null
},
"Enabled": "True",
"Iterations": {
"Filters": null,
"Mappings": null
},
"ReplicateAllExistingNodes": "True",
"ShouldCreateMissingRevisionPaths": "True"
}
}
}
}
Basic Examples
topOverview
These two configuration elements apply after the Areas.Filters
and Iterations.Filters
selectors, i.e.
only on Areas and Iterations that have been selected for migration. They allow
to change the area path, respectively the iteration path, of migrated work items.
These remapping rules are applied both while creating path nodes in the target project and when migrating work items.
These remapping rules are applied with a higher priority than the
PrefixProjectToNodes
option. This means that if no declared rule matches the
path and the PrefixProjectToNodes
option is enabled, then the old behavior is
used.
The syntax uses structured mappings with Match and Replacement properties containing regular expressions and the replacement text.
Warning: These follow the .net regular expression language . The Match property contains a regular expression search pattern, while the Replacement property contains a regular expression replacement pattern. It is therefore possible to use back-references in the replacement string.
Warning: Special characters in the acceptation of regular expressions and
json both need to be escaped. For the Match property, this means, for example, that a
literal backslash must be escaped for the regular expression language \\
and each of these backslashes must then be escaped for the json encoding:
\\\\
. In the Replacement property, a literal $
must be escaped with an
additional $
if it is followed by a number (due to the special meaning in
regular expression replacement strings), while a backslash must be escaped
(\\
) due to the special meaning in json.
Advice: To avoid unexpected results, always match terminating backslashes in the search pattern and replacement string: if a search pattern ends with a backslash, you should also put one in the replacement string, and if the search pattern does not include a terminating backslash, then none should be included in the replacement string.
topConfiguration
"Iterations": {
"Mappings": [
{
"Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2022)(.*)$",
"Replacement": "TargetProject\\AnotherPath\\NewTeam$1"
},
{
"Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2020)(.*)$",
"Replacement": "TargetProject\\AnotherPath\\Archives\\Sprints 2020$1"
},
{
"Match": "^OriginalProject\\\\Path2(.*)$",
"Replacement": "TargetProject\\YetAnotherPath\\Path2$1"
}
]
},
"Areas": {
"Mappings": [
{
"Match": "^OriginalProject\\\\(DescopeThis|DescopeThat)(.*)$",
"Replacement": "TargetProject\\Archive\\Descoped\\$1$2"
},
{
"Match": "^OriginalProject\\\\(?!DescopeThis|DescopeThat)(.*)$",
"Replacement": "TargetProject\\NewArea\\$1"
}
]
}
"Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2022)(.*)$", "Replacement": "TargetProject\\AnotherPath\\NewTeam$1"
In an iteration path,
OriginalProject\Path1
found at the beginning of the path, when followed by\Sprint 2022
, will be replaced byTargetProject\AnotherPath\NewTeam
.OriginalProject\Path1\Sprint 2022\Sprint 01
will becomeTargetProject\AnotherPath\NewTeam\Sprint 2022\Sprint 01
butOriginalProject\Path1\Sprint 2020\Sprint 03
will not be transformed by this rule."Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2020)(.*)$", "Replacement": "TargetProject\\AnotherPath\\Archives\\Sprints 2020$1"
In an iteration path,
OriginalProject\Path1
found at the beginning of the path, when followed by\Sprint 2020
, will be replaced byTargetProject\AnotherPath\Archives\\Sprints 2020
.OriginalProject\Path1\Sprint 2020\Sprint 01
will becomeTargetProject\AnotherPath\Archives\Sprint 2020\Sprint 01
butOriginalProject\Path1\Sprint 2021\Sprint 03
will not be transformed by this rule."Match": "^OriginalProject\\\\Path2(.*)$", "Replacement": "TargetProject\\YetAnotherPath\\Path2$1"
In an iteration path,
OriginalProject\Path2
will be replaced byTargetProject\YetAnotherPath\Path2
."Match": "^OriginalProject\\\\(DescopeThis|DescopeThat)(.*)$", "Replacement": "TargetProject\\Archive\\Descoped\\$1$2"
In an area path,
OriginalProject\
found at the beginning of the path, when followed by eitherDescopeThis
orDescopeThat
will be replaced byTargetProject\Archive\Descoped\
.OriginalProject\DescopeThis\Area
will be transformed toTargetProject\Archive\Descoped\DescopeThis\Area
.OriginalProject\DescopeThat\Product
will be transformed toTargetProject\Archive\Descoped\DescopeThat\Product
."Match": "^OriginalProject\\\\(?!DescopeThis|DescopeThat)(.*)$", "Replacement": "TargetProject\\NewArea\\$1"
In an area path,
OriginalProject\
found at the beginning of the path will be replaced byTargetProject\NewArea\
unless it is followed byDescopeThis
orDescopeThat
.OriginalProject\ValidArea\
would be replaced byTargetProject\NewArea\ValidArea\
butOriginalProject\DescopeThis
would not be modified by this rule.
PrefixProjectToNodes
The PrefixProjectToNodes
was an option that was used to prepend the source project name to the target set of nodes. This was super valuable when the target Project already has nodes and you dont want to merge them all together. This is now replaced by the Areas.Mappings
and Iterations.Mappings
options.
"Iterations": {
"Mappings": [
{
"Match": "^SourceServer\\\\(.*)$",
"Replacement": "TargetServer\\SourceServer\\$1"
}
]
},
"Areas": {
"Mappings": [
{
"Match": "^SourceServer\\\\(.*)$",
"Replacement": "TargetServer\\SourceServer\\$1"
}
]
}
=======
topMore Complex Regex
Before your migration starts it will validate that all of the Areas and Iterations from the Source work items revisions exist on the Target. Any that do not exist will be flagged in the logs and if and the migration will stop just after it outputs a list of the missing nodes.
Our algorithm that converts the Source nodes to Target nodes processes the mappings at that time. This means that any valid mapped nodes will never be caught by the This path is not anchored in the source project
message as they are already altered to be valid.
We recently updated the logging for this part of the system to more easily debug both your mappings and to see what they system is doing with the nodes and their current state. You can set
"LogLevel": "Debug"
to see the details.
To add a mapping, you can follow the documentation with this being the simplest way:
"Iterations": {
"Mappings": [
{
"Match": "WorkItemMovedFromProjectName\\\\Iteration 1",
"Replacement": "TargetProject\\Sprint 1"
}
]
},
"Areas": {
"Mappings": [
{
"Match": "WorkItemMovedFromProjectName\\\\Team 2",
"Replacement": "TargetProject\\ProductA\\Team 2"
}
]
}
Or you can use regular expressions to match the missing area or iteration paths:
"Iterations": {
"Mappings": [
{
"Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2022)(.*)$",
"Replacement": "TargetProject\\AnotherPath\\NewTeam$1"
},
{
"Match": "^OriginalProject\\\\Path1(?=\\\\Sprint 2020)(.*)$",
"Replacement": "TargetProject\\AnotherPath\\Archives\\Sprints 2020$1"
},
{
"Match": "^OriginalProject\\\\Path2(.*)$",
"Replacement": "TargetProject\\YetAnotherPath\\Path2$1"
}
]
},
"Areas": {
"Mappings": [
{
"Match": "^OriginalProject\\\\(DescopeThis|DescopeThat)(.*)$",
"Replacement": "TargetProject\\Archive\\Descoped\\$1$2"
},
{
"Match": "^OriginalProject\\\\(?!DescopeThis|DescopeThat)(.*)$",
"Replacement": "TargetProject\\NewArea\\$1"
}
]
}
If you want to use the matches in the replacement you can use the following:
"Iterations": {
"Mappings": [
{
"Match": "^\\\\oldproject1(?:\\\\([^\\\\]+))?\\\\([^\\\\]+)$",
"Replacement": "TargetProject\\Q1\\$2"
}
]
}
If the old iteration path was \oldproject1\Custom Reporting\Sprint 13
, then this would result in a match for each Iteration node after the project node. You would then be able to reference any of the nodes using “$” and then the number of the match.
Regular expressions are much more difficult to build and debug so it is a good idea to use a regular expression tester to check that you are matching the right things and to build them in ChatGTP.
NOTE: You need \\
to escape a \
the pattern, and \\
to escape a \
in JSON. Therefore in the Match property you need 4 \
to represent the \\
for the pattern and only 2 \
in the Replacement property
Example with PrefixProjectToNodes
This will prepend a bucket to the area and iteration paths. This is useful when you want to keep the original paths but also want to be able to identify them as being from the original project.
"Areas": {
"Mappings": [
{
"Match": "^OriginalProject(?:\\\\([^\\\\]+))?\\\\([^\\\\]+)$",
"Replacement": "TargetProject\\BucketForIncommingAreas\\$2"
}
]
},
"Iterations": {
"Mappings": [
{
"Match": "^OriginalProject(?:\\\\([^\\\\]+))?\\\\([^\\\\]+)$",
"Replacement": "TargetProject\\BucketForIncommingInterations\\$2"
}
]
}
Example with Areas and Iterations
{
"$type": "TfsNodeStructureToolOptions",
"Enabled": true,
"Areas": {
"Filters": [],
"Mappings": [
{
"Match": "^Skypoint Cloud$",
"Replacement": "MigrationTest5"
}
]
},
"Iterations": {
"Filters": [],
"Mappings": [
{
"Match": "^Skypoint Cloud\\\\Sprint 1$",
"Replacement": "MigrationTest5\\Sprint 1"
}
]
},
"ShouldCreateMissingRevisionPaths": true,
"ReplicateAllExistingNodes": true
}
Filters
The Areas.Filters
and Iterations.Filters
entries allow the filtering of the nodes to be replicated on the target projects. To try to explain the correct usage let us assume that we have a source team project SourceProj
with the following node structures
- AreaPath
- SourceProj
- SourceProj\Team 1
- SourceProj\Team 2
- SourceProj\Team 2\Sub-Area 1
- SourceProj\Team 2\Sub-Area 2
- SourceProj\Team 3
- IterationPath
- SourceProj
- SourceProj\Sprint 1
- SourceProj\Sprint 2
- SourceProj\Sprint 2\Sub-Iteration
- SourceProj\Sprint 3
Depending upon what node structures you wish to migrate you would need the following settings. Exclusions are also possible by prefixing a path with an exclamation mark !
. Example are
Intention | Migrate all areas and iterations and all Work Items |
Filters | "Areas": { "Filters": [] }, "Iterations": { "Filters": [] } |
Comment | The same AreaPath and Iteration Paths are created on the target as on the source. Hence, all migrated WI remain in their existing area and iteration paths. This will be affected by the Areas.Mappings and Iterations.Mappings settings. |
Intention | Only migrate area path Team 2 and it associated Work Items, but all iteration paths |
Filters | "Areas": { "Filters": ["*\\Team 2"] }, "Iterations": { "Filters": ["*\\Sprint*"] } |
Comment | Only the area path ending Team 2 will be migrated.The WIQLQuery should be edited to limit the WI migrated to this area path e.g. add AND [System.AreaPath] UNDER 'SampleProject\\Team 2' .The migrated WI will have an area path of TargetProj\Team 2 but retain their iteration paths matching the sprint name on the source |
Intention | Move the Team 2 area, including its Sub-Area , and any others at the same level |
Filters | "Areas": { "Filters": ["*\\Team 2", "Team 2\\*"] }, "Iterations": { "Filters": [] } |
Comment | The Work Items will have to be restricted to the right areas, e.g. with AND [System.AreaPath] UNDER 'SampleProject\\Team 2' AND [System.AreaPath] NOT UNDER 'SampleProject\\Team 2\\Sub-Area' , otherwise their migration will fail |
Intention | Move the Team 2 area, but not its Sub-Area |
Filters | "Areas": { "Filters": ["*\\Team 2", "!Team 2\\SubArea"] }, "Iterations": { "Filters": [] } |
Comment | The Work Items will have to be restricted to the right areas, e.g. with AND [System.AreaPath] UNDER 'SampleProject\\Team 2' AND [System.AreaPath] NOT UNDER 'SampleProject\\Team 2\\Sub-Area' , otherwise their migration will fail |
Patterns
The following patterns are supported:
Wildcard Description Example Matches Does not match * matches any number of any characters including none Law* Law, Laws, or Lawyer ? matches any single character ?at Cat, cat, Bat or bat at [abc] matches one character given in the bracket [CB]at Cat or Bat cat or bat [a-z] matches one character from the range given in the bracket Letter[0-9] Letter0, Letter1, Letter2 up to Letter9 Letters, Letter or Letter10 [!abc] matches one character that is not given in the bracket [!C]at Bat, bat, or cat Cat [!a-z] matches one character that is not from the range given in the bracket Letter[!3-5] Letter1, Letter2, Letter6 up to Letter9 and Letterx etc. Letter3, Letter4, Letter5 or Letterxx
In addition, Glob also supports:
Wildcard | Description | Example | Matches | Does not match |
---|---|---|---|---|
** | matches any number of path / directory segments. When used must be the only contents of a segment. | /**/some.* | /foo/bar/bah/some.txt, /some.txt, or /foo/some.txt |
Escaping special characters
Wrap special characters ?, *, [
in square brackets in order to escape them.
You can also use negation when doing this.
In this article
Project Information
Azure DevOps Marketplace
Maintainer
Created and maintained by Martin Hinshelwood of nkdagility.com
Getting Support
Community Support
The first place to look for usage, configuration, and general help.
Commercial Support
We provide training, ad-hoc support, and full service migrations through our professional services.
Azure DevOps Migration Services