Migrating from TFS to VSTS, Part 3–Areas, Iterations and Queries
Next in the series on migrating from TFS to VSTS we will work on moving the areas, iterations and queries. This will set us up for migrating the work items next.
Part 1 – TfsMigration Tool
Part 2 – Migrating from TFVC to Git
Part 3 – Migrating Areas, Iterations and Queries
Part 4 – Migrating Work Items
Migration Process
Migrating the areas and iterations from TFS is critical so that sprints, releases and the layout of your process remains unchanged. Therefore the migration tool allows you to migrate areas and iterations automatically. This is actually a pretty easy process as, beyond the hierarchy, there are no dependencies on anything else and areas/iterations have only a couple of pieces of data.
When work items get migrated later they will need access to the list of migrated areas/iterations. Additionally a work item may need to add a new area/iteration to the migration list. While the process described here is run before work items are migrated, the same process is triggered during work item migration as needed. The migrated area/iteration information is available because both processes run in the same processor. They are broken up here for convenience.
The WorkItemTracking processor handles the migration of areas, iterations and work items. We will focus on the areas and iterations in this post. In the VSTS REST API areas and iterations are referred to as classification nodes. In fact, other than changing a parameter when making calls to the APIs, they are treated the same. For purposes of this post we will talk about areas but iterations follow the same process.
Migrating Areas
Like the other processors the migration process starts by loading the list of areas to migrate from the settings file. For each area VSTS is checked to see if it already exists. If it does then the area is skipped otherwise it is created. Because there is a hierarchy for areas the processor starts at the root area and creates each child area until the area is fully created. Once the area is created it is stored in the processor as a source-destination pair. This is important for later when queries are migrated.
The settings for areas are stored under the WorkItemTracking\Areas element. Each element has the following attributes.
- SourcePath – the required source path using a TFS item path.
- DestinationPath – the optional destination path using a VSTS item path.
If DestinationPath is specified then the area is renamed when being created in VSTS. This allows areas to be moved and is why the mapping is needed for later calls. It is possible to rename multiple areas to the same destination.
Getting Areas
To determine if the area exists the GetAreaAsync extension method for WorkItemTrackingHttpClient is used. Like most of the work item tracking code, the extension methods are stored in the core assembly so they can be used in multiple processors. The path itself is converted to a NodePath. This type is used to handle the parsing needed by the extension method. Because a hierarchy needs to be created, the type provides helper functionality to get the parent of the current “node”.
For retrieving an area the path is simply passed on to the GetClassificationNodeAsync method of WorkItemTrackingHttpClient. The TreeStructureGroup argument is used to identify it as an area or iteration.The helper method optionally can throw an exception if the area is not found.
Getting all areas is trickier. Since areas can be arbitrarily nested the API requires that you specify the depth to go down. The extension method uses a depth of 20 because this value was the highest that could be used for the migration. If you have areas nested deeper than 20 then it will not find them all. But this method is more for symmetry than for migration purposes.
Creating Areas
Creating an area is straightforward as well. Using the NodePath class, the area is first checked to see if it exists. If it doesn’t then the parent area is retrieved, if any. The parent area is created, if needed. Because this is a hierarchy it recursively works its way back to the root until the parent(s) is/are created.
Migrating Queries
Queries are migrated using the QueryManagement processor. While the work items have not been migrated yet, it is useful to look at the query migration processor at this point. Queries cannot be migrated until area/iterations are migrated because VSTS will fail to create queries with bad paths. Queries can be migrated at any point but since work items and areas/iterations migrate together it makes sense to run the queries after work items.
Query migration is a little more stringent than other migrations. There are many different queries that can be generated using WIQL and therefore attempting to figure out and migrate a query can be very difficult. The processor, therefore, migrates queries mostly unchanged. It is possible that some queries cannot be migrated because of dependencies on things that weren’t migrated. Testing the queries to migrate is very important before doing the production run.
Query settings are stored under QueryManagement. Originally the processor only listed the queries to migrate but if you have a lot of queries then it is easier to list the ones to exclude so the ExcludeQeries specifies the path to the queries to exclude. A query may include an * to filter queries to the left of the wildcard character.
In TFS queries can be either shared (meaning they are under the Shared Queries path) or per user. Since the migration tool is going to run under a migration account it doesn’t make sense to migrate per user queries. If per user queries are desired then they should be moved to shared before the migration.
Migrating queries is relatively straightforward, like areas. First all the shared queries are retrieved from TFS. Each query is then analyzed to determine if it is on the exclude list. If the query is then it is skipped. If the query is not excluded then VSTS is queried to see if the query already exists. The processor does not support renaming queries. If the query already exists and the override setting is set then the query is removed. Otherwise it is added.
Finding Queries
Like areas, the functionality to manage queries are extension methods off of WorkItemTrackingHttpClient. Finding queries requires that we search for them. Queries are hierarchical and are represented as a QueryHierarchyItem. To simplify the extension code there are separate classes for folders and queries. Unfortunately folders have to be handled explicitly since they are not queries. To get all the shared queries we get the contents of the root folder using GetQueryAsync with a depth of 1 and the QueryExpand set to All.
Note: For some reason, if the folder does not exist then the API throws an error with ID TF401243. The code explicitly handles this and returns null.
The processor does not really care about empty folders so it only looks for queries. If a folder is found to have children then they are enumerated as well by making a recursive call. Once again this is to work around the issue of the API client only returning a subset of the data. Upon return all the queries in a given folder and its child folders are contained in a list.
Adding Queries
Adding a query to VSTS first requires the folder to exist. Like areas, a QueryPath class is used to wrap the path to the query. Using recursive calls we can then enumerate the folder(s) to get to the query. When adding the query the folder is first created if it does not exist yet using CreateQueryAsync. In the API the difference between a query and folder (as a QueryHierarchyItem) is the IsFolder property.
Once the folder exists the query is created using the CreateQueryAsync method. The source query is effectively copied to the target query without any modifications. As mentioned earlier, this may fail for some queries. If a query fails then it can be added to the exclude list and migrated manually later.
Next Steps
At this point the necessary infrastructure is in place to migrate the work items. In the next post we will talk about this complicated process.
Download the code on GitHub.