Public/GitMigration/Get-GitHubAccessToken.ps1
|
function Get-GitHubAccessToken { <# .SYNOPSIS Resolves a GitHub token: the signed-in GitHub CLI first, then GITHUB_TOKEN. .DESCRIPTION The GitHub counterpart of Get-AzureDevOpsAccessToken: ambient identity first, stored token as the fallback. Resolution order: 1. The GitHub CLI ('gh auth token') when gh is on PATH and signed in - the interactive-identity default, so day-to-day runs need no stored token at all. 2. The GITHUB_TOKEN environment variable (exported from secrets.json by Set-AutomationSecrets). With -Org, each candidate is validated against the organisation before it is returned, and an unusable one is skipped with a warning instead of being handed to the caller. This matters for SAML/Entra-SSO organisations: the gh CLI's OAuth token only works while the user has an ACTIVE SSO session with the org, so a token that worked an hour ago can answer 403 'Resource protected by organization SAML enforcement' now - in which case an SSO-authorised PAT in GITHUB_TOKEN quietly takes over. Throws with guidance when nothing usable remains. The returned token works for both the REST API (Bearer header) and git-over-HTTPS (Basic, user 'x-access-token'). Never log or echo the returned token. .PARAMETER Org Organisation to validate candidates against (GET /orgs/<org> must succeed). Omit to return the first credential found, unvalidated. .EXAMPLE $token = Get-GitHubAccessToken -Org 'CompuCal-Solutions' #> [CmdletBinding()] param( [string]$Org ) $candidates = [System.Collections.Generic.List[object]]::new() if (Get-Command gh -ErrorAction SilentlyContinue) { # A signed-out gh exits non-zero; that is the fallback path, not an error. $PSNativeCommandUseErrorActionPreference = $false $token = $null try { $token = @(& gh auth token 2>$null) | Select-Object -First 1 } catch { $token = $null } if ($LASTEXITCODE -ne 0) { $token = $null } if (-not [string]::IsNullOrWhiteSpace($token)) { $candidates.Add(@{ Source = 'the gh CLI'; Token = $token }) } } if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { $candidates.Add(@{ Source = 'GITHUB_TOKEN'; Token = $env:GITHUB_TOKEN }) } $lastError = $null foreach ($candidate in $candidates) { if (-not $Org) { Write-DebugLog "GitHub auth: token from {source}" -PropertyValues $candidate.Source return $candidate.Token } try { $null = Invoke-GitHubApi -Path ('orgs/{0}' -f [uri]::EscapeDataString($Org)) -Token $candidate.Token Write-DebugLog "GitHub auth: token from {source}, validated against {org}" -PropertyValues $candidate.Source, $Org return $candidate.Token } catch { $lastError = $_.Exception.Message Write-Warning ("The GitHub credential from {0} cannot access '{1}'; trying the next credential. ({2})" -f ` $candidate.Source, $Org, (($lastError -split "`n")[0])) } } $guidance = "No usable GitHub credential. Sign in with 'gh auth login', or set GITHUB_TOKEN (add the token to secrets\secrets.json with EnvVars ['GITHUB_TOKEN'] and run Set-AutomationSecrets)." if ($Org) { $guidance += " If '$Org' enforces SAML/Entra SSO: refresh your session at https://github.com/orgs/$Org/sso, or use a classic PAT authorised for the org (token settings -> Configure SSO -> Authorize) - unlike the gh CLI's OAuth token, an authorised PAT does not need an active SSO session." } if ($lastError) { $guidance += "`nLast error: $lastError" } throw $guidance } |