Private/Get-GitHubAPI.ps1

function Get-GitHubAPI {
    <#
    .SYNOPSIS
        GitHub REST API helper with authentication and pagination support.
    .DESCRIPTION
        Calls the GitHub REST API, handles authentication via personal access token,
        follows pagination via Link headers, and provides rate-limit awareness.
        Works without a token for public repos (60 req/hr vs 5000 req/hr with token).
    .PARAMETER Endpoint
        The API endpoint path (e.g., "/repos/{owner}/{repo}/issues").
    .PARAMETER Token
        GitHub personal access token. Falls back to $env:GITHUB_TOKEN if not provided.
    .PARAMETER Method
        HTTP method. Default is GET.
    .PARAMETER Body
        Hashtable to send as JSON body for POST/PATCH/PUT requests.
    .EXAMPLE
        Get-GitHubAPI -Endpoint "/repos/larro1991/AD-SecurityAudit/issues" -Token $token
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Endpoint,

        [Parameter()]
        [string]$Token,

        [Parameter()]
        [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
        [string]$Method = 'GET',

        [Parameter()]
        [hashtable]$Body
    )

    $baseUrl = 'https://api.github.com'
    $url = if ($Endpoint.StartsWith('http')) { $Endpoint } else { "$baseUrl$Endpoint" }

    # Build headers
    $headers = @{
        'Accept'               = 'application/vnd.github+json'
        'X-GitHub-Api-Version' = '2022-11-28'
        'User-Agent'           = 'GitHub-RepoWatch-PowerShell'
    }

    # Resolve token: parameter > environment variable
    $resolvedToken = if ($Token) { $Token } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { $null }
    if ($resolvedToken) {
        $headers['Authorization'] = "Bearer $resolvedToken"
    }

    $allResults = [System.Collections.Generic.List[object]]::new()
    $currentUrl = $url

    do {
        $nextUrl = $null

        $splat = @{
            Uri                = $currentUrl
            Method             = $Method
            Headers            = $headers
            ContentType        = 'application/json'
            UseBasicParsing    = $true
            ErrorAction        = 'Stop'
            ResponseHeadersVariable = 'responseHeaders'
        }

        if ($Body -and $Method -ne 'GET') {
            $splat['Body'] = ($Body | ConvertTo-Json -Depth 10)
        }

        try {
            $response = Invoke-RestMethod @splat

            # Check rate limit from response headers
            if ($responseHeaders -and $responseHeaders['X-RateLimit-Remaining']) {
                $remaining = [int]$responseHeaders['X-RateLimit-Remaining'][0]
                if ($remaining -le 5) {
                    $resetEpoch = [int]$responseHeaders['X-RateLimit-Reset'][0]
                    $resetTime = [System.DateTimeOffset]::FromUnixTimeSeconds($resetEpoch).LocalDateTime
                    $waitSeconds = [math]::Max(1, ($resetTime - (Get-Date)).TotalSeconds)
                    Write-Warning "GitHub API rate limit nearly exhausted ($remaining remaining). Waiting $([math]::Ceiling($waitSeconds)) seconds until reset."
                    Start-Sleep -Seconds ([math]::Ceiling($waitSeconds) + 1)
                }
            }

            # Collect results
            if ($response -is [array]) {
                foreach ($item in $response) {
                    $allResults.Add($item)
                }
            }
            else {
                $allResults.Add($response)
            }

            # Parse Link header for pagination (rel="next")
            if ($responseHeaders -and $responseHeaders['Link']) {
                $linkHeader = $responseHeaders['Link'][0]
                if ($linkHeader -match '<([^>]+)>;\s*rel="next"') {
                    $nextUrl = $Matches[1]
                }
            }

            $currentUrl = $nextUrl
        }
        catch {
            $statusCode = $null
            if ($_.Exception.Response) {
                $statusCode = [int]$_.Exception.Response.StatusCode
            }

            switch ($statusCode) {
                401 {
                    Write-Error "GitHub API authentication failed. Check your token. Endpoint: $currentUrl"
                    return
                }
                403 {
                    if ($_.Exception.Response.Headers -and $_.Exception.Response.Headers['X-RateLimit-Remaining']) {
                        Write-Error "GitHub API rate limit exceeded. Wait for reset or provide an authenticated token."
                    }
                    else {
                        Write-Error "GitHub API access forbidden for endpoint: $currentUrl"
                    }
                    return
                }
                404 {
                    Write-Verbose "GitHub API returned 404 for endpoint: $currentUrl (resource not found, returning empty)"
                    return @()
                }
                default {
                    Write-Error "GitHub API error calling $currentUrl : $_"
                    return
                }
            }
        }
    } while ($currentUrl)

    # If only one result and it was not an array response, return the single object
    if ($allResults.Count -eq 1 -and $response -isnot [array]) {
        return $allResults[0]
    }

    return $allResults.ToArray()
}