Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions eng/common/pipelines/templates/steps/login-to-github.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Will output a variable named GH_TOKEN_<Owner> 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 }}'

198 changes: 198 additions & 0 deletions eng/common/scripts/login-to-github.ps1
Original file line number Diff line number Diff line change
@@ -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_<Owner>.

.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
}
}
7 changes: 6 additions & 1 deletion eng/tsp-core/pipelines/jobs/create-github-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 7 additions & 2 deletions eng/tsp-core/pipelines/pr-tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I think we also need to update this code which checked for the az pipeline user to comment to update the comment and not create a new one everytime

displayName: Check already commented
env:
GH_TOKEN: $(azuresdk-github-pat)
GH_TOKEN: $(GH_TOKEN)
VSCODE_DOWNLOAD_URL: $(vscodeUrl)

# - job: change_comment
Expand All @@ -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)
7 changes: 6 additions & 1 deletion packages/http-client-csharp/eng/pipeline/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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') }}
Expand Down
Loading