Templates/migrations/migration-tools/Sync.ps1

<#
.SYNOPSIS
    Runs this migration end-to-end in order: repositories, then pipelines,
    then artifact feeds, then work items.
 
.DESCRIPTION
    Fail-fast orchestrator: each step must succeed before the next runs. Pass
    -WhatIf to preview - it is forwarded to the Run-Migrate-*.ps1 steps and
    turns the devopsmigration.exe calls into print-only no-ops.
 
    Always run `.\Sync.ps1 -WhatIf` first.
 
.PARAMETER SkipArtifacts
    Forwarded to Run-Migrate-Artifacts.ps1: feed-only sync (feeds, upstreams,
    permissions) without moving packages.
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [switch]$SkipArtifacts
)

$ErrorActionPreference = 'Stop'
# Proven on the United-Machine engagement: strict mode turns a typo'd or unset
# variable into an error instead of a silent empty string mid-migration.
Set-StrictMode -Version Latest
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -Scope Global -ErrorAction SilentlyContinue) {
    $PSNativeCommandUseErrorActionPreference = $true
}

$here = $PSScriptRoot
. "$here\..\..\init.ps1"

# Load PATs from secrets\secrets.json into the environment: $ENV:AZDO_PAT_<ORG>
# as Entra fallbacks for the Run-Migrate-*.ps1 configs, plus the
# MigrationTools__...__AccessToken names that devopsmigration.exe binds over its
# JSON configs (devopsmigration.exe cannot use Entra, so those stay PAT-fed).
Set-AutomationSecrets | Out-Null

# Resolve devopsmigration.exe from PATH so a missing tool fails fast with an
# actionable message instead of a cryptic execution error mid-run.
$devopsMigration = Get-Command devopsmigration.exe -ErrorAction SilentlyContinue
if (-not $devopsMigration) {
    throw "devopsmigration.exe was not found on PATH. Install the Azure DevOps Migration Tools (https://devopsmigration.io) and ensure devopsmigration.exe is on PATH."
}

# devopsmigration.exe steps: label + config file (relative to this folder).
$pipelineSteps = @(
    @{ Label = 'Pipelines'; Config = 'configuration-pipelines.json' }
)
$workItemsConfig = 'configuration-workitems.json'

$stepNumber = 0
$totalSteps = $pipelineSteps.Count + 3

function Invoke-DevOpsMigration {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([string]$Label, [string]$ConfigFile)

    $script:stepNumber++
    $configPath = Join-Path $here $ConfigFile
    if (-not (Test-Path -LiteralPath $configPath)) {
        throw "Configuration file not found: $configPath"
    }

    Write-Host ("==> [{0}/{1}] devopsmigration: {2}" -f $stepNumber, $totalSteps, $Label) -ForegroundColor Cyan
    if (-not $PSCmdlet.ShouldProcess($ConfigFile, 'devopsmigration.exe execute')) {
        return
    }
    & $devopsMigration.Source execute --config $configPath
    if ($LASTEXITCODE -ne 0) {
        throw "devopsmigration.exe failed (exit $LASTEXITCODE) for config '$ConfigFile'."
    }
}

function Invoke-RunScript {
    param([string]$Label, [string]$ScriptName, [hashtable]$Arguments = @{})

    $script:stepNumber++
    Write-Host ("==> [{0}/{1}] {2}" -f $stepNumber, $totalSteps, $Label) -ForegroundColor Cyan
    $scriptPath = Join-Path $here $ScriptName
    if (-not (Test-Path -LiteralPath $scriptPath)) {
        throw "Step script not found: $scriptPath"
    }
    & $scriptPath @Arguments
}

try {
    $runArgs = @{}
    if ($WhatIfPreference) { $runArgs['WhatIf'] = $true }

    # 1. Repositories.
    Invoke-RunScript -Label 'Repositories (Run-Migrate-Repos.ps1)' -ScriptName 'Run-Migrate-Repos.ps1' -Arguments $runArgs

    # 2. Pipelines via devopsmigration.exe.
    foreach ($step in $pipelineSteps) {
        Invoke-DevOpsMigration -Label $step.Label -ConfigFile $step.Config
    }

    # 3. Artifact feeds (and packages, unless -SkipArtifacts).
    $artifactArgs = @{} + $runArgs
    if ($SkipArtifacts) { $artifactArgs['SkipArtifacts'] = $true }
    Invoke-RunScript -Label 'Artifacts (Run-Migrate-Artifacts.ps1)' -ScriptName 'Run-Migrate-Artifacts.ps1' -Arguments $artifactArgs

    # 4. Work items last, after repos, pipelines and artifacts.
    Invoke-DevOpsMigration -Label 'Work items' -ConfigFile $workItemsConfig

    Write-Host '==> Sync complete.' -ForegroundColor Green
}
catch {
    Write-Host ("==> Sync FAILED at step {0}/{1}: {2}" -f $stepNumber, $totalSteps, $_.Exception.Message) -ForegroundColor Red
    throw
}