Azure DevOps Migration Tickets & Testplans from one instance to another

Bjego
7 min readAug 21, 2023

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

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

Configure the migration tool

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!

--

--