Private/Find-AvmOpenPullRequests.ps1

function Find-AvmOpenPullRequests {
    <#
    .SYNOPSIS
        Lists open pull requests whose source/head branch starts with the given prefix.

    .DESCRIPTION
        Used to make PR creation idempotent: the caller matches the deterministic branch
        name for the current update set against this list to find a PR to refresh in place,
        and treats any other open PR under the same prefix as stale (same prefix, different
        content hash) to warn about instead of silently leaving duplicates behind.

    .PARAMETER ApprovalMode
        'github' or 'azuredevops'.

    .PARAMETER BranchPrefix
        Prefix shared by all AvmUpdater branches (e.g. 'avm-updates').

    .PARAMETER AzureDevOpsConfig
        Optional hashtable with organization/project/repository overrides, passed through to
        'az repos pr list' when set.

    .OUTPUTS
        Array of PSCustomObject with: number, url, branchName. Empty array if the relevant
        CLI is missing or the list operation fails (treated as "no known open PRs" rather
        than a terminating error, since this is best-effort discovery).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('github', 'azuredevops')]
        [string]$ApprovalMode,

        [Parameter(Mandatory)]
        [string]$BranchPrefix,

        [Parameter()]
        [hashtable]$AzureDevOpsConfig
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    switch ($ApprovalMode) {
        'github' {
            if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { return $results.ToArray() }

            $raw = gh pr list --state open --json number,url,headRefName --limit 200 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Verbose "gh pr list failed: $($raw -join ' ')"
                return $results.ToArray()
            }

            $parsed = @($raw | ConvertFrom-Json -ErrorAction SilentlyContinue)
            foreach ($pr in $parsed) {
                if ($pr.headRefName -like "$BranchPrefix/*") {
                    $results.Add([PSCustomObject]@{ number = $pr.number; url = $pr.url; branchName = $pr.headRefName })
                }
            }
        }
        'azuredevops' {
            if (-not (Get-Command az -ErrorAction SilentlyContinue)) { return $results.ToArray() }

            $argsList = @('repos', 'pr', 'list', '--status', 'active', '-o', 'json')
            if ($AzureDevOpsConfig) {
                if ($AzureDevOpsConfig.organization) { $argsList += @('--organization', $AzureDevOpsConfig.organization) }
                if ($AzureDevOpsConfig.project)      { $argsList += @('--project', $AzureDevOpsConfig.project) }
                if ($AzureDevOpsConfig.repository)   { $argsList += @('--repository', $AzureDevOpsConfig.repository) }
            }

            $raw = & az @argsList 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Verbose "az repos pr list failed: $($raw -join ' ')"
                return $results.ToArray()
            }

            $parsed = @($raw | ConvertFrom-Json -ErrorAction SilentlyContinue)
            foreach ($pr in $parsed) {
                $branchName = $pr.sourceRefName -replace '^refs/heads/', ''
                if ($branchName -like "$BranchPrefix/*") {
                    $url = if ($pr.PSObject.Properties['url']) { $pr.url } else { $null }
                    $results.Add([PSCustomObject]@{ number = $pr.pullRequestId; url = $url; branchName = $branchName })
                }
            }
        }
    }

    return $results.ToArray()
}