functions/invoke-adoworkitemstatemigration.ps1


<#
    .SYNOPSIS
        Migrates states for each inherited WIT.
    .DESCRIPTION
        Migrates states assigned to each inherited WIT in process.
        This includes copying states from the source WITs to the target WITs, ensuring that all customizations are preserved.
        Additionally, it builds an automatic state mapping to facilitate the migration of work items by mapping source states to the most appropriate target states based on name and category.
    .PARAMETER SourceOrganization
        The name of the source Azure DevOps organization.
    .PARAMETER TargetOrganization
        The name of the target Azure DevOps organization.
    .PARAMETER SourceToken
        The authentication token for accessing the source Azure DevOps organization.
    .PARAMETER TargetToken
        The authentication token for accessing the target Azure DevOps organization.
    .PARAMETER SourceProcess
        The source process object containing details about the process to migrate.
    .PARAMETER TargetProcess
        The target process object containing details about the process to migrate to.
    .PARAMETER SourceWitList
        The list of source work item types to migrate.
    .PARAMETER TargetWitList
        The list of target work item types to migrate to.
    .PARAMETER ApiVersion
        The version of the Azure DevOps REST API to use.
    .EXAMPLE
        $apiVersion = '7.1'
        $sourceOrg = 'srcOrg'
        $targetOrg = 'tgtOrg'
        $sourceToken = 'pat-src'
        $targetToken = 'pat-tgt'
        $sourceProjectName = 'Sample'
        $sourceProjectMeta = (Get-ADOProjectList -Organization $sourceOrg -Token $sourceToken -ApiVersion $apiVersion -StateFilter All) | Where-Object name -eq $sourceProjectName
        $sourceProject = Get-ADOProject -Organization $sourceOrg -Token $sourceToken -ProjectId $sourceProjectMeta.id -IncludeCapabilities -ApiVersion $apiVersion
        $proc = Invoke-ADOProcessMigration -SourceOrganization $sourceOrg -TargetOrganization $targetOrg -SourceToken $sourceToken -TargetToken $targetToken -SourceProject $sourceProject -ApiVersion $apiVersion
        $witResult = Invoke-ADOWorkItemTypeMigration -SourceOrganization $sourceOrg -TargetOrganization $targetOrg -SourceToken $sourceToken -TargetToken $targetToken -SourceProcess $proc.SourceProcess -TargetProcess $proc.TargetProcess -ApiVersion $apiVersion
         
        Invoke-ADOWorkItemStateMigration -SourceOrganization $sourceOrg -TargetOrganization $targetOrg `
            -SourceToken $sourceToken -TargetToken $targetToken -SourceProcess $proc.SourceProcess -TargetProcess $proc.TargetProcess `
            -SourceWitList $witResult.SourceList -TargetWitList $witResult.TargetList -ApiVersion $apiVersion
        # Migrates custom states and builds auto state mapping.
    .NOTES
        This function is part of the ADO Tools module and adheres to the conventions used in the module for logging, error handling, and API interaction.
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Invoke-ADOWorkItemStateMigration {
    [CmdletBinding()] param(
        [Parameter(Mandatory)][string]$SourceOrganization,
        [Parameter(Mandatory)][string]$TargetOrganization,
        [Parameter(Mandatory)][string]$SourceToken,
        [Parameter(Mandatory)][string]$TargetToken,
        [Parameter(Mandatory)][pscustomobject]$SourceProcess,
        [Parameter(Mandatory)][pscustomobject]$TargetProcess,
        [Parameter(Mandatory)][System.Collections.IEnumerable]$SourceWitList,
        [Parameter(Mandatory)][System.Collections.IEnumerable]$TargetWitList,
        [Parameter(Mandatory)][string]$ApiVersion
    )
    Convert-FSCPSTextToAscii -Text "Migrate states.." -Font "Standard"
    Write-PSFMessage -Level Host -Message "Starting to process states."
    if (-not $script:ADOStateAutoMap) { $script:ADOStateAutoMap = @{} }
    foreach ($wit in $SourceWitList) {
        Write-PSFMessage -Level Host -Message "Processing states for WIT '$($wit.name)'."
        $targetWit = $TargetWitList.Where({$_.name -eq $wit.name})
        if (-not $targetWit) { continue }
        $sourceStates = Get-ADOWorkItemTypeStateList -Organization $SourceOrganization -Token $SourceToken -ApiVersion $ApiVersion -ProcessId $SourceProcess.typeId -WitRefName $wit.referenceName
        $targetStates = Get-ADOWorkItemTypeStateList -Organization $TargetOrganization -Token $TargetToken -ApiVersion $ApiVersion -ProcessId $TargetProcess.typeId -WitRefName $targetWit.referenceName
        foreach ($state in $sourceStates) {
            Write-PSFMessage -Level Host -Message "Checking state '$($state.name)'."
            $existing = $targetStates.Where({$_.name -eq $state.name})
            $sourceState = Get-ADOWorkItemTypeState -Organization $SourceOrganization -Token $SourceToken -ApiVersion $ApiVersion -ProcessId $SourceProcess.typeId -WitRefName $wit.referenceName -StateId $state.id
            if (-not $existing) {
                Write-PSFMessage -Level Host -Message "State '$($state.name)' does not exist. Adding."
                $body = @{ name=$sourceState.name; color=$sourceState.color; stateCategory=$sourceState.stateCategory; order=$sourceState.order } | ConvertTo-Json -Depth 10
                Write-PSFMessage -Level Verbose -Message "Adding state '$($sourceState.name)' to target process '$($TargetProcess.name)' with body: $body"
                $new = Add-ADOWorkItemTypeState -Organization $TargetOrganization -Token $TargetToken -ApiVersion $ApiVersion -ProcessId $TargetProcess.typeId -WitRefName $targetWit.referenceName -Body $body
            } else { $new = $existing }
            if ($sourceState.hidden -and $sourceState.customizationType -eq "system") {
                try {
                    $null = Hide-ADOWorkItemTypeState -Organization $TargetOrganization -Token $TargetToken -ApiVersion $ApiVersion -ProcessId $TargetProcess.typeId -WitRefName $targetWit.referenceName -StateId $new.id -Hidden 'true' -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
                } catch {
                    Write-PSFMessage -Level Warning -Message "Failed to hide state '$($sourceState.name)' (possibly already hidden)."
                }
            }
        }
        $targetStates = Get-ADOWorkItemTypeStateList -Organization $TargetOrganization -Token $TargetToken -ApiVersion $ApiVersion -ProcessId $TargetProcess.typeId -WitRefName $targetWit.referenceName
        $mapKeyPrefix = $wit.name + '|'
        $visibleTarget = $targetStates | Where-Object { -not $_.hidden }
        foreach ($s in $sourceStates) {
            $candidate = $null
            $exact = $visibleTarget | Where-Object { $_.name -eq $s.name }
            if ($exact) { $candidate = $exact | Select-Object -First 1 }
            else {
                if ($s.stateCategory) {
                    $catMatches = $visibleTarget | Where-Object { $_.stateCategory -eq $s.stateCategory }
                    if ($catMatches) { $candidate = ($catMatches | Sort-Object order | Select-Object -First 1) }
                }
                if (-not $candidate -and $visibleTarget) { $candidate = ($visibleTarget | Sort-Object order | Select-Object -First 1) }
            }
            if ($candidate) {
                $script:ADOStateAutoMap[$mapKeyPrefix + $s.name] = $candidate.name
            }
        }
        $stateMapDisplay = ($sourceStates | ForEach-Object { $_.name }) | ForEach-Object { $_ + '->' + ($script:ADOStateAutoMap[$mapKeyPrefix + $_]) }
        $stateMapJoined  = $stateMapDisplay -join '; '
        Write-PSFMessage -Level Verbose -Message ("State mapping for '{0}': {1}" -f $wit.name, $stateMapJoined)
    }
}