Skip to content
Closed
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
2 changes: 2 additions & 0 deletions cli/azd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Install Go extension https://marketplace.visualstudio.com/items?itemName=golang.
## Contribute

See [CONTRIBUTING.md](./CONTRIBUTING.md) for information on contributing.

Test Changes
10 changes: 7 additions & 3 deletions eng/common/pipelines/templates/steps/login-to-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ parameters:
- name: VariableNamePrefix
type: string
default: GH_TOKEN
- name: ExportAsOutputVariable
type: boolean
default: false
- name: ScriptDirectory
default: eng/common/scripts

steps:
- task: AzureCLI@2
displayName: "Login to GitHub"
name: LoginToGitHub
inputs:
azureSubscription: 'AzureSDKEngKeyVault Secrets'
scriptType: pscore
scriptLocation: scriptPath
scriptPath: ${{ parameters.ScriptDirectory }}/login-to-github.ps1
arguments: >
-InstallationTokenOwners '${{ join(''',''', parameters.TokenOwners) }}'
-VariableNamePrefix '${{ parameters.VariableNamePrefix }}'

-InstallationTokenOwners '${{ join(''',''', parameters.TokenOwners) }}'
-VariableNamePrefix '${{ parameters.VariableNamePrefix }}'
-ExportAsOutputVariable:$${{ parameters.ExportAsOutputVariable }}
161 changes: 120 additions & 41 deletions eng/common/scripts/login-to-github.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
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>.

.PARAMETER ExportAsOutputVariable
When set in Azure DevOps, also exports the variable as an output variable
(##vso[task.setvariable ...;isOutput=true]) for downstream jobs/stages.

.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
Expand All @@ -34,7 +38,8 @@ param(
[string] $KeyName = "azure-sdk-automation",
[string] $GitHubAppId = '1086291', # Azure SDK Automation App ID
[string[]] $InstallationTokenOwners = @("Azure"),
[string] $VariableNamePrefix = "GH_TOKEN"
[string] $VariableNamePrefix = "GH_TOKEN",
[switch] $ExportAsOutputVariable
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -115,6 +120,32 @@ function New-GitHubAppJwt {
return "$UnsignedToken.$Signature"
}

function Get-PropertyValue {
param(
[AllowNull()][object] $InputObject,
[Parameter(Mandatory)][string] $PropertyName
)

if ($null -eq $InputObject) {
return $null
}

if ($InputObject -is [System.Collections.IDictionary]) {
if ($InputObject.Contains($PropertyName)) {
return $InputObject[$PropertyName]
}

return $null
}

$property = $InputObject | Get-Member -Name $PropertyName -MemberType NoteProperty,Property -ErrorAction SilentlyContinue
if ($null -ne $property) {
return $InputObject.$PropertyName
}

return $null
}

function Get-GitHubInstallationId {
param(
[Parameter(Mandatory)][string] $Jwt,
Expand All @@ -128,11 +159,46 @@ function Get-GitHubInstallationId {
$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)
foreach ($installation in $resp) {
$installationId = Get-PropertyValue -InputObject $installation -PropertyName 'id'
$installationLogin = Get-PropertyValue -InputObject $installation -PropertyName 'login'
$installationType = Get-PropertyValue -InputObject $installation -PropertyName 'target_type'

if ($null -eq $installationLogin) {
$installationLogin = Get-PropertyValue -InputObject (Get-PropertyValue -InputObject $installation -PropertyName 'account') -PropertyName 'login'
}

Write-Host " ${installationId}: ${installationLogin} [${installationType}]"
}

$loginMatches = @($resp | Where-Object {
$installationLogin = Get-PropertyValue -InputObject $_ -PropertyName 'login'
if ($null -eq $installationLogin) {
$installationLogin = Get-PropertyValue -InputObject (Get-PropertyValue -InputObject $_ -PropertyName 'account') -PropertyName 'login'
}

$installationLogin -ieq $InstallationTokenOwner
})

$resp = $resp | Where-Object { $_.account.login -ieq $InstallationTokenOwner }
if (!$resp.id) { throw "No installations found for this App." }
return $resp.id
$matchingInstallations = @($loginMatches | Where-Object {
$null -ne (Get-PropertyValue -InputObject $_ -PropertyName 'id')
})

if ($matchingInstallations.Count -eq 0) {
if ($loginMatches.Count -gt 0) {
throw "No installations with a valid id found for '$InstallationTokenOwner' on this App."
}

throw "No installations found for '$InstallationTokenOwner' on this App."
}

if ($matchingInstallations.Count -gt 1) {
Write-Warning "Multiple installations matched '$InstallationTokenOwner'; using the first one."
}

$matchedInstallation = $matchingInstallations[0]
return (Get-PropertyValue -InputObject $matchedInstallation -PropertyName 'id')
}

function New-GitHubInstallationToken {
Expand All @@ -149,50 +215,63 @@ function New-GitHubInstallationToken {
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
function Invoke-LoginToGitHub {
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
foreach ($InstallationTokenOwner in $InstallationTokenOwners) {
# Token owners can be provided as either "owner" or "owner/repo". Normalize to owner.
$normalizedOwner = ($InstallationTokenOwner -split '/')[0]
Write-Host "Fetching installation ID for $InstallationTokenOwner (normalized owner: $normalizedOwner) ..."
$installationId = Get-GitHubInstallationId -Jwt $jwt -ApiBase $GitHubApiBaseUrl -ApiVersion $GitHubApiVersion -InstallationTokenOwner $normalizedOwner

Write-Host "Installation ID resolved: $installationId"
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
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
}
$variableName = $VariableNamePrefix
if ($InstallationTokenOwners.Count -gt 1) {
$variableName = $VariableNamePrefix + "_" + $normalizedOwner
}

Set-Item -Path Env:$variableName -Value $installationToken
Set-Item -Path Env:$variableName -Value $installationToken

# Export for gh CLI & git
Write-Host "$variableName has been set in the current process."
# 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)."
}
# 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."
}
if ($ExportAsOutputVariable) {
Write-Host "##vso[task.setvariable variable=$variableName;issecret=true;isOutput=true]$installationToken"
Write-Host "Azure DevOps output variable '$variableName' has been set (secret)."
}
}

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
# 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
}
}
}

if ($env:PESTER_TEST_RUN -eq 'true') {
return
}

Invoke-LoginToGitHub
80 changes: 56 additions & 24 deletions eng/pipelines/templates/stages/publish-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,59 @@ stages:
value: Registry update

jobs:
- deployment: Publish_Release
condition: >-
and(
succeeded(),
ne('true', variables['Skip.Publish'])
)
# Nightly publishes unattended, so it uses the no-approval 'none'
# environment and is not a governed production release. Manual releases
# go through the approval-gated package-publish environment.
- ${{ if ne(variables['Build.Reason'], 'Schedule') }}:
- job: Create_GitHub_Release
dependsOn: []
condition: >-
and(
succeeded(),
ne('true', variables['Skip.Publish'])
)

pool:
name: $(LINUXPOOL)
image: $(LINUXVMIMAGE)
os: linux

steps:
- checkout: self

- template: /eng/common/pipelines/templates/steps/login-to-github.yml

- template: /eng/pipelines/templates/steps/extension-set-metadata-variables.yml

- task: DownloadPipelineArtifact@2
displayName: Download release artifact
inputs:
artifactName: release
targetPath: release

- task: DownloadPipelineArtifact@2
displayName: Download changelog artifact
inputs:
artifactName: changelog
targetPath: changelog

- template: /eng/pipelines/templates/steps/publish-extension-github-release.yml
parameters:
TagPrefix: azd-ext-${{ parameters.SanitizedExtensionId }}
TagVersion: $(EXT_VERSION)

# Storage upload — runs for both nightly and manual. 1ES releaseJob keeps
# production governance (package-publish approval on manual runs). No
# GH_TOKEN needed here. azcopy --overwrite=true makes retry safe.
- deployment: Publish_Storage
${{ if eq(variables['Build.Reason'], 'Schedule') }}:
dependsOn: []
environment: none
${{ else }}:
dependsOn: Create_GitHub_Release
environment: package-publish
condition: >-
and(
succeeded(),
ne('true', variables['Skip.Publish'])
)

pool:
name: azsdk-pool
Expand All @@ -82,10 +122,6 @@ stages:
artifactName: release
targetPath: release

- input: pipelineArtifact
artifactName: changelog
targetPath: changelog

strategy:
runOnce:
deploy:
Expand All @@ -103,28 +139,22 @@ stages:
displayName: Set StorageUploadLocations (nightly)
- ${{ else }}:
- pwsh: |
# Initial upload locations
$publishUploadLocations = '${{ parameters.SanitizedExtensionId }}/$(EXT_VERSION)'

Write-Host "Setting StorageUploadLocations to $publishUploadLocations"
Write-Host "###vso[task.setvariable variable=StorageUploadLocations]$publishUploadLocations"
displayName: Set StorageUploadLocations

- template: /eng/pipelines/templates/steps/publish-extension.yml
- template: /eng/pipelines/templates/steps/publish-extension-storage.yml
parameters:
PublishUploadLocations: $(StorageUploadLocations)
TagPrefix: azd-ext-${{ parameters.SanitizedExtensionId }}
TagVersion: $(EXT_VERSION)
# Nightly builds publish to storage only; no GitHub release since that'd just be a ton of clutter.
# Nightly extension consumers just grab it from blob storage.
${{ if eq(variables['Build.Reason'], 'Schedule') }}:
CreateGitHubRelease: false


# Updates the selected extension registry after the GitHub Release exists.
# Production registry updates also refresh global command snapshots.
# Mirrors Increment_Version in eng/pipelines/templates/stages/publish.yml.
- job: Update_Registry
dependsOn: Publish_Release
dependsOn:
- Publish_Release
condition: >-
and(
succeeded(),
Expand All @@ -139,6 +169,8 @@ stages:
steps:
- checkout: self

- template: /eng/common/pipelines/templates/steps/login-to-github.yml

- template: /eng/pipelines/templates/steps/extension-set-metadata-variables.yml

- template: /eng/pipelines/templates/steps/setup-go.yml
Expand Down Expand Up @@ -243,7 +275,7 @@ stages:
--version "$(EXT_VERSION)"
displayName: Update $(ExtensionRegistryFile) (azd x publish)
env:
GH_TOKEN: $(azuresdk-github-pat)
GH_TOKEN: $(GH_TOKEN)

- ${{ if ne(parameters.PublishToDevRegistry, true) }}:
- bash: |
Expand Down
Loading
Loading