Private/Invoke-AvmGitHubRequest.ps1
|
function Get-AvmGitHubAuthHeaders { <# .SYNOPSIS Builds HTTP headers for GitHub API/raw-content requests. .DESCRIPTION Always sets a User-Agent (required by the GitHub API). When $env:GITHUB_TOKEN or $env:GH_TOKEN is set (GITHUB_TOKEN takes precedence), adds an 'Authorization: Bearer <token>' header so requests count against the authenticated rate limit (5,000/hour) instead of the unauthenticated one (60/hour). The token value itself is never written to output, verbose, or warning streams. .OUTPUTS Hashtable of headers. #> [CmdletBinding()] [OutputType([hashtable])] param() $headers = @{ 'User-Agent' = 'AvmUpdater' } $token = if ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } elseif ($env:GH_TOKEN) { $env:GH_TOKEN } else { $null } if ($token) { $headers['Authorization'] = "Bearer $token" } return $headers } function Invoke-AvmGitHubRequest { <# .SYNOPSIS Fetches a URL from GitHub (raw content or REST API) with auth headers, an in-memory content cache, and a single rate-limit warning per run. .DESCRIPTION Wraps Invoke-RestMethod for all GitHub requests made by Get-AvmInterfaceDiff and Get-AvmChangelog: - Sends an Authorization: Bearer header when $env:GITHUB_TOKEN/$env:GH_TOKEN is set (see Get-AvmGitHubAuthHeaders). Never logs the token. - Caches successful responses in $script:_AvmGitHubContentCache keyed by the request URI (which already encodes repo path + version/ref), so the same module referenced from many files in a scan is only fetched once — mirrors the pattern of $script:_AvmVersionCache in Get-AvmLatestVersion. - On failure, never throws. Returns $null so callers treat it the same as "content unavailable". If the failure looks like a GitHub rate-limit response, emits ONE Write-Warning for the whole run (tracked via $script:_AvmGitHubRateLimitWarned) instead of one per module/call. .PARAMETER Uri The URL to fetch (raw.githubusercontent.com or api.github.com). .PARAMETER TimeoutSec Per-request timeout in seconds. Defaults to 15. .OUTPUTS The response body (string or deserialized object), or $null on failure. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Uri, [Parameter()] [int]$TimeoutSec = 15 ) if ($script:_AvmGitHubContentCache.ContainsKey($Uri)) { Write-Verbose "GitHub content cache hit: $Uri" return $script:_AvmGitHubContentCache[$Uri] } $headers = Get-AvmGitHubAuthHeaders try { $content = Invoke-RestMethod -Uri $Uri -Headers $headers -TimeoutSec $TimeoutSec -ErrorAction Stop } catch { if (Test-AvmGitHubRateLimitError -ErrorRecord $_) { if (-not $script:_AvmGitHubRateLimitWarned) { Write-Warning 'GitHub rate limit hit — set GITHUB_TOKEN to raise the limit; risk tiers may degrade to UNKNOWN' $script:_AvmGitHubRateLimitWarned = $true } } else { Write-Verbose "GitHub request failed for ${Uri}: $_" } return $null } $script:_AvmGitHubContentCache[$Uri] = $content return $content } function Test-AvmGitHubRateLimitError { <# .SYNOPSIS Determines whether a caught error from Invoke-RestMethod represents a GitHub rate-limit (403) response. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)]$ErrorRecord) $statusCode = $null try { $statusCode = [int]$ErrorRecord.Exception.Response.StatusCode } catch { # Response/StatusCode may not exist on this exception type — fall back to $null # and rely on the message-based checks below. $statusCode = $null } $exceptionMessage = try { $ErrorRecord.Exception.Message } catch { $null } $errorDetailsMessage = try { $ErrorRecord.ErrorDetails.Message } catch { $null } $message = "$exceptionMessage $errorDetailsMessage" if ($message -match '(?i)rate limit') { return $true } if ($statusCode -eq 403 -and $message -match '(?i)forbidden|limit') { return $true } return $false } |