Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
4 changes: 0 additions & 4 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
<clear />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-d839c41" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-runtime-d839c41c/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-runtime -->
<!-- Begin: Package sources from dotnet-winforms -->
<add key="darc-int-dotnet-winforms-fa8da0f" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-winforms-fa8da0ff/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-winforms -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
Expand All @@ -24,10 +22,8 @@
<disabledPackageSources>
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-d839c41" value="true" />
<!-- End: Package sources from dotnet-runtime -->
<!-- Begin: Package sources from dotnet-winforms -->
<add key="darc-int-dotnet-winforms-fa8da0f" value="true" />
<!-- End: Package sources from dotnet-winforms -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
</disabledPackageSources>
Expand Down
16 changes: 8 additions & 8 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,25 +165,25 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.26301.4">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="9.0.0-beta.26406.11">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>efce34e9f9f25af27e2b471fbbf8c21f1ac2e318</Sha>
<Sha>f3bffca1f93573c88a7f9b79fac258dff76f6215</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.CodeAnalysis" Version="9.0.0-beta.26301.4">
<Dependency Name="Microsoft.DotNet.CodeAnalysis" Version="9.0.0-beta.26406.11">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>efce34e9f9f25af27e2b471fbbf8c21f1ac2e318</Sha>
<Sha>f3bffca1f93573c88a7f9b79fac258dff76f6215</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Helix.Sdk" Version="9.0.0-beta.26301.4">
<Dependency Name="Microsoft.DotNet.Helix.Sdk" Version="9.0.0-beta.26406.11">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>efce34e9f9f25af27e2b471fbbf8c21f1ac2e318</Sha>
<Sha>f3bffca1f93573c88a7f9b79fac258dff76f6215</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.ApiCompat" Version="9.0.0-beta.24053.1">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>f4e11a15c7b8a949d4a366e792a9843ff6e88cd5</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.GenAPI" Version="9.0.0-beta.26301.4">
<Dependency Name="Microsoft.DotNet.GenAPI" Version="9.0.0-beta.26406.11">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>efce34e9f9f25af27e2b471fbbf8c21f1ac2e318</Sha>
<Sha>f3bffca1f93573c88a7f9b79fac258dff76f6215</Sha>
</Dependency>
<Dependency Name="Microsoft.SourceLink.AzureRepos.Git" Version="8.0.0-beta.23409.2">
<Uri>https://github.com/dotnet/sourcelink</Uri>
Expand Down
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<!-- Packages that come from https://github.com/dotnet/arcade -->
<PropertyGroup>
<MicrosoftDotNetApiCompatVersion>9.0.0-beta.24053.1</MicrosoftDotNetApiCompatVersion>
<MicrosoftDotNetCodeAnalysisPackageVersion>9.0.0-beta.26301.4</MicrosoftDotNetCodeAnalysisPackageVersion>
<MicrosoftDotNetCodeAnalysisPackageVersion>9.0.0-beta.26406.11</MicrosoftDotNetCodeAnalysisPackageVersion>
</PropertyGroup>
<!-- Sourcelink -->
<PropertyGroup>
Expand Down
154 changes: 154 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Mints a short-lived GitHub App installation access token by signing a JWT
# with a private key stored in Azure Key Vault (RSA, RS256). The signed JWT is
# exchanged with the GitHub API for a token scoped to a single installation.
#
# Requirements:
# - A GitHub App whose private key has been uploaded into Key Vault as an RSA
# key (the PEM converted to a Key Vault *key*, NOT stored as a secret).
# - The caller (the federated Azure service connection used to run this script)
# must have the `Key Vault Crypto User` role (or at minimum the `Sign`
# action) on that key.
# - The App must be installed on the target organization/account
# (`InstallationOwner`) with the permissions/repositories it needs.
#
# Installation tokens (ghs_*) are exempt from the enterprise classic-PAT
# lifetime policy, which is why this replaces the long-lived PAT.

[CmdletBinding()]
param(
# Name of the Key Vault that holds the GitHub App's RSA signing key.
[Parameter(Mandatory = $true)]
[string] $KeyVaultName,

# Name of the RSA key inside the Key Vault (the App's private key).
[Parameter(Mandatory = $true)]
[string] $KeyName,

# The GitHub App's Client ID (the value to put in the `iss` JWT claim).
[Parameter(Mandatory = $true)]
[string] $AppClientId,

# Login of the organization or user account whose installation we should
# mint the token for (e.g. `dotnet`, `microsoft`).
[Parameter(Mandatory = $true)]
[string] $InstallationOwner,

# Optional Azure DevOps pipeline variable name to set with the installation
# token (marked as a secret). When not specified, the token is written to
# stdout instead.
[Parameter(Mandatory = $false)]
[string] $OutputVariableName
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

. $PSScriptRoot\pipeline-logging-functions.ps1

function ConvertTo-Base64Url([byte[]] $bytes) {
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}

# Build JWT header and payload. Use [ordered] hashtables so JSON
# serialization is deterministic.
$jwtHeader = [ordered]@{
alg = 'RS256'
typ = 'JWT'
}
$now = [System.DateTimeOffset]::UtcNow
$jwtPayload = [ordered]@{
iat = $now.AddMinutes(-1).ToUnixTimeSeconds()
exp = $now.AddMinutes(5).ToUnixTimeSeconds()
iss = $AppClientId
}

$headerEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtHeader | ConvertTo-Json -Compress)))
$payloadEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtPayload | ConvertTo-Json -Compress)))
$signingInput = "$headerEncoded.$payloadEncoded"

