Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ bld/
[Ll]ogs/

# .NET Core
.dotnet/
.packages/
.tools/
project.lock.json
project.fragment.lock.json
artifacts/
Expand Down Expand Up @@ -55,3 +58,6 @@ CodeCoverage/
*.VisualState.xml
TestResult.xml
nunit-*.xml

# Keep vendored Arcade platform directories tracked.
!eng/common/**
6 changes: 6 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />
<PropertyGroup>
<ClientToolsArcadePropsImported>true</ClientToolsArcadePropsImported>
</PropertyGroup>
</Project>
6 changes: 6 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />
<PropertyGroup>
<ClientToolsArcadeTargetsImported>true</ClientToolsArcadeTargetsImported>
</PropertyGroup>
</Project>
12 changes: 12 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
</packageSources>
<disabledPackageSources>
<clear />
</disabledPackageSources>
</configuration>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Client.Tools

This repository contains tools shipped by the NuGet Client team to help developers effectively use the latest NuGet features.
This repository contains tools shipped by the NuGet Client team to help developers effectively use the latest NuGet features.
3 changes: 3 additions & 0 deletions build.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off
call "%~dp0eng\common\build.cmd" -restore -build %*
exit /b %ErrorLevel%
5 changes: 5 additions & 0 deletions eng/Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<ProjectToBuild Include="$(RepoRoot)eng\Infrastructure.proj" />
</ItemGroup>
</Project>
21 changes: 21 additions & 0 deletions eng/Infrastructure.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.Build.NoTargets">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsShipping>false</IsShipping>
</PropertyGroup>

<Target Name="ValidateArcadeInfrastructure" BeforeTargets="CollectPackageReferences;Build">
<Error Condition="'$(ClientToolsArcadePropsImported)' != 'true'"
Text="Directory.Build.props must import Microsoft.DotNet.Arcade.Sdk." />
<Error Condition="'$(ClientToolsArcadeTargetsImported)' != 'true'"
Text="Directory.Build.targets must import Microsoft.DotNet.Arcade.Sdk." />
<Error Condition="'$(VersionPrefix)' == ''"
Text="eng\Versions.props must define VersionPrefix." />
<Error Condition="'$(RepoRoot)' == '' or '$(ArtifactsDir)' == '' or '$(ArtifactsLogDir)' == ''"
Text="Arcade repository and artifact paths must be configured." />
<Error Condition="'$(IsPackable)' != 'false' or '$(IsShipping)' != 'false'"
Text="The infrastructure project must remain non-packable and non-shipping." />
<Message Importance="high" Text="Arcade infrastructure validation succeeded." />
</Target>
</Project>
10 changes: 10 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Dependencies>
<ProductDependencies />
<ToolsetDependencies>
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="10.0.0-beta.26505.115">
<Uri>https://github.com/dotnet/arcade</Uri>
<Sha>f6209a1c4124a46b7a202ac73d000e220f562d62</Sha>
</Dependency>
</ToolsetDependencies>
</Dependencies>
6 changes: 6 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>0.1.0</VersionPrefix>
<PreReleaseVersionLabel>beta</PreReleaseVersionLabel>
</PropertyGroup>
</Project>
4 changes: 4 additions & 0 deletions eng/common/BuildConfiguration/build-configuration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"RetryCountLimit": 1,
"RetryByAnyError": false
}
2 changes: 2 additions & 0 deletions eng/common/CIBuild.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*"
165 changes: 165 additions & 0 deletions eng/common/Get-GitHubAppToken.ps1
Original file line number Diff line number Diff 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
}
11 changes: 11 additions & 0 deletions eng/common/PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@{
IncludeRules=@('PSAvoidUsingCmdletAliases',
'PSAvoidUsingWMICmdlet',
'PSAvoidUsingPositionalParameters',
'PSAvoidUsingInvokeExpression',
'PSUseDeclaredVarsMoreThanAssignments',
'PSUseCmdletCorrectly',
'PSStandardDSCFunctionsInResource',
'PSUseIdenticalMandatoryParametersForDSC',
'PSUseIdenticalParametersForDSC')
}
28 changes: 28 additions & 0 deletions eng/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Don't touch this folder

uuuuuuuuuuuuuuuuuuuu
u" uuuuuuuuuuuuuuuuuu "u
u" u$$$$$$$$$$$$$$$$$$$$u "u
u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u
u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
$ $$$" ... "$... ...$" ... "$$$ ... "$$$ $
$ $$$u `"$$$$$$$ $$$ $$$$$ $$ $$$ $$$ $
$ $$$$$$uu "$$$$ $$$ $$$$$ $$ """ u$$$ $
$ $$$""$$$ $$$$ $$$u "$$$" u$$ $$$$$$$$ $
$ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
"u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
"u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
"u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
"u "$$$$$$$$$$$$$$$$$$$$$$$$" u"
"u "$$$$$$$$$$$$$$$$$$$$" u"
"u """""""""""""""""" u"
""""""""""""""""""""

!!! Changes made in this directory are subject to being overwritten by automation !!!

The files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first.
Loading