Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

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.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

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.

2. Complete Git Repository Migration (Mirror Clone Approach)

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.

3. Simple Git Repository Transfer (Detach and Reattach Approach)

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.

4. Using Azure DevOps Migration Tools

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:

  • Migrate from project migrationSource1 to migrationTest5
  • Use personal access tokens (PATs) for secure access
  • Exclude the “Test Suite” and “Test Plan” work items using a WIQL query

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.

5. Using Azure DevOps REST API

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.

5.1. Get-AuthHeader: Authentication Handling

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)"))
}

5.2. Get-Repositories: Retrieve Repositories from Azure DevOps

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"}
}

5.3. Enable-Repository: Activate Disabled Repositories

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': $_"
    }
}

5.4. Test-RepositoryExists: Check If Repository Exists in Target Project

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
}

5.5. Delete-Repository: Remove Repository from Source Project

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': $_"
    }
}

5.6. Move-DeprecatedRepository: Migrate Deprecated Repositories

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
    }
}

5.7. Main Script Execution

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.

6. Conclusion

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.