# Key Vault `sign` expects the *digest* (base64), not the raw bytes.
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput))
$digestBase64 = [Convert]::ToBase64String($digestBytes)

Write-Host "Signing JWT with key '$KeyName' in vault '$KeyVaultName'..."
$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference
try {
# Azure CLI can emit non-fatal Python warnings to stderr even when signing succeeds.
# Use the exit code to determine success for this invocation.
$PSNativeCommandUseErrorActionPreference = $false
$signatureBase64 = az keyvault key sign `
--vault-name $KeyVaultName `
--name $KeyName `
--algorithm RS256 `
--digest $digestBase64 `
--query signature `
--output tsv `
--only-show-errors
$signExitCode = $LASTEXITCODE
}
catch {
Write-PipelineTelemetryError -Category 'Build' -Message "Failed to sign the JWT via Key Vault (key '$KeyName', vault '$KeyVaultName'): $_. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key."
exit 1
}
finally {
$PSNativeCommandUseErrorActionPreference = $previousNativeCommandErrorPreference
}
if ($signExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($signatureBase64)) {
Write-PipelineTelemetryError -Category 'Build' -Message "'az keyvault key sign' exited with code $signExitCode for key '$KeyName' in vault '$KeyVaultName'. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key."
exit 1
}
$signatureUrl = $signatureBase64.Trim().TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$signingInput.$signatureUrl"

$headers = @{
Authorization = "Bearer $jwt"
'X-GitHub-Api-Version' = '2022-11-28'
Accept = 'application/vnd.github+json'
'User-Agent' = 'dotnet-arcade-onelocbuild'
}

Write-Host "Looking up installation for '$InstallationOwner'..."
try {
$installations = @()
$page = 1
do {
$pageInstallations = @(Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get)
$installations += $pageInstallations
$page++
} while ($pageInstallations.Count -eq 100)
}
catch {
Write-PipelineTelemetryError -Category 'Build' -Message "Failed to list GitHub App installations: $_. The signed JWT may be invalid or the App's Client ID ('$AppClientId') may be incorrect."
exit 1
}
$installation = $installations | Where-Object { $_.account.login -ieq $InstallationOwner } | Select-Object -First 1
if (-not $installation) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}

try {
$tokenResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations/$($installation.id)/access_tokens" `
-Headers $headers `
-Method Post `
-ContentType 'application/json'
}
catch {
Write-PipelineTelemetryError -Category 'Build' -Message "Failed to mint an installation access token for '$InstallationOwner' (installation $($installation.id)): $_"
exit 1
}

Write-Host "Got installation token for '$InstallationOwner' (expires $($tokenResponse.expires_at))."
if ($OutputVariableName) {
Write-Host "Setting pipeline variable '$OutputVariableName'."
Write-Host "##vso[task.setvariable variable=$OutputVariableName;issecret=true]$($tokenResponse.token)"
}
else {
Write-Host $tokenResponse.token -ForegroundColor Green
}
49 changes: 47 additions & 2 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ parameters:
CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex
GithubPat: $(BotAccount-dotnet-bot-repo-PAT)

# Service connection for WIF-based Entra authentication to ceapex feeds (replaces CeapexPat).
# When set, dnceng/internal builds acquire a federated Entra token instead of using a PAT.
# All other projects (e.g. DevDiv, public), where this dnceng-scoped service connection does not
# exist, and any pipeline that sets this to '' fall back to PAT-based auth via the CeapexPat parameter.
CeapexServiceConnection: 'dnceng-onelocbuild-ceapex'

# GitHub App authentication for the OneLoc check-in PR (dnceng/internal only).
# The infrastructure identifiers are centralized here and the App path is enabled by default.
# DevDiv requires its own project-scoped service connection before this path can be enabled there.
UseGitHubAppAuthentication: true
GitHubAppServiceConnection: 'dnceng-oneloc-githubapp'
GitHubAppClientId: 'Iv23lijBU8x3gc9lDOc9'
GitHubAppKeyVaultName: 'EngKeyVault'
GitHubAppKeyName: 'oneloc-localization-app-key'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand Down Expand Up @@ -73,6 +88,30 @@ jobs:
displayName: Generate LocProject.json
condition: ${{ parameters.condition }}

