Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoBranchPolicy/New-AzDoBranchPolicy.ps1

<#
.SYNOPSIS
Creates an Azure DevOps branch policy.
 
.DESCRIPTION
Resolves the project, (optionally) the repository and the policy type, then creates the policy
configuration with a scope built from RepositoryName/BranchName/MatchKind - unless the
configuration already supplied its own 'scope' key inside PolicySettings, which is used verbatim.
 
RepositoryName empty builds a cross-repository scope (no repositoryId); BranchName empty builds a
repository-wide scope (no refName) - see the AzDoBranchPolicy class help for when each applies.
MatchKind 'Prefix' scopes to every branch whose ref name starts with BranchName rather than to one
branch exactly.
 
.PARAMETER ProjectName
The Azure DevOps project name.
 
.PARAMETER RepositoryName
The Git repository name. Leave empty for a cross-repository policy scope.
 
.PARAMETER BranchName
The branch ref name (with or without the 'refs/heads/' prefix), or a branch-name prefix when
MatchKind is 'Prefix'. Leave empty for a repository-wide policy scope.
 
.PARAMETER PolicyType
The policy type display name or short alias (e.g. 'MinimumReviewerCount').
 
.PARAMETER PolicyIdentifier
Optional. Not used to build the policy itself - carried through so it is available on
$LookupResult for the round-trip Get() that follows. See Get-AzDoBranchPolicy.
 
.PARAMETER MatchKind
Optional. 'Exact' (default) or 'Prefix'. See BranchName above.
 
.PARAMETER isEnabled
Whether the policy should be enabled.
 
.PARAMETER isBlocking
Whether the policy should be blocking.
 
.PARAMETER PolicySettings
Policy-type-specific settings hashtable.
 
.EXAMPLE
New-AzDoBranchPolicy -ProjectName 'Fabrikam' -RepositoryName 'FabrikamFiber' -BranchName 'main' -PolicyType 'MinimumReviewerCount' -PolicySettings @{ minimumApproverCount = 2 }
 
.EXAMPLE
New-AzDoBranchPolicy -ProjectName 'Fabrikam' -RepositoryName 'FabrikamFiber' -BranchName 'release/' -MatchKind 'Prefix' -PolicyType 'MinimumReviewerCount' -PolicySettings @{ minimumApproverCount = 2 }
#>

