diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index f8720d30..b4b082f4 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -3,13 +3,13 @@
-
+
https://github.com/dotnet/arcade
- 51abc19095ae366ecb773826993966dc8a1063eb
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
-
+
https://github.com/dotnet/arcade
- 51abc19095ae366ecb773826993966dc8a1063eb
+ 63c79a28ca6e086d59c5553e7ed69cacb1d52fbd
diff --git a/eng/Versions.props b/eng/Versions.props
index b3e3d7a1..88fb7e7c 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,7 +15,7 @@
13.0.1
3.2.2146
9.3.3
- 11.0.0-beta.26421.2
+ 12.0.0-beta.26469.3
6.0.3-dotnet.3
diff --git a/eng/common/Get-GitHubAppToken.ps1 b/eng/common/Get-GitHubAppToken.ps1
index 9c7e3dcd..ec005e48 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 = @{
@@ -110,22 +153,23 @@ $headers = @{
Write-Host "Looking up installation for '$InstallationOwner'..."
try {
- $installations = @()
+ $installations = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
- # Assign the response before wrapping it in @(). PowerShell otherwise
- # preserves a top-level JSON array as one nested pipeline object.
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
- $pageInstallations = @($pageResponse)
- $installations += $pageInstallations
+ $pageInstallationCount = 0
+ foreach ($installation in $pageResponse) {
+ $installations.Add($installation)
+ $pageInstallationCount++
+ }
$page++
- } while ($pageInstallations.Count -eq 100)
+ } 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 fee2f839..305132a6 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 109d83ff..f65b048a 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 81ecccdd..7417afdf 100644
--- a/eng/common/core-templates/job/helix-job-monitor.yml
+++ b/eng/common/core-templates/job/helix-job-monitor.yml
@@ -52,16 +52,34 @@ 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
default: 30
+# Maximum number of work items whose results may be downloaded, parsed, and
+# uploaded concurrently.
+- name: testResultUploadParallelism
+ type: number
+ default: 48
+
# When 'true' (the default), Helix work items that exit 0 but have failed AzDO test results
# are treated as failed: they count toward the monitor's exit code and are resubmitted by a
# later invocation's retry pass. Set to 'false' to fall back to exit-code-only outcomes.
@@ -135,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
@@ -208,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 }}'
@@ -215,6 +254,8 @@ jobs:
--max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully.
--stage-name '$(System.StageName)'
--stage-attempt '$(System.StageAttempt)'
+ --job-attempt '$(System.JobAttempt)'
+ --test-result-upload-parallelism '${{ parameters.testResultUploadParallelism }}'
)
organization='${{ parameters.organization }}'
@@ -267,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 cb60f529..5b070260 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 ce077368..b772dc57 100644
--- a/eng/common/core-templates/job/onelocbuild.yml
+++ b/eng/common/core-templates/job/onelocbuild.yml
@@ -5,24 +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).
- # 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.
+ # 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
@@ -50,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 }}"
@@ -81,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
@@ -90,18 +83,15 @@ 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/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 }}
@@ -110,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 }}
@@ -131,16 +121,10 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
- ${{ 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 }}
+ 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/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml
index bac6ac5f..b0dc8f17 100644
--- a/eng/common/core-templates/job/source-index-stage1.yml
+++ b/eng/common/core-templates/job/source-index-stage1.yml
@@ -12,6 +12,7 @@ jobs:
- job: SourceIndexStage1
dependsOn: ${{ parameters.dependsOn }}
condition: ${{ parameters.condition }}
+ continueOnError: true
variables:
- name: BinlogPath
value: ${{ parameters.binlogPath }}
@@ -38,9 +39,11 @@ jobs:
- ${{ each preStep in parameters.preSteps }}:
- ${{ preStep }}
- - script: ${{ parameters.sourceIndexBuildCommand }}
- displayName: Build Repository
+ - ${{ if ne(parameters.sourceIndexBuildCommand, '') }}:
+ - script: ${{ parameters.sourceIndexBuildCommand }}
+ displayName: Build Repository
- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml
parameters:
binLogPath: ${{ parameters.binLogPath }}
+ runAsPublic: ${{ parameters.runAsPublic }}
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 00000000..b914082f
--- /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 6d42a48d..3eeb5a4c 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 ec7a2000..62cce4d4 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/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml
index fdca6223..6d4173aa 100644
--- a/eng/common/core-templates/steps/source-index-stage1-publish.yml
+++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml
@@ -1,7 +1,9 @@
parameters:
- sourceIndexUploadPackageVersion: 2.0.0-20260521.2
- sourceIndexProcessBinlogPackageVersion: 1.0.1-20260521.2
+ runAsPublic: false
+ sourceIndexUploadPackageVersion: '2.0.0-20260521.2'
+ sourceIndexComplogPackageVersion: '*'
sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json
+ sourceIndexPublicPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json
binlogPath: artifacts/log/Debug/Build.binlog
steps:
@@ -14,14 +16,17 @@ steps:
workingDirectory: $(Agent.TempDirectory)
- script: |
- $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools
- $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools
- displayName: "Source Index: Download netsourceindex Tools"
+ $(Agent.TempDirectory)/dotnet/dotnet tool install complog --version "${{parameters.sourceIndexComplogPackageVersion}}" --source ${{parameters.sourceIndexPublicPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools
+ $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version "${{parameters.sourceIndexUploadPackageVersion}}" --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools
+ displayName: "Source Index: Download Tools"
# Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk.
workingDirectory: $(Agent.TempDirectory)
-- script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i ${{parameters.BinlogPath}} -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output
- displayName: "Source Index: Process Binlog into indexable sln"
+- script: |
+ mkdir ".source-index/stage1output"
+ git rev-parse HEAD > .source-index/stage1output/hash
+ $(Agent.TempDirectory)/.source-index/tools/complog create ${{parameters.BinlogPath}} -o .source-index/stage1output/build.complog
+ displayName: "Source Index: Process Binlog into Complog"
- ${{ if and(ne(parameters.runAsPublic, 'true'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- task: AzureCLI@2
diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh
index f58abbd2..3fea306b 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/init-tools-native.sh b/eng/common/init-tools-native.sh
index 3e6a8d6a..4de1a04f 100755
--- a/eng/common/init-tools-native.sh
+++ b/eng/common/init-tools-native.sh
@@ -60,8 +60,8 @@ while (($# > 0)); do
echo " - (default) %USERPROFILE%/.netcoreeng/native"
echo ""
echo " --clean Switch specifying not to install anything, but cleanup native asset folders"
- echo " --donotabortonfailure Switch specifiying whether to abort native tools installation on failure"
- echo " --donotdisplaywarnings Switch specifiying whether to display warnings during native tools installation on failure"
+ echo " --donotabortonfailure Switch specifying whether to abort native tools installation on failure"
+ echo " --donotdisplaywarnings Switch specifying whether to display warnings during native tools installation on failure"
echo " --force Clean and then install tools"
echo " --help Print help and exit"
echo ""
@@ -83,7 +83,7 @@ function ReadGlobalJsonNativeTools {
# KEY="" VALUE=""
# followed by a null byte.
#
- # bash: read line with null byte delimeter and push to array (for later `eval`uation).
+ # bash: read line with null byte delimiter and push to array (for later `eval`uation).
while IFS= read -rd '' line; do
native_assets+=("$line")
diff --git a/eng/common/loc/P22DotNetHtmlLocalization.lss b/eng/common/loc/P22DotNetHtmlLocalization.lss
index 5d892d61..c810350e 100644
--- a/eng/common/loc/P22DotNetHtmlLocalization.lss
+++ b/eng/common/loc/P22DotNetHtmlLocalization.lss
@@ -16,7 +16,7 @@
-