# Acquire an Entra token for ceapex feed access via WIF (dnceng/internal only).
# All other projects use PAT-based auth, since the ceapex service connection is scoped to dnceng/internal.
- ${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
- template: /eng/common/core-templates/steps/get-federated-access-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
federatedServiceConnection: ${{ parameters.CeapexServiceConnection }}
outputVariableName: 'CeapexEntraToken'
condition: ${{ parameters.condition }}

# Mint a short-lived GitHub App installation token for the loc check-in PR (dnceng/internal only).
# All other projects fall back to PAT-based auth, since the app service connection is scoped to dnceng/internal.
- ${{ if and(eq(parameters.RepoType, 'gitHub'), eq(parameters.UseGitHubAppAuthentication, true), eq(variables['System.TeamProject'], 'internal')) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
azureSubscription: ${{ parameters.GitHubAppServiceConnection }}
keyVaultName: ${{ parameters.GitHubAppKeyVaultName }}
keyName: ${{ parameters.GitHubAppKeyName }}
appClientId: ${{ parameters.GitHubAppClientId }}
installationOwner: ${{ parameters.GitHubOrg }}
outputVariableName: 'GitHubAppInstallationToken'
condition: ${{ parameters.condition }}

- task: OneLocBuild@2
displayName: OneLocBuild
env:
Expand All @@ -89,10 +128,16 @@ jobs:
${{ if eq(parameters.RepoType, 'gitHub') }}:
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
patVariable: ${{ parameters.CeapexPat }}
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ if and(eq(parameters.UseGitHubAppAuthentication, true), eq(variables['System.TeamProject'], 'internal')) }}:
gitHubPatVariable: "$(GitHubAppInstallationToken)"
${{ if or(eq(parameters.UseGitHubAppAuthentication, false), ne(variables['System.TeamProject'], 'internal')) }}:
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ if ne(parameters.MirrorRepo, '') }}:
isMirrorRepoSelected: true
gitHubOrganization: ${{ parameters.GitHubOrg }}
Expand Down
2 changes: 0 additions & 2 deletions eng/common/core-templates/job/publish-build-assets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ jobs:
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
- ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- group: Publish-Build-Assets
- group: AzureDevOps-Artifact-Feeds-Pats
- name: runCodesignValidationInjection
value: false
# unconditional - needed for logs publishing (redactor tool version)
Expand Down
1 change: 0 additions & 1 deletion eng/common/core-templates/jobs/codeql-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
enableTelemetry: true

variables:
- group: Publish-Build-Assets
# The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in
# sync with the packages.config file.
- name: DefaultGuardianVersion
Expand Down
2 changes: 0 additions & 2 deletions eng/common/core-templates/post-build/common-variables.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
variables:
- group: Publish-Build-Assets

# Whether the build is internal or not
- name: IsInternalBuild
value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }}
Expand Down
79 changes: 79 additions & 0 deletions eng/common/core-templates/steps/get-github-app-token.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Mints a short-lived GitHub App installation access token by signing a JWT
# with a private key stored in Azure Key Vault (RSA, RS256). The JWT is
# exchanged with the GitHub API for a token scoped to a single installation.
#
# Requirements (per GitHub App you want to authenticate as):
# - A GitHub App with its private key uploaded into Key Vault as an RSA key
# (PEM converted to a key, NOT stored as a secret).
# - The Azure service connection passed via `azureSubscription` must be
# granted the `Key Vault Crypto User` role (or at minimum `Sign` action)
# on that key.
# - The App must be installed on the target organization/account
# (`installationOwner`) with the permissions/repositories you need.
#
# Output: a secret pipeline variable named ${{ parameters.outputVariableName }}
# containing the installation access token. Token lifetime is ~1 hour and is
# automatically scrubbed from logs. Installation tokens are exempt from the
# enterprise classic-PAT lifetime policy.

parameters:
# Azure DevOps service connection (federated) that can call
# `az keyvault key sign` on the App's signing key.
- name: azureSubscription
type: string

# Name of the Key Vault that holds the GitHub App's RSA signing key.
- name: keyVaultName
type: string

# Name of the RSA key inside the Key Vault (the App's private key).
- name: keyName
type: string

# The GitHub App's Client ID (the value to put in the `iss` JWT claim).
# Prefer this over the numeric App ID; GitHub accepts either, but Client ID
# is the documented form going forward.
- name: appClientId
type: string

# Login of the organization or user account whose installation we should
# mint the token for (e.g. `dotnet`, `microsoft`).
- name: installationOwner
type: string

# Name of the pipeline variable that will receive the installation token.
- name: outputVariableName
type: string

- name: is1ESPipeline
type: boolean

- name: stepName
type: string
default: getGitHubAppInstallationToken

- name: condition
type: string
default: ''

- name: displayName
type: string
default: Get GitHub App installation token

steps:
- task: AzureCLI@2
displayName: ${{ parameters.displayName }}
name: ${{ parameters.stepName }}
${{ if ne(parameters.condition, '') }}:
condition: ${{ parameters.condition }}
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
& "$(System.DefaultWorkingDirectory)/eng/common/Get-GitHubAppToken.ps1" `
-KeyVaultName '${{ parameters.keyVaultName }}' `
-KeyName '${{ parameters.keyName }}' `
-AppClientId '${{ parameters.appClientId }}' `
-InstallationOwner '${{ parameters.installationOwner }}' `
-OutputVariableName '${{ parameters.outputVariableName }}'
Loading