diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index d01ed609cb..e7fe9f834a 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -7,34 +7,34 @@
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 66b75e61d883abd9e3af86a811c3809a8d9142ca
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
diff --git a/eng/Versions.props b/eng/Versions.props
index 067b1503dc..36923f573b 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -98,7 +98,7 @@
9.0.0-beta.24212.4
5.0.0-preview.5.20278.1
8.0.0-beta.24525.2
- 11.0.0-beta.26456.1
+ 12.0.0-beta.26469.3
9.0.4
0.0.6-test
0.0.13-test
diff --git a/eng/common/Get-GitHubAppToken.ps1 b/eng/common/Get-GitHubAppToken.ps1
index ea776bd6bc..ec005e487c 100644
--- a/eng/common/Get-GitHubAppToken.ps1
+++ b/eng/common/Get-GitHubAppToken.ps1
@@ -1,13 +1,11 @@
# 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.
+# with an RSA private key (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.
+# - A GitHub App ID and PEM private key stored as Azure Key Vault secrets.
+# - The federated Azure service connection running this script must have
+# `Get` access to those two secrets.
# - The App must be installed on the target organization/account
# (`InstallationOwner`) with the permissions/repositories it needs.
#
@@ -16,17 +14,17 @@
[CmdletBinding()]
param(
- # Name of the Key Vault that holds the GitHub App's RSA signing key.
+ # Name of the Key Vault holding the GitHub App credentials.
[Parameter(Mandatory = $true)]
[string] $KeyVaultName,
- # Name of the RSA key inside the Key Vault (the App's private key).
+ # Secret Manager projection containing the GitHub App ID.
[Parameter(Mandatory = $true)]
- [string] $KeyName,
+ [string] $AppIdSecretName,
- # The GitHub App's Client ID (the value to put in the `iss` JWT claim).
+ # Secret Manager projection containing the PEM private key.
[Parameter(Mandatory = $true)]
- [string] $AppClientId,
+ [string] $AppPrivateKeySecretName,
# Login of the organization or user account whose installation we should
# mint the token for (e.g. `dotnet`, `microsoft`).
@@ -39,16 +37,69 @@ param(
[Parameter(Mandatory = $false)]
[string] $OutputVariableName
)
-
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
. $PSScriptRoot\pipeline-logging-functions.ps1
+if ($KeyVaultName -notmatch '^[A-Za-z][A-Za-z0-9-]{1,22}[A-Za-z0-9]$' -or $KeyVaultName.Contains('--')) {
+ Write-PipelineTelemetryError -Category 'Build' -Message "KeyVaultName '$KeyVaultName' is not a valid Azure Key Vault name."
+ exit 1
+}
+
function ConvertTo-Base64Url([byte[]] $bytes) {
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
+$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference
+try {
+ # Azure CLI can emit non-fatal Python warnings to stderr.
+ $PSNativeCommandUseErrorActionPreference = $false
+ $keyVaultAccessToken = az account get-access-token `
+ --resource https://vault.azure.net `
+ --query accessToken `
+ --output tsv `
+ --only-show-errors
+ $tokenExitCode = $LASTEXITCODE
+}
+catch {
+ Write-PipelineTelemetryError -Category 'Build' -Message "Failed to acquire an Azure Key Vault access token: $_"
+ exit 1
+}
+finally {
+ $PSNativeCommandUseErrorActionPreference = $previousNativeCommandErrorPreference
+}
+if ($tokenExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($keyVaultAccessToken)) {
+ Write-PipelineTelemetryError -Category 'Build' -Message "'az account get-access-token' exited with code $tokenExitCode while acquiring an Azure Key Vault access token."
+ exit 1
+}
+
+function Get-KeyVaultSecret([string] $SecretName) {
+ # Use the data-plane REST API because `az keyvault secret show` can fail
+ # with Errno 22 on hosted Windows agents when reading these projections.
+ $escapedSecretName = [Uri]::EscapeDataString($SecretName)
+ $secretUri = "https://$KeyVaultName.vault.azure.net/secrets/$escapedSecretName`?api-version=7.4"
+ try {
+ $response = Invoke-RestMethod `
+ -Uri $secretUri `
+ -Headers @{ Authorization = "Bearer $keyVaultAccessToken" } `
+ -Method Get
+ }
+ catch {
+ Write-PipelineTelemetryError -Category 'Build' -Message "Failed to read secret '$SecretName' from vault '$KeyVaultName': $_. Verify the secret exists and the service connection has 'Key Vault Secrets User' access to it."
+ exit 1
+ }
+ if ([string]::IsNullOrWhiteSpace($response.value)) {
+ Write-PipelineTelemetryError -Category 'Build' -Message "Secret '$SecretName' in vault '$KeyVaultName' is empty."
+ exit 1
+ }
+ return [string] $response.value
+}
+
+Write-Host "Reading GitHub App credentials from vault '$KeyVaultName'..."
+$appId = Get-KeyVaultSecret $AppIdSecretName
+$privateKey = Get-KeyVaultSecret $AppPrivateKeySecretName
+
# Build JWT header and payload. Use [ordered] hashtables so JSON
# serialization is deterministic.
$jwtHeader = [ordered]@{
@@ -59,46 +110,38 @@ $now = [System.DateTimeOffset]::UtcNow
$jwtPayload = [ordered]@{
iat = $now.AddMinutes(-1).ToUnixTimeSeconds()
exp = $now.AddMinutes(5).ToUnixTimeSeconds()
- iss = $AppClientId
+ iss = $appId
}
$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)
+$sha256 = [System.Security.Cryptography.SHA256]::Create()
+try {
+ $digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput))
+}
+finally {
+ $sha256.Dispose()
+}
-Write-Host "Signing JWT with key '$KeyName' in vault '$KeyVaultName'..."
-$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference
+Write-Host 'Signing JWT with the GitHub App private key...'
+$rsa = [System.Security.Cryptography.RSA]::Create()
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
+ $rsa.ImportFromPem($privateKey)
+ $signatureBytes = $rsa.SignHash(
+ $digestBytes,
+ [System.Security.Cryptography.HashAlgorithmName]::SHA256,
+ [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
+ $signatureUrl = ConvertTo-Base64Url $signatureBytes
}
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."
+ Write-PipelineTelemetryError -Category 'Build' -Message "Failed to sign the GitHub App JWT with the supplied private 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
+ $rsa.Dispose()
}
-$signatureUrl = $signatureBase64.Trim().TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$signingInput.$signatureUrl"
$headers = @{
@@ -126,7 +169,7 @@ try {
} while ($pageInstallationCount -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."
+ Write-PipelineTelemetryError -Category 'Build' -Message "Failed to list GitHub App installations: $_. The signed JWT may be invalid or the App ID may be incorrect."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
diff --git a/eng/common/build.ps1 b/eng/common/build.ps1
index fee2f83991..305132a6d6 100644
--- a/eng/common/build.ps1
+++ b/eng/common/build.ps1
@@ -8,7 +8,7 @@ Param(
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
- [bool][Alias('mt')]$msbuildMultiThreaded = $false,
+ [bool][Alias('mt')]$msbuildMultiThreaded = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
[switch] $deployDeps,
@@ -181,6 +181,10 @@ try {
if (-not $PSBoundParameters.ContainsKey('nodeReuse')) {
$nodeReuse = $false
}
+ # MSBuild's multi-threaded mode isn't run on CI unless it was explicitly requested via -msbuildMultiThreaded.
+ if (-not $PSBoundParameters.ContainsKey('msbuildMultiThreaded')) {
+ $msbuildMultiThreaded = $false
+ }
}
if (-not [string]::IsNullOrEmpty($binaryLogName)) {
diff --git a/eng/common/build.sh b/eng/common/build.sh
index 109d83ff73..f65b048aa8 100755
--- a/eng/common/build.sh
+++ b/eng/common/build.sh
@@ -254,7 +254,7 @@ function Build {
properties+=("/p:Projects=$projects")
fi
- local bl=""
+ local bl=()
if [[ "$binary_log" == true ]]; then
local binary_log_path=""
if [[ -z "$binary_log_name" ]]; then
@@ -266,7 +266,7 @@ function Build {
fi
mkdir -p "$(dirname "$binary_log_path")"
- bl="/bl:\"$binary_log_path\""
+ bl=("/bl:$binary_log_path")
fi
local check=""
@@ -274,8 +274,8 @@ function Build {
check="/check"
fi
- MSBuild $_InitializeToolset \
- $bl \
+ MSBuild "$_InitializeToolset" \
+ ${bl[@]+"${bl[@]}"} \
$check \
/p:Configuration=$configuration \
/p:RepoRoot="$repo_root" \
@@ -299,7 +299,7 @@ function Build {
if [[ "$clean" == true ]]; then
if [ -d "$artifacts_dir" ]; then
- rm -rf $artifacts_dir
+ rm -rf "$artifacts_dir"
echo "Artifacts directory deleted."
fi
exit 0
diff --git a/eng/common/core-templates/job/helix-job-monitor.yml b/eng/common/core-templates/job/helix-job-monitor.yml
index 53bbf74927..7417afdf3e 100644
--- a/eng/common/core-templates/job/helix-job-monitor.yml
+++ b/eng/common/core-templates/job/helix-job-monitor.yml
@@ -52,11 +52,23 @@ parameters:
type: string
default: https://helix.dot.net/
-# Helix API access token forwarded to the tool via the HELIX_ACCESSTOKEN environment variable.
+# Helix API access token forwarded via HELIX_ACCESSTOKEN. Not forwarded when
+# useEntraAuthentication is true.
- name: helixAccessToken
type: string
default: ''
+# Use a refreshable Entra credential instead of a PAT or anonymous access.
+- name: useEntraAuthentication
+ type: boolean
+ default: false
+
+# Azure service connection ID authorized for Helix. Required when
+# useEntraAuthentication is true.
+- name: azureSubscription
+ type: string
+ default: ''
+
# Polling interval in seconds (--polling-interval-seconds).
- name: pollingIntervalSeconds
type: number
@@ -141,6 +153,26 @@ jobs:
- checkout: self
fetchDepth: 1
+ - ${{ if and(eq(parameters.useEntraAuthentication, true), eq(parameters.azureSubscription, '')) }}:
+ - pwsh: throw "azureSubscription must be set when useEntraAuthentication is true."
+ displayName: Validate Helix Entra authentication
+
+ - ${{ if eq(parameters.useEntraAuthentication, true) }}:
+ - task: AzureCLI@2
+ displayName: Initialize Helix Entra authentication
+ inputs:
+ azureSubscription: ${{ parameters.azureSubscription }}
+ addSpnToEnvironment: true
+ scriptType: pscore
+ scriptLocation: inlineScript
+ inlineScript: |
+ if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) {
+ throw "The Helix Azure service connection did not provide a service principal or tenant ID."
+ }
+
+ Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId"
+ Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId"
+
- ${{ if ne(parameters.toolNupkgArtifactName, '') }}:
- task: DownloadPipelineArtifact@2
displayName: Download Helix Job Monitor artifact
@@ -214,6 +246,7 @@ jobs:
toolArgs=(
--helix-base-uri '${{ parameters.helixBaseUri }}'
+ --use-entra-authentication '${{ parameters.useEntraAuthentication }}'
--polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}'
--fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}'
--allow-no-helix-jobs '${{ parameters.allowNoHelixJobs }}'
@@ -275,4 +308,9 @@ jobs:
displayName: Monitor Helix Jobs
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
- HELIX_ACCESSTOKEN: ${{ parameters.helixAccessToken }}
+ ${{ if eq(parameters.useEntraAuthentication, false) }}:
+ HELIX_ACCESSTOKEN: ${{ parameters.helixAccessToken }}
+ ${{ if eq(parameters.useEntraAuthentication, true) }}:
+ AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
+ AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
+ AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.azureSubscription }}
diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml
index cb60f52978..5b070260f0 100644
--- a/eng/common/core-templates/job/job.yml
+++ b/eng/common/core-templates/job/job.yml
@@ -28,6 +28,7 @@ parameters:
enablePublishTestResults: false
enablePublishing: false
enableBuildRetry: false
+ enableAstred: false
mergeTestResults: false
testRunTitle: ''
testResultsFormat: ''
@@ -119,7 +120,14 @@ jobs:
- name: ${{ pair.key }}
value: ${{ pair.value }}
- # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds
+ - ${{ if and(eq(parameters.enableAstred, true), eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal'), notin(variables['Build.Reason'], 'PullRequest')) }}:
+ - name: MSBUILDDEBUGENGINE
+ value: 1
+ - name: MSBUILDDEBUGPATH
+ value: $(Build.ArtifactStagingDirectory)/AstredCapture/binlogs
+
+ # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds.
+ # Entra-enabled Helix templates do not forward this value to their processes.
- ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- group: DotNet-HelixApi-Access
@@ -236,3 +244,8 @@ jobs:
condition: always()
- ${{ each step in parameters.artifactPublishSteps }}:
- ${{ step }}
+
+ - ${{ if and(eq(parameters.enableAstred, true), eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal'), notin(variables['Build.Reason'], 'PullRequest')) }}:
+ - template: /eng/common/core-templates/steps/astred-artifacts.yml
+ parameters:
+ binlogDir: $(MSBUILDDEBUGPATH)
diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml
index e4e6b77fc3..b772dc5788 100644
--- a/eng/common/core-templates/job/onelocbuild.yml
+++ b/eng/common/core-templates/job/onelocbuild.yml
@@ -5,23 +5,14 @@ parameters:
# Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool
pool: ''
- 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).
- # dnceng/internal and DevDiv/DevDiv have same-named, project-scoped connections. Other projects,
- # and any pipeline that sets this to '', fall back to PAT-based auth via the CeapexPat parameter.
+ # Project-scoped WIF service connection for Ceapex feed authentication.
CeapexServiceConnection: 'dnceng-onelocbuild-ceapex'
# GitHub App authentication for the OneLoc check-in PR.
- # dnceng/internal and DevDiv/DevDiv are enabled by default with their project-scoped service
- # connections. Other projects must explicitly opt in after provisioning equivalent infrastructure.
- UseGitHubAppAuthentication: true
- UseGitHubAppAuthenticationInOtherProjects: false
GitHubAppServiceConnection: 'dnceng-oneloc-githubapp'
- GitHubAppClientId: 'Iv23lijBU8x3gc9lDOc9'
GitHubAppKeyVaultName: 'EngKeyVault'
- GitHubAppKeyName: 'oneloc-localization-app-key'
+ GitHubAppIdSecretName: 'oneloc-localization-app-app-id'
+ GitHubAppPrivateKeySecretName: 'oneloc-localization-app-app-private-key'
SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
@@ -49,7 +40,6 @@ jobs:
displayName: OneLocBuild${{ parameters.JobNameSuffix }}
variables:
- - group: OneLocBuildVariables # Contains the CeapexPat and GithubPat
- name: _GenerateLocProjectArguments
value: -SourcesDirectory ${{ parameters.SourcesDirectory }}
-LanguageSet "${{ parameters.LanguageSet }}"
@@ -80,6 +70,10 @@ jobs:
steps:
- ${{ if eq(parameters.is1ESPipeline, '') }}:
- 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error
+ - ${{ if notIn(variables['System.TeamProject'], 'internal', 'DevDiv') }}:
+ - 'OneLocBuild is supported only in dnceng/internal and DevDiv/DevDiv.': error
+ - ${{ if eq(parameters.CeapexServiceConnection, '') }}:
+ - 'CeapexServiceConnection must identify a WIF service connection.': error
- ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}:
- task: Powershell@2
@@ -89,17 +83,15 @@ jobs:
displayName: Generate LocProject.json
condition: ${{ parameters.condition }}
- # Acquire an Entra token for ceapex feed access in the supported internal and DevDiv projects.
- - ${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
- - template: /eng/common/templates/steps/get-federated-access-token.yml
- parameters:
- federatedServiceConnection: ${{ parameters.CeapexServiceConnection }}
- outputVariableName: 'CeapexEntraToken'
- condition: ${{ parameters.condition }}
+ # Acquire a short-lived Entra token for Ceapex feed access.
+ - template: /eng/common/templates/steps/get-federated-access-token.yml
+ parameters:
+ federatedServiceConnection: ${{ parameters.CeapexServiceConnection }}
+ outputVariableName: 'CeapexEntraToken'
+ condition: ${{ parameters.condition }}
- # Mint a short-lived GitHub App installation token for the loc check-in PR. Use the connection
- # provisioned in each supported project; other projects must explicitly opt in and override it.
- - ${{ if and(eq(parameters.RepoType, 'gitHub'), eq(parameters.UseGitHubAppAuthentication, true), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.UseGitHubAppAuthenticationInOtherProjects, true))) }}:
+ # Mint a short-lived GitHub App installation token for the loc check-in PR.
+ - ${{ if eq(parameters.RepoType, 'gitHub') }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
@@ -108,8 +100,8 @@ jobs:
${{ else }}:
azureSubscription: ${{ parameters.GitHubAppServiceConnection }}
keyVaultName: ${{ parameters.GitHubAppKeyVaultName }}
- keyName: ${{ parameters.GitHubAppKeyName }}
- appClientId: ${{ parameters.GitHubAppClientId }}
+ appIdSecretName: ${{ parameters.GitHubAppIdSecretName }}
+ appPrivateKeySecretName: ${{ parameters.GitHubAppPrivateKeySecretName }}
installationOwner: ${{ parameters.GitHubOrg }}
outputVariableName: 'GitHubAppInstallationToken'
condition: ${{ parameters.condition }}
@@ -129,16 +121,10 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
- ${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
- patVariable: $(CeapexEntraToken)
- ${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
- patVariable: ${{ parameters.CeapexPat }}
+ patVariable: $(CeapexEntraToken)
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
- ${{ if and(eq(parameters.UseGitHubAppAuthentication, true), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.UseGitHubAppAuthenticationInOtherProjects, true))) }}:
- gitHubPatVariable: "$(GitHubAppInstallationToken)"
- ${{ else }}:
- gitHubPatVariable: "${{ parameters.GithubPat }}"
+ gitHubPatVariable: "$(GitHubAppInstallationToken)"
${{ if ne(parameters.MirrorRepo, '') }}:
isMirrorRepoSelected: true
gitHubOrganization: ${{ parameters.GitHubOrg }}
diff --git a/eng/common/core-templates/steps/astred-artifacts.yml b/eng/common/core-templates/steps/astred-artifacts.yml
new file mode 100644
index 0000000000..b914082f58
--- /dev/null
+++ b/eng/common/core-templates/steps/astred-artifacts.yml
@@ -0,0 +1,103 @@
+# Astred footer for producing and uploading a portable digest.
+# The calling job must configure its header before any build steps run:
+# MSBUILDDEBUGENGINE=1
+# MSBUILDDEBUGPATH=
+parameters:
+- name: sourcesPath
+ type: string
+ default: $(Build.SourcesDirectory)
+- name: binlogDir
+ type: string
+ default: $(Build.ArtifactStagingDirectory)/AstredCapture/binlogs
+- name: capturePath
+ type: string
+ default: $(Build.ArtifactStagingDirectory)/AstredCapture
+
+steps:
+- task: AstredInstaller@0
+ displayName: Install Astred CLI
+ inputs:
+ Version: '2.14.1'
+ FeedUrl: 'https://pkgs.dev.azure.com/dnceng/_packaging/dotnet-internal-FoSSE/nuget/v3/index.json'
+
+- pwsh: |
+ $ErrorActionPreference = 'Continue'
+ $apjOut = Join-Path $env:ASTRED_CAPTURE_PATH 'apj'
+ New-Item -ItemType Directory -Force -Path $apjOut | Out-Null
+
+ $binlogs = Get-ChildItem -Path $env:ASTRED_BINLOG_DIR -Recurse -Force -Filter *.binlog `
+ -ErrorAction SilentlyContinue
+ if (-not $binlogs) {
+ Write-Host "##vso[task.logissue type=warning]No binlogs found in $env:ASTRED_BINLOG_DIR. Check the calling job's Astred header configuration for MSBUILDDEBUGENGINE / MSBuildDebugEngine and MSBUILDDEBUGPATH."
+ exit 0
+ }
+
+ $project = Join-Path $apjOut '.astred.project.json'
+ $binlogPaths = @($binlogs.FullName)
+ Write-Host "astproj: processing $($binlogPaths.Count) binlog(s)"
+ astred astproj -nofolders @binlogPaths "-o:$project"
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "##vso[task.logissue type=warning]astproj failed (exit $LASTEXITCODE)"
+ }
+ displayName: Generate Astred Project Files
+ workingDirectory: ${{ parameters.sourcesPath }}
+ env:
+ ASTRED_BINLOG_DIR: ${{ parameters.binlogDir }}
+ ASTRED_CAPTURE_PATH: ${{ parameters.capturePath }}
+ condition: succeededOrFailed()
+ continueOnError: true
+
+- pwsh: |
+ $ErrorActionPreference = 'Continue'
+ $apjDir = Join-Path $env:ASTRED_CAPTURE_PATH 'apj'
+ $uploadRoot = Join-Path $env:ASTRED_CAPTURE_PATH 'upload'
+ $project = Join-Path $apjDir '.astred.project.json'
+
+ if (-not (Test-Path $project)) {
+ Write-Host "##vso[task.logissue type=warning]No Astred project file was produced."
+ exit 0
+ }
+
+ $digest = Join-Path $apjDir '.astred.digest.zip'
+ Remove-Item $digest -ErrorAction SilentlyContinue
+ astred "-repo:$env:ASTRED_SOURCES_PATH" "-project:$project" -digest
+ if ($LASTEXITCODE -eq 0 -and (Test-Path $digest)) {
+ $commitTimeText = & git -C $env:ASTRED_SOURCES_PATH show -s --format=%cI $env:BUILD_SOURCEVERSION
+ if ($LASTEXITCODE -ne 0) {
+ throw "Could not read the commit timestamp for $env:BUILD_SOURCEVERSION."
+ }
+
+ $commitTime = [DateTimeOffset]::Parse(
+ $commitTimeText.Trim(),
+ [Globalization.CultureInfo]::InvariantCulture)
+ $eventFolder = '{0}_{1}' -f `
+ $commitTime.UtcDateTime.ToString('yyyy-MM-ddTHH-mm-ssZ'), `
+ $env:BUILD_SOURCEVERSION
+ $targetDir = Join-Path (Join-Path $uploadRoot 'AST') $eventFolder
+ $target = Join-Path $targetDir '.astred.digest.zip'
+ New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
+ Move-Item $digest $target -Force
+ Write-Host "Prepared Astred digest: $target"
+ Write-Host "##vso[task.setvariable variable=ASTRED_DIGEST_READY]true"
+ }
+ elseif ($LASTEXITCODE -ne 0) {
+ Write-Host "##vso[task.logissue type=warning]Digest generation failed for $project (exit $LASTEXITCODE)"
+ Remove-Item $digest -ErrorAction SilentlyContinue
+ }
+ else {
+ Write-Host "##vso[task.logissue type=warning]Digest not produced for $project"
+ }
+ displayName: Package Portable Astred Digests
+ env:
+ ASTRED_SOURCES_PATH: ${{ parameters.sourcesPath }}
+ ASTRED_CAPTURE_PATH: ${{ parameters.capturePath }}
+ condition: succeededOrFailed()
+ continueOnError: true
+
+- task: UploadAstred@0
+ displayName: Upload Digest to Astred
+ condition: and(succeededOrFailed(), eq(variables['ASTRED_DIGEST_READY'], 'true'))
+ inputs:
+ SourcePath: ${{ parameters.capturePath }}/upload
+ env:
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
diff --git a/eng/common/core-templates/steps/get-github-app-token.yml b/eng/common/core-templates/steps/get-github-app-token.yml
index 6d42a48d3c..3eeb5a4c1b 100644
--- a/eng/common/core-templates/steps/get-github-app-token.yml
+++ b/eng/common/core-templates/steps/get-github-app-token.yml
@@ -1,13 +1,11 @@
# 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.
+# with an RSA private key (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.
+# - A GitHub App ID and PEM private key stored as Azure Key Vault secrets.
+# - The Azure service connection passed via `azureSubscription` must have
+# `Get` access to those two secrets.
# - The App must be installed on the target organization/account
# (`installationOwner`) with the permissions/repositories you need.
#
@@ -17,23 +15,18 @@
# enterprise classic-PAT lifetime policy.
parameters:
-# Azure DevOps service connection (federated) that can call
-# `az keyvault key sign` on the App's signing key.
+# Azure DevOps service connection (federated) that can read the App credentials.
- name: azureSubscription
type: string
-# Name of the Key Vault that holds the GitHub App's RSA signing key.
+# Name of the Key Vault holding Secret Manager's github-app-secret projections.
- name: keyVaultName
type: string
-# Name of the RSA key inside the Key Vault (the App's private key).
-- name: keyName
+- name: appIdSecretName
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
+- name: appPrivateKeySecretName
type: string
# Login of the organization or user account whose installation we should
@@ -73,7 +66,7 @@ steps:
inlineScript: |
& "$(System.DefaultWorkingDirectory)/eng/common/Get-GitHubAppToken.ps1" `
-KeyVaultName '${{ parameters.keyVaultName }}' `
- -KeyName '${{ parameters.keyName }}' `
- -AppClientId '${{ parameters.appClientId }}' `
+ -AppIdSecretName '${{ parameters.appIdSecretName }}' `
+ -AppPrivateKeySecretName '${{ parameters.appPrivateKeySecretName }}' `
-InstallationOwner '${{ parameters.installationOwner }}' `
-OutputVariableName '${{ parameters.outputVariableName }}'
diff --git a/eng/common/core-templates/steps/send-to-helix.yml b/eng/common/core-templates/steps/send-to-helix.yml
index ec7a200039..62cce4d4ea 100644
--- a/eng/common/core-templates/steps/send-to-helix.yml
+++ b/eng/common/core-templates/steps/send-to-helix.yml
@@ -4,7 +4,9 @@ parameters:
HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/'
HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number
HelixTargetQueues: '' # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues
- HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group
+ HelixAccessToken: '' # optional -- legacy access token; not forwarded when HelixUseEntraAuthentication is true
+ HelixUseEntraAuthentication: false # optional -- use refreshable Entra authentication instead of a PAT or anonymous access
+ HelixAzureSubscription: '' # required when HelixUseEntraAuthentication is true -- Azure service connection ID authorized for Helix
HelixProjectPath: 'eng/common/helixpublish.proj' # optional -- path to the project file to build relative to BUILD_SOURCESDIRECTORY
HelixProjectArguments: '' # optional -- arguments passed to the build command
HelixConfiguration: '' # optional -- additional property attached to a job
@@ -32,12 +34,35 @@ parameters:
continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false
steps:
+ - ${{ if and(eq(parameters.HelixUseEntraAuthentication, true), eq(parameters.HelixAzureSubscription, '')) }}:
+ - pwsh: throw "HelixAzureSubscription must be set when HelixUseEntraAuthentication is true."
+ displayName: Validate Helix Entra authentication
+ condition: ${{ parameters.condition }}
+
+ - ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
+ - task: AzureCLI@2
+ displayName: Initialize Helix Entra authentication
+ inputs:
+ azureSubscription: ${{ parameters.HelixAzureSubscription }}
+ addSpnToEnvironment: true
+ scriptType: pscore
+ scriptLocation: inlineScript
+ inlineScript: |
+ if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) {
+ throw "The Helix Azure service connection did not provide a service principal or tenant ID."
+ }
+
+ Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId"
+ Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId"
+ condition: ${{ parameters.condition }}
+
- powershell: >
$(Build.SourcesDirectory)\eng\common\msbuild.ps1
$(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }}
/restore
/p:TreatWarningsAsErrors=false
/p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }}
+ /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }}
${{ parameters.HelixProjectArguments }}
/t:Test
/bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog
@@ -49,7 +74,12 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
- HelixAccessToken: ${{ parameters.HelixAccessToken }}
+ ${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
+ HelixAccessToken: ${{ parameters.HelixAccessToken }}
+ ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
+ AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
+ AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
+ AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
@@ -76,6 +106,7 @@ steps:
/restore
/p:TreatWarningsAsErrors=false
/p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }}
+ /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }}
${{ parameters.HelixProjectArguments }}
/t:Test
/bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog
@@ -87,7 +118,12 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
- HelixAccessToken: ${{ parameters.HelixAccessToken }}
+ ${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
+ HelixAccessToken: ${{ parameters.HelixAccessToken }}
+ ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
+ AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
+ AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
+ AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
@@ -108,4 +144,3 @@ steps:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT'))
continueOnError: ${{ parameters.continueOnError }}
-
diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh
index f58abbd2d1..3fea306bc2 100755
--- a/eng/common/cross/build-rootfs.sh
+++ b/eng/common/cross/build-rootfs.sh
@@ -532,27 +532,33 @@ ensureDownloadTool()
}
if [[ "$__CodeName" == "alpine" ]]; then
- __ApkToolsVersion=2.12.11
+ __ApkToolsVersion=2.14.4-r1
__ApkToolsDir="$(mktemp -d)"
__ApkKeysDir="$(mktemp -d)"
arch="$(uname -m)"
__AlpineRepo="${__AlpineRepoOverride:-https://dl-cdn.alpinelinux.org/alpine}"
ensureDownloadTool
+ __ApkToolsPackage="$__ApkToolsDir/apk-tools-static.apk"
+ __ApkToolsUrl="$__AlpineRepo/v3.20/main/$arch/apk-tools-static-$__ApkToolsVersion.apk"
if [[ "$__hasWget" == 1 ]]; then
- wget -P "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static"
+ wget -O "$__ApkToolsPackage" "$__ApkToolsUrl"
else
- curl -SLO --create-dirs --output-dir "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static"
+ curl -fSL -o "$__ApkToolsPackage" "$__ApkToolsUrl"
fi
+
if [[ "$arch" == "x86_64" ]]; then
- __ApkToolsSHA512SUM="53e57b49230da07ef44ee0765b9592580308c407a8d4da7125550957bb72cb59638e04f8892a18b584451c8d841d1c7cb0f0ab680cc323a3015776affaa3be33"
+ __ApkToolsSHA512SUM="b1b3cc382aa0ec26a2c24b742701a1f9885d0678365f9aea15d3d005926b06ecc802659cec8a7deba2717af99c19c708a17c23e1f0f07742268ee5be5400eb9e"
elif [[ "$arch" == "aarch64" ]]; then
- __ApkToolsSHA512SUM="9e2b37ecb2b56c05dad23d379be84fd494c14bd730b620d0d576bda760588e1f2f59a7fcb2f2080577e0085f23a0ca8eadd993b4e61c2ab29549fdb71969afd0"
+ __ApkToolsSHA512SUM="61f9a636c5ac4e96e7a3f69fd65e60fc57b3ec8b23619c4df86f59b89e71d1309b3e406388945bdf0dd9168dac22df376943a70ff3efa179e5687e586f825fb0"
else
- echo "WARNING: add missing hash for your host architecture. To find the value, use: 'find /tmp -name apk.static -exec sha512sum {} \;'"
+ >&2 echo "ERROR: Unsupported apk-tools-static host architecture '$arch'."
+ exit 1
fi
- echo "$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static" | sha512sum -c
+ echo "$__ApkToolsSHA512SUM $__ApkToolsPackage" | sha512sum -c
+ tar -xzf "$__ApkToolsPackage" -C "$__ApkToolsDir" --strip-components=1 sbin/apk.static
+ rm "$__ApkToolsPackage"
chmod +x "$__ApkToolsDir/apk.static"
if [[ "$__AlpineVersion" == "edge" ]]; then
diff --git a/eng/common/msbuild.ps1 b/eng/common/msbuild.ps1
index b6dfb570ea..8076945064 100644
--- a/eng/common/msbuild.ps1
+++ b/eng/common/msbuild.ps1
@@ -3,7 +3,7 @@ Param(
[string] $verbosity = 'minimal',
[bool] $warnAsError = $true,
[bool] $nodeReuse = $true,
- [bool][Alias('mt')]$msbuildMultiThreaded = $false,
+ [bool][Alias('mt')]$msbuildMultiThreaded = $true,
[switch] $ci,
[switch] $prepareMachine,
[switch] $excludePrereleaseVS,
@@ -19,6 +19,11 @@ try {
$nodeReuse = $false
}
+ # MSBuild's multi-threaded mode isn't run on CI unless it was explicitly requested via -msbuildMultiThreaded.
+ if ($ci -and -not $PSBoundParameters.ContainsKey('msbuildMultiThreaded')) {
+ $msbuildMultiThreaded = $false
+ }
+
MSBuild @extraArgs
}
catch {
diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1
index e84033dad9..cea14279b8 100644
--- a/eng/common/tools.ps1
+++ b/eng/common/tools.ps1
@@ -31,9 +31,8 @@
# Set to true to reuse msbuild nodes. Recommended to not reuse on CI.
[bool]$nodeReuse = if (Test-Path variable:nodeReuse) { $nodeReuse } else { !$ci }
-# Set to true to build with MSBuild's multi-threaded mode (-mt). Opt-in for now, so off unless it was
-# explicitly requested. It's intended to become the default for local builds once it has proven out.
-[bool]$msbuildMultiThreaded = if (Test-Path variable:msbuildMultiThreaded) { $msbuildMultiThreaded } else { $false }
+# Set to true to build with MSBuild's multi-threaded mode (-mt). Enabled by default for local builds and not run on CI.
+[bool]$msbuildMultiThreaded = if (Test-Path variable:msbuildMultiThreaded) { $msbuildMultiThreaded } else { !$ci }
# Configures warning treatment in msbuild.
[bool]$warnAsError = if (Test-Path variable:warnAsError) { $warnAsError } else { $true }
diff --git a/eng/common/tools.sh b/eng/common/tools.sh
index 4d14b100b0..fdbc19243f 100755
--- a/eng/common/tools.sh
+++ b/eng/common/tools.sh
@@ -60,10 +60,13 @@ else
node_reuse=${node_reuse:-true}
fi
-# Set to true to build with MSBuild's multi-threaded mode (-mt). Opt-in for now, so off unless it was
-# explicitly requested. It's intended to become the default for local builds once it has proven out.
+# Set to true to build with MSBuild's multi-threaded mode (-mt). Enabled by default for local builds and not run on CI.
msbuild_multi_threaded=$(NormalizeBoolArg "${msbuild_multi_threaded:-}")
-msbuild_multi_threaded=${msbuild_multi_threaded:-false}
+if [[ "$ci" == true ]]; then
+ msbuild_multi_threaded=${msbuild_multi_threaded:-false}
+else
+ msbuild_multi_threaded=${msbuild_multi_threaded:-true}
+fi
# Configures warning treatment in msbuild.
warn_as_error=$(NormalizeBoolArg "${warn_as_error:-}")
diff --git a/global.json b/global.json
index ab5a15ec09..f773783aab 100644
--- a/global.json
+++ b/global.json
@@ -13,8 +13,8 @@
}
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26456.1",
- "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26456.1",
+ "Microsoft.DotNet.Arcade.Sdk": "12.0.0-beta.26469.3",
+ "Microsoft.DotNet.Helix.Sdk": "12.0.0-beta.26469.3",
"Microsoft.Build.NoTargets": "3.7.0",
"Microsoft.Build.Traversal": "3.2.0"
}