microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
eng/common/scripts/login-to-github.ps1
198lines · modecode
| 1 | <# |
| 2 | .SYNOPSIS |
| 3 | Mints a GitHub App installation access token using Azure Key Vault 'sign' (non-exportable key), |
| 4 | and logs in the GitHub CLI by setting GH_TOKEN. |
| 5 | |
| 6 | Works in both Azure DevOps pipelines and GitHub Actions workflows. |
| 7 | Requires Azure CLI to be pre-authenticated (via AzureCLI@2 in ADO, or azure/login in GH Actions). |
| 8 | |
| 9 | .PARAMETER KeyVaultName |
| 10 | Name of the Azure Key Vault containing the non-exportable RSA key. |
| 11 | |
| 12 | .PARAMETER KeyName |
| 13 | Name of the RSA key in Key Vault (imported as a *key*, not a secret). |
| 14 | |
| 15 | .PARAMETER GitHubAppId |
| 16 | Numeric App ID (not client ID) of your GitHub App. |
| 17 | |
| 18 | .PARAMETER InstallationTokenOwners |
| 19 | List of GitHub organizations or users for which to obtain installation tokens. |
| 20 | |
| 21 | .PARAMETER VariableNamePrefix |
| 22 | Prefix for the exported variable name (default: GH_TOKEN). |
| 23 | With a single owner, exports as GH_TOKEN. With multiple owners, exports as GH_TOKEN_<Owner>. |
| 24 | |
| 25 | .OUTPUTS |
| 26 | Sets environment variables in the current process and exports them to the CI system: |
| 27 | - Azure DevOps: sets secret pipeline variables via ##vso logging commands |
| 28 | - GitHub Actions: writes to GITHUB_ENV and masks the token |
| 29 | #> |
| 30 | |
| 31 | [CmdletBinding()] |
| 32 | param( |
| 33 | [string] $KeyVaultName = "azuresdkengkeyvault", |
| 34 | [string] $KeyName = "azure-sdk-automation", |
| 35 | [string] $GitHubAppId = '1086291', # Azure SDK Automation App ID |
| 36 | [string[]] $InstallationTokenOwners = @("Azure"), |
| 37 | [string] $VariableNamePrefix = "GH_TOKEN" |
| 38 | ) |
| 39 | |
| 40 | $ErrorActionPreference = 'Stop' |
| 41 | Set-StrictMode -Version Latest |
| 42 | |
| 43 | $GitHubApiBaseUrl = "https://api.github.com" |
| 44 | $GitHubApiVersion = "2022-11-28" |
| 45 | |
| 46 | function Get-Headers { |
| 47 | param( |
| 48 | [Parameter(Mandatory)][string] $Jwt, |
| 49 | [Parameter(Mandatory)][string] $ApiVersion |
| 50 | ) |
| 51 | return @{ |
| 52 | 'Authorization' = "Bearer $Jwt" |
| 53 | 'Accept' = 'application/vnd.github+json' |
| 54 | 'X-GitHub-Api-Version' = $ApiVersion |
| 55 | 'User-Agent' = 'ado-pwsh-ghapp' |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | function New-GitHubAppJwt { |
| 60 | param( |
| 61 | [Parameter(Mandatory)] [string] $VaultName, |
| 62 | [Parameter(Mandatory)] [string] $KeyName, |
| 63 | [Parameter(Mandatory)] [string] $AppId |
| 64 | ) |
| 65 | |
| 66 | function Base64UrlEncode { |
| 67 | param( |
| 68 | [string]$Data, |
| 69 | [switch]$IsBase64String |
| 70 | ) |
| 71 | if ($IsBase64String) { |
| 72 | $base64 = $Data |
| 73 | } else { |
| 74 | $bytes = [System.Text.Encoding]::UTF8.GetBytes($Data) |
| 75 | $base64 = [Convert]::ToBase64String($bytes) |
| 76 | } |
| 77 | return $base64.TrimEnd('=') -replace '\+', '-' -replace '/', '_' |
| 78 | } |
| 79 | |
| 80 | # === STEP 1: Create JWT Header and Payload === |
| 81 | $Header = @{ |
| 82 | alg = "RS256" |
| 83 | typ = "JWT" |
| 84 | } |
| 85 | $Now = [int][double]::Parse((Get-Date -UFormat %s)) |
| 86 | $Payload = @{ |
| 87 | iat = $Now - 10 # 10 seconds clock skew |
| 88 | exp = $Now + 600 # 10 minutes |
| 89 | iss = $AppId |
| 90 | } |
| 91 | |
| 92 | $EncodedHeader = Base64UrlEncode (ConvertTo-Json $Header -Compress) |
| 93 | $EncodedPayload = Base64UrlEncode (ConvertTo-Json $Payload -Compress) |
| 94 | $UnsignedToken = "$EncodedHeader.$EncodedPayload" |
| 95 | |
| 96 | # === STEP 2: Sign the token using Azure CLI === |
| 97 | $UnsignedTokenBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::ASCII.GetBytes($UnsignedToken)) |
| 98 | $Base64Value = [Convert]::ToBase64String($UnsignedTokenBytes) |
| 99 | |
| 100 | $SignResultJson = az keyvault key sign ` |
| 101 | --vault-name $VaultName ` |
| 102 | --name $KeyName ` |
| 103 | --algorithm RS256 ` |
| 104 | --digest $Base64Value | ConvertFrom-Json |
| 105 | |
| 106 | if ($LASTEXITCODE -ne 0) { |
| 107 | throw "Failed to sign JWT with Azure Key Vault. Error: $($SignResultJson | ConvertTo-Json -Compress)" |
| 108 | } |
| 109 | |
| 110 | if (!$SignResultJson.signature) { |
| 111 | throw "Azure Key Vault response does not contain a signature. Response: $($SignResultJson | ConvertTo-Json -Compress)" |
| 112 | } |
| 113 | |
| 114 | $Signature = Base64UrlEncode -Data $SignResultJson.signature -IsBase64String |
| 115 | return "$UnsignedToken.$Signature" |
| 116 | } |
| 117 | |
| 118 | function Get-GitHubInstallationId { |
| 119 | param( |
| 120 | [Parameter(Mandatory)][string] $Jwt, |
| 121 | [Parameter(Mandatory)][string] $ApiBase, |
| 122 | [Parameter(Mandatory)][string] $ApiVersion, |
| 123 | [Parameter(Mandatory)][string] $InstallationTokenOwner |
| 124 | ) |
| 125 | |
| 126 | $headers = Get-Headers -Jwt $Jwt -ApiVersion $ApiVersion |
| 127 | |
| 128 | $uri = "$ApiBase/app/installations" |
| 129 | $resp = Invoke-RestMethod -Method Get -Headers $headers -Uri $uri -TimeoutSec 30 -MaximumRetryCount 3 |
| 130 | |
| 131 | $resp | Foreach-Object { Write-Host " $($_.id): $($_.account.login) [$($_.target_type)]" } |
| 132 | |
| 133 | $resp = $resp | Where-Object { $_.account.login -ieq $InstallationTokenOwner } |
| 134 | if (!$resp.id) { throw "No installations found for this App." } |
| 135 | return $resp.id |
| 136 | } |
| 137 | |
| 138 | function New-GitHubInstallationToken { |
| 139 | param( |
| 140 | [Parameter(Mandatory)] [string] $Jwt, |
| 141 | [Parameter(Mandatory)] [string] $InstallationId, |
| 142 | [Parameter(Mandatory)] [string] $ApiBase, |
| 143 | [Parameter(Mandatory)] [string] $ApiVersion |
| 144 | ) |
| 145 | $headers = Get-Headers -Jwt $Jwt -ApiVersion $ApiVersion |
| 146 | $uri = "$ApiBase/app/installations/$InstallationId/access_tokens" |
| 147 | $resp = Invoke-RestMethod -Method Post -Headers $headers -Uri $uri -TimeoutSec 30 -MaximumRetryCount 3 |
| 148 | if (!$resp.token) { throw "Failed to obtain installation access token for installation $InstallationId." } |
| 149 | return $resp.token |
| 150 | } |
| 151 | |
| 152 | Write-Host "Generating GitHub App JWT by signing via Azure Key Vault (no key export)..." |
| 153 | $jwt = New-GitHubAppJwt -VaultName $KeyVaultName -KeyName $KeyName -AppId $GitHubAppId |
| 154 | |
| 155 | foreach ($InstallationTokenOwner in $InstallationTokenOwners) |
| 156 | { |
| 157 | Write-Host "Fetching installation ID for $InstallationTokenOwner ..." |
| 158 | $installationId = Get-GitHubInstallationId -Jwt $jwt -ApiBase $GitHubApiBaseUrl -ApiVersion $GitHubApiVersion -InstallationTokenOwner $InstallationTokenOwner |
| 159 | |
| 160 | Write-Host "Installation ID resolved: $installationId" |
| 161 | |
| 162 | Write-Host "Exchanging JWT for installation access token..." |
| 163 | $installationToken = New-GitHubInstallationToken -Jwt $jwt -InstallationId $installationId -ApiBase $GitHubApiBaseUrl -ApiVersion $GitHubApiVersion |
| 164 | |
| 165 | $variableName = $VariableNamePrefix |
| 166 | if ($InstallationTokenOwners.Count -gt 1) |
| 167 | { |
| 168 | $variableName = $VariableNamePrefix + "_" + $InstallationTokenOwner |
| 169 | } |
| 170 | |
| 171 | Set-Item -Path Env:$variableName -Value $installationToken |
| 172 | |
| 173 | # Export for gh CLI & git |
| 174 | Write-Host "$variableName has been set in the current process." |
| 175 | |
| 176 | # Azure DevOps: set secret pipeline variable (so later tasks can reuse it) |
| 177 | if ($null -ne $env:SYSTEM_TEAMPROJECTID) { |
| 178 | Write-Host "##vso[task.setvariable variable=$variableName;issecret=true]$installationToken" |
| 179 | Write-Host "Azure DevOps variable '$variableName' has been set (secret)." |
| 180 | } |
| 181 | |
| 182 | # GitHub Actions: mask the token and export to GITHUB_ENV |
| 183 | if ($env:GITHUB_ACTIONS -eq 'true') { |
| 184 | Write-Host "::add-mask::$installationToken" |
| 185 | Add-Content -Path $env:GITHUB_ENV -Value "$variableName=$installationToken" |
| 186 | Write-Host "GitHub Actions env variable '$variableName' has been exported." |
| 187 | } |
| 188 | |
| 189 | try { |
| 190 | Write-Host "`n--- gh auth status ---" |
| 191 | $gh_token_value_before = $env:GH_TOKEN |
| 192 | $env:GH_TOKEN = $installationToken |
| 193 | & gh auth status |
| 194 | } |
| 195 | finally{ |
| 196 | $env:GH_TOKEN = $gh_token_value_before |
| 197 | } |
| 198 | } |
| 199 | |