functions/github/Invoke-GitHubRestMethod.ps1

function Invoke-GitHubRestMethod
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$True)]
        [uri] $Uri,

        [Parameter()]
        [string] $Verb = 'GET',

        [Parameter()]
        [hashtable] $Body,

        [Parameter()]
        [string] $Token = $env:GITHUB_TOKEN,

        [Parameter()]
        [hashtable] $Headers = @{},

        [Parameter()]
        [int[]] $HttpErrorStatusCodesToIgnore = @(),

        [Parameter()]
        [switch] $AllPages,

        [Parameter()]
        [int] $MaxRetries = 5,

        [Parameter()]
        [float] $RetryBackOffBaseFactor = 1.5,

        [Parameter()]
        [int] $InitialBackOffSeconds = 60

    )

    if ($Headers.Keys -notcontains 'Accept') {
        $Headers += @{ Accept = 'application/vnd.github.machine-man-preview+json' }
    }
    if ($Headers.Keys -notcontains 'Authorization') {
        $Headers += @{ Authorization = "Token $Token" }
    }
    if ($Headers.Keys -notcontains 'Content-Type') {
        $Headers += @{ "Content-Type" = "application/json" }
    }

    $succeeded = $false
    $retryCount = 0
    do {
        try {
            $resp = Invoke-RestMethod -Headers $Headers  `
                                    -Method $Verb `
                                    -Uri $Uri `
                                    -Body ($Body|ConvertTo-Json -Depth 100 -Compress) `
                                    -FollowRelLink:$AllPages

            $succeeded = $true
        }
        catch {
            if (
                    $_.Exception.Response.StatusCode -eq 429 -or `
                    ($_.Exception.Response.StatusCode -eq 403 -and $_.Exception.Message -match 'rate limit')
            ) {
                # Handle rate limit errors as per GitHub's API guidelines
                # ref: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28
                # 1. Check for 429 or 403 with 'rate limit' in the error message
                # 2. Check for 'Retry-After' header
                # 3. Check for 'X-RateLimit-Remaining' = 0, wait until 'X-RateLimit-Reset' header
                # 4. Wait for 60 seconds, expoenentially backing off for subsequent retries

                $retryAfter = $_.Exception.Response.Headers.Contains("Retry-After") ? ($_.Exception.Response.Headers.GetValues("Retry-After")[0]) : $null
                if ($retryAfter) {
                    Write-Host "Rate limit exceeded. Waiting for $retryAfter seconds..."
                }
                elseif (
                    $_.Exception.Response.Headers.Contains('X-RateLimit-Remaining') -and `
                    $_.Exception.Response.Headers.GetValues('X-RateLimit-Remaining')[0] -eq 0 -and `
                    $_.Exception.Response.Headers.Contains('X-RateLimit-Reset')
                ) {
                    $RateLimitReset = [datetime]::FromFileTimeUtc($_.Exception.Response.Headers.GetValues('X-RateLimit-Reset')[0])
                    $retryAfter = ($RateLimitReset - [datetime]::UtcNow).TotalSeconds
                    Write-Host "Rate limit exceeded. Waiting for quota reset in $retryAfter seconds..."
                }
                else {
                    # We have hit a secondary rate limit with no retry context
                    $retryAfter = [math]::Pow($RetryBackOffBaseFactor, $retryCount) * $InitialBackOffSeconds
                    Write-Host "Rate limit exceeded. Exponentional back-off, waiting for $retryAfter seconds..."
                }
                Start-Sleep -Seconds $retryAfter
            }
            elseif ($_.Exception.Response.StatusCode -notin $HttpErrorStatusCodesToIgnore) {
                throw $_
            }

            $retryCount++
        }
    }
    while (!$succeeded -and $retryCount -le $MaxRetries)

    # Flatten the result set in the event we traversed multiple pages
    $resp | ForEach-Object { $_ }
}