Azure DevOps Migration Tickets & Testplans from one instance to another
If you need to migrate Azure DevOps onprem to our cloud instance, or if you need to migrate a third party azure devops cloud instance to our cloud instance, you should try to use the “Azure DevOps Migrations Tools from NKDAgility”. Those tools provide a toolset to migrate almost everything from Azure DevOps.
For this documentation I’ve used version 13.1.0 — which can be downloaded from the GitHub releases page:
Workitem migration
Source Azure DevOps — Add a custom Field
- Open the source Azure DevOps instance (cloud or onprem)
- Open the Process Settings from the project you want to migrate ( Organisation Settings: Boards → Process)
- Usually you have to inherit from an existing process first
- Edit the process and add a custom field to every Object in the process (Task, Bug, etc.)
- The filed is configured like this:
- Name: ReflectedWorkItemId
- Type: Text (single line)
Source Azure DevOps — Create a PAT
- Create a PAT with full righty called “sync” and store it somewhere
- Use personal access tokens — Azure DevOps | Microsoft Learn
Target Azure DevOps — Add custom field and recreate process from source repo
- Open the source Azure DevOps instance (cloud or onprem)
- Open the Process Settings from the project you want to migrate ( Organisation Settings: Boards → Process)
- Usually you have to inherit from an existing process first
- Check all custom fields in the source Azure DevOps and recreate them in the target
- Check all states in all workitem types and copy custom states from the source Azure DevOps
- Edit the process and add a custom field to every Object in the process (Task, Bug, etc.)
- The field is configured like this:
- Name: ReflectedWorkItemId
- Type: Text (single line)
Target Azure DevOps — Recreate existing teams
- Open the source Azure DevOps instance (cloud or onprem)
- Open the Project teams settings
- Create / Rename Teams so that they match the teams from the source repo.
- It’s not a problem if you have more teams in the target Azure DevOps then in the source Azure DevOps.
- You need to have the sprint and ticket related teams in the target Azure DevOps!
Target Azure DevOps — Create a PAT
- Create a PAT with full righty called “sync” and store it somewhere
- Use personal access tokens — Azure DevOps | Microsoft Learn
Configure the migration tool
- Download a current release from Azure DevOps Migration Tools: https://github.com/nkdAgility/azure-devops-migration-tools/releases
- I’ve used 13.1.0 - Generate a default config in a working folder
- .\MigrationTools-13.1.0\migration.exe init
- More information: https://nkdagility.com/learn/azure-devops-migration-tools/getting-started/
Configure source and target section — Keys and Values to configure:
- Collection: Link to the azure devops organization (NOT THE PROJECT)
- Like this https://dev.azure.com/SOMECORP/ - Project: Name of the project
- AuthenticationMode: AccessToken
- PersonalAccessToken: Enter the stored tokens from target and source Azure DevOps
"Source": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/SOURCE/",
"Project": "kim.Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
},
"Target": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/TARGET/",
"Project": "kim_Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
}
Configure FieldMaps section:
- Remove MultiValueConditionalMapConfig
- Remove FieldtoFieldMapConfig
- Remove FieldtoFieldMultiMapConfig
- Remove FieldtoTagMapConfig
- Remove FieldMergeMapConfig
- Remove RegexFieldMapConfig
- Remove FieldValuetoTagMapConfig
- Remove TreeToTagMapConfig
- Keep FieldSkipMapConfig
- Adjust FieldValueMapConfig
- Lookup all values in all workitemtypes in the source
- Check if those exist in the target system
- Enter all of them in the valueMapping
e.g. “New” : “New”
"FieldMaps": [
{
"$type": "FieldSkipMapConfig",
"WorkItemTypeName": "*",
"targetField": "TfsMigrationTool.ReflectedWorkItemId"
},
{
"$type": "FieldValueMapConfig",
"WorkItemTypeName": "*",
"sourceField": "System.State",
"targetField": "System.State",
"defaultValue": "New",
"valueMapping": {
"New": "New",
"Active": "Active",
"In Review": "In Review",
"Resolved": "Resolved",
"Closed": "Closed",
"Removed": "Removed",
"Design": "Design",
"Ready": "Ready",
"Inactive": "Inactive",
"In Planning": "In Planning",
"In Progress": "In Progress",
"Completed": "Completed"
}
}
],
Configure Processors section:
- Remove all processors except of: WorkItemMigrationConfig
Configure Processor WorkItemMigrationConfig:
- Enabled: true
- If you want to sync closed issues as well adjust the WIQLQueryBit — Key:
- Adjust the following query to sync the oldest tickets you need
- “AND ( [Microsoft.VSTS.Common.ClosedDate] > ‘2022–01–01’ OR [Microsoft.VSTS.Common.ClosedDate] = ‘’) AND [System.WorkItemType] NOT IN (‘Test Suite’, ‘Test Plan’,’Shared Steps’,’Shared Parameter’,’Feedback Request’)”
{
"$type": "WorkItemMigrationConfig",
"Enabled": true,
"ReplayRevisions": true,
"PrefixProjectToNodes": false,
"UpdateCreatedDate": true,
"UpdateCreatedBy": true,
"WIQLQueryBit": "AND ( [Microsoft.VSTS.Common.ClosedDate] > '2022-01-01' OR [Microsoft.VSTS.Common.ClosedDate] = '') 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": true,
"PauseAfterEachWorkItem": false,
"AttachmentMaxSize": 480000000,
"AttachRevisionHistory": false,
"LinkMigrationSaveEachAsAdded": false,
"GenerateMigrationComment": true,
"WorkItemIDs": null,
"MaxRevisions": 0,
"UseCommonNodeStructureEnricherConfig": false,
"StopMigrationOnMissingAreaIterationNodes": true,
"NodeBasePaths": [
"Product\\Area\\Path1",
"Product\\Area\\Path2"
],
"AreaMaps": {},
"IterationMaps": {},
"MaxGracefulFailures": 0,
"SkipRevisionWithInvalidIterationPath": false,
"SkipRevisionWithInvalidAreaPath": false
},
Run the migration
- Run the tool with the updated configuration.json from the command line. An initial sync can take quite a while, after the sync has been completed, you can rerun the tool at any time and it’s a bit faster then.
- .\MigrationTools-13.1.0\migration.exe execute — config .\configuration.json
Workitems & Testplans
Prepare
- Get a licence which grants you access to all testplan features (e.g. Basic + Test plans)
- Delete superfluous (no longer used) testplans.
Adjust Processor configuration:
Add some more processors after the WorkItemMigrationConfig processor
- Add: TestVariablesMigrationConfig
- Add: TestConfigurationsMigrationConfig
- Add: TestPlansAndSuitesMigrationConfig
{
"$type": "TestVariablesMigrationConfig",
"Enabled": true
},
{
"$type": "TestConfigurationsMigrationConfig",
"Enabled": true
},
{
"$type": "TestPlansAndSuitesMigrationConfig",
"Enabled": true,
"PrefixProjectToNodes": false,
"OnlyElementsWithTag": null,
"TestPlanQueryBit": null,
"RemoveAllLinks": false,
"MigrationDelay": 0,
"UseCommonNodeStructureEnricherConfig": false,
"NodeBasePaths": null,
"AreaMaps": null,
"IterationMaps": null,
"RemoveInvalidTestSuiteLinks": false,
"FilterCompleted": false
}
Readjust the WorkItemMigrationConfig
- Update the WIQLQueryBit to sync all workitems
- “AND [System.WorkItemType] NOT IN (‘Test Suite’, ‘Test Plan’,’Shared Steps’,’Shared Parameter’,’Feedback Request’)”
{
"$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": true,
"PauseAfterEachWorkItem": false,
"AttachmentMaxSize": 480000000,
"AttachRevisionHistory": false,
"LinkMigrationSaveEachAsAdded": false,
"GenerateMigrationComment": true,
"WorkItemIDs": null,
"MaxRevisions": 0,
"UseCommonNodeStructureEnricherConfig": false,
"StopMigrationOnMissingAreaIterationNodes": true,
"NodeBasePaths": [
"Product\\Area\\Path1",
"Product\\Area\\Path2"
],
"AreaMaps": {},
"IterationMaps": {},
"MaxGracefulFailures": 0,
"SkipRevisionWithInvalidIterationPath": false,
"SkipRevisionWithInvalidAreaPath": false
},
Run the migration:
- Run the tool with the updated configuration.json from the command line. An initial sync can take quite a while, after the sync has been completed, you can rerun the tool at any time and it’s a bit faster then.
- .\MigrationTools-13.1.0\migration.exe execute — config .\configuration.json
Example Workitems
{
"ChangeSetMappingFile": null,
"Source": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/SOURCE/",
"Project": "kim.Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
},
"Target": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/TARGET/",
"Project": "kim_Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
},
"FieldMaps": [
{
"$type": "FieldSkipMapConfig",
"WorkItemTypeName": "*",
"targetField": "TfsMigrationTool.ReflectedWorkItemId"
},
{
"$type": "FieldValueMapConfig",
"WorkItemTypeName": "*",
"sourceField": "System.State",
"targetField": "System.State",
"defaultValue": "New",
"valueMapping": {
"New": "New",
"Active": "Active",
"In Review": "In Review",
"Resolved": "Resolved",
"Closed": "Closed",
"Removed": "Removed",
"Design": "Design",
"Ready": "Ready",
"Inactive": "Inactive",
"In Planning": "In Planning",
"In Progress": "In Progress",
"Completed": "Completed"
}
}
],
"GitRepoMapping": null,
"LogLevel": "Information",
"CommonEnrichersConfig": null,
"Processors": [
{
"$type": "WorkItemMigrationConfig",
"Enabled": true,
"ReplayRevisions": true,
"PrefixProjectToNodes": false,
"UpdateCreatedDate": true,
"UpdateCreatedBy": true,
"WIQLQueryBit": "AND ( [Microsoft.VSTS.Common.ClosedDate] > '2022-01-01' OR [Microsoft.VSTS.Common.ClosedDate] = '') 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": true,
"PauseAfterEachWorkItem": false,
"AttachmentMaxSize": 480000000,
"AttachRevisionHistory": false,
"LinkMigrationSaveEachAsAdded": false,
"GenerateMigrationComment": true,
"WorkItemIDs": null,
"MaxRevisions": 0,
"UseCommonNodeStructureEnricherConfig": false,
"StopMigrationOnMissingAreaIterationNodes": true,
"NodeBasePaths": [
"Product\\Area\\Path1",
"Product\\Area\\Path2"
],
"AreaMaps": {},
"IterationMaps": {},
"MaxGracefulFailures": 0,
"SkipRevisionWithInvalidIterationPath": false,
"SkipRevisionWithInvalidAreaPath": false
}
],
"Version": "13.1",
"workaroundForQuerySOAPBugEnabled": false,
"WorkItemTypeDefinition": {
"sourceWorkItemTypeName": "targetWorkItemTypeName"
},
"Endpoints": {
"InMemoryWorkItemEndpoints": [
{
"Name": "Source",
"EndpointEnrichers": null
},
{
"Name": "Target",
"EndpointEnrichers": null
}
]
}
}
Example Workitems & Testplans
{
"ChangeSetMappingFile": null,
"Source": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/SOURCE/",
"Project": "kim.Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
},
"Target": {
"$type": "TfsTeamProjectConfig",
"Collection": "https://dev.azure.com/TARGET/",
"Project": "kim_Schaden",
"ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId",
"AllowCrossProjectLinking": false,
"AuthenticationMode": "AccessToken",
"PersonalAccessToken": "PAT",
"PersonalAccessTokenVariableName": "",
"LanguageMaps": {
"AreaPath": "Area",
"IterationPath": "Iteration"
}
},
"FieldMaps": [
{
"$type": "FieldSkipMapConfig",
"WorkItemTypeName": "*",
"targetField": "TfsMigrationTool.ReflectedWorkItemId"
},
{
"$type": "FieldValueMapConfig",
"WorkItemTypeName": "*",
"sourceField": "System.State",
"targetField": "System.State",
"defaultValue": "New",
"valueMapping": {
"New": "New",
"Active": "Active",
"In Review": "In Review",
"Resolved": "Resolved",
"Closed": "Closed",
"Removed": "Removed",
"Design": "Design",
"Ready": "Ready",
"Inactive": "Inactive",
"In Planning": "In Planning",
"In Progress": "In Progress",
"Completed": "Completed"
}
}
],
"GitRepoMapping": null,
"LogLevel": "Information",
"CommonEnrichersConfig": null,
"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": true,
"PauseAfterEachWorkItem": false,
"AttachmentMaxSize": 480000000,
"AttachRevisionHistory": false,
"LinkMigrationSaveEachAsAdded": false,
"GenerateMigrationComment": true,
"WorkItemIDs": null,
"MaxRevisions": 0,
"UseCommonNodeStructureEnricherConfig": false,
"StopMigrationOnMissingAreaIterationNodes": true,
"NodeBasePaths": [
"Product\\Area\\Path1",
"Product\\Area\\Path2"
],
"AreaMaps": {},
"IterationMaps": {},
"MaxGracefulFailures": 0,
"SkipRevisionWithInvalidIterationPath": false,
"SkipRevisionWithInvalidAreaPath": false
},
{
"$type": "TestVariablesMigrationConfig",
"Enabled": true
},
{
"$type": "TestConfigurationsMigrationConfig",
"Enabled": true
},
{
"$type": "TestPlansAndSuitesMigrationConfig",
"Enabled": true,
"PrefixProjectToNodes": false,
"OnlyElementsWithTag": null,
"TestPlanQueryBit": null,
"RemoveAllLinks": false,
"MigrationDelay": 0,
"UseCommonNodeStructureEnricherConfig": false,
"NodeBasePaths": null,
"AreaMaps": null,
"IterationMaps": null,
"RemoveInvalidTestSuiteLinks": false,
"FilterCompleted": false
}
],
"Version": "13.1",
"workaroundForQuerySOAPBugEnabled": false,
"WorkItemTypeDefinition": {
"sourceWorkItemTypeName": "targetWorkItemTypeName"
},
"Endpoints": {
"InMemoryWorkItemEndpoints": [
{
"Name": "Source",
"EndpointEnrichers": null
},
{
"Name": "Target",
"EndpointEnrichers": null
}
]
}
}
Conclusion
I hope this article helps you — migrating your Azure DevOps instance. I tried to document the necessary steps and configuration parts as good as possible.
If you still have troubles migrating Azure DevOps — you may should get in touch with nkdagility as they offer professional support for this tool.
Thanks for reading!