Function New-AzDoBranchPolicy
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)][string]$ProjectName,
        [Parameter()][string]$RepositoryName = '',
        [Parameter()][string]$BranchName = '',
        [Parameter(Mandatory = $true)][string]$PolicyType,
        [Parameter()][string]$PolicyIdentifier,
        [Parameter()][ValidateSet('Exact', 'Prefix')][string]$MatchKind = 'Exact',
        [Parameter()][bool]$isEnabled = $true,
        [Parameter()][bool]$isBlocking = $true,
        [Parameter()][HashTable]$PolicySettings,
        [Parameter()][HashTable]$LookupResult,
        [Parameter()][Ensure]$Ensure,
        [Parameter()][System.Management.Automation.SwitchParameter]$Force
    )

    Write-Verbose "[New-AzDoBranchPolicy] Creating branch policy '$PolicyType' on '$BranchName'."

    $OrgName    = Get-AzDoOrganizationName
    $project    = Get-CacheItem -Key $ProjectName -Type 'LiveProjects'
    if (-not $project)
    {
        Write-Verbose "[New-AzDoBranchPolicy] Project '$ProjectName' not in cache — falling back to live API lookup."
        $project = Invoke-AzDevOpsApiRestMethod -Uri "https://dev.azure.com/$OrgName/_apis/projects/${ProjectName}?api-version=7.1-preview.4" -Method Get
        if ($project) { Add-CacheItem -Key $ProjectName -Value $project -Type 'LiveProjects' }
    }

    $repository    = $null
    $hasRepository = -not [string]::IsNullOrWhiteSpace($RepositoryName)

    if ($hasRepository)
    {
        $repoCacheKey = '{0}\{1}' -f $ProjectName, $RepositoryName
        $repository   = Get-CacheItem -Key $repoCacheKey -Type 'LiveRepositories'
        if (-not $repository)
        {
            Write-Verbose "[New-AzDoBranchPolicy] Repository '$RepositoryName' not in cache — falling back to live API lookup."
            $allRepos   = Invoke-AzDevOpsApiRestMethod -Uri "https://dev.azure.com/$OrgName/$ProjectName/_apis/git/repositories?api-version=7.1-preview.1" -Method Get
            $repository = $allRepos.value | Where-Object { $_.name -eq $RepositoryName } | Select-Object -First 1
            if ($repository) { Add-CacheItem -Key $repoCacheKey -Value $repository -Type 'LiveRepositories' }
        }
    }

    if ((-not $project) -or ($hasRepository -and (-not $repository)))
    {
        Write-Error "[New-AzDoBranchPolicy] Project or Repository not found in cache."
        return
    }

    # Build the scope/settings for the policy. A configuration that already supplied its own
    # 'scope' key (an advanced/manual scope) is respected as-is and nothing here is overridden.
    $settings = if ($PolicySettings) { $PolicySettings } else { @{} }
    if (-not $settings.ContainsKey('scope'))
    {
        $scopeEntry = @{ matchKind = $MatchKind.ToLowerInvariant() }

        if ($repository) { $scopeEntry.repositoryId = $repository.id }

        if (-not [string]::IsNullOrWhiteSpace($BranchName))
        {
            $scopeEntry.refName = Format-AzDoBranchRefName -BranchName $BranchName
        }

        $settings['scope'] = @($scopeEntry)
    }

    # Look up the policy type by name, falling back to a live API call if not cached
    $policyTypeObj = Get-CacheItem -Key ('{0}\{1}' -f $ProjectName, $PolicyType) -Type 'LivePolicyTypes'

    if (-not $policyTypeObj)
    {
        Write-Verbose "[New-AzDoBranchPolicy] PolicyType '$PolicyType' not in cache, querying API."
        $orgUri = 'https://dev.azure.com/{0}/' -f $OrgName
        # Map camelCase/short names to actual Azure DevOps display names returned by the API
        $policyDisplayNameAliases = @{
            'MinimumReviewerCount' = 'Minimum number of reviewers'
            'BuildValidation'      = 'Build'
            'CommentRequirements'  = 'Comment requirements'
            'WorkItemLinking'      = 'Work item linking'
            'MergeStrategy'        = 'Require a merge strategy'
            'StatusCheck'          = 'Status'
        }
        $lookupName = if ($policyDisplayNameAliases.ContainsKey($PolicyType)) { $policyDisplayNameAliases[$PolicyType] } else { $PolicyType }
        $policyTypes = List-DevOpsPolicyTypes -ApiUri $orgUri -ProjectName $ProjectName
        $policyTypeObj = $policyTypes | Where-Object { $_.displayName -eq $lookupName }
        # Also store using both the short name and the actual display name as keys
        if ($policyTypeObj)
        {
            foreach ($pt in $policyTypes)
            {
                Add-CacheItem -Key ('{0}\{1}' -f $ProjectName, $pt.displayName) -Value $pt -Type 'LivePolicyTypes' -SuppressWarning
            }
        }
    }

    # Fall back to well-known policy type GUIDs (Azure DevOps built-in types)
    if (-not $policyTypeObj)
    {
        $wellKnownTypeIds = @{
            'MinimumReviewerCount' = 'fa4e907d-c16b-452d-8106-7efa0cb84489'
            'BuildValidation'      = '0609b952-1397-4640-95ec-e00a01b2f659'
            'CommentRequirements'  = 'c6a1889d-b943-4856-b76f-9e46bb6b0df3'
            'WorkItemLinking'      = '40e92b44-2fe1-4dd6-b3d8-74a9c21d0c6e'
        }
        if ($wellKnownTypeIds.ContainsKey($PolicyType))
        {
            $policyTypeObj = [PSCustomObject]@{ id = $wellKnownTypeIds[$PolicyType]; displayName = $PolicyType }
        }
    }

    if (-not $policyTypeObj)
    {
        Write-Error "[New-AzDoBranchPolicy] PolicyType '$PolicyType' not found in cache, API, or well-known types."
        return
    }

    $params = @{
        ApiUri       = 'https://dev.azure.com/{0}/' -f $OrgName
        ProjectName  = $ProjectName
        PolicyTypeId = $policyTypeObj.id
        IsEnabled    = $isEnabled
        IsBlocking   = $isBlocking
        Settings     = $settings
    }

    $value = New-DevOpsBranchPolicy @params

    if ($null -eq $value)
    {
        Write-Error "[New-AzDoBranchPolicy] New-DevOpsBranchPolicy returned null. Check authentication token and organization settings."
        return
    }

    $cacheKeyParts = @($ProjectName, $RepositoryName, $BranchName, $PolicyType)
    if (-not [string]::IsNullOrWhiteSpace($PolicyIdentifier)) { $cacheKeyParts += $PolicyIdentifier }
    if ($MatchKind -ne 'Exact') { $cacheKeyParts += $MatchKind }
    $cacheKey = $cacheKeyParts -join '\'

    Add-CacheItem -Key $cacheKey -Value $value -Type 'LiveBranchPolicies'
    Export-CacheObject -CacheType 'LiveBranchPolicies' -Content $AzDoLiveBranchPolicies
    Refresh-CacheObject -CacheType 'LiveBranchPolicies'
    Write-Verbose "[New-AzDoBranchPolicy] Branch policy created."
}