Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: April 22, 2025
Azure DevOps (ADO) is a powerful platform for managing software development, but sometimes we need to move a project from one organization to another. It could be due to company mergers, restructuring, compliance needs, or reorganization of teams. Unfortunately, Azure DevOps does not support a direct approach to move a full project smoothly. That means we need to employ other ways to ensure our repositories, work items, pipelines, and other resources are moved properly.
In this article, we’ll explore different ways to move an Azure DevOps project between organizations. We choose the right migration approach according to our project size, data complexity, and specific needs. Therefore, below we are defining some common approaches.
We can manually migrate projects when the project size is on a small scale. It requires manually re-creating code pipelines, repositories, and work items in the new organization.
A key part of manual migration is transferring Git repositories. This requires moving repositories individually from the old organization to the new one. To accomplish this, we first clone the source repository and then push it to the new destination:
git clone --mirror https://dev.azure.com/{source-org}/{project}/_git/{repo}
cd {repo}
After we clone the repository locally, we push it to our new repository. For that, we create a new repository in the destination organization through the Azure DevOps Portal. After that, we can push the mirrored repository to our newly created repository:
git push --mirror https://dev.azure.com/{target-org}/{project}/_git/{repo}
In this example, we use git clone –mirror to fetch all repository data, including branches and tags. Then, we switch into the cloned repo’s directory and push everything to the new organization using git push –mirror. This method ensures a complete repository transfer.
This is also one of the manual migration approaches. We use this method if our Azure DevOps project only tracks code versions using a single Git repo, hence no boards, user stories, tasks, or pipelines. Let’s explore this approach.
At first, if not already cloned, we download the repository:
git clone https://dev.azure.com/{source-org}/{project}/_git/{repo}
cd {repo}
After that, we remove the existing remote association:
git remote rm origin
Then, we create a new repository in the target organization, as we mentioned before. After creating the new repository, we link the local repo to the new remote repository and push it:
git remote add origin https://dev.azure.com/{target-org}/{project}/_git/{repo}
git push -u origin --all
After that, we will check if our pushed content is in the new organization or not. If needed, we can also delete the old project in the original organization. The manual migration process is simple to do. However, it lacks full fidelity, and process templates must be recreated as inherited templates.
If we need a medium to large-scale migration with more complex data, we can use open-source tools. One such tool is Azure DevOps Migration Tools. It does support a “lift and shift” of an entire Azure DevOps Server collection into a new Azure DevOps Services organization. At first, let’s start by installing the tools in Windows:
winget install nkdAgility.AzureDevOpsMigrationTools
We demonstrated the use of winget here only. There are other options mentioned in the Azure DevOps Migration Tools Documentation.
Once installed, we create a configuration file. For that, we navigate to our working directory and initialize the config:
devopsmigration init --options Basic
This command creates a configuration.json file that we modify according to our migration needs.
After that, we configure source and target project details inside this file. If we have the following goals:
then we can specify these in their appropriate sections:
{
"Serilog": {
"MinimumLevel": "Information"
},
"MigrationTools": {
"Version": "16.0",
"Endpoints": {
"Source": {
"EndpointType": "TfsTeamProjectEndpoint",
"Collection": "https://dev.azure.com/nkdagility-preview/",
"Project": "migrationSource1",
"Authentication": {
"AuthenticationMode": "AccessToken",
"AccessToken": "jkashdjksahsjkfghsjkdaghvisdhuisvhladvnb"
}
},
"Target": {
"EndpointType": "TfsTeamProjectEndpoint",
"Collection": "https://dev.azure.com/nkdagility-preview/",
"Project": "migrationTest5",
"Authentication": {
"AuthenticationMode": "AccessToken",
"AccessToken": "lkasjioryislaniuhfhklasnhfklahlvlsdvnls"
},
"ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId"
}
},
"CommonTools": {},
"Processors": [
{
"ProcessorType": "TfsWorkItemMigrationProcessor",
"Enabled": true,
"WIQLQuery": "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request') ORDER BY [System.ChangedDate] desc",
}
]
}
}
This configuration provides a good starting point and can be used as a template for other migrations with different project names, a different work item (WIDL) query, or additional processors for different data types.
After creating this configuration file, we execute the migration. Let’s start the migration:
devopsmigration execute --config .\configuration.json
After that, we check the migrated work items in the targeted project to validate the attachments, links, and history. Though this tool can handle complex migrations, it does not support selective migrations or any transformation of the data structure.
For more control than the tool gives us, we can use the API-based approach. This is ideal for large data migrations or automating processes. Azure DevOps REST APIs will allow us to transfer data between organizations. Let’s start the process by calling the Azure Repositories API using a PowerShell script.
To start this process, we create a Personal Access Token (PAT) and a new SSH key. At the beginning, we provide some necessary inputs in the script:
param (
[string]$PAT
)
# Set required variables
$organizationName = "ourorg" # Source Azure DevOps Organization
$projectNameFrom = "Project1" # Source Project
$projectNameTo = "Project2" # Destination Project
$scriptLocation = $PSScriptRoot # Location of the script
After initializing the script, we provide some key functions to handle authentication, repository retrieval, enabling disabled repositories, checking repository existence, deleting repositories, and migrating them. Let’s see each of the functions one by one.
This function returns a Base64-encoded authentication string using a PAT. It is required to authenticate requests to the Azure DevOps REST API:
function Get-AuthHeader {
param (
[string]$token
)
return [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($token)"))
}
This function fetches all repositories from a specified project. It helps in identifying which repositories need to be migrated:
function Get-Repositories {
param (
[string]$organization,
[string]$project,
[string]$authHeader
)
$repoUrl = "https://dev.azure.com/$organization/$project/_apis/git/repositories/?api-version=7.1-preview.1"
return Invoke-RestMethod -Uri $repoUrl -Method Get -Headers @{Authorization = "Basic $authHeader"}
}
Some repositories may be disabled. This function mentioned below enables them before migration:
function Enable-Repository {
param (
[string]$organization,
[string]$project,
[string]$repoId,
[string]$authHeader
)
$updateRepoUrl = "https://dev.azure.com/$organization/$project/_apis/git/repositories/$repoId?api-version=7.1-preview.1"
$updateRepoBody = @{ isDisabled = $false } | ConvertTo-Json
try {
Invoke-RestMethod -Uri $updateRepoUrl -Method Patch -Body $updateRepoBody -Headers @{Authorization = "Basic $authHeader"} -ContentType "application/json"
Write-Host "Repository '$repoId' has been enabled."
} catch {
Write-Host "Failed to enable repository '$repoId': $_"
}
}
Before migrating, we check if the repository already exists in the destination project:
function Test-RepositoryExists {
param (
[string]$organization,
[string]$project,
[string]$repoName,
[string]$authHeader
)
$reposResponse = Get-Repositories -organization $organization -project $project -authHeader $authHeader
$repositoriesList = $reposResponse.value
return ($repositoriesList | Where-Object { $_.name -eq $repoName }).Count -ne 0
}
Once the migration is complete, this function deletes the repository from the source project:
function Delete-Repository {
param (
[string]$organization,
[string]$project,
[string]$repoId,
[string]$authHeader
)
$deleteRepoUrl = "https://dev.azure.com/$organization/$project/_apis/git/repositories/$repoId?api-version=7.1-preview.1"
try {
Invoke-RestMethod -Uri $deleteRepoUrl -Method Delete -Headers @{Authorization = "Basic $authHeader"}
Write-Host "Repository '$repoId' has been deleted from '$project'."
} catch {
Write-Host "Failed to delete repository '$repoId': $_"
}
}
This function creates a new repository in the target project, clones the source repo with git clone –mirror, adds a new remote URL, and pushes all branches and tags. After a successful push, it deletes the source repository:
function Move-DeprecatedRepository {
param (
[string]$organization,
[string]$projectFrom,
[string]$projectTo,
[string]$repoId,
[string]$repoName,
[string]$repoSshUrl,
[string]$authHeader
)
try {
$repoToUrl = "https://dev.azure.com/$organization/$projectTo/_apis/git/repositories?api-version=7.1-preview.1"
$repoToBody = @{ name = $repoName } | ConvertTo-Json
$repoToResponse = Invoke-RestMethod -Uri $repoToUrl -Method Post -Body $repoToBody -Headers @{Authorization = "Basic $authHeader"} -ContentType "application/json"
$newRepoUrl = $repoToResponse.sshUrl
Write-Host "Repository creation successful for '$repoName'."
# Clone, Add Remote, and Push
$localClonePath = "$scriptLocation\$repoName"
git clone --mirror $repoSshUrl $localClonePath
Set-Location -Path $localClonePath
git remote add new-origin $newRepoUrl
git push new-origin --all
git push new-origin --tags
# Clean up
Set-Location -Path ..
Remove-Item -Recurse -Force $localClonePath
Write-Host "Repository '$repoName' moved successfully."
# Delete original repository
Delete-Repository -organization $organization -project $projectFrom -repoId $repoId -authHeader $authHeader
return $true
} catch {
Write-Host "Error moving repository '$repoName': $_"
return $false
}
}
Now, let’s put it all together.
We’ll begin with authenticating and retrieving the list of repositories from the source Azure DevOps project. We continue to verify if there are any disabled repositories that need to be enabled, and then it continues and performs the migration. Then, we remove repositories with the word “Deprecated” in their names, which are the repositories that are eligible for migration.
The script finally copies the discovered repositories into the destination project, reporting on any failures it experiences during migration. This approach ensures a smooth transfer while keeping track of any issues that arise:
try {
$authHeader = Get-AuthHeader -token $personalAccessToken
$FromRepoResponse = Get-Repositories -organization $organizationName -project $projectNameFrom -authHeader $authHeader
$repositoriesList = $FromRepoResponse.value
# Enable disabled repositories
foreach ($repo in $repositoriesList | Where-Object { $_.IsDisabled -eq $true }) {
Enable-Repository -organization $organizationName -project $projectNameFrom -repoId $repo.id -authHeader $authHeader
}
# Move deprecated repositories
$failedRepositories = @()
foreach ($repo in $repositoriesList | Where-Object { $_.name -like "*Deprecated*" }) {
if (-not (Move-DeprecatedRepository -organization $organizationName -projectFrom $projectNameFrom -projectTo $projectNameTo -repoId $repo.id -repoName $repo.name -repoSshUrl $repo.sshUrl -authHeader $authHeader)) {
$failedRepositories += $repo.name
}
}
# Display failures
if ($failedRepositories.Count -gt 0) {
Write-Host "`nRepositories that failed to move:" $failedRepositories
}
} catch {
Write-Host "An error occurred: $_"
}
This PowerShell script automates repository migration across Azure DevOps projects. The modular design allows customization for different scenarios. This approach is efficient, reliable, and scalable for bulk repository transfers.
In this article, we have explored a number of methods to transfer projects from one organization to another organization. Moving an Azure DevOps project to a different organization is not a one-click activity, but with a good approach, we can transfer repositories, work items, and pipelines successfully. Whether we choose manual migration, open-source tools, or automation with the Azure DevOps REST API, careful planning and execution enable us to avoid data loss and minimize disruption.
By following best practices, workflow continuity is ensured, and CI/CD pipelines are preserved. Testing everything after the migration ensures that all problems are caught early enough, ensuring that the project operates as anticipated within the new company. This procedure requires effort, but selecting the right approach according to the complexity of the project and the resources available makes it achievable. With proper preparation, migrating to Azure DevOps projects can be done securely without dislocating significantly. As always, the complete code of section 5 is available over on GitHub.