diff --git a/.azure/pipelines/ci-public.yml b/.azure/pipelines/ci-public.yml index e0d7b2fae27e..507850d7f776 100644 --- a/.azure/pipelines/ci-public.yml +++ b/.azure/pipelines/ci-public.yml @@ -636,8 +636,8 @@ stages: agentOs: Windows isAzDOTestingJob: true timeoutInMinutes: 240 - # Temporarily disabled due to https://github.com/dotnet/aspnetcore/issues/63140 - condition: 'false' + # Re-enabled after fixing template isolation conflicts + condition: 'true' steps: - script: git submodule update --init displayName: Update submodules diff --git a/CI_TEMPLATE_TESTS_FIX_SUMMARY.md b/CI_TEMPLATE_TESTS_FIX_SUMMARY.md new file mode 100644 index 000000000000..754c5d83a67a --- /dev/null +++ b/CI_TEMPLATE_TESTS_FIX_SUMMARY.md @@ -0,0 +1,110 @@ +# CI Template Tests Fix Summary + +## Original Issue +The CI job at `.azure/pipelines/ci-public.yml:632` was disabled due to conflicts with concurrent template tests, causing build failures when both the local development validation and Helix template tests ran simultaneously. + +## Root Cause Analysis + +### Conflict Sources +1. **Global Template Installation Conflicts** + - Helix template tests used isolated custom hive (`--debug:custom-hive`) + - Blazor web script installed templates globally in user profile + - Concurrent install/uninstall operations caused race conditions + +2. **Shared Template Engine State** + - Both workflows accessed the same global template engine state + - Template packages were installed/uninstalled without isolation + - Led to "template not found" or "template already installed" errors + +3. **Environment Variable Pollution** + - Blazor web script modified global `DOTNET_ROOT`, `DOTNET_ROOT_X86`, and `Path` + - Could interfere with concurrent test execution environments + +## Solution Implemented + +### Template Isolation Strategy +Adopted the same isolation pattern already proven in Helix template tests: + +#### Before (Conflicting): +```powershell +# Global template installation +dotnet new install $PackagePath +# Global template usage +dotnet new $templateName --no-restore +``` + +#### After (Isolated): +```powershell +# Unique hive per test run +$CustomHivePath = "$PSScriptRoot/.templatehive-$templateName" +# Isolated installation +dotnet new install $PackagePath --debug:disable-sdk-templates --debug:custom-hive $CustomHivePath +# Isolated usage +dotnet new $templateName --no-restore --debug:custom-hive $CustomHivePath --debug:disable-sdk-templates +``` + +### Key Components +1. **Unique Custom Hive**: `$PSScriptRoot/.templatehive-$templateName` per test +2. **SDK Template Exclusion**: `--debug:disable-sdk-templates` prevents interference +3. **Automatic Cleanup**: Custom hive removed in `finally` block +4. **Process-Scoped Environment**: Variables scoped to avoid global pollution + +## Files Modified +- `src/ProjectTemplates/scripts/Test-Template.psm1`: Added template isolation +- `.azure/pipelines/ci-public.yml`: Re-enabled disabled job (condition: 'false' → 'true') + +## How to Test Similar Issues in the Future + +### 1. Identify Conflicts +```bash +# Check for global template operations +grep -r "dotnet new install" --exclude-dir=.git +grep -r "\.templateengine" --exclude-dir=.git + +# Check for environment variable modifications +grep -r "env:DOTNET_ROOT" --exclude-dir=.git +grep -r "env:Path" --exclude-dir=.git +``` + +### 2. Implement Isolation +- Use `--debug:custom-hive` for template operations +- Use unique directory names per test run +- Clean up custom hives in finally blocks +- Scope environment variables to process level + +### 3. Validate Solution +- Run both workflows individually to ensure they still work +- Run both workflows concurrently to verify no conflicts +- Check CI logs for template-related errors + +## Monitoring Points for Re-enabled Functionality + +### Success Indicators +- Both template test jobs complete successfully +- No "template not found" errors in logs +- No "template already installed" errors in logs +- No timeout issues in template operations +- Proper artifact flow between jobs + +### Warning Signs +- Template installation/uninstallation errors +- Test isolation failures +- Environment variable conflicts +- Resource access conflicts +- Concurrent access exceptions + +## Technical Notes + +### Template Engine Isolation +The `--debug:custom-hive` flag creates a completely isolated template engine instance: +- Templates installed in custom hive don't affect global template state +- Multiple custom hives can operate simultaneously without conflicts +- Each hive maintains its own template package registry + +### Command Syntax +The solution uses the proven command pattern from existing template tests: +```powershell +dotnet new {command} --debug:disable-sdk-templates --debug:custom-hive "{path}" +``` + +This ensures compatibility with the existing template test infrastructure while providing complete isolation. \ No newline at end of file diff --git a/src/ProjectTemplates/scripts/Test-Template.psm1 b/src/ProjectTemplates/scripts/Test-Template.psm1 index 9f63b5201cae..04d49954b76f 100644 --- a/src/ProjectTemplates/scripts/Test-Template.psm1 +++ b/src/ProjectTemplates/scripts/Test-Template.psm1 @@ -33,9 +33,15 @@ function Test-Template { Write-Verbose "Copying $PSScriptRoot/.runtime/shared/Microsoft.AspNetCore.App to $PSScriptRoot/.dotnet/shared"; Copy-Item -Path "$PSScriptRoot/.runtime/shared/Microsoft.AspNetCore.App" -Destination "$PSScriptRoot/.dotnet/shared" -Recurse -Force; + # Use process-scoped environment variables to avoid conflicts with concurrent tests $env:DOTNET_ROOT = "$PSScriptRoot/.dotnet"; $env:DOTNET_ROOT_X86 = "$PSScriptRoot/.dotnet"; $env:Path = "$PSScriptRoot/.dotnet;$env:Path"; + + # Use a unique custom hive for template isolation to avoid conflicts with concurrent template tests + $CustomHivePath = "$PSScriptRoot/.templatehive-$templateName"; + Remove-Item -Path $CustomHivePath -Recurse -ErrorAction Ignore; + $tmpDir = "$PSScriptRoot/$templateName"; Remove-Item -Path $tmpDir -Recurse -ErrorAction Ignore; Push-Location ..; @@ -50,30 +56,16 @@ function Test-Template { $PackageName = (Get-Item $PackagePath).Name; - if (-not (Test-Path "$($env:USERPROFILE)/.templateengine/packages/$PackageName")) { - Write-Verbose "Installing package from $PackagePath"; - dotnet new install $PackagePath; - } - else { - Write-Verbose "Uninstalling package from $PackagePath"; - if (-not ($PackageName -match $PackagePattern)) { - Write-Error "$PackageName did not match $PackagePattern"; - } - $PackageId = $Matches["PackageId"]; - $PackageVersion = $Matches["Version"]; - Write-Verbose "Uninstalling existing package $PackageId.$PackageVersion"; - dotnet new uninstall "$PackageId.$PackageVersion"; - - Write-Verbose "Installing package from $PackagePath"; - dotnet new install $PackagePath; - } + # Use custom hive for template installation to avoid conflicts with concurrent template tests + Write-Verbose "Installing package from $PackagePath using custom hive $CustomHivePath"; + dotnet new install $PackagePath --debug:disable-sdk-templates --debug:custom-hive $CustomHivePath; Write-Verbose "Creating directory $tmpDir" New-Item -ErrorAction Ignore -Path $tmpDir -ItemType Directory | Out-Null; Push-Location $tmpDir -StackName TemplateFolder; try { - $TemplateArguments = , "new" + $TemplateArguments + , "--no-restore"; + $TemplateArguments = , "new" + $TemplateArguments + , "--no-restore" + , "--debug:custom-hive" + , $CustomHivePath + , "--debug:disable-sdk-templates"; Write-Verbose "Running dotnet command with arguments: $TemplateArguments"; dotnet @TemplateArguments; @@ -124,6 +116,10 @@ function Test-Template { } finally { Pop-Location -StackName TemplateFolder; + + # Clean up custom hive to avoid conflicts with future runs + Write-Verbose "Cleaning up custom hive $CustomHivePath"; + Remove-Item -Path $CustomHivePath -Recurse -ErrorAction Ignore; } }