1. 引言

Azure DevOps(简称 ADO)是一个功能强大的软件开发管理平台。但在实际工作中,我们有时需要将一个项目从一个组织迁移到另一个组织。迁移的原因可能包括公司合并、组织结构调整、合规性要求或团队重组等。

⚠️ 但 Azure DevOps 并不直接支持一键迁移整个项目的功能,因此我们必须通过一些替代方法,确保仓库、工作项、流水线等资源能完整迁移。

本文将介绍几种常见的迁移方式,帮助你根据项目规模、数据复杂度和具体需求选择最合适的迁移策略。


2. 完整 Git 仓库迁移(镜像克隆法)

适用场景:项目规模较小,可接受手动操作。

该方法需要手动在目标组织中重新创建代码仓库、流水线和工作项,适用于数据量不大、结构不复杂的项目。

迁移 Git 仓库是关键步骤之一,我们可以使用 git clone --mirror 来克隆源仓库,然后推送到目标组织的新仓库中:

git clone --mirror https://dev.azure.com/{源组织}/{项目}/_git/{仓库}
cd {仓库}

然后创建目标仓库,并执行推送操作:

git push --mirror https://dev.azure.com/{目标组织}/{项目}/_git/{仓库}

⚠️ 该方式会迁移所有分支、标签等 Git 元数据,适合需要保留完整提交历史的场景。


3. 简单 Git 仓库迁移(断开重连法)

适用场景:项目仅包含 Git 仓库,没有使用看板、用户故事、任务或流水线。

这是一种更轻量级的迁移方式,适用于仅需迁移代码仓库的场景。操作流程如下:

  1. 克隆源仓库:
git clone https://dev.azure.com/{源组织}/{项目}/_git/{仓库}
cd {仓库}
  1. 移除远程配置:
git remote rm origin
  1. 创建目标仓库并关联远程:
git remote add origin https://dev.azure.com/{目标组织}/{项目}/_git/{仓库}
git push -u origin --all

⚠️ 该方法不会迁移工作项、看板、构建流水线等数据,仅适用于代码迁移。


4. 使用 Azure DevOps 迁移工具

适用场景:中大型项目,需要迁移工作项、构建定义等复杂数据。

对于数据量较大且结构复杂的项目,可以使用开源工具 Azure DevOps Migration Tools

该工具支持“整体迁移”,包括工作项、构建定义、测试计划等资源。安装方式如下(Windows 环境):

winget install nkdAgility.AzureDevOpsMigrationTools

初始化配置文件:

devopsmigration init --options Basic

生成的 configuration.json 文件中可以配置源和目标组织信息、认证方式、过滤规则等。例如:

{
  "MigrationTools": {
    "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"
        }
      }
    },
    "Processors": [
      {
        "ProcessorType": "TfsWorkItemMigrationProcessor",
        "Enabled": true,
        "WIQLQuery": "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan') ORDER BY [System.ChangedDate] desc"
      }
    ]
  }
}

迁移执行命令:

devopsmigration execute --config .\configuration.json

⚠️ 该工具虽功能强大,但不支持选择性迁移或结构转换,迁移前建议先做测试。


5. 使用 Azure DevOps REST API

适用场景:需要高度控制、自动化迁移大量数据。

通过 REST API 可以实现更灵活的迁移流程,适用于需要自动化脚本控制的场景。

下面是一个 PowerShell 脚本示例,演示如何通过 REST API 获取仓库、创建新仓库并迁移数据:

5.1 认证函数:Get-AuthHeader

function Get-AuthHeader {
    param (
        [string]$token
    )
    return [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($token)"))
}

5.2 获取仓库:Get-Repositories

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

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

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

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

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 主执行流程

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

✅ 该脚本结构清晰、可扩展性强,适合大规模仓库迁移任务。


6. 总结

Azure DevOps 的项目迁移不是一键操作,但通过本文介绍的几种方式,你可以根据项目复杂度选择合适的迁移策略:

方法 适用场景 优点 缺点
手动镜像克隆 小型项目,仅需代码迁移 简单直接 无法迁移工作项、流水线
断开重连法 仅需 Git 仓库迁移 操作简单 数据完整性差
开源工具迁移 中大型项目,需完整迁移 支持工作项、构建等迁移 不支持结构转换
REST API 脚本 高度定制化、自动化 灵活、可扩展 实现复杂

无论选择哪种方式,迁移前务必进行测试,确保工作项、构建、测试计划等资源完整无误,避免对业务造成影响。

完整脚本和工具配置可在 GitHub 上获取:GitHub 链接



原始标题:Migrating a Project to a Different Organization in Azure DevOps