diff --git a/eng/common/pipelines/templates/steps/login-to-github.yml b/eng/common/pipelines/templates/steps/login-to-github.yml new file mode 100644 index 00000000000..3df66925da2 --- /dev/null +++ b/eng/common/pipelines/templates/steps/login-to-github.yml @@ -0,0 +1,25 @@ +# Will output a variable named GH_TOKEN_ for each owner in TokenOwners if there is only one owner it will just output GH_TOKEN + +parameters: +- name: TokenOwners + type: object + default: + - Azure +- name: VariableNamePrefix + type: string + default: GH_TOKEN +- name: ScriptDirectory + default: eng/common/scripts + +steps: +- task: AzureCLI@2 + displayName: "Login to GitHub" + inputs: + azureSubscription: 'AzureSDKEngKeyVault Secrets' + scriptType: pscore + scriptLocation: scriptPath + scriptPath: ${{ parameters.ScriptDirectory }}/login-to-github.ps1 + arguments: > + -InstallationTokenOwners '${{ join(''',''', parameters.TokenOwners) }}' + -VariableNamePrefix '${{ parameters.VariableNamePrefix }}' + \ No newline at end of file diff --git a/eng/common/scripts/login-to-github.ps1 b/eng/common/scripts/login-to-github.ps1 new file mode 100644 index 00000000000..129946a409b --- /dev/null +++ b/eng/common/scripts/login-to-github.ps1 @@ -0,0 +1,198 @@ +<# +.SYNOPSIS + Mints a GitHub App installation access token using Azure Key Vault 'sign' (non-exportable key), + and logs in the GitHub CLI by setting GH_TOKEN. + + Works in both Azure DevOps pipelines and GitHub Actions workflows. + Requires Azure CLI to be pre-authenticated (via AzureCLI@2 in ADO, or azure/login in GH Actions). + +.PARAMETER KeyVaultName + Name of the Azure Key Vault containing the non-exportable RSA key. + +.PARAMETER KeyName + Name of the RSA key in Key Vault (imported as a *key*, not a secret). + +.PARAMETER GitHubAppId + Numeric App ID (not client ID) of your GitHub App. + +.PARAMETER InstallationTokenOwners + List of GitHub organizations or users for which to obtain installation tokens. + +.PARAMETER VariableNamePrefix + Prefix for the exported variable name (default: GH_TOKEN). + With a single owner, exports as GH_TOKEN. With multiple owners, exports as GH_TOKEN_. + +.OUTPUTS + Sets environment variables in the current process and exports them to the CI system: + - Azure DevOps: sets secret pipeline variables via ##vso logging commands + - GitHub Actions: writes to GITHUB_ENV and masks the token +#> + +[CmdletBinding()] +param( + [string] $KeyVaultName = "azuresdkengkeyvault", + [string] $KeyName = "azure-sdk-automation", + [string] $GitHubAppId = '1086291', # Azure SDK Automation App ID + [string[]] $InstallationTokenOwners = @("Azure"), + [string] $VariableNamePrefix = "GH_TOKEN" +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$GitHubApiBaseUrl = "https://api.github.com" +$GitHubApiVersion = "2022-11-28" + +function Get-Headers { + param( + [Parameter(Mandatory)][string] $Jwt, + [Parameter(Mandatory)][string] $ApiVersion + ) + return @{ + 'Authorization' = "Bearer $Jwt" + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = $ApiVersion + 'User-Agent' = 'ado-pwsh-ghapp' + } +} + +function New-GitHubAppJwt { + param( + [Parameter(Mandatory)] [string] $VaultName, + [Parameter(Mandatory)] [string] $KeyName, + [Parameter(Mandatory)] [string] $AppId + ) + + function Base64UrlEncode { + param( + [string]$Data, + [switch]$IsBase64String + ) + if ($IsBase64String) { + $base64 = $Data + } else { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Data) + $base64 = [Convert]::ToBase64String($bytes) + } + return $base64.TrimEnd('=') -replace '\+', '-' -replace '/', '_' + } + + # === STEP 1: Create JWT Header and Payload === + $Header = @{ + alg = "RS256" + typ = "JWT" + } + $Now = [int][double]::Parse((Get-Date -UFormat %s)) + $Payload = @{ + iat = $Now - 10 # 10 seconds clock skew + exp = $Now + 600 # 10 minutes + iss = $AppId + } + + $EncodedHeader = Base64UrlEncode (ConvertTo-Json $Header -Compress) + $EncodedPayload = Base64UrlEncode (ConvertTo-Json $Payload -Compress) + $UnsignedToken = "$EncodedHeader.$EncodedPayload" + + # === STEP 2: Sign the token using Azure CLI === + $UnsignedTokenBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::ASCII.GetBytes($UnsignedToken)) + $Base64Value = [Convert]::ToBase64String($UnsignedTokenBytes) + + $SignResultJson = az keyvault key sign ` + --vault-name $VaultName ` + --name $KeyName ` + --algorithm RS256 ` + --digest $Base64Value | ConvertFrom-Json + + if ($LASTEXITCODE -ne 0) { + throw "Failed to sign JWT with Azure Key Vault. Error: $($SignResultJson | ConvertTo-Json -Compress)" + } + + if (!$SignResultJson.signature) { + throw "Azure Key Vault response does not contain a signature. Response: $($SignResultJson | ConvertTo-Json -Compress)" + } + + $Signature = Base64UrlEncode -Data $SignResultJson.signature -IsBase64String + return "$UnsignedToken.$Signature" +} + +function Get-GitHubInstallationId { + param( + [Parameter(Mandatory)][string] $Jwt, + [Parameter(Mandatory)][string] $ApiBase, + [Parameter(Mandatory)][string] $ApiVersion, + [Parameter(Mandatory)][string] $InstallationTokenOwner + ) + + $headers = Get-Headers -Jwt $Jwt -ApiVersion $ApiVersion + + $uri = "$ApiBase/app/installations" + $resp = Invoke-RestMethod -Method Get -Headers $headers -Uri $uri -TimeoutSec 30 -MaximumRetryCount 3 + + $resp | Foreach-Object { Write-Host " $($_.id): $($_.account.login) [$($_.target_type)]" } + + $resp = $resp | Where-Object { $_.account.login -ieq $InstallationTokenOwner } + if (!$resp.id) { throw "No installations found for this App." } + return $resp.id +} + +function New-GitHubInstallationToken { + param( + [Parameter(Mandatory)] [string] $Jwt, + [Parameter(Mandatory)] [string] $InstallationId, + [Parameter(Mandatory)] [string] $ApiBase, + [Parameter(Mandatory)] [string] $ApiVersion + ) + $headers = Get-Headers -Jwt $Jwt -ApiVersion $ApiVersion + $uri = "$ApiBase/app/installations/$InstallationId/access_tokens" + $resp = Invoke-RestMethod -Method Post -Headers $headers -Uri $uri -TimeoutSec 30 -MaximumRetryCount 3 + if (!$resp.token) { throw "Failed to obtain installation access token for installation $InstallationId." } + return $resp.token +} + +Write-Host "Generating GitHub App JWT by signing via Azure Key Vault (no key export)..." +$jwt = New-GitHubAppJwt -VaultName $KeyVaultName -KeyName $KeyName -AppId $GitHubAppId + +foreach ($InstallationTokenOwner in $InstallationTokenOwners) +{ + Write-Host "Fetching installation ID for $InstallationTokenOwner ..." + $installationId = Get-GitHubInstallationId -Jwt $jwt -ApiBase $GitHubApiBaseUrl -ApiVersion $GitHubApiVersion -InstallationTokenOwner $InstallationTokenOwner + + Write-Host "Installation ID resolved: $installationId" + + Write-Host "Exchanging JWT for installation access token..." + $installationToken = New-GitHubInstallationToken -Jwt $jwt -InstallationId $installationId -ApiBase $GitHubApiBaseUrl -ApiVersion $GitHubApiVersion + + $variableName = $VariableNamePrefix + if ($InstallationTokenOwners.Count -gt 1) + { + $variableName = $VariableNamePrefix + "_" + $InstallationTokenOwner + } + + Set-Item -Path Env:$variableName -Value $installationToken + + # Export for gh CLI & git + Write-Host "$variableName has been set in the current process." + + # Azure DevOps: set secret pipeline variable (so later tasks can reuse it) + if ($null -ne $env:SYSTEM_TEAMPROJECTID) { + Write-Host "##vso[task.setvariable variable=$variableName;issecret=true]$installationToken" + Write-Host "Azure DevOps variable '$variableName' has been set (secret)." + } + + # GitHub Actions: mask the token and export to GITHUB_ENV + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Host "::add-mask::$installationToken" + Add-Content -Path $env:GITHUB_ENV -Value "$variableName=$installationToken" + Write-Host "GitHub Actions env variable '$variableName' has been exported." + } + + try { + Write-Host "`n--- gh auth status ---" + $gh_token_value_before = $env:GH_TOKEN + $env:GH_TOKEN = $installationToken + & gh auth status + } + finally{ + $env:GH_TOKEN = $gh_token_value_before + } +} diff --git a/eng/tsp-core/pipelines/jobs/create-github-release.yml b/eng/tsp-core/pipelines/jobs/create-github-release.yml index f5eaad7b9f4..539c4d3660d 100644 --- a/eng/tsp-core/pipelines/jobs/create-github-release.yml +++ b/eng/tsp-core/pipelines/jobs/create-github-release.yml @@ -26,7 +26,12 @@ jobs: cat "$(Pipeline.Workspace)/${{ parameters.artifactName }}/publish-summary.json" displayName: Log publish summary + - template: /eng/common/pipelines/templates/steps/login-to-github.yml + parameters: + TokenOwners: + - microsoft + - script: pnpm chronus-github create-releases --repo microsoft/typespec --publish-summary "$(Pipeline.Workspace)/${{ parameters.artifactName }}/publish-summary.json" displayName: Create github releases env: - GITHUB_TOKEN: $(azuresdk-github-pat) + GITHUB_TOKEN: $(GH_TOKEN) diff --git a/eng/tsp-core/pipelines/pr-tools.yml b/eng/tsp-core/pipelines/pr-tools.yml index fca22f4a0a1..458606ffdd8 100644 --- a/eng/tsp-core/pipelines/pr-tools.yml +++ b/eng/tsp-core/pipelines/pr-tools.yml @@ -90,10 +90,15 @@ extends: Write-Host "##vso[task.setvariable variable=vscodeUrl]$downloadUrl" displayName: Get vscode artifact URL + - template: /eng/common/pipelines/templates/steps/login-to-github.yml + parameters: + TokenOwners: + - microsoft + - script: npx tsx eng/tsp-core/scripts/create-tryit-comment.ts displayName: Check already commented env: - GH_TOKEN: $(azuresdk-github-pat) + GH_TOKEN: $(GH_TOKEN) VSCODE_DOWNLOAD_URL: $(vscodeUrl) # - job: change_comment @@ -105,4 +110,4 @@ extends: # - script: npx -p @chronus/github-pr-commenter@0.5.0 chronus-github-pr-commenter # displayName: Make comment about changes # env: - # GITHUB_TOKEN: $(azuresdk-github-pat) + # GITHUB_TOKEN: $(GH_TOKEN) diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index ac1f6075764..1c42d40252d 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -211,6 +211,11 @@ extends: displayName: "Log npm configuration" condition: eq(${{ parameters.CreateAzureSdkForNetPR }}, true) + - template: /eng/common/pipelines/templates/steps/login-to-github.yml + parameters: + TokenOwners: + - Azure + - task: PowerShell@2 displayName: Generate emitter-package.json files & create PR in azure-sdk-for-net inputs: @@ -219,7 +224,7 @@ extends: arguments: > -PackageVersion '$(PackageVersion)' -TypeSpecCommitUrl '$(TypeSpecCommitUrl)' - -AuthToken '$(azuresdk-github-pat)' + -AuthToken '$(GH_TOKEN)' -TypeSpecSourcePackageJsonPath '$(Build.SourcesDirectory)/packages/http-client-csharp/package.json' ${{ replace(replace('True', eq(variables['Build.SourceBranchName'], 'main'), ''), 'True', '-Internal') }} ${{ replace(replace('True', eq(parameters.RegenerateAzureLibraries, false), ''), 'True', '-RegenerateAzureLibraries') }}