From 8776a7fddf664ed35451459e2f50aba8ce23dc51 Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Wed, 11 Feb 2026 10:53:05 -0800 Subject: [PATCH 01/10] Add a script for startup performance measurement (#14345) * Add startup perf collection script * Analyze trace more efficiently * Increase pause between iterations * Fix TraceAnalyzer * Add startup-perf skill --- .github/skills/startup-perf/SKILL.md | 193 +++++ AGENTS.md | 1 + docs/getting-perf-traces.md | 10 +- tools/perf/Measure-StartupPerformance.ps1 | 678 ++++++++++++++++++ tools/perf/TraceAnalyzer/Program.cs | 80 +++ tools/perf/TraceAnalyzer/TraceAnalyzer.csproj | 16 + 6 files changed, 977 insertions(+), 1 deletion(-) create mode 100644 .github/skills/startup-perf/SKILL.md create mode 100644 tools/perf/Measure-StartupPerformance.ps1 create mode 100644 tools/perf/TraceAnalyzer/Program.cs create mode 100644 tools/perf/TraceAnalyzer/TraceAnalyzer.csproj diff --git a/.github/skills/startup-perf/SKILL.md b/.github/skills/startup-perf/SKILL.md new file mode 100644 index 00000000000..33ca4d3875f --- /dev/null +++ b/.github/skills/startup-perf/SKILL.md @@ -0,0 +1,193 @@ +--- +name: startup-perf +description: Measures Aspire application startup performance using dotnet-trace and the TraceAnalyzer tool. Use this when asked to measure impact of a code change on Aspire application startup performance. +--- + +# Aspire Startup Performance Measurement + +This skill provides patterns and practices for measuring .NET Aspire application startup performance using the `Measure-StartupPerformance.ps1` script and the companion `TraceAnalyzer` tool. + +## Overview + +The startup performance tooling collects `dotnet-trace` traces from an Aspire AppHost application and computes the startup duration from `AspireEventSource` events. Specifically, it measures the time between the `DcpModelCreationStart` (event ID 17) and `DcpModelCreationStop` (event ID 18) events emitted by the `Microsoft-Aspire-Hosting` EventSource provider. + +**Script Location**: `tools/perf/Measure-StartupPerformance.ps1` +**TraceAnalyzer Location**: `tools/perf/TraceAnalyzer/` +**Documentation**: `docs/getting-perf-traces.md` + +## Prerequisites + +- PowerShell 7+ +- `dotnet-trace` global tool (`dotnet tool install -g dotnet-trace`) +- .NET SDK (restored via `./restore.cmd` or `./restore.sh`) + +## Quick Start + +### Single Measurement + +```powershell +# From repository root — measures the default TestShop.AppHost +.\tools\perf\Measure-StartupPerformance.ps1 +``` + +### Multiple Iterations with Statistics + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 +``` + +### Custom Project + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -ProjectPath "path\to\MyApp.AppHost.csproj" -Iterations 3 +``` + +### Preserve Traces for Manual Analysis + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 3 -PreserveTraces -TraceOutputDirectory "C:\traces" +``` + +### Verbose Output + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Verbose +``` + +## Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `ProjectPath` | TestShop.AppHost | Path to the AppHost `.csproj` to measure | +| `Iterations` | 1 | Number of measurement runs (1–100) | +| `PreserveTraces` | `$false` | Keep `.nettrace` files after analysis | +| `TraceOutputDirectory` | temp folder | Directory for preserved trace files | +| `SkipBuild` | `$false` | Skip `dotnet build` before running | +| `TraceDurationSeconds` | 60 | Maximum trace collection time (1–86400) | +| `PauseBetweenIterationsSeconds` | 45 | Pause between iterations (0–3600) | +| `Verbose` | `$false` | Show detailed output | + +## How It Works + +The script follows this sequence: + +1. **Prerequisites check** — Verifies `dotnet-trace` is installed and the project exists. +2. **Build** — Builds the AppHost project in Release configuration (unless `-SkipBuild`). +3. **Build TraceAnalyzer** — Builds the companion `tools/perf/TraceAnalyzer` project. +4. **For each iteration:** + a. Locates the compiled executable (Arcade-style or traditional output paths). + b. Reads `launchSettings.json` for environment variables. + c. Launches the AppHost as a separate process. + d. Attaches `dotnet-trace` to the running process with the `Microsoft-Aspire-Hosting` provider. + e. Waits for the trace to complete (duration timeout or process exit). + f. Runs the TraceAnalyzer to extract the startup duration from the `.nettrace` file. + g. Cleans up processes. +5. **Reports results** — Prints per-iteration times and statistics (min, max, average, std dev). + +## TraceAnalyzer Tool + +The `tools/perf/TraceAnalyzer` is a small .NET console app that parses `.nettrace` files using the `Microsoft.Diagnostics.Tracing.TraceEvent` library. + +### What It Does + +- Opens the `.nettrace` file with `EventPipeEventSource` +- Listens for events from the `Microsoft-Aspire-Hosting` provider +- Extracts timestamps for `DcpModelCreationStart` (ID 17) and `DcpModelCreationStop` (ID 18) +- Outputs the duration in milliseconds (or `"null"` if events are not found) + +### Standalone Usage + +```bash +dotnet run --project tools/perf/TraceAnalyzer -c Release -- +``` + +## Understanding Output + +### Successful Run + +``` +================================================== + Aspire Startup Performance Measurement +================================================== + +Project: TestShop.AppHost +Iterations: 3 +... + +Iteration 1 +---------------------------------------- +Starting TestShop.AppHost... +Attaching trace collection to PID 12345... +Collecting performance trace... +Trace collection completed. +Analyzing trace: ... +Startup time: 1234.56 ms + +... + +================================================== + Results Summary +================================================== + +Iteration StartupTimeMs +--------- ------------- + 1 1234.56 + 2 1189.23 + 3 1201.45 + +Statistics: + Successful iterations: 3 / 3 + Minimum: 1189.23 ms + Maximum: 1234.56 ms + Average: 1208.41 ms + Std Dev: 18.92 ms +``` + +### Common Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `dotnet-trace is not installed` | Missing global tool | Run `dotnet tool install -g dotnet-trace` | +| `Could not find compiled executable` | Project not built | Remove `-SkipBuild` or build manually | +| `Could not find DcpModelCreation events` | Trace too short or events not emitted | Increase `-TraceDurationSeconds` | +| `Application exited immediately` | App crash on startup | Check app logs, ensure dependencies are available | +| `dotnet-trace exited with code != 0` | Trace collection error | Check verbose output; trace file may still be valid | + +## Comparing Before/After Performance + +To measure the impact of a code change: + +```powershell +# 1. Measure baseline (on main branch) +git checkout main +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 -PreserveTraces -TraceOutputDirectory "C:\traces\baseline" + +# 2. Measure with changes +git checkout my-feature-branch +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 -PreserveTraces -TraceOutputDirectory "C:\traces\feature" + +# 3. Compare the reported averages and std devs +``` + +Use enough iterations (5+) and a consistent pause between iterations for reliable comparisons. + +## Collecting Traces for Manual Analysis + +If you need to inspect trace files manually (e.g., in PerfView or Visual Studio): + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -PreserveTraces -TraceOutputDirectory "C:\my-traces" +``` + +See `docs/getting-perf-traces.md` for guidance on analyzing traces with PerfView or `dotnet trace report`. + +## EventSource Provider Details + +The `Microsoft-Aspire-Hosting` EventSource emits events for key Aspire lifecycle milestones. The startup performance script focuses on: + +| Event ID | Event Name | Description | +|----------|------------|-------------| +| 17 | `DcpModelCreationStart` | Marks the beginning of DCP model creation | +| 18 | `DcpModelCreationStop` | Marks the completion of DCP model creation | + +The measured startup time is the wall-clock difference between these two events, representing the time to create all application services and supporting dependencies. diff --git a/AGENTS.md b/AGENTS.md index cb4d5711eb1..cb6596c3d31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -355,6 +355,7 @@ The following specialized skills are available in `.github/skills/`: - **test-management**: Quarantines or disables flaky/problematic tests using the QuarantineTools utility - **connection-properties**: Expert for creating and improving Connection Properties in Aspire resources - **dependency-update**: Guides dependency version updates by checking nuget.org, triggering the dotnet-migrate-package Azure DevOps pipeline, and monitoring runs +- **startup-perf**: Measures Aspire application startup performance using dotnet-trace and the TraceAnalyzer tool ## Pattern-Based Instructions diff --git a/docs/getting-perf-traces.md b/docs/getting-perf-traces.md index a669c591ee0..94a5a14a0d5 100644 --- a/docs/getting-perf-traces.md +++ b/docs/getting-perf-traces.md @@ -28,8 +28,16 @@ Once you are ready, hit "Start Collection" button and run your scenario. When done with the scenario, hit "Stop Collection". Wait for PerfView to finish merging and analyzing data (the "working" status bar stops flashing). -### Verify that the trace contains Aspire data +### Verify that PerfView trace contains Aspire data This is an optional step, but if you are wondering if your trace has been captured properly, you can check the following: 1. Open the trace (usually named PerfViewData.etl, if you haven't changed the name) and double click Events view. Verify you have a bunch of events from the Microsoft-Aspire-Hosting provider. + +## Profiling scripts + +The `tools/perf` folder in the repository contains scripts that help quickly assess the impact of code changes on key performance scenarios. Currently available scripts are: + +| Script | Description | +| --- | --------- | +| `Measure-StartupPerformance.ps1` | Measures startup time for a specific Aspire project. More specifically, the script measures the time to get all application services and supporting dependencies CREATED; the application is not necessarily responsive after measured time. | diff --git a/tools/perf/Measure-StartupPerformance.ps1 b/tools/perf/Measure-StartupPerformance.ps1 new file mode 100644 index 00000000000..626adff10ed --- /dev/null +++ b/tools/perf/Measure-StartupPerformance.ps1 @@ -0,0 +1,678 @@ +<# +.SYNOPSIS + Measures .NET Aspire application startup performance by collecting ETW traces. + +.DESCRIPTION + This script runs an Aspire application, collects a performance trace + using dotnet-trace, and computes the startup time from AspireEventSource events. + The trace collection ends when the DcpModelCreationStop event is fired. + +.PARAMETER ProjectPath + Path to the AppHost project (.csproj) to measure. Can be absolute or relative. + Defaults to the TestShop.AppHost project in the playground folder. + +.PARAMETER Iterations + Number of times to run the scenario and collect traces. Defaults to 1. + +.PARAMETER PreserveTraces + If specified, trace files are preserved after the run. By default, traces are + stored in a temporary folder and deleted after analysis. + +.PARAMETER TraceOutputDirectory + Directory where trace files will be saved when PreserveTraces is set. + Defaults to a 'traces' subdirectory in the script folder. + +.PARAMETER SkipBuild + If specified, skips building the project before running. + +.PARAMETER TraceDurationSeconds + Duration in seconds for the trace collection. Defaults to 60 (1 minute). + The value is automatically converted to the dd:hh:mm:ss format required by dotnet-trace. + +.PARAMETER PauseBetweenIterationsSeconds + Number of seconds to pause between iterations. Defaults to 15. + Set to 0 to disable the pause. + +.PARAMETER Verbose + If specified, shows detailed output during execution. + +.EXAMPLE + .\Measure-StartupPerformance.ps1 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 5 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -ProjectPath "C:\MyApp\MyApp.AppHost.csproj" -Iterations 3 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 3 -PreserveTraces -TraceOutputDirectory "C:\traces" + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -TraceDurationSeconds 120 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 5 -PauseBetweenIterationsSeconds 30 + +.NOTES + Requires: + - PowerShell 7+ + - dotnet-trace global tool (dotnet tool install -g dotnet-trace) + - .NET SDK +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$ProjectPath, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 100)] + [int]$Iterations = 1, + + [Parameter(Mandatory = $false)] + [switch]$PreserveTraces, + + [Parameter(Mandatory = $false)] + [string]$TraceOutputDirectory, + + [Parameter(Mandatory = $false)] + [switch]$SkipBuild, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 86400)] + [int]$TraceDurationSeconds = 60, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, 3600)] + [int]$PauseBetweenIterationsSeconds = 45 +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +# Constants +$EventSourceName = 'Microsoft-Aspire-Hosting' +$DcpModelCreationStartEventId = 17 +$DcpModelCreationStopEventId = 18 + +# Get repository root (script is in tools/perf) +$ScriptDir = $PSScriptRoot +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..' '..')).Path + +# Resolve project path +if (-not $ProjectPath) { + # Default to TestShop.AppHost + $ProjectPath = Join-Path $RepoRoot 'playground' 'TestShop' 'TestShop.AppHost' 'TestShop.AppHost.csproj' +} +elseif (-not [System.IO.Path]::IsPathRooted($ProjectPath)) { + # Relative path - resolve from current directory + $ProjectPath = (Resolve-Path $ProjectPath -ErrorAction Stop).Path +} + +$AppHostProject = $ProjectPath +$AppHostDir = Split-Path $AppHostProject -Parent +$AppHostName = [System.IO.Path]::GetFileNameWithoutExtension($AppHostProject) + +# Determine output directory for traces - always use temp directory unless explicitly specified +if ($TraceOutputDirectory) { + $OutputDirectory = $TraceOutputDirectory +} +else { + # Always use a temp directory for traces + $OutputDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-perf-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" +} + +# Only delete temp directory if not preserving traces and no custom directory was specified +$ShouldCleanupDirectory = -not $PreserveTraces -and -not $TraceOutputDirectory + +# Ensure output directory exists +if (-not (Test-Path $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +} + +# Verify prerequisites +function Test-Prerequisites { + Write-Host "Checking prerequisites..." -ForegroundColor Cyan + + # Check dotnet-trace is installed + $dotnetTrace = Get-Command 'dotnet-trace' -ErrorAction SilentlyContinue + if (-not $dotnetTrace) { + throw "dotnet-trace is not installed. Install it with: dotnet tool install -g dotnet-trace" + } + Write-Verbose "dotnet-trace found at: $($dotnetTrace.Source)" + + # Check project exists + if (-not (Test-Path $AppHostProject)) { + throw "AppHost project not found at: $AppHostProject" + } + Write-Verbose "AppHost project found at: $AppHostProject" + + Write-Host "Prerequisites check passed." -ForegroundColor Green +} + +# Build the project +function Build-AppHost { + Write-Host "Building $AppHostName..." -ForegroundColor Cyan + + Push-Location $AppHostDir + try { + $buildOutput = & dotnet build -c Release --nologo 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host ($buildOutput -join "`n") -ForegroundColor Red + throw "Failed to build $AppHostName" + } + Write-Verbose ($buildOutput -join "`n") + Write-Host "Build completed successfully." -ForegroundColor Green + } + finally { + Pop-Location + } +} + +# Run a single iteration of the performance test +function Invoke-PerformanceIteration { + param( + [int]$IterationNumber, + [string]$TraceOutputPath + ) + + Write-Host "`nIteration $IterationNumber" -ForegroundColor Yellow + Write-Host ("-" * 40) -ForegroundColor Yellow + + $nettracePath = "$TraceOutputPath.nettrace" + $appProcess = $null + $traceProcess = $null + + try { + # Find the compiled executable - we need the path to launch it + $exePath = $null + $dllPath = $null + + # Search in multiple possible output locations: + # 1. Arcade-style: artifacts/bin//Release// + # 2. Traditional: /bin/Release// + $searchPaths = @( + (Join-Path $RepoRoot 'artifacts' 'bin' $AppHostName 'Release'), + (Join-Path $AppHostDir 'bin' 'Release') + ) + + foreach ($basePath in $searchPaths) { + if (-not (Test-Path $basePath)) { + continue + } + + # Find TFM subdirectories (e.g., net8.0, net9.0, net10.0) + $tfmDirs = Get-ChildItem -Path $basePath -Directory -Filter 'net*' -ErrorAction SilentlyContinue + foreach ($tfmDir in $tfmDirs) { + $candidateExe = Join-Path $tfmDir.FullName "$AppHostName.exe" + $candidateDll = Join-Path $tfmDir.FullName "$AppHostName.dll" + + if (Test-Path $candidateExe) { + $exePath = $candidateExe + Write-Verbose "Found executable at: $exePath" + break + } + elseif (Test-Path $candidateDll) { + $dllPath = $candidateDll + Write-Verbose "Found DLL at: $dllPath" + break + } + } + + if ($exePath -or $dllPath) { + break + } + } + + if (-not $exePath -and -not $dllPath) { + $searchedPaths = $searchPaths -join "`n - " + throw "Could not find compiled executable or DLL. Searched in:`n - $searchedPaths`nPlease build the project first (without -SkipBuild)." + } + + # Read launchSettings.json to get environment variables + $launchSettingsPath = Join-Path $AppHostDir 'Properties' 'launchSettings.json' + $envVars = @{} + if (Test-Path $launchSettingsPath) { + Write-Verbose "Reading launch settings from: $launchSettingsPath" + try { + # Read the file and remove JSON comments (// style) before parsing + # Only remove lines that start with // (after optional whitespace) to avoid breaking URLs like https:// + $jsonLines = Get-Content $launchSettingsPath + $filteredLines = $jsonLines | Where-Object { $_.Trim() -notmatch '^//' } + $jsonContent = $filteredLines -join "`n" + $launchSettings = $jsonContent | ConvertFrom-Json + + # Try to find a suitable profile (prefer 'http' for simplicity, then first available) + $profile = $null + if ($launchSettings.profiles.http) { + $profile = $launchSettings.profiles.http + Write-Verbose "Using 'http' launch profile" + } + elseif ($launchSettings.profiles.https) { + $profile = $launchSettings.profiles.https + Write-Verbose "Using 'https' launch profile" + } + else { + # Use first profile that has environmentVariables + foreach ($prop in $launchSettings.profiles.PSObject.Properties) { + if ($prop.Value.environmentVariables) { + $profile = $prop.Value + Write-Verbose "Using '$($prop.Name)' launch profile" + break + } + } + } + + if ($profile -and $profile.environmentVariables) { + foreach ($prop in $profile.environmentVariables.PSObject.Properties) { + $envVars[$prop.Name] = $prop.Value + Write-Verbose " Environment: $($prop.Name)=$($prop.Value)" + } + } + + # Use applicationUrl to set ASPNETCORE_URLS if not already set + if ($profile -and $profile.applicationUrl -and -not $envVars.ContainsKey('ASPNETCORE_URLS')) { + $envVars['ASPNETCORE_URLS'] = $profile.applicationUrl + Write-Verbose " Environment: ASPNETCORE_URLS=$($profile.applicationUrl) (from applicationUrl)" + } + } + catch { + Write-Warning "Failed to parse launchSettings.json: $_" + } + } + else { + Write-Verbose "No launchSettings.json found at: $launchSettingsPath" + } + + # Always ensure Development environment is set + if (-not $envVars.ContainsKey('DOTNET_ENVIRONMENT')) { + $envVars['DOTNET_ENVIRONMENT'] = 'Development' + } + if (-not $envVars.ContainsKey('ASPNETCORE_ENVIRONMENT')) { + $envVars['ASPNETCORE_ENVIRONMENT'] = 'Development' + } + + # Start the AppHost application as a separate process + Write-Host "Starting $AppHostName..." -ForegroundColor Cyan + + $appPsi = [System.Diagnostics.ProcessStartInfo]::new() + if ($exePath) { + $appPsi.FileName = $exePath + $appPsi.Arguments = '' + } + else { + $appPsi.FileName = 'dotnet' + $appPsi.Arguments = "`"$dllPath`"" + } + $appPsi.WorkingDirectory = $AppHostDir + $appPsi.UseShellExecute = $false + $appPsi.RedirectStandardOutput = $true + $appPsi.RedirectStandardError = $true + $appPsi.CreateNoWindow = $true + + # Set environment variables from launchSettings.json + foreach ($key in $envVars.Keys) { + $appPsi.Environment[$key] = $envVars[$key] + } + + $appProcess = [System.Diagnostics.Process]::Start($appPsi) + $appPid = $appProcess.Id + + Write-Verbose "$AppHostName started with PID: $appPid" + + # Give the process a moment to initialize before attaching + Start-Sleep -Milliseconds 200 + + # Verify the process is still running + if ($appProcess.HasExited) { + $stdout = $appProcess.StandardOutput.ReadToEnd() + $stderr = $appProcess.StandardError.ReadToEnd() + throw "Application exited immediately with code $($appProcess.ExitCode).`nStdOut: $stdout`nStdErr: $stderr" + } + + # Start dotnet-trace to attach to the running process + Write-Host "Attaching trace collection to PID $appPid..." -ForegroundColor Cyan + + # Use dotnet-trace with the EventSource provider + # Format: ProviderName:Keywords:Level + # Keywords=0xFFFFFFFF (all), Level=5 (Verbose) + $providers = "${EventSourceName}" + + # Convert TraceDurationSeconds to dd:hh:mm:ss format required by dotnet-trace + $days = [math]::Floor($TraceDurationSeconds / 86400) + $hours = [math]::Floor(($TraceDurationSeconds % 86400) / 3600) + $minutes = [math]::Floor(($TraceDurationSeconds % 3600) / 60) + $seconds = $TraceDurationSeconds % 60 + $traceDuration = '{0:00}:{1:00}:{2:00}:{3:00}' -f $days, $hours, $minutes, $seconds + + $traceArgs = @( + 'collect', + '--process-id', $appPid, + '--providers', $providers, + '--output', $nettracePath, + '--format', 'nettrace', + '--duration', $traceDuration, + '--buffersize', '8192' + ) + + Write-Verbose "dotnet-trace arguments: $($traceArgs -join ' ')" + + $tracePsi = [System.Diagnostics.ProcessStartInfo]::new() + $tracePsi.FileName = 'dotnet-trace' + $tracePsi.Arguments = $traceArgs -join ' ' + $tracePsi.WorkingDirectory = $AppHostDir + $tracePsi.UseShellExecute = $false + $tracePsi.RedirectStandardOutput = $true + $tracePsi.RedirectStandardError = $true + $tracePsi.CreateNoWindow = $true + + $traceProcess = [System.Diagnostics.Process]::Start($tracePsi) + + Write-Host "Collecting performance trace..." -ForegroundColor Cyan + + # Wait for trace to complete + $traceProcess.WaitForExit() + + # Read app process output (what was captured while trace was running) + # Use async read to avoid blocking - read whatever is available + $appStdout = "" + $appStderr = "" + if ($appProcess -and -not $appProcess.HasExited) { + # Process is still running, we can try to read available output + # Note: ReadToEnd would block, so we read what's available after stopping + } + + $traceOutput = $traceProcess.StandardOutput.ReadToEnd() + $traceError = $traceProcess.StandardError.ReadToEnd() + + if ($traceOutput) { Write-Verbose "dotnet-trace output: $traceOutput" } + if ($traceError) { Write-Verbose "dotnet-trace stderr: $traceError" } + + # Check if trace file was created despite any errors + # dotnet-trace may report errors during cleanup but the trace file is often still valid + if ($traceProcess.ExitCode -ne 0) { + if (Test-Path $nettracePath) { + Write-Warning "dotnet-trace exited with code $($traceProcess.ExitCode), but trace file was created. Attempting to analyze." + } + else { + Write-Warning "dotnet-trace exited with code $($traceProcess.ExitCode) and no trace file was created." + return $null + } + } + + Write-Host "Trace collection completed." -ForegroundColor Green + + return $nettracePath + } + finally { + # Clean up the application process and capture its output + if ($appProcess) { + # Read any remaining output before killing the process + $appStdout = "" + $appStderr = "" + try { + # Give a moment for any buffered output + Start-Sleep -Milliseconds 100 + + # We need to read asynchronously since the process may still be running + # Read what's available without blocking indefinitely + $stdoutTask = $appProcess.StandardOutput.ReadToEndAsync() + $stderrTask = $appProcess.StandardError.ReadToEndAsync() + + # Wait briefly for output + [System.Threading.Tasks.Task]::WaitAll(@($stdoutTask, $stderrTask), 1000) | Out-Null + + if ($stdoutTask.IsCompleted) { + $appStdout = $stdoutTask.Result + } + if ($stderrTask.IsCompleted) { + $appStderr = $stderrTask.Result + } + } + catch { + # Ignore errors reading output + } + + if ($appStdout) { + Write-Verbose "Application stdout:`n$appStdout" + } + if ($appStderr) { + Write-Verbose "Application stderr:`n$appStderr" + } + + if (-not $appProcess.HasExited) { + Write-Verbose "Stopping $AppHostName (PID: $($appProcess.Id))..." + try { + # Try graceful shutdown first + $appProcess.Kill($true) + $appProcess.WaitForExit(5000) | Out-Null + } + catch { + Write-Warning "Failed to stop application: $_" + } + } + $appProcess.Dispose() + } + + # Clean up trace process + if ($traceProcess) { + if (-not $traceProcess.HasExited) { + try { + $traceProcess.Kill() + $traceProcess.WaitForExit(2000) | Out-Null + } + catch { + # Ignore errors killing trace process + } + } + $traceProcess.Dispose() + } + } +} + +# Path to the trace analyzer tool +$TraceAnalyzerDir = Join-Path $ScriptDir 'TraceAnalyzer' +$TraceAnalyzerProject = Join-Path $TraceAnalyzerDir 'TraceAnalyzer.csproj' + +# Build the trace analyzer tool +function Build-TraceAnalyzer { + if (-not (Test-Path $TraceAnalyzerProject)) { + Write-Warning "TraceAnalyzer project not found at: $TraceAnalyzerProject" + return $false + } + + Write-Verbose "Building TraceAnalyzer tool..." + $buildOutput = & dotnet build $TraceAnalyzerProject -c Release --verbosity quiet 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to build TraceAnalyzer: $buildOutput" + return $false + } + + Write-Verbose "TraceAnalyzer built successfully" + return $true +} + +# Parse nettrace file using the TraceAnalyzer tool +function Get-StartupTiming { + param( + [string]$TracePath + ) + + Write-Host "Analyzing trace: $TracePath" -ForegroundColor Cyan + + if (-not (Test-Path $TracePath)) { + Write-Warning "Trace file not found: $TracePath" + return $null + } + + try { + $output = & dotnet run --project $TraceAnalyzerProject -c Release --no-build -- $TracePath 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "TraceAnalyzer failed: $output" + return $null + } + + $result = $output | Select-Object -Last 1 + if ($result -eq 'null') { + Write-Warning "Could not find DcpModelCreation events in the trace" + return $null + } + + $duration = [double]::Parse($result, [System.Globalization.CultureInfo]::InvariantCulture) + Write-Verbose "Calculated duration: $duration ms" + return $duration + } + catch { + Write-Warning "Error parsing trace: $_" + return $null + } +} + +# Main execution +function Main { + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host " Aspire Startup Performance Measurement" -ForegroundColor Cyan + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Project: $AppHostName" + Write-Host "Project Path: $AppHostProject" + Write-Host "Iterations: $Iterations" + Write-Host "Trace Duration: $TraceDurationSeconds seconds" + Write-Host "Pause Between Iterations: $PauseBetweenIterationsSeconds seconds" + Write-Host "Preserve Traces: $PreserveTraces" + if ($PreserveTraces -or $TraceOutputDirectory) { + Write-Host "Trace Directory: $OutputDirectory" + } + Write-Host "" + + Test-Prerequisites + + # Build the TraceAnalyzer tool for parsing traces + $traceAnalyzerAvailable = Build-TraceAnalyzer + + # Ensure output directory exists + if (-not (Test-Path $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null + } + + if (-not $SkipBuild) { + Build-AppHost + } + else { + Write-Host "Skipping build (SkipBuild flag set)" -ForegroundColor Yellow + } + + $results = @() + $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' + + try { + for ($i = 1; $i -le $Iterations; $i++) { + $traceBaseName = "${AppHostName}_startup_${timestamp}_iter${i}" + $traceOutputPath = Join-Path $OutputDirectory $traceBaseName + + $tracePath = Invoke-PerformanceIteration -IterationNumber $i -TraceOutputPath $traceOutputPath + + if ($tracePath -and (Test-Path $tracePath)) { + $duration = $null + if ($traceAnalyzerAvailable) { + $duration = Get-StartupTiming -TracePath $tracePath + } + + if ($null -ne $duration) { + $results += [PSCustomObject]@{ + Iteration = $i + TracePath = $tracePath + StartupTimeMs = [math]::Round($duration, 2) + } + Write-Host "Startup time: $([math]::Round($duration, 2)) ms" -ForegroundColor Green + } + else { + $results += [PSCustomObject]@{ + Iteration = $i + TracePath = $tracePath + StartupTimeMs = $null + } + Write-Host "Trace collected: $tracePath" -ForegroundColor Green + } + } + else { + Write-Warning "No trace file generated for iteration $i" + } + + # Pause between iterations + if ($i -lt $Iterations -and $PauseBetweenIterationsSeconds -gt 0) { + Write-Verbose "Pausing for $PauseBetweenIterationsSeconds seconds before next iteration..." + Start-Sleep -Seconds $PauseBetweenIterationsSeconds + } + } + } + finally { + # Clean up temporary trace directory if not preserving traces + if ($ShouldCleanupDirectory -and (Test-Path $OutputDirectory)) { + Write-Verbose "Cleaning up temporary trace directory: $OutputDirectory" + Remove-Item -Path $OutputDirectory -Recurse -Force -ErrorAction SilentlyContinue + } + } + + # Summary + Write-Host "" + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host " Results Summary" -ForegroundColor Cyan + Write-Host "==================================================" -ForegroundColor Cyan + + # Wrap in @() to ensure array even with single/null results + $validResults = @($results | Where-Object { $null -ne $_.StartupTimeMs }) + + if ($validResults.Count -gt 0) { + Write-Host "" + # Only show TracePath in summary if PreserveTraces is set + if ($PreserveTraces) { + $results | Format-Table -AutoSize + } + else { + $results | Select-Object Iteration, StartupTimeMs | Format-Table -AutoSize + } + + $times = @($validResults | ForEach-Object { $_.StartupTimeMs }) + $avg = ($times | Measure-Object -Average).Average + $min = ($times | Measure-Object -Minimum).Minimum + $max = ($times | Measure-Object -Maximum).Maximum + + Write-Host "" + Write-Host "Statistics:" -ForegroundColor Yellow + Write-Host " Successful iterations: $($validResults.Count) / $Iterations" + Write-Host " Minimum: $([math]::Round($min, 2)) ms" + Write-Host " Maximum: $([math]::Round($max, 2)) ms" + Write-Host " Average: $([math]::Round($avg, 2)) ms" + + if ($validResults.Count -gt 1) { + $stdDev = [math]::Sqrt(($times | ForEach-Object { [math]::Pow($_ - $avg, 2) } | Measure-Object -Average).Average) + Write-Host " Std Dev: $([math]::Round($stdDev, 2)) ms" + } + + if ($PreserveTraces) { + Write-Host "" + Write-Host "Trace files saved to: $OutputDirectory" -ForegroundColor Cyan + } + } + elseif ($results.Count -gt 0) { + Write-Host "" + Write-Host "Collected $($results.Count) trace(s) but could not extract timing." -ForegroundColor Yellow + if ($PreserveTraces) { + Write-Host "" + Write-Host "Trace files saved to: $OutputDirectory" -ForegroundColor Cyan + $results | Select-Object Iteration, TracePath | Format-Table -AutoSize + Write-Host "" + Write-Host "Open traces in PerfView or Visual Studio to analyze startup timing." -ForegroundColor Yellow + } + } + else { + Write-Warning "No traces were collected." + } + + return $results +} + +# Run the script +Main diff --git a/tools/perf/TraceAnalyzer/Program.cs b/tools/perf/TraceAnalyzer/Program.cs new file mode 100644 index 00000000000..76ffe45d44d --- /dev/null +++ b/tools/perf/TraceAnalyzer/Program.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Tool to analyze .nettrace files and extract Aspire startup timing information. +// Usage: dotnet run -- +// Output: Prints the startup duration in milliseconds to stdout, or "null" if events not found. + +using Microsoft.Diagnostics.Tracing; + +if (args.Length == 0) +{ + Console.Error.WriteLine("Usage: TraceAnalyzer "); + return 1; +} + +var tracePath = args[0]; + +if (!File.Exists(tracePath)) +{ + Console.Error.WriteLine($"Error: File not found: {tracePath}"); + return 1; +} + +// Event IDs from AspireEventSource +const int DcpModelCreationStartEventId = 17; +const int DcpModelCreationStopEventId = 18; + +const string AspireHostingProviderName = "Microsoft-Aspire-Hosting"; + +try +{ + double? startTime = null; + double? stopTime = null; + + using (var source = new EventPipeEventSource(tracePath)) + { + source.Dynamic.AddCallbackForProviderEvents((string pName, string eName) => + { + if (pName != AspireHostingProviderName) + { + return EventFilterResponse.RejectProvider; + } + if (eName == null || eName.StartsWith("DcpModelCreation", StringComparison.Ordinal)) + { + return EventFilterResponse.AcceptEvent; + } + return EventFilterResponse.RejectEvent; + }, + (TraceEvent traceEvent) => + { + if ((int)traceEvent.ID == DcpModelCreationStartEventId) + { + startTime = traceEvent.TimeStampRelativeMSec; + } + else if ((int)traceEvent.ID == DcpModelCreationStopEventId) + { + stopTime = traceEvent.TimeStampRelativeMSec; + } + }); + + source.Process(); + } + + if (startTime.HasValue && stopTime.HasValue) + { + var duration = stopTime.Value - startTime.Value; + Console.WriteLine(duration.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)); + return 0; + } + else + { + Console.WriteLine("null"); + return 0; + } +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error parsing trace: {ex.Message}"); + return 1; +} diff --git a/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj b/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj new file mode 100644 index 00000000000..f984521fbc3 --- /dev/null +++ b/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + false + + + + + + + From 5aa8a61518fb2fa7b4581ba36342fb61d98fe79a Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 11 Feb 2026 17:21:49 -0800 Subject: [PATCH 02/10] Add backmerge release workflow to automate merging changes from release/13.2 to main (#14453) * Add backmerge release workflow to automate merging changes from release/13.2 to main * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply more fixes and use dotnet's action --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/README.md | 23 ++++ .github/workflows/backmerge-release.yml | 152 ++++++++++++++++++++++++ .github/workflows/ci.yml | 1 + 3 files changed, 176 insertions(+) create mode 100644 .github/workflows/backmerge-release.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 06975dcd71c..e802e904706 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -99,3 +99,26 @@ When you comment on a PR (not an issue), the workflow will automatically push ch ### Concurrency The workflow uses concurrency groups based on the issue/PR number to prevent race conditions when multiple commands are issued on the same issue. + +## Backmerge Release Workflow + +The `backmerge-release.yml` workflow automatically creates PRs to merge changes from `release/13.2` back into `main`. + +### Schedule + +Runs daily at 00:00 UTC (4pm PT during standard time, 5pm PT during daylight saving time). Can also be triggered manually via `workflow_dispatch`. + +### Behavior + +1. **Change Detection**: Checks if `release/13.2` has commits not in `main` +2. **PR Creation**: If changes exist, creates a PR to merge `release/13.2` → `main` +3. **Auto-merge**: Enables GitHub's auto-merge feature, so the PR merges automatically once approved +4. **Conflict Handling**: If merge conflicts occur, creates an issue instead of a PR + +### Assignees + +PRs and conflict issues are automatically assigned to @joperezr and @radical. + +### Manual Trigger + +To trigger manually, go to Actions → "Backmerge Release to Main" → "Run workflow". diff --git a/.github/workflows/backmerge-release.yml b/.github/workflows/backmerge-release.yml new file mode 100644 index 00000000000..05cd2c507d1 --- /dev/null +++ b/.github/workflows/backmerge-release.yml @@ -0,0 +1,152 @@ +name: Backmerge Release to Main + +on: + schedule: + - cron: '0 0 * * *' # Runs daily at 00:00 UTC (16:00 PST / 17:00 PDT) + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + backmerge: + runs-on: ubuntu-latest + timeout-minutes: 15 + if: ${{ github.repository_owner == 'dotnet' }} + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 # Full history needed for merge + + - name: Check for changes to backmerge + id: check + run: | + git fetch origin main release/13.2 + BEHIND_COUNT=$(git rev-list --count origin/main..origin/release/13.2) + echo "behind_count=$BEHIND_COUNT" >> $GITHUB_OUTPUT + if [ "$BEHIND_COUNT" -gt 0 ]; then + echo "changes=true" >> $GITHUB_OUTPUT + echo "Found $BEHIND_COUNT commits in release/13.2 not in main" + else + echo "changes=false" >> $GITHUB_OUTPUT + echo "No changes to backmerge - release/13.2 is up-to-date with main" + fi + + - name: Attempt merge and create branch + if: steps.check.outputs.changes == 'true' + id: merge + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout origin/main + git checkout -b backmerge/release-13.2-to-main + + # Attempt the merge + if git merge origin/release/13.2 --no-edit; then + echo "merge_success=true" >> $GITHUB_OUTPUT + git push origin backmerge/release-13.2-to-main --force + echo "Merge successful, branch pushed" + else + echo "merge_success=false" >> $GITHUB_OUTPUT + git merge --abort + echo "Merge conflicts detected" + fi + + - name: Create Pull Request + if: steps.check.outputs.changes == 'true' && steps.merge.outputs.merge_success == 'true' + id: create-pr + uses: dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + head: backmerge/release-13.2-to-main + base: main + title: "[Automated] Backmerge release/13.2 to main" + labels: area-engineering-systems + body: | + ## Automated Backmerge + + This PR merges changes from `release/13.2` back into `main`. + + **Commits to merge:** ${{ steps.check.outputs.behind_count }} + + This PR was created automatically to keep `main` up-to-date with release branch changes. + Once approved, it will auto-merge. + + --- + *This PR was generated by the [backmerge-release](${{ github.server_url }}/${{ github.repository }}/actions/workflows/backmerge-release.yml) workflow.* + + - name: Add assignees and enable auto-merge + if: steps.create-pr.outputs.pull-request-number + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr edit ${{ steps.create-pr.outputs.pull-request-number }} --add-assignee joperezr,radical + gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --merge + + - name: Create issue for merge conflicts + if: steps.check.outputs.changes == 'true' && steps.merge.outputs.merge_success == 'false' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const workflowRunUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + // Check if there's already an open issue for this + const existingIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'backmerge-conflict', + creator: 'github-actions[bot]' + }); + + if (existingIssues.data.length > 0) { + console.log(`Existing backmerge conflict issue found: #${existingIssues.data[0].number}`); + // Add a comment to the existing issue + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssues.data[0].number, + body: `⚠️ Merge conflicts still exist.\n\n**Workflow run:** ${workflowRunUrl}\n\nPlease resolve the conflicts manually.` + }); + return; + } + + // Create a new issue + const issueBody = [ + '## Backmerge Conflict', + '', + 'The automated backmerge from `release/13.2` to `main` failed due to merge conflicts.', + '', + '### What to do', + '', + '1. Checkout main and attempt the merge locally:', + ' ```bash', + ' git checkout main', + ' git pull origin main', + ' git merge origin/release/13.2', + ' ```', + '2. Resolve the conflicts', + '3. Push the merge commit or create a PR manually', + '', + '### Details', + '', + `**Workflow run:** ${workflowRunUrl}`, + '**Commits to merge:** ${{ steps.check.outputs.behind_count }}', + '', + '---', + `*This issue was created automatically by the [backmerge-release](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/workflows/backmerge-release.yml) workflow.*` + ].join('\n'); + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '[Backmerge] Merge conflicts between release/13.2 and main', + body: issueBody, + assignees: ['joperezr', 'radical'], + labels: ['area-engineering-systems', 'backmerge-conflict'] + }); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0007e8030f..0901bf2f888 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: eng/pipelines/.* eng/test-configuration.json \.github/workflows/apply-test-attributes.yml + \.github/workflows/backmerge-release.yml \.github/workflows/backport.yml \.github/workflows/dogfood-comment.yml \.github/workflows/generate-api-diffs.yml From 62d5c9777626a7c51525420657352cc1dd0421e0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:23:17 -0800 Subject: [PATCH 03/10] Bump Aspire branding from 13.2 to 13.3 (#14456) * Initial plan * Bump Aspire branding from 13.2 to 13.3 Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- .github/policies/milestoneAssignment.prClosed.yml | 8 ++++---- eng/Versions.props | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/policies/milestoneAssignment.prClosed.yml b/.github/policies/milestoneAssignment.prClosed.yml index 1ec03595d00..ad9aaad2f57 100644 --- a/.github/policies/milestoneAssignment.prClosed.yml +++ b/.github/policies/milestoneAssignment.prClosed.yml @@ -16,16 +16,16 @@ configuration: branch: main then: - addMilestone: - milestone: 13.2 + milestone: 13.3 description: '[Milestone Assignments] Assign Milestone to PRs merged to the `main` branch' - if: - payloadType: Pull_Request - isAction: action: Closed - targetsBranch: - branch: release/13.1 + branch: release/13.2 then: - removeMilestone - addMilestone: - milestone: 13.1.1 - description: '[Milestone Assignments] Assign Milestone to PRs merged to release/13.1 branch' + milestone: 13.2 + description: '[Milestone Assignments] Assign Milestone to PRs merged to release/13.2 branch' diff --git a/eng/Versions.props b/eng/Versions.props index a2d25ba628a..84da946872a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ 13 - 2 + 3 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) preview.1 From 8a8262135d4dbc79795de3b7f72bc263d3ded42a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 12 Feb 2026 09:54:37 -0600 Subject: [PATCH 04/10] Update Azure.Core to latest version - lift all runtime dependencies to latest (#14361) * Update to Azure.Core 1.51.1 Use latest versions for all dotnet/runtime nuget packages. This simplifies our dependency management. Remove ForceLatestDotnetVersions property from multiple project files * Update AzureDeployerTests to use WaitForShutdown instead of StopAsync There is a timing issue when using Start/Stop since the background pipeline might still be running and it cancels the pipeline before it can complete. * Fix AuxiliaryBackchannelTests by adding a Task that completes when the AuxiliaryBackchannelService is listening and ready for connections. * Remove double registration of AuxiliaryBackchannelService as an IHostedService. * Fix ResourceLoggerForwarderServiceTests to ensure the ResourceLoggerForwarderService has started before signalling the stopping token. --- Directory.Packages.props | 70 ++++++----------- .../AzureAIFoundryEndToEnd.WebStory.csproj | 3 - .../AzureOpenAIEndToEnd.WebStory.csproj | 3 - .../GitHubModelsEndToEnd.WebStory.csproj | 3 - .../OpenAIEndToEnd.WebStory.csproj | 3 - .../AuxiliaryBackchannelService.cs | 10 +++ .../Aspire.Azure.AI.Inference.csproj | 2 - .../Aspire.Azure.AI.OpenAI.csproj | 2 - .../Aspire.OpenAI/Aspire.OpenAI.csproj | 2 - .../Aspire.Azure.AI.Inference.Tests.csproj | 3 - .../Aspire.Azure.AI.OpenAI.Tests.csproj | 3 - .../AzureDeployerTests.cs | 4 +- .../ResourceLoggerForwarderServiceTests.cs | 18 +++++ .../Backchannel/AuxiliaryBackchannelTests.cs | 75 +++++-------------- .../Aspire.OpenAI.Tests.csproj | 3 - 15 files changed, 71 insertions(+), 133 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e4674e5743..097be0ed6d4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -175,17 +175,18 @@ - + - + + - + @@ -195,6 +196,23 @@ + + + + + + + + + + + + + + + + @@ -218,18 +236,6 @@ - - - - - - - - - - - - @@ -254,26 +260,8 @@ - - - - - - - - - - - - - - - - - - - + @@ -295,17 +283,5 @@ - - - - - - - - - - - - diff --git a/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.WebStory/AzureAIFoundryEndToEnd.WebStory.csproj b/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.WebStory/AzureAIFoundryEndToEnd.WebStory.csproj index cd25e05445f..aa19fad6805 100644 --- a/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.WebStory/AzureAIFoundryEndToEnd.WebStory.csproj +++ b/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.WebStory/AzureAIFoundryEndToEnd.WebStory.csproj @@ -4,9 +4,6 @@ $(DefaultTargetFramework) enable enable - - - true diff --git a/playground/AzureOpenAIEndToEnd/AzureOpenAIEndToEnd.WebStory/AzureOpenAIEndToEnd.WebStory.csproj b/playground/AzureOpenAIEndToEnd/AzureOpenAIEndToEnd.WebStory/AzureOpenAIEndToEnd.WebStory.csproj index cefd34325a6..ede52a85213 100644 --- a/playground/AzureOpenAIEndToEnd/AzureOpenAIEndToEnd.WebStory/AzureOpenAIEndToEnd.WebStory.csproj +++ b/playground/AzureOpenAIEndToEnd/AzureOpenAIEndToEnd.WebStory/AzureOpenAIEndToEnd.WebStory.csproj @@ -4,9 +4,6 @@ $(DefaultTargetFramework) enable enable - - - true diff --git a/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.WebStory/GitHubModelsEndToEnd.WebStory.csproj b/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.WebStory/GitHubModelsEndToEnd.WebStory.csproj index cd25e05445f..aa19fad6805 100644 --- a/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.WebStory/GitHubModelsEndToEnd.WebStory.csproj +++ b/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.WebStory/GitHubModelsEndToEnd.WebStory.csproj @@ -4,9 +4,6 @@ $(DefaultTargetFramework) enable enable - - - true diff --git a/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj b/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj index 2df50b9502b..60f4356d1a3 100644 --- a/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj +++ b/playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/OpenAIEndToEnd.WebStory.csproj @@ -4,9 +4,6 @@ $(DefaultTargetFramework) enable enable - - - true diff --git a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelService.cs b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelService.cs index 6bf750cff6a..2ef434dd1af 100644 --- a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelService.cs +++ b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelService.cs @@ -22,12 +22,21 @@ internal sealed class AuxiliaryBackchannelService( : BackgroundService { private Socket? _serverSocket; + private readonly TaskCompletionSource _listeningTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// Gets the Unix socket path where the auxiliary backchannel is listening. /// public string? SocketPath { get; private set; } + /// + /// Gets a task that completes when the server socket is bound and listening for connections. + /// + /// + /// Used by tests to wait until the backchannel is ready before attempting to connect. + /// + internal Task ListeningTask => _listeningTcs.Task; + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try @@ -72,6 +81,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _serverSocket.Listen(backlog: 10); // Allow multiple pending connections logger.LogDebug("Auxiliary backchannel listening on {SocketPath}", SocketPath); + _listeningTcs.TrySetResult(); // Accept connections in a loop (supporting multiple concurrent connections) while (!stoppingToken.IsCancellationRequested) diff --git a/src/Components/Aspire.Azure.AI.Inference/Aspire.Azure.AI.Inference.csproj b/src/Components/Aspire.Azure.AI.Inference/Aspire.Azure.AI.Inference.csproj index 7370e834905..7926621c583 100644 --- a/src/Components/Aspire.Azure.AI.Inference/Aspire.Azure.AI.Inference.csproj +++ b/src/Components/Aspire.Azure.AI.Inference/Aspire.Azure.AI.Inference.csproj @@ -9,8 +9,6 @@ $(NoWarn);SYSLIB1100;SYSLIB1101 true - - true diff --git a/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj b/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj index 99d48574b2e..db2deffe6bc 100644 --- a/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj +++ b/src/Components/Aspire.Azure.AI.OpenAI/Aspire.Azure.AI.OpenAI.csproj @@ -10,8 +10,6 @@ $(NoWarn);SYSLIB1100;SYSLIB1101;AOAI001 true - - true diff --git a/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj b/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj index 836c1d9d11c..6b76d148c20 100644 --- a/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj +++ b/src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj @@ -8,8 +8,6 @@ $(NoWarn);SYSLIB1100;SYSLIB1101 true - - true diff --git a/tests/Aspire.Azure.AI.Inference.Tests/Aspire.Azure.AI.Inference.Tests.csproj b/tests/Aspire.Azure.AI.Inference.Tests/Aspire.Azure.AI.Inference.Tests.csproj index cca72f73ac9..948e92b0096 100644 --- a/tests/Aspire.Azure.AI.Inference.Tests/Aspire.Azure.AI.Inference.Tests.csproj +++ b/tests/Aspire.Azure.AI.Inference.Tests/Aspire.Azure.AI.Inference.Tests.csproj @@ -2,9 +2,6 @@ $(AllTargetFrameworks) - - - true diff --git a/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj b/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj index a27e07a3a63..2f45183b667 100644 --- a/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj +++ b/tests/Aspire.Azure.AI.OpenAI.Tests/Aspire.Azure.AI.OpenAI.Tests.csproj @@ -2,9 +2,6 @@ $(AllTargetFrameworks) - - - true diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 0c9ff5c0492..09460f25c8d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -1072,7 +1072,7 @@ public async Task DeployAsync_WithAzureResourceDependencies_DoesNotHang(string s // Act using var app = builder.Build(); await app.StartAsync(); - await app.StopAsync(); + await app.WaitForShutdownAsync(); if (step == "diagnostics") { @@ -1159,7 +1159,7 @@ public async Task DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDep // Act using var app = builder.Build(); await app.StartAsync(); - await app.StopAsync(); + await app.WaitForShutdownAsync(); // In diagnostics mode, verify the deployment graph shows correct dependencies var logs = mockActivityReporter.LoggedMessages diff --git a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs index 2e1084139c8..d10d3e2775f 100644 --- a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs @@ -35,11 +35,29 @@ public async Task ExecuteDoesNotThrowOperationCanceledWhenAppStoppingTokenSignal var loggerFactory = new NullLoggerFactory(); var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, hostEnvironment, loggerFactory); + // use a task to signal when the resourceLogForwarder has started executing + var subscribedTcs = new TaskCompletionSource(); + var subscriberLoop = Task.Run(async () => + { + await foreach (var sub in resourceLoggerService.WatchAnySubscribersAsync(hostApplicationLifetime.ApplicationStopping)) + { + subscribedTcs.TrySetResult(); + return; + } + }); + await resourceLogForwarder.StartAsync(hostApplicationLifetime.ApplicationStopping); Assert.NotNull(resourceLogForwarder.ExecuteTask); Assert.Equal(TaskStatus.WaitingForActivation, resourceLogForwarder.ExecuteTask.Status); + // Publish an update to the resource to kickstart the notification service loop + var myresource = new CustomResource("myresource"); + await resourceNotificationService.PublishUpdateAsync(myresource, snapshot => snapshot with { State = "Running" }); + + // Wait for the log stream to begin + await subscribedTcs.Task.WaitAsync(TimeSpan.FromSeconds(15)); + // Signal the stopping token hostApplicationLifetime.StopApplication(); diff --git a/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelTests.cs b/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelTests.cs index 86f7622be67..1c06ea8f482 100644 --- a/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelTests.cs +++ b/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelTests.cs @@ -28,16 +28,13 @@ public async Task CanStartAuxiliaryBackchannelService() return Task.CompletedTask; }); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service and verify it started var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); Assert.True(File.Exists(service.SocketPath)); @@ -71,25 +68,22 @@ public async Task CanConnectMultipleClientsToAuxiliaryBackchannel() return Task.CompletedTask; }); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect multiple clients concurrently var client1Socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); var client2Socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); var client3Socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - + var endpoint = new UnixDomainSocketEndPoint(service.SocketPath); - + await client1Socket.ConnectAsync(endpoint).WaitAsync(TimeSpan.FromSeconds(60)); await client2Socket.ConnectAsync(endpoint).WaitAsync(TimeSpan.FromSeconds(60)); await client3Socket.ConnectAsync(endpoint).WaitAsync(TimeSpan.FromSeconds(60)); @@ -116,16 +110,13 @@ public async Task CanInvokeRpcMethodOnAuxiliaryBackchannel() // When the Dashboard is not part of the app model, null should be returned using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -154,16 +145,13 @@ public async Task GetAppHostInformationAsyncReturnsAppHostPath() // This test verifies that GetAppHostInformationAsync returns the AppHost path using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -198,16 +186,13 @@ public async Task MultipleClientsCanInvokeRpcMethodsConcurrently() // When the Dashboard is not part of the app model, null should be returned using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Create multiple clients and invoke RPC methods concurrently @@ -245,16 +230,13 @@ public async Task GetAppHostInformationAsyncReturnsFilePathWithExtension() // For .csproj-based AppHosts, it should include the .csproj extension using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -275,10 +257,10 @@ public async Task GetAppHostInformationAsyncReturnsFilePathWithExtension() Assert.NotNull(appHostInfo); Assert.NotNull(appHostInfo.AppHostPath); Assert.NotEmpty(appHostInfo.AppHostPath); - + // The path should be an absolute path Assert.True(Path.IsPathRooted(appHostInfo.AppHostPath), $"Expected absolute path but got: {appHostInfo.AppHostPath}"); - + // In test scenarios where assembly metadata is not available, we may get a path without extension // (falling back to AppHost:Path). In real scenarios with proper metadata, we should get .csproj or .cs // So we just verify the path is non-empty and rooted @@ -294,22 +276,19 @@ public async Task SocketPathUsesAuxiPrefix() // to avoid Windows reserved device name issues (AUX is reserved on Windows < 11) using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Verify that the socket path uses "auxi.sock." prefix var fileName = Path.GetFileName(service.SocketPath); Assert.StartsWith("auxi.sock.", fileName); - + // Verify that the socket file can be created (not blocked by Windows reserved names) Assert.True(File.Exists(service.SocketPath), $"Socket file should exist at: {service.SocketPath}"); @@ -328,16 +307,13 @@ public async Task CallResourceMcpToolAsyncThrowsWhenResourceNotFound() // Add a simple container resource (without MCP) builder.AddContainer("mycontainer", "nginx"); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -372,16 +348,13 @@ public async Task CallResourceMcpToolAsyncThrowsWhenResourceHasNoMcpAnnotation() // Add a simple container resource (without MCP) builder.AddContainer("mycontainer", "nginx"); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -412,16 +385,13 @@ public async Task StopAppHostAsyncInitiatesShutdown() // This test verifies that StopAppHostAsync initiates AppHost shutdown using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -464,16 +434,13 @@ public async Task GetCapabilitiesAsyncReturnsV1AndV2() // This test verifies that GetCapabilitiesAsync returns both v1 and v2 capabilities using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -505,16 +472,13 @@ public async Task GetAppHostInfoAsyncV2ReturnsAppHostInfo() // This test verifies that the v2 GetAppHostInfoAsync returns AppHost info using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(outputHelper); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client @@ -551,16 +515,13 @@ public async Task GetResourcesAsyncV2ReturnsResources() // Add a simple parameter resource builder.AddParameter("myparam"); - // Register the auxiliary backchannel service - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - using var app = builder.Build(); await app.StartAsync().WaitAsync(TestConstants.DefaultTimeoutTimeSpan); // Get the service var service = app.Services.GetRequiredService(); + await service.ListeningTask.WaitAsync(TimeSpan.FromSeconds(60)); Assert.NotNull(service.SocketPath); // Connect a client diff --git a/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj b/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj index 62892924e61..dec5f627481 100644 --- a/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj +++ b/tests/Aspire.OpenAI.Tests/Aspire.OpenAI.Tests.csproj @@ -2,9 +2,6 @@ $(AllTargetFrameworks) - - - true From 6d4f74759870b5d9e926870f937f4be2a1fa06f5 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:06:22 +0000 Subject: [PATCH 05/10] Update Arcade to latest version from the .NET 10 Eng channel (#13556) Co-authored-by: Jose Perez Rodriguez --- eng/Version.Details.xml | 28 +- eng/Versions.props | 6 +- eng/build.sh | 2 +- eng/common/SetupNugetSources.ps1 | 17 +- eng/common/SetupNugetSources.sh | 17 +- eng/common/build.ps1 | 2 - eng/common/build.sh | 7 +- eng/common/core-templates/job/job.yml | 8 - .../job/publish-build-assets.yml | 18 +- .../core-templates/job/source-build.yml | 8 +- .../core-templates/post-build/post-build.yml | 463 +++++++++--------- .../core-templates/steps/generate-sbom.yml | 2 +- .../steps/install-microbuild-impl.yml | 34 -- .../steps/install-microbuild.yml | 64 ++- .../core-templates/steps/source-build.yml | 2 +- .../steps/source-index-stage1-publish.yml | 8 +- eng/common/darc-init.sh | 2 +- eng/common/dotnet-install.sh | 2 +- eng/common/dotnet.sh | 2 +- eng/common/internal-feed-operations.sh | 2 +- eng/common/native/install-dependencies.sh | 4 +- eng/common/post-build/redact-logs.ps1 | 3 +- .../templates/variables/pool-providers.yml | 2 +- eng/common/tools.ps1 | 17 +- eng/common/tools.sh | 4 + eng/restore-toolset.sh | 2 +- global.json | 6 +- 27 files changed, 357 insertions(+), 375 deletions(-) delete mode 100644 eng/common/core-templates/steps/install-microbuild-impl.yml diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ea697099b37..0002a2353b2 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -179,33 +179,33 @@ - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d - + https://github.com/dotnet/arcade - 27e190e2a8053738859c082e2f70df62e01ff524 + 4bf37ce670528cf2aef4d9b1cd892554b1b02d9d diff --git a/eng/Versions.props b/eng/Versions.props index 84da946872a..cc14524afa9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -38,9 +38,9 @@ 0.22.4 0.22.4 - 11.0.0-beta.25610.3 - 11.0.0-beta.25610.3 - 11.0.0-beta.25610.3 + 10.0.0-beta.26110.1 + 10.0.0-beta.26110.1 + 10.0.0-beta.26110.1 10.0.2 10.2.0 diff --git a/eng/build.sh b/eng/build.sh index c80b2c68aba..58596335da2 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -150,7 +150,7 @@ while [[ $# > 0 ]]; do ;; -mauirestore) - extraargs="$extraargs -restoreMaui" + export restore_maui=true shift 1 ;; diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index fc8d618014e..65ed3a8adef 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -1,6 +1,7 @@ # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -173,4 +174,16 @@ foreach ($dotnetVersion in $dotnetVersions) { } } +# Check for dotnet-eng and add dotnet-eng-internal if present +$dotnetEngSource = $sources.SelectSingleNode("add[@key='dotnet-eng']") +if ($dotnetEngSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-eng-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + +# Check for dotnet-tools and add dotnet-tools-internal if present +$dotnetToolsSource = $sources.SelectSingleNode("add[@key='dotnet-tools']") +if ($dotnetToolsSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-tools-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + $doc.Save($filename) diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index b97cc536379..b2163abbe71 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -173,6 +174,18 @@ for DotNetVersion in ${DotNetVersions[@]} ; do fi done +# Check for dotnet-eng and add dotnet-eng-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-eng-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$FeedSuffix" +fi + +# Check for dotnet-tools and add dotnet-tools-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-tools-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$FeedSuffix" +fi + # I want things split line by line PrevIFS=$IFS IFS=$'\n' diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index c10aba98ac6..8cfee107e7a 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -30,7 +30,6 @@ Param( [string] $runtimeSourceFeedKey = '', [switch] $excludePrereleaseVS, [switch] $nativeToolsOnMachine, - [switch] $restoreMaui, [switch] $help, [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -77,7 +76,6 @@ function Print-Usage() { Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" Write-Host " -buildCheck Sets /check msbuild parameter" Write-Host " -fromVMR Set when building from within the VMR" - Write-Host " -restoreMaui Restore the MAUI workload after restore (only on Windows/macOS)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." diff --git a/eng/common/build.sh b/eng/common/build.sh index 09d1f8e6d9c..9767bb411a4 100755 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -44,7 +44,6 @@ usage() echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" echo " --buildCheck Sets /check msbuild parameter" echo " --fromVMR Set when building from within the VMR" - echo " --restoreMaui Restore the MAUI workload after restore (only on macOS)" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." @@ -77,7 +76,6 @@ sign=false public=false ci=false clean=false -restore_maui=false warn_as_error=true node_reuse=true @@ -94,7 +92,7 @@ runtime_source_feed='' runtime_source_feed_key='' properties=() -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -help|-h) @@ -185,9 +183,6 @@ while [[ $# -gt 0 ]]; do -buildcheck) build_check=true ;; - -restoremaui|-restore-maui) - restore_maui=true - ;; -runtimesourcefeed) runtime_source_feed=$2 shift diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index 748c4f07a64..5ce51840619 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,8 +19,6 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false - enablePreviewMicrobuild: false - microbuildPluginVersion: 'latest' enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false @@ -73,8 +71,6 @@ jobs: templateContext: ${{ parameters.templateContext }} variables: - - name: AllowPtrToDetectTestRunRetryFiles - value: true - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE value: '$(Build.Repository.Uri)' @@ -132,8 +128,6 @@ jobs: - template: /eng/common/core-templates/steps/install-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} microbuildUseESRP: ${{ parameters.microbuildUseESRP }} continueOnError: ${{ parameters.continueOnError }} @@ -159,8 +153,6 @@ jobs: - template: /eng/common/core-templates/steps/cleanup-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 8b5c635fe80..b955fac6e13 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -80,7 +80,7 @@ jobs: # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -91,8 +91,8 @@ jobs: fetchDepth: 3 clean: true - - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: - - ${{ if eq(parameters.publishingVersion, 3) }}: + - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: + - ${{ if eq(parameters.publishingVersion, 3) }}: - task: DownloadPipelineArtifact@2 displayName: Download Asset Manifests inputs: @@ -117,7 +117,7 @@ jobs: flattenFolders: true condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: NuGetAuthenticate@1 # Populate internal runtime variables. @@ -125,7 +125,7 @@ jobs: ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: parameters: legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - + - template: /eng/common/templates/steps/enable-internal-runtimes.yml - task: AzureCLI@2 @@ -145,7 +145,7 @@ jobs: condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: powershell@2 displayName: Create ReleaseConfigs Artifact inputs: @@ -173,7 +173,7 @@ jobs: artifactName: AssetManifests displayName: 'Publish Merged Manifest' retryCountOnTaskFailure: 10 # for any logs being locked - sbomEnabled: false # we don't need SBOM for logs + sbomEnabled: false # we don't need SBOM for logs - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: @@ -190,7 +190,7 @@ jobs: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - + # Darc is targeting 8.0, so make sure it's installed - task: UseDotNet@2 inputs: @@ -218,4 +218,4 @@ jobs: - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - JobLabel: 'Publish_Artifacts_Logs' + JobLabel: 'Publish_Artifacts_Logs' diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index 9d820f97421..1997c2ae00d 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -60,19 +60,19 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals build.ubuntu.2204.amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - image: 1es-azurelinux-3 + image: build.azurelinux.3.amd64 os: linux ${{ else }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64 ${{ if ne(parameters.platform.pool, '') }}: pool: ${{ parameters.platform.pool }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 06864cd1feb..b942a79ef02 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -1,106 +1,106 @@ parameters: -# Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. -# Publishing V1 is no longer supported -# Publishing V2 is no longer supported -# Publishing V3 is the default -- name: publishingInfraVersion - displayName: Which version of publishing should be used to promote the build definition? - type: number - default: 3 - values: - - 3 - -- name: BARBuildId - displayName: BAR Build Id - type: number - default: 0 - -- name: PromoteToChannelIds - displayName: Channel to promote BARBuildId to - type: string - default: '' - -- name: enableSourceLinkValidation - displayName: Enable SourceLink validation - type: boolean - default: false - -- name: enableSigningValidation - displayName: Enable signing validation - type: boolean - default: true - -- name: enableSymbolValidation - displayName: Enable symbol validation - type: boolean - default: false - -- name: enableNugetValidation - displayName: Enable NuGet validation - type: boolean - default: true - -- name: publishInstallersAndChecksums - displayName: Publish installers and checksums - type: boolean - default: true - -- name: requireDefaultChannels - displayName: Fail the build if there are no default channel(s) registrations for the current build - type: boolean - default: false - -- name: SDLValidationParameters - type: object - default: - enable: false - publishGdn: false - continueOnError: false - params: '' - artifactNames: '' - downloadArtifacts: true - -- name: isAssetlessBuild - type: boolean - displayName: Is Assetless Build - default: false - -# These parameters let the user customize the call to sdk-task.ps1 for publishing -# symbols & general artifacts as well as for signing validation -- name: symbolPublishingAdditionalParameters - displayName: Symbol publishing additional parameters - type: string - default: '' - -- name: artifactsPublishingAdditionalParameters - displayName: Artifact publishing additional parameters - type: string - default: '' - -- name: signingValidationAdditionalParameters - displayName: Signing validation additional parameters - type: string - default: '' - -# Which stages should finish execution before post-build stages start -- name: validateDependsOn - type: object - default: - - build - -- name: publishDependsOn - type: object - default: - - Validate - -# Optional: Call asset publishing rather than running in a separate stage -- name: publishAssetsImmediately - type: boolean - default: false - -- name: is1ESPipeline - type: boolean - default: false + # Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. + # Publishing V1 is no longer supported + # Publishing V2 is no longer supported + # Publishing V3 is the default + - name: publishingInfraVersion + displayName: Which version of publishing should be used to promote the build definition? + type: number + default: 3 + values: + - 3 + + - name: BARBuildId + displayName: BAR Build Id + type: number + default: 0 + + - name: PromoteToChannelIds + displayName: Channel to promote BARBuildId to + type: string + default: '' + + - name: enableSourceLinkValidation + displayName: Enable SourceLink validation + type: boolean + default: false + + - name: enableSigningValidation + displayName: Enable signing validation + type: boolean + default: true + + - name: enableSymbolValidation + displayName: Enable symbol validation + type: boolean + default: false + + - name: enableNugetValidation + displayName: Enable NuGet validation + type: boolean + default: true + + - name: publishInstallersAndChecksums + displayName: Publish installers and checksums + type: boolean + default: true + + - name: requireDefaultChannels + displayName: Fail the build if there are no default channel(s) registrations for the current build + type: boolean + default: false + + - name: SDLValidationParameters + type: object + default: + enable: false + publishGdn: false + continueOnError: false + params: '' + artifactNames: '' + downloadArtifacts: true + + - name: isAssetlessBuild + type: boolean + displayName: Is Assetless Build + default: false + + # These parameters let the user customize the call to sdk-task.ps1 for publishing + # symbols & general artifacts as well as for signing validation + - name: symbolPublishingAdditionalParameters + displayName: Symbol publishing additional parameters + type: string + default: '' + + - name: artifactsPublishingAdditionalParameters + displayName: Artifact publishing additional parameters + type: string + default: '' + + - name: signingValidationAdditionalParameters + displayName: Signing validation additional parameters + type: string + default: '' + + # Which stages should finish execution before post-build stages start + - name: validateDependsOn + type: object + default: + - build + + - name: publishDependsOn + type: object + default: + - Validate + + # Optional: Call asset publishing rather than running in a separate stage + - name: publishAssetsImmediately + type: boolean + default: false + + - name: is1ESPipeline + type: boolean + default: false stages: - ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: @@ -108,10 +108,10 @@ stages: dependsOn: ${{ parameters.validateDependsOn }} displayName: Validate Build Assets variables: - - template: /eng/common/core-templates/post-build/common-variables.yml - - template: /eng/common/core-templates/variables/pool-providers.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} + - template: /eng/common/core-templates/post-build/common-variables.yml + - template: /eng/common/core-templates/variables/pool-providers.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: NuGet Validation @@ -134,28 +134,28 @@ stages: demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Package Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: PackageArtifacts - checkDownloadedFiles: true - - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 - arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ - job: displayName: Signing Validation @@ -169,7 +169,7 @@ stages: os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows @@ -177,46 +177,46 @@ stages: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Package Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: PackageArtifacts - checkDownloadedFiles: true - - # This is necessary whenever we want to publish/restore to an AzDO private feed - # Since sdk-task.ps1 tries to restore packages we need to do this authentication here - # otherwise it'll complain about accessing a private feed. - - task: NuGetAuthenticate@1 - displayName: 'Authenticate to AzDO Feeds' - - # Signing validation will optionally work with the buildmanifest file which is downloaded from - # Azure DevOps above. - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: eng\common\sdk-task.ps1 - arguments: -task SigningValidation -restore -msbuildEngine vs - /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' - /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' - ${{ parameters.signingValidationAdditionalParameters }} - - - template: /eng/common/core-templates/steps/publish-logs.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} - StageLabel: 'Validation' - JobLabel: 'Signing' - BinlogToolVersion: $(BinlogToolVersion) + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + # This is necessary whenever we want to publish/restore to an AzDO private feed + # Since sdk-task.ps1 tries to restore packages we need to do this authentication here + # otherwise it'll complain about accessing a private feed. + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to AzDO Feeds' + + # Signing validation will optionally work with the buildmanifest file which is downloaded from + # Azure DevOps above. + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task SigningValidation -restore -msbuildEngine vs + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' + /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' + ${{ parameters.signingValidationAdditionalParameters }} + + - template: /eng/common/core-templates/steps/publish-logs.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} + StageLabel: 'Validation' + JobLabel: 'Signing' + BinlogToolVersion: $(BinlogToolVersion) - job: displayName: SourceLink Validation @@ -230,7 +230,7 @@ stages: os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows @@ -238,33 +238,33 @@ stages: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Blob Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: BlobArtifacts - checkDownloadedFiles: true - - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 - arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ - -ExtractPath $(Agent.BuildDirectory)/Extract/ - -GHRepoName $(Build.Repository.Name) - -GHCommit $(Build.SourceVersion) - -SourcelinkCliVersion $(SourceLinkCLIVersion) - continueOnError: true + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: BlobArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) + -GHCommit $(Build.SourceVersion) + -SourcelinkCliVersion $(SourceLinkCLIVersion) + continueOnError: true - ${{ if ne(parameters.publishAssetsImmediately, 'true') }}: - stage: publish_using_darc @@ -274,10 +274,10 @@ stages: dependsOn: ${{ parameters.validateDependsOn }} displayName: Publish using Darc variables: - - template: /eng/common/core-templates/post-build/common-variables.yml - - template: /eng/common/core-templates/variables/pool-providers.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} + - template: /eng/common/core-templates/post-build/common-variables.yml + - template: /eng/common/core-templates/variables/pool-providers.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: Publish Using Darc @@ -291,41 +291,42 @@ stages: os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows ${{ else }}: name: NetCore1ESPool-Publishing-Internal - demands: ImageOverride -equals windows.vs2019.amd64 + demands: ImageOverride -equals windows.vs2022.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: NuGetAuthenticate@1 - - # Populate internal runtime variables. - - template: /eng/common/templates/steps/enable-internal-sources.yml - parameters: - legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - - - template: /eng/common/templates/steps/enable-internal-runtimes.yml - - - task: UseDotNet@2 - inputs: - version: 8.0.x - - - task: AzureCLI@2 - displayName: Publish Using Darc - inputs: - azureSubscription: "Darc: Maestro Production" - scriptType: ps - scriptLocation: scriptPath - scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 - arguments: > + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: NuGetAuthenticate@1 + + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + + # Darc is targeting 8.0, so make sure it's installed + - task: UseDotNet@2 + inputs: + version: 8.0.x + + - task: AzureCLI@2 + displayName: Publish Using Darc + inputs: + azureSubscription: "Darc: Maestro Production" + scriptType: ps + scriptLocation: scriptPath + scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} -AzdoToken '$(System.AccessToken)' diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index 003f7eae0fa..c05f6502797 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -5,7 +5,7 @@ # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: - PackageVersion: 11.0.0 + PackageVersion: 10.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom diff --git a/eng/common/core-templates/steps/install-microbuild-impl.yml b/eng/common/core-templates/steps/install-microbuild-impl.yml deleted file mode 100644 index b9e0143ee92..00000000000 --- a/eng/common/core-templates/steps/install-microbuild-impl.yml +++ /dev/null @@ -1,34 +0,0 @@ -parameters: - - name: microbuildTaskInputs - type: object - default: {} - - - name: microbuildEnv - type: object - default: {} - - - name: enablePreviewMicrobuild - type: boolean - default: false - - - name: condition - type: string - - - name: continueOnError - type: boolean - -steps: -- ${{ if eq(parameters.enablePreviewMicrobuild, 'true') }}: - - task: MicroBuildSigningPluginPreview@4 - displayName: Install Preview MicroBuild plugin - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} -- ${{ else }}: - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml index 4f4b56ed2a6..553fce66b94 100644 --- a/eng/common/core-templates/steps/install-microbuild.yml +++ b/eng/common/core-templates/steps/install-microbuild.yml @@ -4,8 +4,6 @@ parameters: # Enable install tasks for MicroBuild on Mac and Linux # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' enableMicrobuildForMacAndLinux: false - # Enable preview version of MB signing plugin - enablePreviewMicrobuild: false # Determines whether the ESRP service connection information should be passed to the signing plugin. # This overlaps with _SignType to some degree. We only need the service connection for real signing. # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. @@ -15,8 +13,6 @@ parameters: microbuildUseESRP: true # Microbuild installation directory microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild - # Microbuild version - microbuildPluginVersion: 'latest' continueOnError: false @@ -73,46 +69,42 @@ steps: # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, # we can avoid including the MB install step if not enabled at all. This avoids a bunch of # extra pipeline authorizations, since most pipelines do not sign on non-Windows. - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (Windows) + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) + + - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (non-Windows) + inputs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} + workingDirectory: ${{ parameters.microBuildOutputFolder }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca - microbuildEnv: + ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc + env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) - - - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} - workingDirectory: ${{ parameters.microBuildOutputFolder }} - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 - ${{ else }}: - ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc - microbuildEnv: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index acf16ed3496..b9c86c18ae4 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -24,7 +24,7 @@ steps: # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey '$(dotnetbuilds-internal-container-read-token-base64)'' fi buildConfig=Release diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index ac019e2d033..e9a694afa58 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -1,6 +1,6 @@ parameters: - sourceIndexUploadPackageVersion: 2.0.0-20250906.1 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20250906.1 + sourceIndexUploadPackageVersion: 2.0.0-20250818.1 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250818.1 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json binlogPath: artifacts/log/Debug/Build.binlog @@ -14,8 +14,8 @@ steps: workingDirectory: $(Agent.TempDirectory) - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index 9f5ad6b763b..e889f439b8d 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -5,7 +5,7 @@ darcVersion='' versionEndpoint='https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20' verbosity='minimal' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --darcversion) diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh index 61f302bb677..7b9d97e3bd4 100755 --- a/eng/common/dotnet-install.sh +++ b/eng/common/dotnet-install.sh @@ -18,7 +18,7 @@ architecture='' runtime='dotnet' runtimeSourceFeed='' runtimeSourceFeedKey='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in -version|-v) diff --git a/eng/common/dotnet.sh b/eng/common/dotnet.sh index f6d24871c1d..2ef68235675 100644 --- a/eng/common/dotnet.sh +++ b/eng/common/dotnet.sh @@ -19,7 +19,7 @@ source $scriptroot/tools.sh InitializeDotNetCli true # install # Invoke acquired SDK with args if they are provided -if [[ $# -gt 0 ]]; then +if [[ $# > 0 ]]; then __dotnetDir=${_InitializeDotNetCli} dotnetPath=${__dotnetDir}/dotnet ${dotnetPath} "$@" diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh index 6299e7effd4..9378223ba09 100755 --- a/eng/common/internal-feed-operations.sh +++ b/eng/common/internal-feed-operations.sh @@ -100,7 +100,7 @@ operation='' authToken='' repoName='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --operation) diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh index 64b87d0bcc3..477a44f335b 100644 --- a/eng/common/native/install-dependencies.sh +++ b/eng/common/native/install-dependencies.sh @@ -27,11 +27,9 @@ case "$os" in libssl-dev libkrb5-dev pigz cpio localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ] || [ "$ID" = "centos"]; then + elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio - elif [ "$ID" = "amzn" ]; then - dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio elif [ "$ID" = "alpine" ]; then apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio else diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index fc0218a013d..472d5bb562c 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -9,8 +9,7 @@ param( [Parameter(Mandatory=$false)][string] $TokensFilePath, [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact, [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, - [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey -) + [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey) try { $ErrorActionPreference = 'Stop' diff --git a/eng/common/templates/variables/pool-providers.yml b/eng/common/templates/variables/pool-providers.yml index e0b19c14a07..18693ea120d 100644 --- a/eng/common/templates/variables/pool-providers.yml +++ b/eng/common/templates/variables/pool-providers.yml @@ -23,7 +23,7 @@ # # pool: # name: $(DncEngInternalBuildPool) -# demands: ImageOverride -equals windows.vs2019.amd64 +# demands: ImageOverride -equals windows.vs2022.amd64 variables: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - template: /eng/common/templates-official/variables/pool-providers.yml diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index e8e9f7615f1..049fe6db994 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -157,6 +157,9 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { return $global:_DotNetInstallDir } + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + $env:DOTNET_MULTILEVEL_LOOKUP=0 + # Disable first run since we do not need all ASP.NET packages restored. $env:DOTNET_NOLOGO=1 @@ -222,6 +225,7 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { # Make Sure that our bootstrapped dotnet cli is available in future steps of the Azure Pipelines build Write-PipelinePrependPath -Path $dotnetRoot + Write-PipelineSetVariable -Name 'DOTNET_MULTILEVEL_LOOKUP' -Value '0' Write-PipelineSetVariable -Name 'DOTNET_NOLOGO' -Value '1' return $global:_DotNetInstallDir = $dotnetRoot @@ -556,19 +560,26 @@ function LocateVisualStudio([object]$vsRequirements = $null){ }) } - if (!$vsRequirements) { $vsRequirements = $GlobalJson.tools.vs } + if (!$vsRequirements) { + if (Get-Member -InputObject $GlobalJson.tools -Name 'vs' -ErrorAction SilentlyContinue) { + $vsRequirements = $GlobalJson.tools.vs + } else { + $vsRequirements = $null + } + } + $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') if (!$excludePrereleaseVS) { $args += '-prerelease' } - if (Get-Member -InputObject $vsRequirements -Name 'version') { + if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'version' -ErrorAction SilentlyContinue)) { $args += '-version' $args += $vsRequirements.version } - if (Get-Member -InputObject $vsRequirements -Name 'components') { + if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'components' -ErrorAction SilentlyContinue)) { foreach ($component in $vsRequirements.components) { $args += '-requires' $args += $component diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 6c121300ac7..c1841c9dfd0 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -115,6 +115,9 @@ function InitializeDotNetCli { local install=$1 + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + export DOTNET_MULTILEVEL_LOOKUP=0 + # Disable first run since we want to control all package sources export DOTNET_NOLOGO=1 @@ -163,6 +166,7 @@ function InitializeDotNetCli { # build steps from using anything other than what we've downloaded. Write-PipelinePrependPath -path "$dotnet_root" + Write-PipelineSetVariable -name "DOTNET_MULTILEVEL_LOOKUP" -value "0" Write-PipelineSetVariable -name "DOTNET_NOLOGO" -value "1" # return value diff --git a/eng/restore-toolset.sh b/eng/restore-toolset.sh index 8a7bb526c06..cdcf18f1d19 100644 --- a/eng/restore-toolset.sh +++ b/eng/restore-toolset.sh @@ -3,7 +3,7 @@ # Install MAUI workload if -restoreMaui was passed # Only on macOS (MAUI doesn't support Linux, Windows uses .cmd) -if [[ "$restore_maui" == true ]]; then +if [[ "${restore_maui:-false}" == true ]]; then # Check if we're on macOS if [[ "$(uname -s)" == "Darwin" ]]; then echo "" diff --git a/global.json b/global.json index 087505bbcae..39ccee4a4d2 100644 --- a/global.json +++ b/global.json @@ -33,8 +33,8 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.25610.3", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.25610.3", - "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.25610.3" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26110.1", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26110.1", + "Microsoft.DotNet.SharedFramework.Sdk": "10.0.0-beta.26110.1" } } From fb80caff01f839571218cb4290c7eb0c548da3d2 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 12 Feb 2026 13:59:10 -0800 Subject: [PATCH 06/10] Refactor backmerge PR creation to update existing PRs and streamline body formatting (#14476) --- .github/workflows/backmerge-release.yml | 67 ++++++++++++++++--------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/.github/workflows/backmerge-release.yml b/.github/workflows/backmerge-release.yml index 05cd2c507d1..b8c92f0d4fc 100644 --- a/.github/workflows/backmerge-release.yml +++ b/.github/workflows/backmerge-release.yml @@ -57,36 +57,57 @@ jobs: echo "Merge conflicts detected" fi - - name: Create Pull Request + - name: Create or update Pull Request if: steps.check.outputs.changes == 'true' && steps.merge.outputs.merge_success == 'true' id: create-pr - uses: dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - head: backmerge/release-13.2-to-main - base: main - title: "[Automated] Backmerge release/13.2 to main" - labels: area-engineering-systems - body: | - ## Automated Backmerge - - This PR merges changes from `release/13.2` back into `main`. - - **Commits to merge:** ${{ steps.check.outputs.behind_count }} - - This PR was created automatically to keep `main` up-to-date with release branch changes. - Once approved, it will auto-merge. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if a PR already exists for this branch + EXISTING_PR=$(gh pr list --head backmerge/release-13.2-to-main --base main --json number --jq '.[0].number // empty') - --- - *This PR was generated by the [backmerge-release](${{ github.server_url }}/${{ github.repository }}/actions/workflows/backmerge-release.yml) workflow.* + if [ -n "$EXISTING_PR" ]; then + echo "PR #$EXISTING_PR already exists, updating it" + echo "pull_request_number=$EXISTING_PR" >> $GITHUB_OUTPUT + else + PR_BODY="## Automated Backmerge + + This PR merges changes from \`release/13.2\` back into \`main\`. + + **Commits to merge:** ${{ steps.check.outputs.behind_count }} + + This PR was created automatically to keep \`main\` up-to-date with release branch changes. + Once approved, it will auto-merge. + + --- + *This PR was generated by the [backmerge-release](${{ github.server_url }}/${{ github.repository }}/actions/workflows/backmerge-release.yml) workflow.*" + + # Remove leading whitespace from heredoc-style body + PR_BODY=$(echo "$PR_BODY" | sed 's/^ //') + + PR_URL=$(gh pr create \ + --head backmerge/release-13.2-to-main \ + --base main \ + --title "[Automated] Backmerge release/13.2 to main" \ + --body "$PR_BODY" \ + --assignee joperezr,radical \ + --label area-engineering-systems) + + PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + if [ -z "$PR_NUMBER" ]; then + echo "::error::Failed to extract PR number from: $PR_URL" + exit 1 + fi + echo "pull_request_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "Created PR #$PR_NUMBER" + fi - - name: Add assignees and enable auto-merge - if: steps.create-pr.outputs.pull-request-number + - name: Enable auto-merge + if: steps.create-pr.outputs.pull_request_number env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh pr edit ${{ steps.create-pr.outputs.pull-request-number }} --add-assignee joperezr,radical - gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --merge + gh pr merge ${{ steps.create-pr.outputs.pull_request_number }} --auto --merge - name: Create issue for merge conflicts if: steps.check.outputs.changes == 'true' && steps.merge.outputs.merge_success == 'false' From e9c1fc1d7f874a8e582f215a511664c41ef0af07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:03:40 -0800 Subject: [PATCH 07/10] [main] Fix transitive Azure role assignments through WaitFor dependencies (#14478) * Initial plan * Fix transitive Azure role assignments through WaitFor dependencies Remove CollectAnnotationDependencies calls from CollectDependenciesFromValue to prevent WaitFor/parent/connection-string-redirect annotations from referenced resources being included as direct dependencies of the caller. Add tests verifying: - DirectOnly mode excludes WaitFor deps from referenced resources - WaitFor doesn't create transitive role assignments in Azure publish Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- .../ApplicationModel/ResourceExtensions.cs | 2 -- .../RoleAssignmentTests.cs | 30 +++++++++++++++++++ .../ResourceDependencyTests.cs | 25 ++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 4836d473b9a..fec5e55d6c7 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1416,7 +1416,6 @@ private static void CollectDependenciesFromValue(object? value, HashSet("server", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints() + .WithReference(cache) + .WaitFor(cache); + + builder.AddProject("webfrontend", launchProfileName: null) + .WithReference(server) + .WaitFor(server); + + var app = builder.Build(); + var model = app.Services.GetRequiredService(); + + await ExecuteBeforeStartHooksAsync(app, default); + + // The server should have a role assignment to the cache since it directly references it + Assert.Single(model.Resources.OfType(), r => r.Name == "server-roles-cache"); + + // The webfrontend should NOT have a role assignment to the cache since it only references the server + Assert.DoesNotContain(model.Resources, r => r.Name == "webfrontend-roles-cache"); + } + private static async Task RoleAssignmentTest( string azureResourceName, Action configureBuilder, diff --git a/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs b/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs index 65ae7925b24..0688a07e352 100644 --- a/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs @@ -545,6 +545,31 @@ public async Task DirectOnlyIncludesReferencedResourcesFromConnectionString() Assert.Contains(postgres.Resource, dependencies); } + [Fact] + public async Task DirectOnlyDoesNotIncludeWaitForDependenciesFromReferencedResources() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + // Chain: A -> (ref) B -> (waitfor) C + // A has WithReference(B) and WaitFor(B) + // B has WaitFor(C) but A does NOT reference C directly + var c = builder.AddRedis("c"); + var b = builder.AddContainer("b", "alpine") + .WithHttpEndpoint(5000, 5000, "http") + .WaitFor(c); + var a = builder.AddContainer("a", "alpine") + .WithReference(b.GetEndpoint("http")) + .WaitFor(b); + + var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); + var dependencies = await a.Resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.DirectOnly); + + // A depends on B (via WithReference and WaitFor) + Assert.Contains(b.Resource, dependencies); + // A should NOT depend on C because C is only a WaitFor dependency of B, not of A + Assert.DoesNotContain(c.Resource, dependencies); + } + [Fact] public async Task DefaultOverloadUsesTransitiveClosure() { From 8c34f07c71a3f072a5209e5f4330ac10a6e29248 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:52:12 +0000 Subject: [PATCH 08/10] Initial plan From 0349c0973461093fc64ed5379f73282f77bfd72e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:56:46 +0000 Subject: [PATCH 09/10] Update all Windows image names to 1es-windows-2022 Co-authored-by: radical <1472+radical@users.noreply.github.com> --- eng/pipelines/azure-pipelines-unofficial.yml | 4 ++-- eng/pipelines/azure-pipelines.yml | 4 ++-- eng/pipelines/release-publish-nuget.yml | 12 ++++++------ eng/pipelines/templates/build_sign_native.yml | 2 +- eng/pipelines/templates/public-pipeline-template.yml | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml index ef0ca001eb1..daab5de0533 100644 --- a/eng/pipelines/azure-pipelines-unofficial.yml +++ b/eng/pipelines/azure-pipelines-unofficial.yml @@ -45,7 +45,7 @@ extends: sdl: sourceAnalysisPool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows containers: linux_x64: @@ -135,7 +135,7 @@ extends: pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows variables: diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 3cc62a0c7ea..cfce9cf8ec2 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -120,7 +120,7 @@ extends: justificationForDisabling: 'see https://portal.microsofticm.com/imp/v3/incidents/incident/482258316/summary' sourceAnalysisPool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows tsa: enabled: true @@ -214,7 +214,7 @@ extends: pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows variables: diff --git a/eng/pipelines/release-publish-nuget.yml b/eng/pipelines/release-publish-nuget.yml index f2e25aefaa5..774fc01f4a7 100644 --- a/eng/pipelines/release-publish-nuget.yml +++ b/eng/pipelines/release-publish-nuget.yml @@ -63,7 +63,7 @@ extends: sdl: sourceAnalysisPool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows stages: @@ -74,7 +74,7 @@ extends: displayName: 'Validate Release Inputs' pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows steps: - checkout: none @@ -109,7 +109,7 @@ extends: displayName: 'Extract BAR Build ID from Build Tags' pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows steps: - checkout: none @@ -164,7 +164,7 @@ extends: timeoutInMinutes: 60 pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows steps: - checkout: self @@ -368,7 +368,7 @@ extends: displayName: 'Promote Build to GA Channel' pool: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: 1es-windows-2022 os: windows steps: - checkout: self @@ -439,7 +439,7 @@ extends: displayName: 'Print Release Summary' pool: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows steps: - checkout: none diff --git a/eng/pipelines/templates/build_sign_native.yml b/eng/pipelines/templates/build_sign_native.yml index 9b8faab6a2c..0b99931dc0f 100644 --- a/eng/pipelines/templates/build_sign_native.yml +++ b/eng/pipelines/templates/build_sign_native.yml @@ -39,7 +39,7 @@ jobs: pool: ${{ if eq(parameters.agentOs, 'windows') }}: name: NetCore1ESPool-Internal - image: windows.vs2022preview.amd64 + image: 1es-windows-2022 os: windows ${{ if eq(parameters.agentOs, 'linux') }}: name: NetCore1ESPool-Internal diff --git a/eng/pipelines/templates/public-pipeline-template.yml b/eng/pipelines/templates/public-pipeline-template.yml index ad9e96a7a56..d74d31ba354 100644 --- a/eng/pipelines/templates/public-pipeline-template.yml +++ b/eng/pipelines/templates/public-pipeline-template.yml @@ -86,7 +86,7 @@ stages: pool: name: $(DncEngPublicBuildPool) - demands: ImageOverride -equals windows.vs2022preview.amd64.open + demands: ImageOverride -equals 1es-windows-2022-open variables: - name: _buildScript From 385db4f34651748d6176f0ae2782f533f6a965e8 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 12 Feb 2026 18:15:47 -0800 Subject: [PATCH 10/10] Update pipeline image names in public-pipeline-template (#14486) --- eng/pipelines/templates/public-pipeline-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/templates/public-pipeline-template.yml b/eng/pipelines/templates/public-pipeline-template.yml index d74d31ba354..5c394939d03 100644 --- a/eng/pipelines/templates/public-pipeline-template.yml +++ b/eng/pipelines/templates/public-pipeline-template.yml @@ -119,7 +119,7 @@ stages: pool: name: $(DncEngPublicBuildPool) - demands: ImageOverride -equals build.ubuntu.2204.amd64.open + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open variables: - name: _buildScript