Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions eng/Version.Details.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,13 +51,13 @@
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26379.2">
<Dependency Name="Microsoft.DotNet.Build.Tasks.Packaging" Version="10.0.0-beta.26431.5">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>c5d54a9de6e0e20a85e37fa3576a37235276772b</Sha>
<Sha>c642c365e52bd6c5bd979f759963260fb79d710a</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
</PropertyGroup>
<PropertyGroup>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26379.2</MicrosoftDotNetBuildTasksPackagingVersion>
<MicrosoftDotNetBuildTasksPackagingVersion>10.0.0-beta.26431.5</MicrosoftDotNetBuildTasksPackagingVersion>
<!-- Python -->
<runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxarm64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
<runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>10.0.0-alpha.1.26407.2</runtimeosxx64MicrosoftNETCoreRuntimeWasmPythonTransportVersion>
Expand Down
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
# 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 = [System.Collections.Generic.List[object]]::new()
$page = 1
do {
$pageResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations?per_page=100&page=$page" `
-Headers $headers `
-Method Get
$pageInstallationCount = 0
foreach ($installation in $pageResponse) {
$installations.Add($installation)
$pageInstallationCount++
}
$page++
} 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."
exit 1
}
$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner })
if ($matchingInstallations.Count -eq 0) {
$found = ($installations | ForEach-Object { $_.account.login }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found"
exit 1
}
if ($matchingInstallations.Count -ne 1) {
$matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', '
Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds"
exit 1
}
$installation = $matchingInstallations[0]
Write-Host "Using installation $($installation.id) for '$($installation.account.login)'."

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
}
2 changes: 2 additions & 0 deletions eng/common/build.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ Param(
[string][Alias('v')]$verbosity = "minimal",
[string] $msbuildEngine = $null,
[bool] $warnAsError = $true,
[string] $warnNotAsError = '',
[bool] $nodeReuse = $true,
[switch] $buildCheck = $false,
[switch][Alias('r')]$restore,
Expand DownExpand Up@@ -70,6 +71,7 @@ function Print-Usage() {
Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)"
Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build"
Write-Host " -warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
Write-Host " -warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
Write-Host " -msbuildEngine <value> Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)."
Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio"
Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)"
Expand Down
6 changes: 6 additions & 0 deletions eng/common/build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ usage()
echo " --prepareMachine Prepare machine for CI run, clean up processes after build"
echo " --nodeReuse <value> Sets nodereuse msbuild parameter ('true' or 'false')"
echo " --warnAsError <value> Sets warnaserror msbuild parameter ('true' or 'false')"
echo " --warnNotAsError <value> Sets a semi-colon delimited list of warning codes that should not be treated as errors"
echo " --buildCheck <value> Sets /check msbuild parameter"
echo " --fromVMR Set when building from within the VMR"
echo ""
Expand DownExpand Up@@ -78,6 +79,7 @@ ci=false
clean=false

warn_as_error=true
warn_not_as_error=''
node_reuse=true
build_check=false
binary_log=false
Expand DownExpand Up@@ -176,6 +178,10 @@ while [[ $# > 0 ]]; do
warn_as_error=$2
shift
;;
-warnnotaserror)
warn_not_as_error=$2
shift
;;
-nodereuse)
node_reuse=$2
shift
Expand Down
46 changes: 37 additions & 9 deletions eng/common/core-templates/job/onelocbuild.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,11 +9,20 @@ parameters:
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.
# The `internal` and `DevDiv` System.TeamProject values have same-named, project-scoped
# connections. Other values, and any pipeline that sets this to '', use the CeapexPat parameter.
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'

SourcesDirectory: $(System.DefaultWorkingDirectory)
CreatePr: true
AutoCompletePr: false
Expand DownExpand Up@@ -79,15 +88,31 @@ 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')) }}:
# Acquire an Entra token when System.TeamProject is `internal` or `DevDiv`.
- ${{ 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 }}

# 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))) }}:
- template: /eng/common/core-templates/steps/get-github-app-token.yml
parameters:
is1ESPipeline: ${{ parameters.is1ESPipeline }}
${{ if and(eq(variables['System.TeamProject'], 'DevDiv'), eq(parameters.GitHubAppServiceConnection, 'dnceng-oneloc-githubapp')) }}:
azureSubscription: 'devdiv-oneloc-githubapp'
${{ else }}:
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@@ -103,13 +128,16 @@ jobs:
isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}
isShouldReusePrSelected: ${{ parameters.ReusePr }}
packageSourceAuth: patAuth
${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}:
${{ if and(ne(parameters.CeapexServiceConnection, ''), or(eq(variables['System.TeamProject'], 'internal'), eq(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: $(CeapexEntraToken)
${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}:
${{ if or(eq(parameters.CeapexServiceConnection, ''), and(ne(variables['System.TeamProject'], 'internal'), ne(variables['System.TeamProject'], 'DevDiv'))) }}:
patVariable: ${{ parameters.CeapexPat }}
${{ if eq(parameters.RepoType, 'gitHub') }}:
repoType: ${{ parameters.RepoType }}
gitHubPatVariable: "${{ parameters.GithubPat }}"
${{ 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 }}"
${{ 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 numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,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 numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,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 numberDiff line numberDiff 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
Loading