Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoWIPTagHygiene/Set-AzDoWIPTagHygiene.ps1

<#
.SYNOPSIS
Applies the tag corrections reported by Get, when the configuration asks for them.
 
.DESCRIPTION
Merges each misaligned tag into its canonical name by renaming it. Azure DevOps merges a tag
into an existing one on rename, re-tagging every affected work item, so each correction is a
single call.
 
Under the default RemediationAction of 'Report' this function changes nothing and writes a
warning per misalignment instead. A tag merge is irreversible and project-wide, so applying one
is opt-in rather than the default.
 
Corrections are addressed by tag id rather than by name: a chain of merges removes names as it
goes, and a later rename addressed by name would fail once its target had already been merged.
 
.PARAMETER ProjectName
The name of the Azure DevOps project.
 
.PARAMETER CanonicalTags
The approved tag vocabulary.
 
.PARAMETER Aliases
Explicit From/To mappings.
 
.PARAMETER MatchStrategy
'Exact', 'Fuzzy' or 'Both'.
 
.PARAMETER SimilarityThreshold
The minimum normalized similarity for a fuzzy match.
 
.PARAMETER MinimumTagLength
Tags shorter than this are never fuzzy-matched.
 
.PARAMETER ExcludedTags
Tags that are never touched.
 
.PARAMETER RemediationAction
'Report' reports without changing anything. 'Merge' applies the corrections.
 
.PARAMETER MaxAutoCorrections
The safety cap on how many merges may be applied in one run.
 
.PARAMETER LookupResult
The lookup result from Get, supplied by the DSC base class.
 
.PARAMETER Ensure
The desired state, supplied by the DSC base class.
 
.PARAMETER Force
Forces the operation, supplied by the DSC base class.
 
.EXAMPLE
Set-AzDoWIPTagHygiene -ProjectName 'Contoso' -CanonicalTags @('Bug') -RemediationAction 'Merge'
#>

Function Set-AzDoWIPTagHygiene
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Name')]
        [System.String]$ProjectName,

        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [Alias('WITTagList')]
        [System.String[]]$CanonicalTags,

        [Parameter()]
        [AllowEmptyCollection()]
        [HashTable[]]$Aliases,

        [Parameter()]
        [System.String]$MatchStrategy = 'Exact',

        [Parameter()]
        [System.Int32]$SimilarityThreshold = 85,

        [Parameter()]
        [System.Int32]$MinimumTagLength = 5,

        [Parameter()]
        [AllowEmptyCollection()]
        [System.String[]]$ExcludedTags,

        [Parameter()]
        [System.String]$RemediationAction = 'Report',

        [Parameter()]
        [System.Int32]$MaxAutoCorrections = 25,

        [Parameter()]
        [HashTable]$LookupResult,

        [Parameter()]
        [Ensure]$Ensure,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]$Force
    )

    Write-Verbose "[Set-AzDoWIPTagHygiene] Started."

    # Get reports the cap breach as an error state, but the base class still routes an error
    # state here - so the refusal has to be repeated rather than assumed.
    if ($LookupResult.reason -eq 'MaxAutoCorrectionsExceeded')
    {
        Write-Error "[Set-AzDoWIPTagHygiene] Refusing to merge tags in project '$ProjectName': the number of misalignments exceeds MaxAutoCorrections ($MaxAutoCorrections)."
        return
    }

    $misalignments = @($LookupResult.propertiesChanged)

    if ($misalignments.Count -eq 0)
    {
        Write-Verbose "[Set-AzDoWIPTagHygiene] No misaligned tags to correct in project '$ProjectName'."
        return
    }

    if ($RemediationAction -ne 'Merge')
    {
        Write-Warning "[Set-AzDoWIPTagHygiene] $($misalignments.Count) misaligned tag(s) found in project '$ProjectName'. RemediationAction is '$RemediationAction', so no changes have been made."

        foreach ($misalignment in $misalignments)
        {
            Write-Warning "[Set-AzDoWIPTagHygiene] '$($misalignment.From)' should be '$($misalignment.To)' ($($misalignment.Reason), score $($misalignment.Score))."
        }

        Write-Warning "[Set-AzDoWIPTagHygiene] Set RemediationAction to 'Merge' to apply these corrections. Note that a tag merge cannot be undone."
        return
    }

    $organization = Get-AzDoOrganizationName
    $merged = 0

    foreach ($misalignment in $misalignments)
    {
        if ([String]::IsNullOrWhiteSpace($misalignment.TagId))
        {
            Write-Warning "[Set-AzDoWIPTagHygiene] No tag id was resolved for '$($misalignment.From)'. Skipping it rather than renaming by a name that may already have been merged away."
            continue
        }

        Write-Verbose "[Set-AzDoWIPTagHygiene] Merging '$($misalignment.From)' into '$($misalignment.To)'."

        $updated = Update-WITTags -Organization $organization -ProjectName $ProjectName `
            -TagId $misalignment.TagId -NewName $misalignment.To

        if ($null -eq $updated)
        {
            Write-Error "[Set-AzDoWIPTagHygiene] Failed to merge '$($misalignment.From)' into '$($misalignment.To)' in project '$ProjectName'."
            continue
        }

        $merged++
    }

    Write-Verbose "[Set-AzDoWIPTagHygiene] Merged $merged of $($misalignments.Count) misaligned tag(s) in project '$ProjectName'."
}