From cbf6323b24534410d243a97ce542e9bfe4dc8b30 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 3 Mar 2026 16:46:58 +0100 Subject: [PATCH 01/26] Make emulator startup and provisioning more robust Improve CI reliability for Android and iOS by hardening emulator/simulator startup and provisioning. Key changes: - .github/scripts/shared/Start-Emulator.ps1: remove auto-use of DEVICE_UDID from env and remove shutdown-of-other-simulators logic; shorten success message. - eng/pipelines/ci-copilot.yml: refactor job pool selection (use Ubuntu for Android), create artifact dirs early, and make provisioning parameters conditional by platform. - Replace earlier PS-based Android emulator invocation with a cross-platform shell flow: fix KVM permissions, install dependencies, locate AVDs, start emulator directly, wait for adb and boot completion, export DEVICE_UDID, and tighten timeouts. - Add Linux/macOS Java home detection, Node.js/Appium/Copilot/GH installs and authentication flows, Appium driver installs, and PATH handling. - Add emulator health checks, restart/reuse logic, and more robust artifact collection and cleanup around the PR reviewer step; make sed usage portable between Linux and macOS. - Improve iOS simulator handling: select runtime priority (iOS 18/17/26), prefer iPhone Xs or fallback to iPhone 11 Pro, create device if missing, shut down other booted sims, and set DEVICE_UDID. - eng/pipelines/common/provision.yml: when exact Xcode version isn't found, fall back to the latest available Xcode on the agent instead of failing. Overall effect: more resilient CI runs across Ubuntu/macOS agents, fewer false failures from missing runtimes, AVDs, or incompatible Xcode versions, and improved tooling installation and auth handling for the PR reviewer agent. --- .github/scripts/shared/Start-Emulator.ps1 | 22 +- eng/pipelines/ci-copilot.yml | 752 +++++++++++++--------- 2 files changed, 466 insertions(+), 308 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index a5d90f192530..b842d840a6cc 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -339,12 +339,7 @@ if ($Platform -eq "android") { exit 1 } - # Get device UDID if not provided - check env var first - if (-not $DeviceUdid -and $env:DEVICE_UDID) { - Write-Info "Using DEVICE_UDID from environment: $($env:DEVICE_UDID)" - $DeviceUdid = $env:DEVICE_UDID - } - + # Get device UDID if not provided if (-not $DeviceUdid) { Write-Info "Auto-detecting iOS simulator..." $simList = xcrun simctl list devices available --json | ConvertFrom-Json @@ -459,19 +454,6 @@ if ($Platform -eq "android") { Write-Success "iOS simulator: $deviceName ($DeviceUdid)" - # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device - $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json - $otherBooted = $bootedSims.devices.PSObject.Properties.Value | - ForEach-Object { $_ } | - Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } - - if ($otherBooted) { - foreach ($sim in $otherBooted) { - Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" - xcrun simctl shutdown $sim.udid 2>$null - } - } - # Boot simulator if not already booted Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null @@ -488,7 +470,7 @@ if ($Platform -eq "android") { exit 1 } - Write-Success "Simulator is booted and ready: $deviceName" + Write-Success "Simulator is booted and ready" #endregion } diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 05e4d11a2796..c051a9a58fb9 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -27,6 +27,8 @@ parameters: type: object default: name: AcesShared + demands: + - ImageOverride -equals ACES_VM_SharedPool_Tahoe variables: - template: /eng/pipelines/common/variables.yml@self @@ -41,7 +43,13 @@ stages: jobs: - job: CopilotReview displayName: 'Run Copilot PR Reviewer Agent' - pool: ${{ parameters.pool }} + # Android needs Linux (KVM for emulator), iOS needs macOS (Xcode/simulators) + ${{ if eq(parameters.Platform, 'android') }}: + pool: + name: Azure Pipelines + vmImage: ubuntu-22.04 + ${{ else }}: + pool: ${{ parameters.pool }} timeoutInMinutes: 180 steps: - checkout: self @@ -61,15 +69,21 @@ stages: echo "##vso[build.updatebuildnumber]PR ${{ parameters.PRNumber }} ${{ parameters.Platform }}" displayName: 'Set Pipeline Run Title' + # Create artifact directories early (prevents publish failures if steps are skipped) + - script: | + mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs + mkdir -p $(Build.ArtifactStagingDirectory)/logs + displayName: 'Create Artifact Directories' + # Provision environment (Xcode, .NET SDK, Android SDK, etc.) - template: common/provision.yml parameters: - skipXcode: false - skipProvisionator: false + skipXcode: ${{ eq(parameters.Platform, 'android') }} + skipProvisionator: true skipAndroidCommonSdks: ${{ eq(parameters.Platform, 'ios') }} skipAndroidPlatformApis: ${{ eq(parameters.Platform, 'ios') }} skipJdk: ${{ eq(parameters.Platform, 'ios') }} - skipSimulatorSetup: false + skipSimulatorSetup: ${{ eq(parameters.Platform, 'android') }} skipCertificates: true # Android emulator setup (skip for iOS) skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }} @@ -125,12 +139,32 @@ stages: # Auto-detect and set JAVA_HOME if not already set if (-not $env:JAVA_HOME) { - $jvmDir = "/Library/Java/JavaVirtualMachines" - $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($msJdk) { - $javaHome = "$($msJdk.FullName)/Contents/Home" - Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome" - Write-Host "Set JAVA_HOME to: $javaHome" + if ($IsLinux) { + # On Linux, look for JDK in standard locations + $jvmDirs = @("/usr/lib/jvm", "/usr/java") + foreach ($jvmDir in $jvmDirs) { + $jdk = Get-ChildItem "$jvmDir/java-17*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $jdk) { + $jdk = Get-ChildItem "$jvmDir/microsoft-*17*" -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if (-not $jdk) { + $jdk = Get-ChildItem "$jvmDir/temurin-17*" -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if ($jdk) { + Write-Host "##vso[task.setvariable variable=JAVA_HOME]$($jdk.FullName)" + Write-Host "Set JAVA_HOME to: $($jdk.FullName)" + break + } + } + } else { + # macOS + $jvmDir = "/Library/Java/JavaVirtualMachines" + $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($msJdk) { + $javaHome = "$($msJdk.FullName)/Contents/Home" + Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome" + Write-Host "Set JAVA_HOME to: $javaHome" + } } } displayName: 'Configure Android SDK PATH' @@ -138,61 +172,150 @@ stages: env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - # Start Android Emulator EARLY (using Start-Emulator.ps1 script) - - pwsh: | - Write-Host "=== Starting Android Emulator ===" - Write-Host "Working directory: $(Get-Location)" - Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT" - Write-Host "JAVA_HOME: $env:JAVA_HOME" - Write-Host "PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))" + # Start Android Emulator directly + # On Linux CI (Azure Pipelines Ubuntu): AVDs may be in ~/.config/.android/avd/ + # We set ANDROID_AVD_HOME so emulator can find them + - script: | + echo "=== Starting Android Emulator ===" + echo "ANDROID_SDK_ROOT: $ANDROID_SDK_ROOT" + echo "JAVA_HOME: $JAVA_HOME" + + # Fix KVM permissions (Azure Pipelines Ubuntu user not in kvm group by default) + echo "=== Fixing KVM permissions ===" + if [ -e /dev/kvm ]; then + echo "KVM device exists, checking group membership..." + echo "Current groups: $(groups)" + echo "KVM group: $(grep kvm /etc/group)" + sudo gpasswd -a $USER kvm + echo "Added $USER to kvm group" + # Also set permissions directly (group change needs relogin) + sudo chmod 666 /dev/kvm + echo "Set /dev/kvm permissions to 666" + ls -la /dev/kvm + fi - # Verify adb is in path - $adbPath = Get-Command adb -ErrorAction SilentlyContinue - if ($adbPath) { - Write-Host "adb found at: $($adbPath.Source)" - } else { - Write-Host "adb NOT in PATH, checking manually..." - $manualAdb = Join-Path $env:ANDROID_SDK_ROOT "platform-tools/adb" - if (Test-Path $manualAdb) { - Write-Host "adb exists at $manualAdb but not in PATH" - } - } + # Install missing shared libraries (libpulse needed by emulator) + echo "=== Installing emulator dependencies ===" + sudo apt-get update -qq + sudo apt-get install -y -qq libpulse0 > /dev/null 2>&1 || true + + EMULATOR_BIN="$ANDROID_SDK_ROOT/emulator/emulator" + echo "Emulator binary: $EMULATOR_BIN" + $EMULATOR_BIN -version 2>&1 | head -3 + + # Find AVD location - check multiple possible paths + AVD_HOME="" + for candidate in "$HOME/.android/avd" "$HOME/.config/.android/avd"; do + if [ -d "$candidate/Emulator_30.avd" ]; then + AVD_HOME="$candidate" + echo "Found AVD at: $candidate/Emulator_30.avd" + break + fi + done - $scriptPath = ".github/scripts/shared/Start-Emulator.ps1" - Write-Host "Script path: $scriptPath" + if [ -z "$AVD_HOME" ]; then + echo "##[error]Could not find Emulator_30 AVD in any known location" + find /home -name "Emulator_30.avd" -type d 2>/dev/null || true + exit 1 + fi - if (-not (Test-Path $scriptPath)) { - Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath" + export ANDROID_AVD_HOME="$AVD_HOME" + echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" + + # Verify emulator can see the AVD + echo "Available AVDs:" + $EMULATOR_BIN -list-avds 2>/dev/null || true + + # Check KVM access + echo "=== Hardware acceleration check ===" + $EMULATOR_BIN -accel-check 2>&1 || true + + # Start the emulator + echo "=== Starting emulator ===" + EMULATOR_LOG="/tmp/emulator-Emulator_30.log" + nohup $EMULATOR_BIN -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -no-metrics > "$EMULATOR_LOG" 2>&1 & + EMULATOR_PID=$! + echo "Emulator PID: $EMULATOR_PID" + + # Wait briefly and check if process is still alive + sleep 5 + if ! kill -0 $EMULATOR_PID 2>/dev/null; then + echo "##[error]Emulator process died immediately" + cat "$EMULATOR_LOG" 2>/dev/null | tail -50 exit 1 - } - Write-Host "Script exists: OK" + fi + echo "Emulator process is running" - $ErrorActionPreference = "Continue" - try { - Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_30..." - & "./$scriptPath" -Platform android -DeviceUdid Emulator_30 2>&1 | ForEach-Object { Write-Host $_ } - $exitCode = $LASTEXITCODE - Write-Host "Script returned exit code: $exitCode" - } catch { - Write-Host "##vso[task.logissue type=error]Exception: $_" - Write-Host $_.ScriptStackTrace - $exitCode = 1 - } + # Wait for ADB device + echo "=== Waiting for ADB device ===" + adb start-server 2>/dev/null || true + device_timeout=120 + device_waited=0 + while ! adb devices 2>/dev/null | grep -q "emulator.*device"; do + if adb devices 2>/dev/null | grep -q "emulator.*offline"; then + echo "Device found but offline, waiting..." + fi + sleep 5 + device_waited=$((device_waited + 5)) + if [ $device_waited -ge $device_timeout ]; then + echo "##[error]Emulator device did not appear after ${device_timeout}s" + cat "$EMULATOR_LOG" 2>/dev/null | tail -30 + exit 1 + fi + if ! kill -0 $EMULATOR_PID 2>/dev/null; then + echo "##[error]Emulator process died while waiting for device" + cat "$EMULATOR_LOG" 2>/dev/null | tail -30 + exit 1 + fi + done - if ($exitCode -ne 0) { - Write-Host "##vso[task.logissue type=error]Start-Emulator failed with code $exitCode" - exit $exitCode - } + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') + echo "✅ Emulator detected: $DEVICE_ID" + + # Wait for boot completion + echo "=== Waiting for boot ===" + boot_timeout=300 + boot_waited=0 + while [ "$(adb -s $DEVICE_ID shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do + sleep 5 + boot_waited=$((boot_waited + 5)) + if [ $boot_waited -ge $boot_timeout ]; then + echo "##[error]Boot timeout after ${boot_timeout}s" + exit 1 + fi + done + echo "✅ Emulator fully booted: $DEVICE_ID" - Write-Host "=== Android Emulator Started Successfully ===" + # Wait for package manager + echo "Waiting for package manager service..." + pm_timeout=60 + pm_waited=0 + while ! adb -s $DEVICE_ID shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 3 + pm_waited=$((pm_waited + 3)) + if [ $pm_waited -ge $pm_timeout ]; then + echo "⚠️ Package manager not ready (non-fatal)" + break + fi + done + if [ $pm_waited -lt $pm_timeout ]; then + echo "✅ Package manager service is ready" + fi + + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + echo "" + echo "=== ✅ Android Emulator Started Successfully ===" + adb devices -l displayName: 'Start Android Emulator' condition: eq('${{ parameters.Platform }}', 'android') - timeoutInMinutes: 35 + timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME) - # Install .NET and workloads via build.ps1 - - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic + # Install .NET and workloads via Cake (build.ps1 works on both Linux and macOS via pwsh) + - pwsh: | + ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic displayName: 'Install .NET and workloads' retryCountOnTaskFailure: 2 env: @@ -203,7 +326,8 @@ stages: displayName: 'Add .NET to PATH' # Build MSBuild tasks (required for MAUI builds) - - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic + - pwsh: | + ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic displayName: 'Build MSBuild Tasks' retryCountOnTaskFailure: 1 env: @@ -236,10 +360,38 @@ stages: echo "✓ Android SDK: $SDK_PATH" fi fi + + # Check emulator binary + echo "Checking Android Emulator..." + if which emulator > /dev/null 2>&1; then + echo "✓ emulator: $(which emulator)" + emulator -version 2>&1 | head -1 || true + else + echo "⚠️ emulator not in PATH (may be at \$ANDROID_SDK_ROOT/emulator/emulator)" + fi + + # Check adb + echo "Checking adb..." + if which adb > /dev/null 2>&1; then + echo "✓ adb: $(which adb)" + else + echo "⚠️ adb not in PATH" + fi + + # Check KVM (Linux) or HVF (macOS) hardware acceleration + echo "Checking hardware virtualization..." + if [ "$(uname)" = "Linux" ]; then + if [ -e /dev/kvm ]; then + echo "✓ KVM available (/dev/kvm exists)" + ls -la /dev/kvm + else + ERRORS="${ERRORS}\n- KVM not available (/dev/kvm missing)" + fi + fi fi - # Check Xcode (macOS only) - if [ "$(uname)" = "Darwin" ]; then + # Check Xcode (macOS only, iOS only) + if [ "$(uname)" = "Darwin" ] && [ "${{ parameters.Platform }}" = "ios" ]; then echo "Checking Xcode..." if ! xcodebuild -version; then ERRORS="${ERRORS}\n- Xcode not available" @@ -287,94 +439,32 @@ stages: echo "Tools restored successfully" displayName: 'Restore .NET Tools' - # Boot Android emulator for UI tests + # Verify Android emulator is running - script: | - echo "=== Booting Android Emulator ===" - - # Match maui-pr Cake script: on macOS use default GPU (hardware-accelerated), - # only use swiftshader on Linux. AcesShared agents are ARM64 macOS with HVF. - # Start emulator in background with logging for debugging - EMULATOR_LOG="/tmp/emulator-boot.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & - EMULATOR_PID=$! - echo "Emulator started with PID: $EMULATOR_PID" - - # Give emulator a moment to start and verify process is running - sleep 3 - if ! pgrep -f 'qemu-system' > /dev/null; then - echo "##vso[task.logissue type=error]Emulator process did not start" - echo "=== Emulator Log ===" - cat "$EMULATOR_LOG" 2>/dev/null || echo "No log available" + echo "=== Verifying Android Emulator ===" + adb devices -l + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') + if [ -z "$DEVICE_ID" ]; then + echo "##vso[task.logissue type=error]No emulator device found" exit 1 fi - echo "Emulator process verified running" - - # Wait for device to appear with timeout (don't use adb wait-for-device - can hang forever) - echo "Waiting for emulator device..." - device_timeout=60 - device_waited=0 - while ! adb devices | grep -q "emulator.*device"; do - sleep 2 - device_waited=$((device_waited + 2)) - if [ $device_waited -ge $device_timeout ]; then - echo "##vso[task.logissue type=error]Emulator device did not appear in time" - echo "=== Emulator Log (last 50 lines) ===" - tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" - adb devices -l - exit 1 - fi - done - echo "Emulator device detected" - - # Wait for boot_completed - echo "Waiting for emulator to finish booting..." - timeout=600 - waited=0 - while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do - sleep 2 - waited=$((waited + 2)) - echo "Waiting for boot... ($waited/$timeout seconds)" - if [ $waited -ge $timeout ]; then - echo "##vso[task.logissue type=error]Emulator did not boot in time" - echo "=== Emulator Log (last 50 lines) ===" - tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available" - adb devices -l - exit 1 - fi - done - echo "Boot completed flag set" - - # Wait for package manager service to be available (critical for app installation) - echo "Waiting for package manager service..." - pm_timeout=120 - pm_waited=0 - while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do - sleep 3 - pm_waited=$((pm_waited + 3)) - echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" - if [ $pm_waited -ge $pm_timeout ]; then - echo "##vso[task.logissue type=error]Package manager service did not start" - echo "=== Checking services ===" - adb shell service list 2>/dev/null | head -20 || echo "Cannot list services" - exit 1 - fi - done - echo "Package manager service is ready" - - echo "=== Emulator booted successfully! ===" - adb devices -l - displayName: 'Boot Android Emulator' + echo "✅ Emulator running: $DEVICE_ID" + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + displayName: 'Verify Android Emulator' condition: eq('${{ parameters.Platform }}', 'android') - continueOnError: true - timeoutInMinutes: 15 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME_17_X64) + timeoutInMinutes: 5 + # Install Node.js (cross-platform: apt on Linux, brew on macOS) - script: | - echo "Installing Node.js 22..." - brew install node@22 - brew link --overwrite node@22 + echo "Installing Node.js..." + if [ "$(uname)" = "Linux" ]; then + # Use NodeSource for Node.js 22 on Linux + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + sudo apt-get install -y nodejs + else + brew install node@22 + brew link --overwrite node@22 + fi if ! node --version; then echo "##vso[task.logissue type=error]Failed to install Node.js" exit 1 @@ -383,103 +473,92 @@ stages: echo "Node.js installed successfully" displayName: 'Install Node.js' + # Install Appium and platform drivers - script: | - echo "Installing Appium and platform driver..." - + echo "Installing Appium and platform drivers..." + # Get npm global bin directory and add to PATH NPM_BIN=$(npm config get prefix)/bin echo "NPM global bin directory: $NPM_BIN" export PATH="$NPM_BIN:$PATH" - + # Install Appium globally npm install -g appium - - # Install both drivers — the agent may switch platforms if the bug - # only affects one platform (e.g., iOS bug triggered via Android run) + + # Install both drivers echo "Installing UiAutomator2 driver for Android..." appium driver install uiautomator2 echo "Installing XCUITest driver for iOS..." appium driver install xcuitest - + # Verify installation if ! which appium; then echo "##vso[task.logissue type=error]Failed to install Appium" exit 1 fi - + APPIUM_PATH=$(which appium) echo "Appium path: $APPIUM_PATH" echo "Appium version: $(appium --version)" echo "Appium drivers installed:" appium driver list --installed - - # Export PATH for subsequent steps (Azure DevOps specific) + + # Export PATH for subsequent steps echo "##vso[task.prependpath]$NPM_BIN" - + echo "Appium installed successfully" displayName: 'Install Appium' - - script: | - echo "Installing GitHub CLI..." - brew install gh - if ! gh --version; then - echo "##vso[task.logissue type=error]Failed to install GitHub CLI" - exit 1 - fi - echo "GitHub CLI installed successfully" - displayName: 'Install GitHub CLI' + # Verify/refresh Android Emulator before PR Reviewer runs + # Reuses the already-running emulator if healthy (avoids disk space issues on hosted agents) - script: | - echo "Authenticating with GitHub CLI..." - if [ -z "$(GH_CLI_TOKEN)" ]; then - echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." - exit 1 - fi - echo "$(GH_CLI_TOKEN)" | gh auth login --with-token - if ! gh auth status; then - echo "##vso[task.logissue type=error]GitHub CLI authentication failed" - exit 1 - fi - displayName: 'Authenticate GitHub CLI' - env: - GH_CLI_TOKEN: $(GH_CLI_TOKEN) + echo "=== Checking Android Emulator for PR Reviewer ===" - - script: | - echo "Installing GitHub Copilot CLI..." - npm install -g @github/copilot - if ! which copilot; then - echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" - exit 1 + # Check if emulator is still running and healthy + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') + if [ -n "$DEVICE_ID" ]; then + BOOT_DONE=$(adb -s "$DEVICE_ID" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$BOOT_DONE" = "1" ]; then + echo "✅ Emulator $DEVICE_ID is running and booted" + # Verify package manager is still responsive + if adb -s "$DEVICE_ID" shell pm list packages 2>/dev/null | grep -q "package:"; then + echo "✅ Package manager is responsive" + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + echo "=== Android Emulator Ready (reused) ===" + adb devices -l + exit 0 + fi + fi fi - echo "Copilot CLI installed successfully" - displayName: 'Install GitHub Copilot CLI' - # Restart Android emulator to ensure it's fresh before PR Reviewer runs - # The emulator can become unstable after running for a long time - - script: | - echo "=== Restarting Android Emulator for PR Reviewer ===" - + echo "⚠️ Emulator not healthy, attempting restart..." + + # Find AVD home (same logic as Start step) + AVD_HOME="" + for candidate in "$HOME/.android/avd" "$HOME/.config/.android/avd"; do + if [ -d "$candidate/Emulator_30.avd" ]; then + AVD_HOME="$candidate" + break + fi + done + export ANDROID_AVD_HOME="${AVD_HOME:-$HOME/.android/avd}" + # Kill any existing emulator - echo "Killing existing emulator..." - pkill -f 'qemu-system' 2>/dev/null || true + adb devices | grep emulator | awk '{print $1}' | xargs -I{} adb -s {} emu kill 2>/dev/null || true sleep 5 - + # Restart ADB server - echo "Restarting ADB server..." adb kill-server sleep 2 adb start-server sleep 2 - + # Start fresh emulator EMULATOR_LOG="/tmp/emulator-fresh.log" - echo "Starting fresh emulator..." - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 & - EMULATOR_PID=$! - echo "Emulator started with PID: $EMULATOR_PID" - - # Wait for device to appear - echo "Waiting for emulator device..." + nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -no-metrics > "$EMULATOR_LOG" 2>&1 & + + # Wait for device device_timeout=300 device_waited=0 while ! adb devices | grep -q "emulator.*device"; do @@ -490,15 +569,13 @@ stages: cat "$EMULATOR_LOG" 2>/dev/null | tail -30 exit 1 fi - echo "Waiting for device... ($device_waited/$device_timeout seconds)" done - echo "Emulator device detected" - - # Wait for boot_completed - echo "Waiting for boot_completed..." + + # Wait for boot + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') boot_timeout=600 boot_waited=0 - while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do + while [ "$(adb -s $DEVICE_ID shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do sleep 5 boot_waited=$((boot_waited + 5)) if [ $boot_waited -ge $boot_timeout ]; then @@ -506,37 +583,26 @@ stages: exit 1 fi done - echo "Boot completed" - - # Wait for package manager - echo "Waiting for package manager service..." - pm_timeout=120 - pm_waited=0 - while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do - sleep 5 - pm_waited=$((pm_waited + 5)) - if [ $pm_waited -ge $pm_timeout ]; then - echo "##vso[task.logissue type=error]Package manager service did not start" - adb shell service list 2>/dev/null | head -20 - exit 1 - fi - echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)" - done - + + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" echo "=== Fresh Android Emulator Ready! ===" adb devices -l displayName: 'Restart Android Emulator (Fresh)' condition: eq('${{ parameters.Platform }}', 'android') + continueOnError: true timeoutInMinutes: 15 env: ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) + JAVA_HOME: $(JAVA_HOME) # Boot iOS Simulator (only for iOS platform) - # UI test baseline screenshots are captured on iPhone Xs - must use same device - script: | echo "=== Booting iOS Simulator ===" - - # Find the latest stable iOS runtime (prefer 18.x, fallback to 17.x) + echo "Available runtimes:" + xcrun simctl list runtimes available + + # Find iOS runtime in priority order: 18.x > 17.x > 26.x + # iOS 26.x comes with Xcode 26 and may be the only runtime available RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' [.runtimes[] | select(.name | test("iOS 18"))] | sort_by(.version) | last | .identifier // empty ') @@ -545,62 +611,94 @@ stages: [.runtimes[] | select(.name | test("iOS 17"))] | sort_by(.version) | last | .identifier // empty ') fi + if [ -z "$RUNTIME" ]; then + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 26"))] | sort_by(.version) | last | .identifier // empty + ') + fi + + if [ -z "$RUNTIME" ]; then + echo "##vso[task.logissue type=error]No iOS runtime found (tried iOS 18, 17, 26)" + xcrun simctl list runtimes available + exit 1 + fi echo "Selected iOS runtime: $RUNTIME" - - # Look for iPhone Xs (matches UI test baselines - required for snapshot tests) - UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + + # Determine the correct device type based on runtime version + # iOS 26.x → iPhone 11 Pro (matches UITest.cs ios-26 environment and snapshot baselines) + # iOS 18.x/17.x → iPhone Xs (matches UITest.cs ios environment and snapshot baselines) + # Both devices have the same screen resolution: 375×812 pt, 1125×2436 px @3x + IS_IOS26=false + if echo "$RUNTIME" | grep -q "iOS-26"; then + IS_IOS26=true + fi + + if [ "$IS_IOS26" = "true" ]; then + PRIMARY_DEVICE="iPhone 11 Pro" + PRIMARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro" + SECONDARY_DEVICE="iPhone Xs" + SECONDARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-XS" + else + PRIMARY_DEVICE="iPhone Xs" + PRIMARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-XS" + SECONDARY_DEVICE="iPhone 11 Pro" + SECONDARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro" + fi + echo "Target device: $PRIMARY_DEVICE (fallback: $SECONDARY_DEVICE)" + + # Look for existing primary device + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" --arg name "$PRIMARY_DEVICE" ' .devices[$rt] // [] | - map(select(.name == "iPhone Xs")) | + map(select(.name == $name)) | .[0].udid // empty ') - - # If iPhone Xs doesn't exist, try iPhone 11 Pro (same 1125×2436 resolution) + + # Fallback to secondary device (same resolution) if [ -z "$UDID" ]; then - UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" --arg name "$SECONDARY_DEVICE" ' .devices[$rt] // [] | - map(select(.name == "iPhone 11 Pro")) | + map(select(.name == $name)) | .[0].udid // empty ') if [ -n "$UDID" ]; then - echo "Found existing iPhone 11 Pro (same resolution as iPhone Xs): $UDID" + echo "Found existing $SECONDARY_DEVICE: $UDID" fi else - echo "Found existing iPhone Xs: $UDID" + echo "Found existing $PRIMARY_DEVICE: $UDID" fi - - # If neither exists, try to create them + + # If neither exists, try to create one if [ -z "$UDID" ]; then - echo "No matching device found - attempting to create one for runtime $RUNTIME..." - - # Try iPhone Xs first - UDID=$(xcrun simctl create "iPhone Xs" com.apple.CoreSimulator.SimDeviceType.iPhone-Xs "$RUNTIME" 2>&1) + echo "No matching device found - creating $PRIMARY_DEVICE for runtime $RUNTIME..." + UDID=$(xcrun simctl create "$PRIMARY_DEVICE" "$PRIMARY_DEVICE_TYPE" "$RUNTIME" 2>&1) if [ $? -ne 0 ]; then - echo "iPhone Xs device type unavailable: $UDID" - # Try iPhone 11 Pro (same 1125×2436 resolution) - UDID=$(xcrun simctl create "iPhone 11 Pro" com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro "$RUNTIME" 2>&1) + echo "$PRIMARY_DEVICE device type unavailable: $UDID" + echo "Trying $SECONDARY_DEVICE..." + UDID=$(xcrun simctl create "$SECONDARY_DEVICE" "$SECONDARY_DEVICE_TYPE" "$RUNTIME" 2>&1) if [ $? -ne 0 ]; then - echo "##vso[task.logissue type=warning]Failed to create iPhone 11 Pro: $UDID" - # Last resort: first available iPhone - UDID=$(xcrun simctl list devices available --json | jq -r ' - .devices | to_entries | - map(.value) | flatten | + echo "##vso[task.logissue type=warning]Failed to create $SECONDARY_DEVICE: $UDID" + echo "Falling back to any available iPhone..." + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | map(select(.name | test("iPhone"))) | - .[0].udid + .[0].udid // empty ') else - echo "Created iPhone 11 Pro simulator: $UDID" + echo "Created $SECONDARY_DEVICE simulator: $UDID" fi else - echo "Created iPhone Xs simulator: $UDID" + echo "Created $PRIMARY_DEVICE simulator: $UDID" fi fi - + if [ -z "$UDID" ]; then echo "##vso[task.logissue type=error]No iOS simulator found" + echo "Available devices:" + xcrun simctl list devices available exit 1 fi - - # Shutdown any other booted simulators to avoid Appium connecting to wrong device + + # Shutdown any other booted simulators to avoid resource contention xcrun simctl list devices booted --json | jq -r ' .devices | to_entries | map(.value) | flatten | map(select(.state == "Booted" and .udid != "'"$UDID"'")) | @@ -609,51 +707,137 @@ stages: echo "Shutting down other simulator: $OTHER_UDID" xcrun simctl shutdown "$OTHER_UDID" 2>/dev/null || true done - + echo "Booting simulator: $UDID" xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" sleep 10 - + + # Log the selected device details for debugging + echo "=== Selected Simulator Details ===" + DEVICE_INFO=$(xcrun simctl list devices --json | jq -r --arg udid "$UDID" ' + .devices | to_entries[] | .value[] | select(.udid == $udid) | "\(.name) (\(.udid))" + ') + echo "Device: $DEVICE_INFO" + echo "Runtime: $RUNTIME" + echo "iOS 26 mode: $IS_IOS26" echo "Booted simulators:" xcrun simctl list devices booted - + echo "##vso[task.setvariable variable=DEVICE_UDID]$UDID" echo "iOS Simulator UDID: $UDID" displayName: 'Boot iOS Simulator' condition: eq('${{ parameters.Platform }}', 'ios') timeoutInMinutes: 5 + # Install GitHub CLI (cross-platform: apt on Linux, brew on macOS) + - script: | + echo "Installing GitHub CLI..." + if [ "$(uname)" = "Linux" ]; then + (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + else + brew install gh + fi + if ! gh --version; then + echo "##vso[task.logissue type=error]Failed to install GitHub CLI" + exit 1 + fi + echo "GitHub CLI installed successfully" + displayName: 'Install GitHub CLI' + + # Authenticate GitHub CLI by writing token directly to gh config + # (bypasses read:org scope validation that gh auth login requires) + - script: | + echo "Authenticating with GitHub CLI..." + if [ -z "$GH_CLI_TOKEN" ]; then + echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." + exit 1 + fi + # Write token to gh config directly (avoids read:org scope check) + mkdir -p "$HOME/.config/gh" + printf 'github.com:\n oauth_token: %s\n git_protocol: https\n' "$GH_CLI_TOKEN" > "$HOME/.config/gh/hosts.yml" + # Verify token works with a simple API call + LOGIN=$(gh api user --jq '.login' 2>&1) + if [ $? -ne 0 ]; then + echo "##vso[task.logissue type=error]GitHub CLI authentication failed: $LOGIN" + exit 1 + fi + echo "GitHub CLI authenticated successfully as: $LOGIN" + displayName: 'Authenticate GitHub CLI' + env: + GH_CLI_TOKEN: $(GH_CLI_TOKEN) + + # Install GitHub Copilot CLI + - script: | + echo "Installing GitHub Copilot CLI..." + npm install -g @github/copilot + if ! which copilot; then + echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" + exit 1 + fi + copilot --version + echo "Copilot CLI installed successfully" + displayName: 'Install GitHub Copilot CLI' + + # Authenticate Copilot CLI + - script: | + echo "Authenticating Copilot CLI..." + if [ -z "$COPILOT_GITHUB_TOKEN" ]; then + echo "##vso[task.logissue type=error]COPILOT_GITHUB_TOKEN is not set. Please configure the COPILOT_TOKEN pipeline variable with a fine-grained PAT." + exit 1 + fi + echo "COPILOT_GITHUB_TOKEN is set (${#COPILOT_GITHUB_TOKEN} chars)" + + # Verify gh CLI auth is available for copilot's fallback path + if gh auth status >/dev/null 2>&1; then + echo "✅ GitHub CLI is authenticated" + else + echo "⚠️ GitHub CLI not authenticated, copilot will use env var only" + fi + + echo "✅ Copilot CLI authentication configured" + displayName: 'Authenticate Copilot CLI' + env: + COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) + GH_TOKEN: $(GH_CLI_TOKEN) + + # Run the PR Reviewer Agent - script: | echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." - - # Configure git identity (required for merge operations on self-hosted agents) + + # Configure git identity git config user.email "copilot-ci@microsoft.com" git config user.name "Copilot CI" echo "Git identity configured" - + # Create Directory.Build.Override.props to skip Xcode version check - # AcesShared agents may have a newer Xcode than the .NET iOS SDK expects cp Directory.Build.Override.props.in Directory.Build.Override.props - # Insert ValidateXcodeVersion before closing tag - sed -i '' 's|| false\n|' Directory.Build.Override.props - + # sed -i syntax differs: Linux uses sed -i, macOS uses sed -i '' + if [ "$(uname)" = "Linux" ]; then + sed -i 's|| false\n|' Directory.Build.Override.props + else + sed -i '' 's|| false\n|' Directory.Build.Override.props + fi + # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - - # Invoke the PR reviewer using our PowerShell script - # The script will merge the PR into the current branch - # -PostSummaryComment and -RunFinalize handle posting comments + + # Invoke the PR reviewer set +e pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" COPILOT_EXIT_CODE=$? set -e - + echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" - - # Terminate any orphaned copilot CLI processes that could hold this step's - # stdout fd open and prevent the bash step from exiting. - # Only target processes whose command line includes the copilot CLI path. + + # Terminate any orphaned copilot CLI processes echo "Cleaning up orphaned copilot processes..." SELF_PID=$$ for proc in $(pgrep -f "[c]opilot" 2>/dev/null || true); do @@ -665,47 +849,40 @@ stages: fi fi done - - # Copy any Copilot session files + + # Copy Copilot session files if [ -d "$HOME/.copilot" ]; then echo "Copying Copilot session state..." cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state || true fi - + # Copy CustomAgentLogsTmp if it exists if [ -d "CustomAgentLogsTmp" ]; then echo "Copying CustomAgentLogsTmp..." cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ || true fi - + # Copy any Review_Feedback files find . -name "Review_Feedback_*.md" -type f -exec cp {} $(Build.ArtifactStagingDirectory)/copilot-logs/ \; 2>/dev/null || true - + # Copy any .github/agent-pr-session files if [ -d ".github/agent-pr-session" ]; then echo "Copying agent-pr-session..." cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ || true fi - - # Check for failure indicators in output + + # Check for failure if [ $COPILOT_EXIT_CODE -ne 0 ]; then echo "##vso[task.logissue type=error]Review-PR.ps1 exited with code $COPILOT_EXIT_CODE" - # Don't exit yet - let artifacts be published first echo "##vso[task.setvariable variable=CopilotFailed]true" fi - - # Check output for common failure patterns - if grep -qi "error\|failed\|exception" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then - if grep -qi "simulator.*not\|emulator.*not\|workload.*not\|sdk.*not found" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then - echo "##vso[task.logissue type=warning]Copilot encountered environment issues. Check artifacts for details." - fi - fi - + echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" displayName: 'Run PR Reviewer Agent' env: COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) GH_TOKEN: $(GH_COMMENT_TOKEN) + GITHUB_TOKEN: $(GH_CLI_TOKEN) DEVICE_UDID: $(DEVICE_UDID) # Publish Copilot logs and session artifacts @@ -733,4 +910,3 @@ stages: exit 1 fi displayName: 'Check Copilot Result' - condition: succeededOrFailed() From 5064c10a2f5c13c2b810d654cec4390729a091bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:33:23 -0600 Subject: [PATCH 02/26] Remove iOS-specific Start-Emulator.ps1 changes --- .github/scripts/shared/Start-Emulator.ps1 | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index b842d840a6cc..a5d90f192530 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -339,7 +339,12 @@ if ($Platform -eq "android") { exit 1 } - # Get device UDID if not provided + # Get device UDID if not provided - check env var first + if (-not $DeviceUdid -and $env:DEVICE_UDID) { + Write-Info "Using DEVICE_UDID from environment: $($env:DEVICE_UDID)" + $DeviceUdid = $env:DEVICE_UDID + } + if (-not $DeviceUdid) { Write-Info "Auto-detecting iOS simulator..." $simList = xcrun simctl list devices available --json | ConvertFrom-Json @@ -454,6 +459,19 @@ if ($Platform -eq "android") { Write-Success "iOS simulator: $deviceName ($DeviceUdid)" + # Shutdown any OTHER booted simulators to avoid Appium connecting to the wrong device + $bootedSims = xcrun simctl list devices --json | ConvertFrom-Json + $otherBooted = $bootedSims.devices.PSObject.Properties.Value | + ForEach-Object { $_ } | + Where-Object { $_.state -eq "Booted" -and $_.udid -ne $DeviceUdid } + + if ($otherBooted) { + foreach ($sim in $otherBooted) { + Write-Info "Shutting down other booted simulator: $($sim.name) ($($sim.udid))" + xcrun simctl shutdown $sim.udid 2>$null + } + } + # Boot simulator if not already booted Write-Info "Booting simulator (if not already running)..." xcrun simctl boot $DeviceUdid 2>$null @@ -470,7 +488,7 @@ if ($Platform -eq "android") { exit 1 } - Write-Success "Simulator is booted and ready" + Write-Success "Simulator is booted and ready: $deviceName" #endregion } From ffb5175f67d9c1687cf2bbd843da4d0f67e23eaa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:58:28 -0600 Subject: [PATCH 03/26] Simplify ci-copilot.yml: reuse enable-kvm, provision, Cake boot, and ProvisionAppium templates Replace ~600 lines of manual Android setup with existing pipeline templates: - enable-kvm.yml for KVM permissions (same as uitests/device-tests) - provision.yml for SDK/JDK provisioning (same parameters as ui-tests-steps.yml) - Cake android.cake --target=boot for emulator lifecycle (same as device-tests) - UseNode@1 + ProvisionAppium for Appium install (same as ui-tests-steps.yml) - Remove iOS-specific code (Android-only scope) - Remove redundant environment verification and emulator restart steps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 778 ++++------------------------------- 1 file changed, 84 insertions(+), 694 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index c051a9a58fb9..3d42e524295e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -2,6 +2,9 @@ # This pipeline installs the Copilot CLI and invokes the PR reviewer agent # to conduct automated code reviews on pull requests. # +# Environment setup follows the same patterns as ui-tests-steps.yml and +# device-tests-steps.yml: enable-kvm → provision → dotnet install → appium. +# # For more information, see: # https://github.com/dotnet/maui/wiki/PR-Reviewer-Agent @@ -36,6 +39,8 @@ variables: value: false - name: Codeql.SkipTaskAutoInjection value: true + - name: APPIUM_HOME + value: $(System.DefaultWorkingDirectory)/.appium/ stages: - stage: ReviewPR @@ -75,248 +80,35 @@ stages: mkdir -p $(Build.ArtifactStagingDirectory)/logs displayName: 'Create Artifact Directories' - # Provision environment (Xcode, .NET SDK, Android SDK, etc.) + ################################################## + # Provision Machine (same as uitests) # + ################################################## + + # Enable KVM for Android emulator on Linux (same as ui-tests-steps.yml and device-tests-steps.yml) + - ${{ if eq(parameters.Platform, 'android') }}: + - template: common/enable-kvm.yml + + # Provision SDKs (same parameters as ui-tests-steps.yml) - template: common/provision.yml parameters: skipXcode: ${{ eq(parameters.Platform, 'android') }} skipProvisionator: true - skipAndroidCommonSdks: ${{ eq(parameters.Platform, 'ios') }} - skipAndroidPlatformApis: ${{ eq(parameters.Platform, 'ios') }} - skipJdk: ${{ eq(parameters.Platform, 'ios') }} + skipJdk: ${{ ne(parameters.Platform, 'android') }} + skipAndroidCommonSdks: ${{ ne(parameters.Platform, 'android') }} + skipAndroidPlatformApis: true + onlyAndroidPlatformDefaultApis: true + skipAndroidEmulatorImages: ${{ ne(parameters.Platform, 'android') }} + skipAndroidCreateAvds: true + androidEmulatorApiLevel: '30' skipSimulatorSetup: ${{ eq(parameters.Platform, 'android') }} skipCertificates: true - # Android emulator setup (skip for iOS) - skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }} - skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }} - androidEmulatorApiLevel: '30' - - # Configure AVD for hardware acceleration (AcesShared ARM64 macOS agents have HVF) - # Match maui-pr Cake script: no GPU override on macOS (uses default hw GPU) - - script: | - echo "=== Configuring AVD for hardware-accelerated emulation ===" - AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini" - if [ -f "$AVD_CONFIG" ]; then - echo "Found AVD config: $AVD_CONFIG" - cat "$AVD_CONFIG" - else - echo "##vso[task.logissue type=warning]AVD config not found at: $AVD_CONFIG" - ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found" - fi - displayName: 'Configure AVD' - condition: eq('${{ parameters.Platform }}', 'android') - # Set up Android SDK PATH (required on self-hosted agents) - - pwsh: | - if ($env:ANDROID_SDK_ROOT) { - $platformTools = Join-Path $env:ANDROID_SDK_ROOT "platform-tools" - $emulatorPath = Join-Path $env:ANDROID_SDK_ROOT "emulator" - $cmdlineTools = Join-Path $env:ANDROID_SDK_ROOT "cmdline-tools/latest/bin" - - # Use Azure Pipelines' prependpath command to persist across steps - Write-Host "##vso[task.prependpath]$platformTools" - Write-Host "##vso[task.prependpath]$emulatorPath" - Write-Host "##vso[task.prependpath]$cmdlineTools" - - Write-Host "Added to PATH (will apply to subsequent steps):" - Write-Host " platform-tools: $platformTools" - Write-Host " emulator: $emulatorPath" - Write-Host " cmdline-tools: $cmdlineTools" - - # Verify the tools exist - if (Test-Path (Join-Path $platformTools "adb")) { - Write-Host "✅ adb found at: $platformTools/adb" - } else { - Write-Host "⚠️ adb NOT found at expected location" - } - if (Test-Path (Join-Path $emulatorPath "emulator")) { - Write-Host "✅ emulator found at: $emulatorPath/emulator" - } else { - Write-Host "⚠️ emulator NOT found at expected location" - } - } else { - Write-Host "##vso[task.logissue type=warning]ANDROID_SDK_ROOT not set, skipping PATH setup" - } - - # Auto-detect and set JAVA_HOME if not already set - if (-not $env:JAVA_HOME) { - if ($IsLinux) { - # On Linux, look for JDK in standard locations - $jvmDirs = @("/usr/lib/jvm", "/usr/java") - foreach ($jvmDir in $jvmDirs) { - $jdk = Get-ChildItem "$jvmDir/java-17*" -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $jdk) { - $jdk = Get-ChildItem "$jvmDir/microsoft-*17*" -ErrorAction SilentlyContinue | Select-Object -First 1 - } - if (-not $jdk) { - $jdk = Get-ChildItem "$jvmDir/temurin-17*" -ErrorAction SilentlyContinue | Select-Object -First 1 - } - if ($jdk) { - Write-Host "##vso[task.setvariable variable=JAVA_HOME]$($jdk.FullName)" - Write-Host "Set JAVA_HOME to: $($jdk.FullName)" - break - } - } - } else { - # macOS - $jvmDir = "/Library/Java/JavaVirtualMachines" - $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($msJdk) { - $javaHome = "$($msJdk.FullName)/Contents/Home" - Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome" - Write-Host "Set JAVA_HOME to: $javaHome" - } - } - } - displayName: 'Configure Android SDK PATH' - condition: eq('${{ parameters.Platform }}', 'android') - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - - # Start Android Emulator directly - # On Linux CI (Azure Pipelines Ubuntu): AVDs may be in ~/.config/.android/avd/ - # We set ANDROID_AVD_HOME so emulator can find them - - script: | - echo "=== Starting Android Emulator ===" - echo "ANDROID_SDK_ROOT: $ANDROID_SDK_ROOT" - echo "JAVA_HOME: $JAVA_HOME" - - # Fix KVM permissions (Azure Pipelines Ubuntu user not in kvm group by default) - echo "=== Fixing KVM permissions ===" - if [ -e /dev/kvm ]; then - echo "KVM device exists, checking group membership..." - echo "Current groups: $(groups)" - echo "KVM group: $(grep kvm /etc/group)" - sudo gpasswd -a $USER kvm - echo "Added $USER to kvm group" - # Also set permissions directly (group change needs relogin) - sudo chmod 666 /dev/kvm - echo "Set /dev/kvm permissions to 666" - ls -la /dev/kvm - fi - - # Install missing shared libraries (libpulse needed by emulator) - echo "=== Installing emulator dependencies ===" - sudo apt-get update -qq - sudo apt-get install -y -qq libpulse0 > /dev/null 2>&1 || true - - EMULATOR_BIN="$ANDROID_SDK_ROOT/emulator/emulator" - echo "Emulator binary: $EMULATOR_BIN" - $EMULATOR_BIN -version 2>&1 | head -3 - - # Find AVD location - check multiple possible paths - AVD_HOME="" - for candidate in "$HOME/.android/avd" "$HOME/.config/.android/avd"; do - if [ -d "$candidate/Emulator_30.avd" ]; then - AVD_HOME="$candidate" - echo "Found AVD at: $candidate/Emulator_30.avd" - break - fi - done - - if [ -z "$AVD_HOME" ]; then - echo "##[error]Could not find Emulator_30 AVD in any known location" - find /home -name "Emulator_30.avd" -type d 2>/dev/null || true - exit 1 - fi - - export ANDROID_AVD_HOME="$AVD_HOME" - echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" - - # Verify emulator can see the AVD - echo "Available AVDs:" - $EMULATOR_BIN -list-avds 2>/dev/null || true - - # Check KVM access - echo "=== Hardware acceleration check ===" - $EMULATOR_BIN -accel-check 2>&1 || true - - # Start the emulator - echo "=== Starting emulator ===" - EMULATOR_LOG="/tmp/emulator-Emulator_30.log" - nohup $EMULATOR_BIN -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -no-metrics > "$EMULATOR_LOG" 2>&1 & - EMULATOR_PID=$! - echo "Emulator PID: $EMULATOR_PID" - - # Wait briefly and check if process is still alive - sleep 5 - if ! kill -0 $EMULATOR_PID 2>/dev/null; then - echo "##[error]Emulator process died immediately" - cat "$EMULATOR_LOG" 2>/dev/null | tail -50 - exit 1 - fi - echo "Emulator process is running" - - # Wait for ADB device - echo "=== Waiting for ADB device ===" - adb start-server 2>/dev/null || true - device_timeout=120 - device_waited=0 - while ! adb devices 2>/dev/null | grep -q "emulator.*device"; do - if adb devices 2>/dev/null | grep -q "emulator.*offline"; then - echo "Device found but offline, waiting..." - fi - sleep 5 - device_waited=$((device_waited + 5)) - if [ $device_waited -ge $device_timeout ]; then - echo "##[error]Emulator device did not appear after ${device_timeout}s" - cat "$EMULATOR_LOG" 2>/dev/null | tail -30 - exit 1 - fi - if ! kill -0 $EMULATOR_PID 2>/dev/null; then - echo "##[error]Emulator process died while waiting for device" - cat "$EMULATOR_LOG" 2>/dev/null | tail -30 - exit 1 - fi - done - - DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') - echo "✅ Emulator detected: $DEVICE_ID" - - # Wait for boot completion - echo "=== Waiting for boot ===" - boot_timeout=300 - boot_waited=0 - while [ "$(adb -s $DEVICE_ID shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do - sleep 5 - boot_waited=$((boot_waited + 5)) - if [ $boot_waited -ge $boot_timeout ]; then - echo "##[error]Boot timeout after ${boot_timeout}s" - exit 1 - fi - done - echo "✅ Emulator fully booted: $DEVICE_ID" - - # Wait for package manager - echo "Waiting for package manager service..." - pm_timeout=60 - pm_waited=0 - while ! adb -s $DEVICE_ID shell pm list packages 2>/dev/null | grep -q "package:"; do - sleep 3 - pm_waited=$((pm_waited + 3)) - if [ $pm_waited -ge $pm_timeout ]; then - echo "⚠️ Package manager not ready (non-fatal)" - break - fi - done - if [ $pm_waited -lt $pm_timeout ]; then - echo "✅ Package manager service is ready" - fi - - echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - echo "" - echo "=== ✅ Android Emulator Started Successfully ===" - adb devices -l - displayName: 'Start Android Emulator' - condition: eq('${{ parameters.Platform }}', 'android') - timeoutInMinutes: 15 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME) + ################################################## + # Install .NET (same as uitests) # + ################################################## - # Install .NET and workloads via Cake (build.ps1 works on both Linux and macOS via pwsh) - - pwsh: | - ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic - displayName: 'Install .NET and workloads' + - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic + displayName: 'Install .NET' retryCountOnTaskFailure: 2 env: DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) @@ -326,408 +118,61 @@ stages: displayName: 'Add .NET to PATH' # Build MSBuild tasks (required for MAUI builds) - - pwsh: | - ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic + - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="Release" --verbosity=diagnostic displayName: 'Build MSBuild Tasks' retryCountOnTaskFailure: 1 env: DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) PRIVATE_BUILD: $(PrivateBuild) - - # Verify environment is ready - - script: | - echo "=== Verifying Build Environment ===" - ERRORS="" - - # Check .NET SDK - echo "Checking .NET SDK..." - if ! dotnet --version; then - ERRORS="${ERRORS}\n- .NET SDK not available" - else - echo "✓ .NET SDK: $(dotnet --version)" - fi - - # Check Android SDK (only for Android platform) - if [ "${{ parameters.Platform }}" = "android" ]; then - echo "Checking Android SDK..." - if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then - ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set" - else - SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" - if [ ! -d "$SDK_PATH" ]; then - ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH" - else - echo "✓ Android SDK: $SDK_PATH" - fi - fi - - # Check emulator binary - echo "Checking Android Emulator..." - if which emulator > /dev/null 2>&1; then - echo "✓ emulator: $(which emulator)" - emulator -version 2>&1 | head -1 || true - else - echo "⚠️ emulator not in PATH (may be at \$ANDROID_SDK_ROOT/emulator/emulator)" - fi - - # Check adb - echo "Checking adb..." - if which adb > /dev/null 2>&1; then - echo "✓ adb: $(which adb)" - else - echo "⚠️ adb not in PATH" - fi - - # Check KVM (Linux) or HVF (macOS) hardware acceleration - echo "Checking hardware virtualization..." - if [ "$(uname)" = "Linux" ]; then - if [ -e /dev/kvm ]; then - echo "✓ KVM available (/dev/kvm exists)" - ls -la /dev/kvm - else - ERRORS="${ERRORS}\n- KVM not available (/dev/kvm missing)" - fi - fi - fi - - # Check Xcode (macOS only, iOS only) - if [ "$(uname)" = "Darwin" ] && [ "${{ parameters.Platform }}" = "ios" ]; then - echo "Checking Xcode..." - if ! xcodebuild -version; then - ERRORS="${ERRORS}\n- Xcode not available" - else - echo "✓ Xcode available" - fi - - # Check iOS simulators - echo "Checking iOS simulators..." - if ! xcrun simctl list devices available | grep -q "iPhone"; then - ERRORS="${ERRORS}\n- No iOS simulators available" - else - echo "✓ iOS simulators available" - fi - fi - - # Check Java/JDK (only for Android platform) - if [ "${{ parameters.Platform }}" = "android" ]; then - echo "Checking JDK..." - if ! java -version 2>&1; then - ERRORS="${ERRORS}\n- JDK not available" - else - echo "✓ JDK available" - fi - fi - - # Report errors - if [ -n "$ERRORS" ]; then - echo "" - echo "##vso[task.logissue type=error]=== Environment Verification FAILED ===" - echo -e "Missing dependencies:$ERRORS" - echo "##vso[task.logissue type=error]Build environment is not properly configured. See above for details." - exit 1 - fi - - echo "" - echo "=== Environment Verification PASSED ===" - displayName: 'Verify Build Environment' - # Restore .NET tools (includes xharness) - - script: | - echo "Restoring .NET tools..." - dotnet tool restore - echo "Tools restored successfully" + - script: dotnet tool restore displayName: 'Restore .NET Tools' - # Verify Android emulator is running - - script: | - echo "=== Verifying Android Emulator ===" - adb devices -l - DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') - if [ -z "$DEVICE_ID" ]; then - echo "##vso[task.logissue type=error]No emulator device found" - exit 1 - fi - echo "✅ Emulator running: $DEVICE_ID" - echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - displayName: 'Verify Android Emulator' - condition: eq('${{ parameters.Platform }}', 'android') - timeoutInMinutes: 5 - - # Install Node.js (cross-platform: apt on Linux, brew on macOS) - - script: | - echo "Installing Node.js..." - if [ "$(uname)" = "Linux" ]; then - # Use NodeSource for Node.js 22 on Linux - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - - sudo apt-get install -y nodejs - else - brew install node@22 - brew link --overwrite node@22 - fi - if ! node --version; then - echo "##vso[task.logissue type=error]Failed to install Node.js" - exit 1 - fi - npm --version - echo "Node.js installed successfully" - displayName: 'Install Node.js' - - # Install Appium and platform drivers - - script: | - echo "Installing Appium and platform drivers..." - - # Get npm global bin directory and add to PATH - NPM_BIN=$(npm config get prefix)/bin - echo "NPM global bin directory: $NPM_BIN" - export PATH="$NPM_BIN:$PATH" - - # Install Appium globally - npm install -g appium - - # Install both drivers - echo "Installing UiAutomator2 driver for Android..." - appium driver install uiautomator2 - echo "Installing XCUITest driver for iOS..." - appium driver install xcuitest - - # Verify installation - if ! which appium; then - echo "##vso[task.logissue type=error]Failed to install Appium" - exit 1 - fi - - APPIUM_PATH=$(which appium) - echo "Appium path: $APPIUM_PATH" - echo "Appium version: $(appium --version)" - echo "Appium drivers installed:" - appium driver list --installed - - # Export PATH for subsequent steps - echo "##vso[task.prependpath]$NPM_BIN" - - echo "Appium installed successfully" - displayName: 'Install Appium' - - - # Verify/refresh Android Emulator before PR Reviewer runs - # Reuses the already-running emulator if healthy (avoids disk space issues on hosted agents) - - script: | - echo "=== Checking Android Emulator for PR Reviewer ===" - - # Check if emulator is still running and healthy - DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') - if [ -n "$DEVICE_ID" ]; then - BOOT_DONE=$(adb -s "$DEVICE_ID" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [ "$BOOT_DONE" = "1" ]; then - echo "✅ Emulator $DEVICE_ID is running and booted" - # Verify package manager is still responsive - if adb -s "$DEVICE_ID" shell pm list packages 2>/dev/null | grep -q "package:"; then - echo "✅ Package manager is responsive" - echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - echo "=== Android Emulator Ready (reused) ===" - adb devices -l - exit 0 - fi - fi - fi - - echo "⚠️ Emulator not healthy, attempting restart..." - - # Find AVD home (same logic as Start step) - AVD_HOME="" - for candidate in "$HOME/.android/avd" "$HOME/.config/.android/avd"; do - if [ -d "$candidate/Emulator_30.avd" ]; then - AVD_HOME="$candidate" - break - fi - done - export ANDROID_AVD_HOME="${AVD_HOME:-$HOME/.android/avd}" - - # Kill any existing emulator - adb devices | grep emulator | awk '{print $1}' | xargs -I{} adb -s {} emu kill 2>/dev/null || true - sleep 5 - - # Restart ADB server - adb kill-server - sleep 2 - adb start-server - sleep 2 - - # Start fresh emulator - EMULATOR_LOG="/tmp/emulator-fresh.log" - nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim -gpu swiftshader_indirect -no-metrics > "$EMULATOR_LOG" 2>&1 & - - # Wait for device - device_timeout=300 - device_waited=0 - while ! adb devices | grep -q "emulator.*device"; do - sleep 5 - device_waited=$((device_waited + 5)) - if [ $device_waited -ge $device_timeout ]; then - echo "##vso[task.logissue type=error]Emulator device did not appear in time" - cat "$EMULATOR_LOG" 2>/dev/null | tail -30 + ################################################## + # Boot Android Emulator via Cake (same as # + # device-tests-steps.yml / uitests) # + ################################################## + + - ${{ if eq(parameters.Platform, 'android') }}: + - script: dotnet cake eng/devices/android.cake --target=boot --device="android-emulator-64_30" --apiversion="30" --verbosity=diagnostic + displayName: 'Boot Android Emulator (Cake)' + timeoutInMinutes: 15 + + # Capture emulator device ID for Copilot agent + - script: | + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') + if [ -z "$DEVICE_ID" ]; then + echo "##vso[task.logissue type=error]No emulator device found after Cake boot" + adb devices -l exit 1 fi - done + echo "✅ Emulator running: $DEVICE_ID" + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + displayName: 'Capture Emulator UDID' + timeoutInMinutes: 5 - # Wait for boot - DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') - boot_timeout=600 - boot_waited=0 - while [ "$(adb -s $DEVICE_ID shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do - sleep 5 - boot_waited=$((boot_waited + 5)) - if [ $boot_waited -ge $boot_timeout ]; then - echo "##vso[task.logissue type=error]Emulator did not boot in time" - exit 1 - fi - done + ################################################## + # Install Node.js + Appium (same as uitests) # + ################################################## - echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - echo "=== Fresh Android Emulator Ready! ===" - adb devices -l - displayName: 'Restart Android Emulator (Fresh)' - condition: eq('${{ parameters.Platform }}', 'android') - continueOnError: true - timeoutInMinutes: 15 - env: - ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT) - JAVA_HOME: $(JAVA_HOME) - - # Boot iOS Simulator (only for iOS platform) - - script: | - echo "=== Booting iOS Simulator ===" - echo "Available runtimes:" - xcrun simctl list runtimes available - - # Find iOS runtime in priority order: 18.x > 17.x > 26.x - # iOS 26.x comes with Xcode 26 and may be the only runtime available - RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' - [.runtimes[] | select(.name | test("iOS 18"))] | sort_by(.version) | last | .identifier // empty - ') - if [ -z "$RUNTIME" ]; then - RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' - [.runtimes[] | select(.name | test("iOS 17"))] | sort_by(.version) | last | .identifier // empty - ') - fi - if [ -z "$RUNTIME" ]; then - RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' - [.runtimes[] | select(.name | test("iOS 26"))] | sort_by(.version) | last | .identifier // empty - ') - fi - - if [ -z "$RUNTIME" ]; then - echo "##vso[task.logissue type=error]No iOS runtime found (tried iOS 18, 17, 26)" - xcrun simctl list runtimes available - exit 1 - fi - echo "Selected iOS runtime: $RUNTIME" - - # Determine the correct device type based on runtime version - # iOS 26.x → iPhone 11 Pro (matches UITest.cs ios-26 environment and snapshot baselines) - # iOS 18.x/17.x → iPhone Xs (matches UITest.cs ios environment and snapshot baselines) - # Both devices have the same screen resolution: 375×812 pt, 1125×2436 px @3x - IS_IOS26=false - if echo "$RUNTIME" | grep -q "iOS-26"; then - IS_IOS26=true - fi - - if [ "$IS_IOS26" = "true" ]; then - PRIMARY_DEVICE="iPhone 11 Pro" - PRIMARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro" - SECONDARY_DEVICE="iPhone Xs" - SECONDARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-XS" - else - PRIMARY_DEVICE="iPhone Xs" - PRIMARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-XS" - SECONDARY_DEVICE="iPhone 11 Pro" - SECONDARY_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro" - fi - echo "Target device: $PRIMARY_DEVICE (fallback: $SECONDARY_DEVICE)" - - # Look for existing primary device - UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" --arg name "$PRIMARY_DEVICE" ' - .devices[$rt] // [] | - map(select(.name == $name)) | - .[0].udid // empty - ') - - # Fallback to secondary device (same resolution) - if [ -z "$UDID" ]; then - UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" --arg name "$SECONDARY_DEVICE" ' - .devices[$rt] // [] | - map(select(.name == $name)) | - .[0].udid // empty - ') - if [ -n "$UDID" ]; then - echo "Found existing $SECONDARY_DEVICE: $UDID" - fi - else - echo "Found existing $PRIMARY_DEVICE: $UDID" - fi - - # If neither exists, try to create one - if [ -z "$UDID" ]; then - echo "No matching device found - creating $PRIMARY_DEVICE for runtime $RUNTIME..." - UDID=$(xcrun simctl create "$PRIMARY_DEVICE" "$PRIMARY_DEVICE_TYPE" "$RUNTIME" 2>&1) - if [ $? -ne 0 ]; then - echo "$PRIMARY_DEVICE device type unavailable: $UDID" - echo "Trying $SECONDARY_DEVICE..." - UDID=$(xcrun simctl create "$SECONDARY_DEVICE" "$SECONDARY_DEVICE_TYPE" "$RUNTIME" 2>&1) - if [ $? -ne 0 ]; then - echo "##vso[task.logissue type=warning]Failed to create $SECONDARY_DEVICE: $UDID" - echo "Falling back to any available iPhone..." - UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' - .devices[$rt] // [] | - map(select(.name | test("iPhone"))) | - .[0].udid // empty - ') - else - echo "Created $SECONDARY_DEVICE simulator: $UDID" - fi - else - echo "Created $PRIMARY_DEVICE simulator: $UDID" - fi - fi - - if [ -z "$UDID" ]; then - echo "##vso[task.logissue type=error]No iOS simulator found" - echo "Available devices:" - xcrun simctl list devices available - exit 1 - fi + - task: UseNode@1 + inputs: + version: "20.3.1" + displayName: 'Install Node.js' - # Shutdown any other booted simulators to avoid resource contention - xcrun simctl list devices booted --json | jq -r ' - .devices | to_entries | map(.value) | flatten | - map(select(.state == "Booted" and .udid != "'"$UDID"'")) | - .[].udid - ' | while read OTHER_UDID; do - echo "Shutting down other simulator: $OTHER_UDID" - xcrun simctl shutdown "$OTHER_UDID" 2>/dev/null || true - done + - pwsh: | + $skipAppiumDoctor = if ($IsMacOS -or $IsLinux) { "true" } else { "false" } + dotnet build ./src/Provisioning/Provisioning.csproj -t:ProvisionAppium -p:SkipAppiumDoctor="$skipAppiumDoctor" -bl:"$(LogDirectory)/provision-appium.binlog" + displayName: 'Install Appium' + retryCountOnTaskFailure: 2 + timeoutInMinutes: 10 + env: + APPIUM_HOME: $(APPIUM_HOME) - echo "Booting simulator: $UDID" - xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" - sleep 10 - - # Log the selected device details for debugging - echo "=== Selected Simulator Details ===" - DEVICE_INFO=$(xcrun simctl list devices --json | jq -r --arg udid "$UDID" ' - .devices | to_entries[] | .value[] | select(.udid == $udid) | "\(.name) (\(.udid))" - ') - echo "Device: $DEVICE_INFO" - echo "Runtime: $RUNTIME" - echo "iOS 26 mode: $IS_IOS26" - echo "Booted simulators:" - xcrun simctl list devices booted - - echo "##vso[task.setvariable variable=DEVICE_UDID]$UDID" - echo "iOS Simulator UDID: $UDID" - displayName: 'Boot iOS Simulator' - condition: eq('${{ parameters.Platform }}', 'ios') - timeoutInMinutes: 5 + ################################################## + # Copilot-specific setup and execution # + ################################################## # Install GitHub CLI (cross-platform: apt on Linux, brew on macOS) - script: | @@ -744,63 +189,34 @@ stages: else brew install gh fi - if ! gh --version; then - echo "##vso[task.logissue type=error]Failed to install GitHub CLI" - exit 1 - fi - echo "GitHub CLI installed successfully" + gh --version displayName: 'Install GitHub CLI' - # Authenticate GitHub CLI by writing token directly to gh config - # (bypasses read:org scope validation that gh auth login requires) + # Authenticate GitHub CLI - script: | - echo "Authenticating with GitHub CLI..." - if [ -z "$GH_CLI_TOKEN" ]; then - echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." - exit 1 - fi - # Write token to gh config directly (avoids read:org scope check) mkdir -p "$HOME/.config/gh" printf 'github.com:\n oauth_token: %s\n git_protocol: https\n' "$GH_CLI_TOKEN" > "$HOME/.config/gh/hosts.yml" - # Verify token works with a simple API call LOGIN=$(gh api user --jq '.login' 2>&1) if [ $? -ne 0 ]; then echo "##vso[task.logissue type=error]GitHub CLI authentication failed: $LOGIN" exit 1 fi - echo "GitHub CLI authenticated successfully as: $LOGIN" + echo "GitHub CLI authenticated as: $LOGIN" displayName: 'Authenticate GitHub CLI' env: GH_CLI_TOKEN: $(GH_CLI_TOKEN) - # Install GitHub Copilot CLI + # Install and authenticate Copilot CLI - script: | - echo "Installing GitHub Copilot CLI..." npm install -g @github/copilot - if ! which copilot; then - echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" - exit 1 - fi copilot --version - echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' - # Authenticate Copilot CLI - script: | - echo "Authenticating Copilot CLI..." if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - echo "##vso[task.logissue type=error]COPILOT_GITHUB_TOKEN is not set. Please configure the COPILOT_TOKEN pipeline variable with a fine-grained PAT." + echo "##vso[task.logissue type=error]COPILOT_GITHUB_TOKEN is not set" exit 1 fi - echo "COPILOT_GITHUB_TOKEN is set (${#COPILOT_GITHUB_TOKEN} chars)" - - # Verify gh CLI auth is available for copilot's fallback path - if gh auth status >/dev/null 2>&1; then - echo "✅ GitHub CLI is authenticated" - else - echo "⚠️ GitHub CLI not authenticated, copilot will use env var only" - fi - echo "✅ Copilot CLI authentication configured" displayName: 'Authenticate Copilot CLI' env: @@ -809,24 +225,20 @@ stages: # Run the PR Reviewer Agent - script: | - echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." # Configure git identity git config user.email "copilot-ci@microsoft.com" git config user.name "Copilot CI" - echo "Git identity configured" - # Create Directory.Build.Override.props to skip Xcode version check + # Skip Xcode version check cp Directory.Build.Override.props.in Directory.Build.Override.props - # sed -i syntax differs: Linux uses sed -i, macOS uses sed -i '' if [ "$(uname)" = "Linux" ]; then sed -i 's|| false\n|' Directory.Build.Override.props else sed -i '' 's|| false\n|' Directory.Build.Override.props fi - # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs # Invoke the PR reviewer @@ -837,55 +249,35 @@ stages: echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" - # Terminate any orphaned copilot CLI processes - echo "Cleaning up orphaned copilot processes..." - SELF_PID=$$ + # Cleanup orphaned copilot processes for proc in $(pgrep -f "[c]opilot" 2>/dev/null || true); do - if [ -n "$proc" ] && [ "$proc" != "$SELF_PID" ]; then - PROC_CMD=$(ps -p "$proc" -o args= 2>/dev/null || true) - if echo "$PROC_CMD" | grep -q "copilot"; then - echo " Stopping copilot process $proc: $PROC_CMD" - kill "$proc" 2>/dev/null || true - fi + if [ -n "$proc" ] && [ "$proc" != "$$" ]; then + kill "$proc" 2>/dev/null || true fi done - # Copy Copilot session files - if [ -d "$HOME/.copilot" ]; then - echo "Copying Copilot session state..." - cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state || true - fi - - # Copy CustomAgentLogsTmp if it exists - if [ -d "CustomAgentLogsTmp" ]; then - echo "Copying CustomAgentLogsTmp..." - cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ || true - fi - - # Copy any Review_Feedback files + # Collect artifacts + cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state 2>/dev/null || true + cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ 2>/dev/null || true find . -name "Review_Feedback_*.md" -type f -exec cp {} $(Build.ArtifactStagingDirectory)/copilot-logs/ \; 2>/dev/null || true + cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ 2>/dev/null || true - # Copy any .github/agent-pr-session files - if [ -d ".github/agent-pr-session" ]; then - echo "Copying agent-pr-session..." - cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ || true - fi - - # Check for failure if [ $COPILOT_EXIT_CODE -ne 0 ]; then echo "##vso[task.logissue type=error]Review-PR.ps1 exited with code $COPILOT_EXIT_CODE" echo "##vso[task.setvariable variable=CopilotFailed]true" fi - - echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" displayName: 'Run PR Reviewer Agent' env: COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) GH_TOKEN: $(GH_COMMENT_TOKEN) GITHUB_TOKEN: $(GH_CLI_TOKEN) DEVICE_UDID: $(DEVICE_UDID) + APPIUM_HOME: $(APPIUM_HOME) + + ################################################## + # Publish # + ################################################## - # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 displayName: 'Publish Copilot Logs' inputs: @@ -894,7 +286,6 @@ stages: publishLocation: 'pipeline' condition: succeededOrFailed() - # Publish build logs if they exist - task: PublishPipelineArtifact@1 displayName: 'Publish Build Logs' inputs: @@ -903,7 +294,6 @@ stages: publishLocation: 'pipeline' condition: and(succeededOrFailed(), ne(variables['LogDirectory'], '')) - # Fail the pipeline if Copilot failed - script: | if [ "$(CopilotFailed)" = "true" ]; then echo "##vso[task.logissue type=error]Copilot PR review failed. Check CopilotLogs artifact for details." From 8ea07292417050519250d634f353e7be246c77ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:08:34 -0600 Subject: [PATCH 04/26] Align ci-copilot.yml Android emulator setup with device-tests/uitests Replace ~400 lines of manual emulator management with existing templates: - Add enable-kvm.yml for Linux KVM permissions - Align provision.yml params with ui-tests-steps.yml - Replace inline AVD/SDK/emulator boot with Cake boot (android.cake --target=boot) - Replace brew Node.js with UseNode@1 task - Replace npm Appium with ProvisionAppium via Provisioning.csproj - Remove redundant Verify Build Environment step - Remove Restart Android Emulator (Fresh) step - Add APPIUM_HOME and LogDirectory variables Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 283 ++++++++++++++++++++++------------- 1 file changed, 181 insertions(+), 102 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 3d42e524295e..a44c4184a390 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -2,9 +2,6 @@ # This pipeline installs the Copilot CLI and invokes the PR reviewer agent # to conduct automated code reviews on pull requests. # -# Environment setup follows the same patterns as ui-tests-steps.yml and -# device-tests-steps.yml: enable-kvm → provision → dotnet install → appium. -# # For more information, see: # https://github.com/dotnet/maui/wiki/PR-Reviewer-Agent @@ -30,8 +27,6 @@ parameters: type: object default: name: AcesShared - demands: - - ImageOverride -equals ACES_VM_SharedPool_Tahoe variables: - template: /eng/pipelines/common/variables.yml@self @@ -41,6 +36,8 @@ variables: value: true - name: APPIUM_HOME value: $(System.DefaultWorkingDirectory)/.appium/ + - name: LogDirectory + value: $(Build.ArtifactStagingDirectory)/logs stages: - stage: ReviewPR @@ -48,13 +45,7 @@ stages: jobs: - job: CopilotReview displayName: 'Run Copilot PR Reviewer Agent' - # Android needs Linux (KVM for emulator), iOS needs macOS (Xcode/simulators) - ${{ if eq(parameters.Platform, 'android') }}: - pool: - name: Azure Pipelines - vmImage: ubuntu-22.04 - ${{ else }}: - pool: ${{ parameters.pool }} + pool: ${{ parameters.pool }} timeoutInMinutes: 180 steps: - checkout: self @@ -74,17 +65,7 @@ stages: echo "##vso[build.updatebuildnumber]PR ${{ parameters.PRNumber }} ${{ parameters.Platform }}" displayName: 'Set Pipeline Run Title' - # Create artifact directories early (prevents publish failures if steps are skipped) - - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - mkdir -p $(Build.ArtifactStagingDirectory)/logs - displayName: 'Create Artifact Directories' - - ################################################## - # Provision Machine (same as uitests) # - ################################################## - - # Enable KVM for Android emulator on Linux (same as ui-tests-steps.yml and device-tests-steps.yml) + # Enable KVM for Android emulator on Linux (same as ui-tests-steps.yml / device-tests-steps.yml) - ${{ if eq(parameters.Platform, 'android') }}: - template: common/enable-kvm.yml @@ -103,12 +84,9 @@ stages: skipSimulatorSetup: ${{ eq(parameters.Platform, 'android') }} skipCertificates: true - ################################################## - # Install .NET (same as uitests) # - ################################################## - + # Install .NET and workloads via build.ps1 - pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic - displayName: 'Install .NET' + displayName: 'Install .NET and workloads' retryCountOnTaskFailure: 2 env: DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) @@ -124,16 +102,12 @@ stages: env: DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) PRIVATE_BUILD: $(PrivateBuild) - + # Restore .NET tools (includes xharness) - script: dotnet tool restore displayName: 'Restore .NET Tools' - ################################################## - # Boot Android Emulator via Cake (same as # - # device-tests-steps.yml / uitests) # - ################################################## - + # Boot Android Emulator via Cake (same as device-tests-steps.yml / ui-tests-steps.yml) - ${{ if eq(parameters.Platform, 'android') }}: - script: dotnet cake eng/devices/android.cake --target=boot --device="android-emulator-64_30" --apiversion="30" --verbosity=diagnostic displayName: 'Boot Android Emulator (Cake)' @@ -150,12 +124,8 @@ stages: echo "✅ Emulator running: $DEVICE_ID" echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" displayName: 'Capture Emulator UDID' - timeoutInMinutes: 5 - - ################################################## - # Install Node.js + Appium (same as uitests) # - ################################################## + # Install Node.js and Appium (same as ui-tests-steps.yml) - task: UseNode@1 inputs: version: "20.3.1" @@ -170,114 +140,220 @@ stages: env: APPIUM_HOME: $(APPIUM_HOME) - ################################################## - # Copilot-specific setup and execution # - ################################################## - - # Install GitHub CLI (cross-platform: apt on Linux, brew on macOS) - script: | echo "Installing GitHub CLI..." - if [ "$(uname)" = "Linux" ]; then - (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ - && sudo mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && sudo apt update \ - && sudo apt install gh -y - else - brew install gh + brew install gh + if ! gh --version; then + echo "##vso[task.logissue type=error]Failed to install GitHub CLI" + exit 1 fi - gh --version + echo "GitHub CLI installed successfully" displayName: 'Install GitHub CLI' - # Authenticate GitHub CLI - script: | - mkdir -p "$HOME/.config/gh" - printf 'github.com:\n oauth_token: %s\n git_protocol: https\n' "$GH_CLI_TOKEN" > "$HOME/.config/gh/hosts.yml" - LOGIN=$(gh api user --jq '.login' 2>&1) - if [ $? -ne 0 ]; then - echo "##vso[task.logissue type=error]GitHub CLI authentication failed: $LOGIN" + echo "Authenticating with GitHub CLI..." + if [ -z "$(GH_CLI_TOKEN)" ]; then + echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." + exit 1 + fi + echo "$(GH_CLI_TOKEN)" | gh auth login --with-token + if ! gh auth status; then + echo "##vso[task.logissue type=error]GitHub CLI authentication failed" exit 1 fi - echo "GitHub CLI authenticated as: $LOGIN" displayName: 'Authenticate GitHub CLI' env: GH_CLI_TOKEN: $(GH_CLI_TOKEN) - # Install and authenticate Copilot CLI - script: | + echo "Installing GitHub Copilot CLI..." npm install -g @github/copilot - copilot --version + if ! which copilot; then + echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" + exit 1 + fi + echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' + + # Boot iOS Simulator (only for iOS platform) + # UI test baseline screenshots are captured on iPhone Xs - must use same device - script: | - if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - echo "##vso[task.logissue type=error]COPILOT_GITHUB_TOKEN is not set" + echo "=== Booting iOS Simulator ===" + + # Find the latest stable iOS runtime (prefer 18.x, fallback to 17.x) + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 18"))] | sort_by(.version) | last | .identifier // empty + ') + if [ -z "$RUNTIME" ]; then + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + [.runtimes[] | select(.name | test("iOS 17"))] | sort_by(.version) | last | .identifier // empty + ') + fi + echo "Selected iOS runtime: $RUNTIME" + + # Look for iPhone Xs (matches UI test baselines - required for snapshot tests) + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone Xs")) | + .[0].udid // empty + ') + + # If iPhone Xs doesn't exist, try iPhone 11 Pro (same 1125×2436 resolution) + if [ -z "$UDID" ]; then + UDID=$(xcrun simctl list devices available --json | jq -r --arg rt "$RUNTIME" ' + .devices[$rt] // [] | + map(select(.name == "iPhone 11 Pro")) | + .[0].udid // empty + ') + if [ -n "$UDID" ]; then + echo "Found existing iPhone 11 Pro (same resolution as iPhone Xs): $UDID" + fi + else + echo "Found existing iPhone Xs: $UDID" + fi + + # If neither exists, try to create them + if [ -z "$UDID" ]; then + echo "No matching device found - attempting to create one for runtime $RUNTIME..." + + # Try iPhone Xs first + UDID=$(xcrun simctl create "iPhone Xs" com.apple.CoreSimulator.SimDeviceType.iPhone-Xs "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "iPhone Xs device type unavailable: $UDID" + # Try iPhone 11 Pro (same 1125×2436 resolution) + UDID=$(xcrun simctl create "iPhone 11 Pro" com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro "$RUNTIME" 2>&1) + if [ $? -ne 0 ]; then + echo "##vso[task.logissue type=warning]Failed to create iPhone 11 Pro: $UDID" + # Last resort: first available iPhone + UDID=$(xcrun simctl list devices available --json | jq -r ' + .devices | to_entries | + map(.value) | flatten | + map(select(.name | test("iPhone"))) | + .[0].udid + ') + else + echo "Created iPhone 11 Pro simulator: $UDID" + fi + else + echo "Created iPhone Xs simulator: $UDID" + fi + fi + + if [ -z "$UDID" ]; then + echo "##vso[task.logissue type=error]No iOS simulator found" exit 1 fi - echo "✅ Copilot CLI authentication configured" - displayName: 'Authenticate Copilot CLI' - env: - COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) - GH_TOKEN: $(GH_CLI_TOKEN) + + # Shutdown any other booted simulators to avoid Appium connecting to wrong device + xcrun simctl list devices booted --json | jq -r ' + .devices | to_entries | map(.value) | flatten | + map(select(.state == "Booted" and .udid != "'"$UDID"'")) | + .[].udid + ' | while read OTHER_UDID; do + echo "Shutting down other simulator: $OTHER_UDID" + xcrun simctl shutdown "$OTHER_UDID" 2>/dev/null || true + done + + echo "Booting simulator: $UDID" + xcrun simctl boot "$UDID" 2>/dev/null || echo "Simulator may already be booted" + sleep 10 + + echo "Booted simulators:" + xcrun simctl list devices booted + + echo "##vso[task.setvariable variable=DEVICE_UDID]$UDID" + echo "iOS Simulator UDID: $UDID" + displayName: 'Boot iOS Simulator' + condition: eq('${{ parameters.Platform }}', 'ios') + timeoutInMinutes: 5 - # Run the PR Reviewer Agent - script: | + echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." - - # Configure git identity + + # Configure git identity (required for merge operations on self-hosted agents) git config user.email "copilot-ci@microsoft.com" git config user.name "Copilot CI" - - # Skip Xcode version check + echo "Git identity configured" + + # Create Directory.Build.Override.props to skip Xcode version check + # AcesShared agents may have a newer Xcode than the .NET iOS SDK expects cp Directory.Build.Override.props.in Directory.Build.Override.props - if [ "$(uname)" = "Linux" ]; then - sed -i 's|| false\n|' Directory.Build.Override.props - else - sed -i '' 's|| false\n|' Directory.Build.Override.props - fi - + # Insert ValidateXcodeVersion before closing tag + sed -i '' 's|| false\n|' Directory.Build.Override.props + + # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs - - # Invoke the PR reviewer + + # Invoke the PR reviewer using our PowerShell script + # The script will merge the PR into the current branch + # -PostSummaryComment and -RunFinalize handle posting comments set +e pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" COPILOT_EXIT_CODE=$? set -e - + echo "Review-PR.ps1 exit code: $COPILOT_EXIT_CODE" - - # Cleanup orphaned copilot processes + + # Terminate any orphaned copilot CLI processes that could hold this step's + # stdout fd open and prevent the bash step from exiting. + # Only target processes whose command line includes the copilot CLI path. + echo "Cleaning up orphaned copilot processes..." + SELF_PID=$$ for proc in $(pgrep -f "[c]opilot" 2>/dev/null || true); do - if [ -n "$proc" ] && [ "$proc" != "$$" ]; then - kill "$proc" 2>/dev/null || true + if [ -n "$proc" ] && [ "$proc" != "$SELF_PID" ]; then + PROC_CMD=$(ps -p "$proc" -o args= 2>/dev/null || true) + if echo "$PROC_CMD" | grep -q "copilot"; then + echo " Stopping copilot process $proc: $PROC_CMD" + kill "$proc" 2>/dev/null || true + fi fi done - - # Collect artifacts - cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state 2>/dev/null || true - cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ 2>/dev/null || true + + # Copy any Copilot session files + if [ -d "$HOME/.copilot" ]; then + echo "Copying Copilot session state..." + cp -r "$HOME/.copilot" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot-session-state || true + fi + + # Copy CustomAgentLogsTmp if it exists + if [ -d "CustomAgentLogsTmp" ]; then + echo "Copying CustomAgentLogsTmp..." + cp -r CustomAgentLogsTmp $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Copy any Review_Feedback files find . -name "Review_Feedback_*.md" -type f -exec cp {} $(Build.ArtifactStagingDirectory)/copilot-logs/ \; 2>/dev/null || true - cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ 2>/dev/null || true - + + # Copy any .github/agent-pr-session files + if [ -d ".github/agent-pr-session" ]; then + echo "Copying agent-pr-session..." + cp -r .github/agent-pr-session $(Build.ArtifactStagingDirectory)/copilot-logs/ || true + fi + + # Check for failure indicators in output if [ $COPILOT_EXIT_CODE -ne 0 ]; then echo "##vso[task.logissue type=error]Review-PR.ps1 exited with code $COPILOT_EXIT_CODE" + # Don't exit yet - let artifacts be published first echo "##vso[task.setvariable variable=CopilotFailed]true" fi + + # Check output for common failure patterns + if grep -qi "error\|failed\|exception" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + if grep -qi "simulator.*not\|emulator.*not\|workload.*not\|sdk.*not found" $(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md 2>/dev/null; then + echo "##vso[task.logissue type=warning]Copilot encountered environment issues. Check artifacts for details." + fi + fi + + echo "Review output saved to $(Build.ArtifactStagingDirectory)/copilot-logs/" displayName: 'Run PR Reviewer Agent' env: COPILOT_GITHUB_TOKEN: $(COPILOT_TOKEN) GH_TOKEN: $(GH_COMMENT_TOKEN) - GITHUB_TOKEN: $(GH_CLI_TOKEN) DEVICE_UDID: $(DEVICE_UDID) - APPIUM_HOME: $(APPIUM_HOME) - - ################################################## - # Publish # - ################################################## + # Publish Copilot logs and session artifacts - task: PublishPipelineArtifact@1 displayName: 'Publish Copilot Logs' inputs: @@ -286,6 +362,7 @@ stages: publishLocation: 'pipeline' condition: succeededOrFailed() + # Publish build logs if they exist - task: PublishPipelineArtifact@1 displayName: 'Publish Build Logs' inputs: @@ -294,9 +371,11 @@ stages: publishLocation: 'pipeline' condition: and(succeededOrFailed(), ne(variables['LogDirectory'], '')) + # Fail the pipeline if Copilot failed - script: | if [ "$(CopilotFailed)" = "true" ]; then echo "##vso[task.logissue type=error]Copilot PR review failed. Check CopilotLogs artifact for details." exit 1 fi displayName: 'Check Copilot Result' + condition: succeededOrFailed() From e02807643df19dd9ea0cc9d1355aac788cc30891 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:35:16 -0600 Subject: [PATCH 05/26] Fix: Add ANDROID_SDK_ROOT/platform-tools to PATH for adb access Provision.yml sets ANDROID_SDK_ROOT but doesn't add platform-tools to PATH. Cake boot finds adb internally via the SDK root, but subsequent steps need it in PATH too. Also persist the PATH for later steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a44c4184a390..a61ed06630b3 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -115,6 +115,7 @@ stages: # Capture emulator device ID for Copilot agent - script: | + export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH" DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') if [ -z "$DEVICE_ID" ]; then echo "##vso[task.logissue type=error]No emulator device found after Cake boot" @@ -123,6 +124,8 @@ stages: fi echo "✅ Emulator running: $DEVICE_ID" echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools" + echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator" displayName: 'Capture Emulator UDID' # Install Node.js and Appium (same as ui-tests-steps.yml) From be08f8db78e919a5782864b4e8bcd88c3c818fa2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:44:39 -0600 Subject: [PATCH 06/26] Switch pool to Linux ubuntu-22.04 to match device-tests/uitests Android device-tests and uitests both run on Linux with KVM-accelerated emulation. Switch ci-copilot to the same so enable-kvm.yml actually runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a61ed06630b3..5e5819c390c3 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -26,7 +26,8 @@ parameters: - name: pool type: object default: - name: AcesShared + name: Azure Pipelines + vmImage: ubuntu-22.04 variables: - template: /eng/pipelines/common/variables.yml@self From 209e98a6581765e12cde0164e0b1727c92ba7a5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:12:35 -0600 Subject: [PATCH 07/26] Fix: Wait for emulator boot after Cake launches it in background Cake --target=boot fires the emulator process and returns immediately. Device-tests and uitests use --target=testOnly/uitest which waits internally, but ci-copilot needs the emulator ready before the agent runs. Add explicit wait for device, boot_completed, and package manager. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 56 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 5e5819c390c3..ba8dc39b4e75 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -108,26 +108,58 @@ stages: - script: dotnet tool restore displayName: 'Restore .NET Tools' - # Boot Android Emulator via Cake (same as device-tests-steps.yml / ui-tests-steps.yml) + # Boot Android Emulator via Cake (creates AVD and launches emulator in background) - ${{ if eq(parameters.Platform, 'android') }}: - script: dotnet cake eng/devices/android.cake --target=boot --device="android-emulator-64_30" --apiversion="30" --verbosity=diagnostic displayName: 'Boot Android Emulator (Cake)' timeoutInMinutes: 15 - # Capture emulator device ID for Copilot agent + # Wait for emulator to fully boot (Cake boot fires emulator in background) - script: | - export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH" + echo "Waiting for emulator device to appear..." + timeout=120 + waited=0 + while ! adb devices | grep -q "emulator.*device"; do + sleep 5 + waited=$((waited + 5)) + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Emulator device did not appear after ${timeout}s" + adb devices -l + exit 1 + fi + echo " Waiting for device... ($waited/${timeout}s)" + done + echo "Emulator device detected, waiting for boot_completed..." + + timeout=300 + waited=0 + while [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do + sleep 5 + waited=$((waited + 5)) + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Emulator did not finish booting after ${timeout}s" + adb shell getprop sys.boot_completed 2>/dev/null || true + exit 1 + fi + done + echo "Boot completed, waiting for package manager..." + + timeout=120 + waited=0 + while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do + sleep 5 + waited=$((waited + 5)) + if [ $waited -ge $timeout ]; then + echo "##vso[task.logissue type=error]Package manager not ready after ${timeout}s" + exit 1 + fi + done + DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') - if [ -z "$DEVICE_ID" ]; then - echo "##vso[task.logissue type=error]No emulator device found after Cake boot" - adb devices -l - exit 1 - fi - echo "✅ Emulator running: $DEVICE_ID" + echo "✅ Emulator fully booted: $DEVICE_ID" echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools" - echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator" - displayName: 'Capture Emulator UDID' + displayName: 'Wait for Emulator Boot and Capture UDID' + timeoutInMinutes: 10 # Install Node.js and Appium (same as ui-tests-steps.yml) - task: UseNode@1 From 90a6015f06bb166b56ed9ec74a8760e8490277ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:28:57 -0600 Subject: [PATCH 08/26] Fix: Add Android SDK platform-tools to PATH in wait step On Linux hosted agents, adb is not in PATH by default. Use ANDROID_SDK_ROOT (set by provision.yml) with fallback to the standard hosted image path. Also persist the PATH addition for subsequent steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ba8dc39b4e75..d4204d98b41c 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -116,6 +116,11 @@ stages: # Wait for emulator to fully boot (Cake boot fires emulator in background) - script: | + # Add Android SDK tools to PATH (provision.yml sets ANDROID_SDK_ROOT variable) + export PATH="${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/platform-tools:${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/emulator:$PATH" + echo "Using ANDROID_SDK_ROOT: ${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}" + echo "adb location: $(which adb 2>/dev/null || echo 'not found')" + echo "Waiting for emulator device to appear..." timeout=120 waited=0 @@ -158,6 +163,9 @@ stages: DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') echo "✅ Emulator fully booted: $DEVICE_ID" echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" + # Persist PATH for subsequent steps + echo "##vso[task.prependpath]${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/platform-tools" + echo "##vso[task.prependpath]${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/emulator" displayName: 'Wait for Emulator Boot and Capture UDID' timeoutInMinutes: 10 From f14d9344cf4731bd84369da76bf97da338b25665 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:46:01 -0600 Subject: [PATCH 09/26] Fix: Replace Cake boot with direct emulator launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cake's Teardown kills the emulator when the boot target exits. The uitest/testOnly targets keep it alive but also run tests. Since we just need a running emulator for the Copilot agent, create AVD and launch emulator directly — provision.yml already installed the SDK and images. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index d4204d98b41c..4b54e1451518 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -108,19 +108,23 @@ stages: - script: dotnet tool restore displayName: 'Restore .NET Tools' - # Boot Android Emulator via Cake (creates AVD and launches emulator in background) + # Create AVD and boot Android Emulator - ${{ if eq(parameters.Platform, 'android') }}: - - script: dotnet cake eng/devices/android.cake --target=boot --device="android-emulator-64_30" --apiversion="30" --verbosity=diagnostic - displayName: 'Boot Android Emulator (Cake)' - timeoutInMinutes: 15 - - # Wait for emulator to fully boot (Cake boot fires emulator in background) + # Use Cake to create the AVD (connectToDevice handles AVD creation + emulator launch) + # but uitest-prepare is the only target that skips Teardown cleanup. + # Since uitest-prepare also builds the test app (which we don't need), launch the emulator manually. - script: | - # Add Android SDK tools to PATH (provision.yml sets ANDROID_SDK_ROOT variable) - export PATH="${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/platform-tools:${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/emulator:$PATH" - echo "Using ANDROID_SDK_ROOT: ${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}" - echo "adb location: $(which adb 2>/dev/null || echo 'not found')" + export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}" + export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH" + echo "=== Creating AVD ===" + echo "no" | avdmanager create avd -n Emulator_30 -k "system-images;android-30;google_apis_playstore;x86_64" --device "Nexus 5X" --force + + echo "=== Starting Emulator ===" + nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim & + EMULATOR_PID=$! + echo "Emulator PID: $EMULATOR_PID" + echo "Waiting for emulator device to appear..." timeout=120 waited=0 @@ -143,7 +147,6 @@ stages: waited=$((waited + 5)) if [ $waited -ge $timeout ]; then echo "##vso[task.logissue type=error]Emulator did not finish booting after ${timeout}s" - adb shell getprop sys.boot_completed 2>/dev/null || true exit 1 fi done @@ -163,11 +166,10 @@ stages: DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') echo "✅ Emulator fully booted: $DEVICE_ID" echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" - # Persist PATH for subsequent steps - echo "##vso[task.prependpath]${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/platform-tools" - echo "##vso[task.prependpath]${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}/emulator" - displayName: 'Wait for Emulator Boot and Capture UDID' - timeoutInMinutes: 10 + echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools" + echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator" + displayName: 'Create AVD and Boot Android Emulator' + timeoutInMinutes: 15 # Install Node.js and Appium (same as ui-tests-steps.yml) - task: UseNode@1 From 53ecc41891936925e2eeb0e5a6ed4fd92ec407b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:02:18 -0600 Subject: [PATCH 10/26] Fix: Add -partition-size 2048 for limited disk on hosted agents Hosted ubuntu-22.04 agents have ~4.2GB free in home directory, but the default userdata partition needs 7.3GB. Use -partition-size 2048 to reduce to 2GB which is sufficient for testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 4b54e1451518..944032b6132e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -121,7 +121,7 @@ stages: echo "no" | avdmanager create avd -n Emulator_30 -k "system-images;android-30;google_apis_playstore;x86_64" --device "Nexus 5X" --force echo "=== Starting Emulator ===" - nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim & + nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 & EMULATOR_PID=$! echo "Emulator PID: $EMULATOR_PID" From 472d05b638c11a371922ec0687d56fa4ce9ce54c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:18:19 -0600 Subject: [PATCH 11/26] Fix: Reduce AVD disk.dataPartition.size in config.ini after creation The -partition-size flag doesn't override config.ini. Modify config.ini directly after AVD creation to set disk.dataPartition.size=2048m, which fits within the ~4.2GB free on hosted ubuntu agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 944032b6132e..3d31fb60f0f9 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -120,6 +120,13 @@ stages: echo "=== Creating AVD ===" echo "no" | avdmanager create avd -n Emulator_30 -k "system-images;android-30;google_apis_playstore;x86_64" --device "Nexus 5X" --force + # Reduce userdata partition to fit on hosted agents (~4.2GB free) + AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini" + if [ -f "$AVD_CONFIG" ]; then + sed -i 's/disk.dataPartition.size=.*/disk.dataPartition.size=2048m/' "$AVD_CONFIG" + echo "Updated disk.dataPartition.size to 2048m" + fi + echo "=== Starting Emulator ===" nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 & EMULATOR_PID=$! From 8869d1a52db43712b3f235f0bb7d6604c08115de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:35:30 -0600 Subject: [PATCH 12/26] Fix: Free disk space before emulator launch on hosted agents The google_apis_playstore system image for API 30 needs 7.3GB for the userdata partition, but hosted ubuntu agents only have ~4.2GB free. Remove unnecessary tools (CodeQL, Go, Python, Chromium, extra .NET SDKs, Swift) to free ~10GB before launching the emulator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 3d31fb60f0f9..2cfbb89c323a 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -110,9 +110,18 @@ stages: # Create AVD and boot Android Emulator - ${{ if eq(parameters.Platform, 'android') }}: - # Use Cake to create the AVD (connectToDevice handles AVD creation + emulator launch) - # but uitest-prepare is the only target that skips Teardown cleanup. - # Since uitest-prepare also builds the test app (which we don't need), launch the emulator manually. + # Free disk space on hosted agents (emulator needs ~7GB for userdata partition) + - script: | + echo "=== Disk space before cleanup ===" + df -h /home + echo "Removing unnecessary tools to free space..." + sudo rm -rf /usr/share/dotnet /usr/local/share/powershell /usr/local/share/chromium 2>/dev/null || true + sudo rm -rf /opt/hostedtoolcache/CodeQL /opt/hostedtoolcache/go /opt/hostedtoolcache/Python 2>/dev/null || true + sudo rm -rf /usr/share/swift 2>/dev/null || true + echo "=== Disk space after cleanup ===" + df -h /home + displayName: 'Free Disk Space for Emulator' + - script: | export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}" export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH" From 2797816a98c0dcc1f69224f1f6d52a543d66da83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:53:41 -0600 Subject: [PATCH 13/26] Fix: Restart adb server before emulator, use adb wait-for-device The emulator boots successfully but adb doesn't see it. Restart the adb server fresh before launching emulator, use adb wait-for-device instead of polling, and capture emulator log for debugging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 2cfbb89c323a..34b279b61a07 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -137,24 +137,24 @@ stages: fi echo "=== Starting Emulator ===" - nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 & + # Kill any stale adb server and restart + adb kill-server 2>/dev/null || true + sleep 1 + adb start-server + + nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 > /tmp/emulator.log 2>&1 & EMULATOR_PID=$! echo "Emulator PID: $EMULATOR_PID" - echo "Waiting for emulator device to appear..." - timeout=120 - waited=0 - while ! adb devices | grep -q "emulator.*device"; do - sleep 5 - waited=$((waited + 5)) - if [ $waited -ge $timeout ]; then - echo "##vso[task.logissue type=error]Emulator device did not appear after ${timeout}s" - adb devices -l - exit 1 - fi - echo " Waiting for device... ($waited/${timeout}s)" - done - echo "Emulator device detected, waiting for boot_completed..." + echo "Waiting for emulator device (adb wait-for-device)..." + timeout 120 adb wait-for-device + if [ $? -ne 0 ]; then + echo "##vso[task.logissue type=error]adb wait-for-device timed out after 120s" + adb devices -l + cat /tmp/emulator.log | tail -30 + exit 1 + fi + echo "Device detected: $(adb devices -l | grep emulator)" timeout=300 waited=0 From aa25cdbcab48902eba427ea4fc8ab1dc23b2a90e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:41:18 -0600 Subject: [PATCH 14/26] Fix: Use GH_TOKEN env var for gh auth on Linux Newer gh CLI versions on ubuntu-22.04 require read:org scope which the token may not have. Using GH_TOKEN env var bypasses scope validation while still authenticating. Falls back to gh auth login if needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 34b279b61a07..edfae0c50b11 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -218,11 +218,18 @@ stages: echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable." exit 1 fi - echo "$(GH_CLI_TOKEN)" | gh auth login --with-token - if ! gh auth status; then - echo "##vso[task.logissue type=error]GitHub CLI authentication failed" - exit 1 + # Use GH_TOKEN env var to avoid scope validation issues with newer gh versions + export GH_TOKEN="$(GH_CLI_TOKEN)" + gh auth status + if [ $? -ne 0 ]; then + # Fallback: try direct login + echo "$(GH_CLI_TOKEN)" | gh auth login --with-token 2>/dev/null || true + if ! gh auth status; then + echo "##vso[task.logissue type=error]GitHub CLI authentication failed" + exit 1 + fi fi + echo "GitHub CLI authenticated successfully" displayName: 'Authenticate GitHub CLI' env: GH_CLI_TOKEN: $(GH_CLI_TOKEN) From e6b4066d3938d62c2f00edc589ed6eb0fa8655ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:34:08 -0600 Subject: [PATCH 15/26] Fix: Handle GNU vs BSD sed for Linux/macOS compatibility GNU sed (Linux) uses 'sed -i' while BSD sed (macOS) uses 'sed -i ""'. Also re-triggered pipeline with unquoted parameters to fix PRNumber receiving 'Platform=android' as part of its value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index edfae0c50b11..6e2f3d36fdc8 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -350,7 +350,12 @@ stages: # AcesShared agents may have a newer Xcode than the .NET iOS SDK expects cp Directory.Build.Override.props.in Directory.Build.Override.props # Insert ValidateXcodeVersion before closing tag - sed -i '' 's|| false\n|' Directory.Build.Override.props + # GNU sed (Linux) uses -i without suffix; BSD sed (macOS) uses -i '' + if [[ "$(uname)" == "Linux" ]]; then + sed -i 's|| false\n|' Directory.Build.Override.props + else + sed -i '' 's|| false\n|' Directory.Build.Override.props + fi # Create artifacts directory for Copilot outputs mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs From 4d1ab4770dbfaa3f970c8bea92e988a57906bd5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:57:17 -0600 Subject: [PATCH 16/26] Fix: Ensure Copilot CLI npm bin is on PATH for subsequent steps On Linux, UseNode@1 installs to /opt/hostedtoolcache/node/*/x64/bin/ which may not persist to pwsh subprocesses. Explicitly prepend the copilot binary directory to PATH via ##vso[task.prependpath]. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 6e2f3d36fdc8..c2931916c9bf 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -237,10 +237,11 @@ stages: - script: | echo "Installing GitHub Copilot CLI..." npm install -g @github/copilot - if ! which copilot; then - echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI" - exit 1 - fi + # Ensure npm global bin is on PATH for subsequent steps (Linux UseNode installs to toolcache) + COPILOT_BIN_DIR=$(dirname "$(which copilot)") + echo "Copilot binary at: $COPILOT_BIN_DIR/copilot" + echo "##vso[task.prependpath]$COPILOT_BIN_DIR" + copilot --version || true echo "Copilot CLI installed successfully" displayName: 'Install GitHub Copilot CLI' From 5e31b52fb76eb05ab9381f35a48e8907d47af212 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:35:30 -0600 Subject: [PATCH 17/26] Fix: Explicitly add copilot to PATH in PR reviewer step The ##vso[task.prependpath] from the install step should propagate, but pwsh subprocess may not inherit it. Find the copilot binary and add its directory to PATH explicitly in the reviewer step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index c2931916c9bf..51511ead77af 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -342,6 +342,15 @@ stages: echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." + # Ensure copilot CLI is on PATH (npm global bin from UseNode@1) + COPILOT_PATH=$(which copilot 2>/dev/null || find /opt/hostedtoolcache/node -name copilot -type f 2>/dev/null | head -1) + if [ -n "$COPILOT_PATH" ]; then + export PATH="$(dirname "$COPILOT_PATH"):$PATH" + echo "Added $(dirname "$COPILOT_PATH") to PATH" + fi + echo "PATH=$PATH" + echo "copilot location: $(which copilot 2>/dev/null || echo 'not found')" + # Configure git identity (required for merge operations on self-hosted agents) git config user.email "copilot-ci@microsoft.com" git config user.name "Copilot CI" From 589da332cbc1da6fecd798f4b31699b09349ed5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:57:09 -0600 Subject: [PATCH 18/26] Fix: Symlink copilot to /usr/local/bin and use pwsh -NoProfile PATH exported from bash doesn't reliably propagate to pwsh subprocess on Linux hosted agents. Create a symlink in /usr/local/bin which is universally on PATH. Also use -NoProfile to prevent pwsh profiles from interfering with PATH resolution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 51511ead77af..9a46df166f4c 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -342,14 +342,18 @@ stages: echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." - # Ensure copilot CLI is on PATH (npm global bin from UseNode@1) + # Ensure copilot CLI is accessible to pwsh subprocess. + # npm global install on Linux goes to UseNode@1 toolcache path which may not + # be on PATH inside pwsh even when exported from bash. Create a symlink in + # /usr/local/bin which is universally on PATH for all shells. COPILOT_PATH=$(which copilot 2>/dev/null || find /opt/hostedtoolcache/node -name copilot -type f 2>/dev/null | head -1) - if [ -n "$COPILOT_PATH" ]; then - export PATH="$(dirname "$COPILOT_PATH"):$PATH" - echo "Added $(dirname "$COPILOT_PATH") to PATH" + if [ -n "$COPILOT_PATH" ] && [ ! -f /usr/local/bin/copilot ]; then + sudo ln -sf "$COPILOT_PATH" /usr/local/bin/copilot + echo "Symlinked copilot to /usr/local/bin/copilot" fi - echo "PATH=$PATH" echo "copilot location: $(which copilot 2>/dev/null || echo 'not found')" + # Verify pwsh can find it + pwsh -NoProfile -c 'Write-Host "pwsh sees copilot at: $(Get-Command copilot -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)"' # Configure git identity (required for merge operations on self-hosted agents) git config user.email "copilot-ci@microsoft.com" @@ -374,7 +378,7 @@ stages: # The script will merge the PR into the current branch # -PostSummaryComment and -RunFinalize handle posting comments set +e - pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" + pwsh -NoProfile .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md" COPILOT_EXIT_CODE=$? set -e From 1d5f709bfa45c3600b5717fb71fd0ad5a0127647 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:55:07 -0600 Subject: [PATCH 19/26] Fix: Use Get-Command for Copilot CLI detection in Review-PR.ps1 copilot --version outputs to stderr, so '2>$null' discards the output and the check always fails on Linux. Use Get-Command (reliable) and merge stderr with '2>&1' when capturing the version string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/Review-PR.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 9a83d73634b2..4b96d435f8c1 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -131,12 +131,14 @@ if (-not $ghVersion) { } Write-Host " ✅ GitHub CLI: $ghVersion" -ForegroundColor Green -# Check Copilot CLI -$copilotVersion = copilot --version 2>$null -if (-not $copilotVersion) { +# Check Copilot CLI - use Get-Command (reliable) then get version with merged streams +$copilotCmd = Get-Command copilot -ErrorAction SilentlyContinue +if (-not $copilotCmd) { Write-Error "Copilot CLI is not installed. Install with: npm install -g @github/copilot" exit 1 } +$copilotVersion = (& copilot --version 2>&1 | Out-String).Trim() +if (-not $copilotVersion) { $copilotVersion = $copilotCmd.Source } Write-Host " ✅ Copilot CLI: $copilotVersion" -ForegroundColor Green # Check PR exists From 609b94c15936091a343534bcd666fb3cc4757e78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:21:24 -0600 Subject: [PATCH 20/26] Fix: Upgrade Node.js to v24 for Copilot CLI requirement Copilot CLI now requires Node.js v24+. UseNode@1 was pinned to 20.3.1 which causes 'GitHub Copilot CLI requires Node.js v24 or higher'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 9a46df166f4c..e60ea4e2e27b 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -190,7 +190,7 @@ stages: # Install Node.js and Appium (same as ui-tests-steps.yml) - task: UseNode@1 inputs: - version: "20.3.1" + version: "24.x" displayName: 'Install Node.js' - pwsh: | From 63cb319248e7f6db40281c4e9bac39816532efae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 07:19:14 -0600 Subject: [PATCH 21/26] Fix: AVD name truncation bug in Start-Emulator.ps1 When emulator -list-avds returns a single AVD, PowerShell stores it as a string not an array. $avdList[0] then returns the first character ('E' from 'Emulator_30') instead of the full name. Fix by wrapping with @() and [string[]] to force array type. Also fix selection regex: 'API.*30' didn't match 'Emulator_30' since there's no 'API' prefix. Changed to match '30' anywhere in the name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/shared/Start-Emulator.ps1 | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1 index a5d90f192530..b1d7875326ba 100644 --- a/.github/scripts/shared/Start-Emulator.ps1 +++ b/.github/scripts/shared/Start-Emulator.ps1 @@ -65,7 +65,8 @@ if ($Platform -eq "android") { # Check if DeviceUdid is an AVD name (not an emulator-XXXX format) if ($DeviceUdid -and $DeviceUdid -notmatch "^emulator-\d+$") { # DeviceUdid is likely an AVD name - check if it's in the AVD list - $avdList = emulator -list-avds 2>$null + # Force array output - single AVD returns a string which breaks -contains + [string[]]$avdList = @(emulator -list-avds 2>$null) if ($avdList -contains $DeviceUdid) { Write-Info "DeviceUdid '$DeviceUdid' is an AVD name. Will boot this emulator..." $selectedAvd = $DeviceUdid @@ -103,7 +104,8 @@ if ($Platform -eq "android") { # Get list of available AVDs (if not already set from parameter) if (-not $selectedAvd) { - $avdList = emulator -list-avds 2>$null + # Force array output - single AVD returns a string which breaks indexing + [string[]]$avdList = @(emulator -list-avds 2>$null) if (-not $avdList -or $avdList.Count -eq 0) { Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio." @@ -119,7 +121,7 @@ if ($Platform -eq "android") { # Selection priority: # 1. API 34 device (matches CI provisioning) # 2. API 30 Nexus device - # 3. Any API 30 device + # 3. Any API 30 device (matches names like "Emulator_30", "API_30_xxx", etc.) # 4. Any Nexus device # 5. First available device @@ -132,16 +134,16 @@ if ($Platform -eq "android") { # Try to find API 30 Nexus device if (-not $selectedAvd) { - $api30Nexus = $avdList | Where-Object { $_ -match "API.*30" -and $_ -match "Nexus" } | Select-Object -First 1 + $api30Nexus = $avdList | Where-Object { $_ -match "30" -and $_ -match "Nexus" } | Select-Object -First 1 if ($api30Nexus) { $selectedAvd = $api30Nexus Write-Info "Selected API 30 Nexus device: $selectedAvd" } } - # Try to find any API 30 device + # Try to find any API 30 device (match "30" anywhere in name) if (-not $selectedAvd) { - $api30Device = $avdList | Where-Object { $_ -match "API.*30" } | Select-Object -First 1 + $api30Device = $avdList | Where-Object { $_ -match "30" } | Select-Object -First 1 if ($api30Device) { $selectedAvd = $api30Device Write-Info "Selected API 30 device: $selectedAvd" From e93d97ab97d5e2d18422b3d6d59e946b6efaee54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:38:26 -0600 Subject: [PATCH 22/26] Fix: Add emulator CI preparation to prevent ANR - Pre-authorize ADB keys before boot (mirrors android.cake HandleVirtualDevice) - Restart ADB server at 90s boot wait (mirrors android.cake PrepareDevice) - Disable all animations (window, transition, animator) to reduce CPU load - Set infinite screen timeout and stay-awake to prevent screen lock - Wake screen and dismiss system dialogs after boot - Clear logcat buffer for clean agent logs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index e60ea4e2e27b..28dbd266d3a7 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -135,6 +135,19 @@ stages: sed -i 's/disk.dataPartition.size=.*/disk.dataPartition.size=2048m/' "$AVD_CONFIG" echo "Updated disk.dataPartition.size to 2048m" fi + + # Pre-authorize ADB keys (mirrors android.cake HandleVirtualDevice) + echo "=== Pre-authorizing ADB keys ===" + mkdir -p "$HOME/.android" + if [ ! -f "$HOME/.android/adbkey" ]; then + adb keygen "$HOME/.android/adbkey" 2>/dev/null || true + fi + ADB_KEY_PUB="$HOME/.android/adbkey.pub" + AVD_DIR="$HOME/.android/avd/Emulator_30.avd" + if [ -f "$ADB_KEY_PUB" ] && [ -d "$AVD_DIR" ]; then + cp "$ADB_KEY_PUB" "$AVD_DIR/adbkey.pub" + echo "ADB key pre-authorized for emulator" + fi echo "=== Starting Emulator ===" # Kill any stale adb server and restart @@ -165,6 +178,15 @@ stages: echo "##vso[task.logissue type=error]Emulator did not finish booting after ${timeout}s" exit 1 fi + # At 90 seconds, restart ADB server to recover from auth issues (mirrors android.cake) + if [ $waited -eq 90 ]; then + echo "Boot taking longer than expected (90/${timeout}s). Restarting ADB server..." + adb kill-server 2>/dev/null || true + sleep 2 + adb start-server + sleep 2 + echo "ADB server restarted. Continuing to wait..." + fi done echo "Boot completed, waiting for package manager..." @@ -181,6 +203,25 @@ stages: DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}') echo "✅ Emulator fully booted: $DEVICE_ID" + + # Prepare emulator for CI use — keeps device responsive during idle period + echo "=== Preparing emulator for CI ===" + # Disable all animations (reduces CPU load and flakiness) + adb -s $DEVICE_ID shell settings put global window_animation_scale 0.0 + adb -s $DEVICE_ID shell settings put global transition_animation_scale 0.0 + adb -s $DEVICE_ID shell settings put global animator_duration_scale 0.0 + # Prevent screen from turning off (emulator simulates AC charging) + adb -s $DEVICE_ID shell settings put system screen_off_timeout 2147483647 + adb -s $DEVICE_ID shell svc power stayon true + # Wake screen and dismiss any lock screen + adb -s $DEVICE_ID shell input keyevent 82 + sleep 1 + # Dismiss any "System UI has stopped" or crash dialogs + adb -s $DEVICE_ID shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true + # Clear logcat buffer so agent sees only fresh logs + adb -s $DEVICE_ID logcat -c 2>/dev/null || true + echo "Emulator preparation complete" + echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID" echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools" echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator" From 059891165bd82cfa4f67bc8c1fc6d6610aa7d2a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:42:35 -0600 Subject: [PATCH 23/26] Fix: Add emulator warmup step before agent to prevent SystemUI ANR The emulator sits idle 15-30 min while Node/Appium/CLI are installed. SystemUI can ANR during this idle period on low-resource CI agents. Adds a dedicated warmup step right before the reviewer agent: - Wakes screen and dismisses any ANR/system dialogs - Opens/closes Settings to exercise the system - Verifies device responsiveness before handing off to agent - Also adds stability wait after boot to fix 'device offline' errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index 28dbd266d3a7..abb91500bb78 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -206,6 +206,14 @@ stages: # Prepare emulator for CI use — keeps device responsive during idle period echo "=== Preparing emulator for CI ===" + # Wait for device to stabilize after boot (transient offline state) + for i in $(seq 1 10); do + if adb -s $DEVICE_ID shell echo ok 2>/dev/null | grep -q ok; then + break + fi + echo "Device offline, retrying ($i/10)..." + sleep 3 + done # Disable all animations (reduces CPU load and flakiness) adb -s $DEVICE_ID shell settings put global window_animation_scale 0.0 adb -s $DEVICE_ID shell settings put global transition_animation_scale 0.0 @@ -379,6 +387,57 @@ stages: condition: eq('${{ parameters.Platform }}', 'ios') timeoutInMinutes: 5 + # Warm up the emulator right before the agent runs. + # The emulator may have been idle for 15-30 min while Appium/Node/CLI were installed. + # Without this, SystemUI can ANR when the agent first touches it. + - script: | + set -e + DEVICE_ID="$(DEVICE_UDID)" + if [ -z "$DEVICE_ID" ]; then + echo "No DEVICE_UDID set — skipping warmup" + exit 0 + fi + + echo "=== Emulator warmup before agent ===" + # Verify device is still connected + if ! adb -s "$DEVICE_ID" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then + echo "Device not responding. Restarting ADB server..." + adb kill-server 2>/dev/null || true + sleep 2 + adb start-server + sleep 2 + timeout 60 adb wait-for-device + fi + + # Wake screen and dismiss lock + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_WAKEUP 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_MENU 2>/dev/null || true + sleep 2 + + # Force-stop any ANR'd system processes and dismiss dialogs + adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true + # Press ENTER and BACK to dismiss any remaining dialogs + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_ENTER 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true + sleep 1 + + # Open and close Settings to exercise the system and confirm responsiveness + adb -s "$DEVICE_ID" shell am start -a android.settings.SETTINGS 2>/dev/null || true + sleep 3 + adb -s "$DEVICE_ID" shell am force-stop com.android.settings 2>/dev/null || true + + # Final ANR dialog sweep + adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true + + # Clear logcat so agent gets clean logs + adb -s "$DEVICE_ID" logcat -c 2>/dev/null || true + + echo "✅ Emulator warmed up and responsive" + displayName: 'Warm Up Android Emulator' + condition: and(succeeded(), eq('${{ parameters.Platform }}', 'android')) + timeoutInMinutes: 3 + - script: | echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..." echo "Reviewing PR #${{ parameters.PRNumber }}..." From 055a5d492d44a91753e72b0692827a19a4f15ef2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:21:19 -0500 Subject: [PATCH 24/26] Fix: APK install retry (P1) and ANR dialog dismissal (P2) P1: Add retry logic for Android APK install in Build-AndDeploy.ps1. On ADB0010/broken-pipe failure (transient API 30 bug), uninstall existing packages, restart ADB, and retry once. P2: Add ANR dialog dismissal in BuildAndRunHostApp.ps1 before each test run. Checks dumpsys window for lingering ANR dialogs and force-dismisses with HOME+BACK. Also enhances the pipeline warmup step with a two-pass dismiss loop and explicit ANR detection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/BuildAndRunHostApp.ps1 | 22 ++++++++++ .github/scripts/shared/Build-AndDeploy.ps1 | 49 +++++++++++++++++++--- eng/pipelines/ci-copilot.yml | 34 ++++++++++----- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/.github/scripts/BuildAndRunHostApp.ps1 b/.github/scripts/BuildAndRunHostApp.ps1 index 0702c73b015a..b349753980eb 100644 --- a/.github/scripts/BuildAndRunHostApp.ps1 +++ b/.github/scripts/BuildAndRunHostApp.ps1 @@ -232,6 +232,28 @@ if ($Category) { if ($Platform -eq "android") { Write-Info "Clearing Android logcat buffer before test..." & adb -s $DeviceUdid logcat -c + + # Dismiss any ANR dialogs that may have appeared during build/deploy. + # The emulator can sit idle during long builds, causing SystemUI ANR. + Write-Info "Dismissing any system dialogs before test..." + & adb -s $DeviceUdid shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>$null + & adb -s $DeviceUdid shell input keyevent KEYCODE_ENTER 2>$null + & adb -s $DeviceUdid shell input keyevent KEYCODE_BACK 2>$null + Start-Sleep -Seconds 1 + & adb -s $DeviceUdid shell input keyevent KEYCODE_WAKEUP 2>$null + & adb -s $DeviceUdid shell input keyevent KEYCODE_MENU 2>$null + Start-Sleep -Seconds 1 + + # Check for lingering ANR dialogs via window dump + $windowDump = & adb -s $DeviceUdid shell dumpsys window 2>$null | Select-String "Application Not Responding|ANR" + if ($windowDump) { + Write-Warn "ANR dialog detected — force-dismissing..." + & adb -s $DeviceUdid shell input keyevent KEYCODE_HOME 2>$null + Start-Sleep -Seconds 2 + & adb -s $DeviceUdid shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>$null + & adb -s $DeviceUdid shell input keyevent KEYCODE_BACK 2>$null + Start-Sleep -Seconds 1 + } } # Capture test start time for iOS logs diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1 index f57d5c088094..ae81e05a1ea8 100644 --- a/.github/scripts/shared/Build-AndDeploy.ps1 +++ b/.github/scripts/shared/Build-AndDeploy.ps1 @@ -85,19 +85,56 @@ if ($Platform -eq "android") { Write-Info "Build command: dotnet build $($buildArgs -join ' ')" $buildStartTime = Get-Date + $maxAttempts = 2 + $buildExitCode = 1 + + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + if ($attempt -gt 1) { + Write-Warn "Retrying build/deploy (attempt $attempt of $maxAttempts)..." + + # Uninstall any MAUI test packages to clear bad state + $installedPkg = & adb shell pm list packages 2>$null | Select-String "maui" | ForEach-Object { ($_ -replace "package:", "").Trim() } + if ($installedPkg) { + foreach ($pkg in $installedPkg) { + Write-Info "Uninstalling $pkg before retry..." + & adb uninstall $pkg 2>$null + } + } + + # Restart ADB server to recover from broken pipe / transient errors + Write-Info "Restarting ADB server..." + & adb kill-server 2>$null + Start-Sleep -Seconds 2 + & adb start-server + Start-Sleep -Seconds 2 + & adb wait-for-device + Start-Sleep -Seconds 3 + } + + & dotnet build @buildArgs + $buildExitCode = $LASTEXITCODE + + if ($buildExitCode -eq 0) { + break + } + + if ($attempt -lt $maxAttempts) { + Write-Warn "Build/deploy failed (attempt $attempt). ADB0010/broken-pipe errors are transient on API 30 — will retry." + } + } - # Build and deploy in one step (Run target handles both) - & dotnet build @buildArgs - - $buildExitCode = $LASTEXITCODE $buildDuration = (Get-Date) - $buildStartTime if ($buildExitCode -ne 0) { - Write-Error "Build/deploy failed with exit code $buildExitCode" + Write-Error "Build/deploy failed after $maxAttempts attempts with exit code $buildExitCode" exit $buildExitCode } - Write-Success "Build and deploy completed in $($buildDuration.TotalSeconds) seconds" + if ($attempt -gt 1) { + Write-Success "Build and deploy succeeded on attempt $attempt in $($buildDuration.TotalSeconds) seconds" + } else { + Write-Success "Build and deploy completed in $($buildDuration.TotalSeconds) seconds" + } #endregion diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index abb91500bb78..f5b17a3c033e 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -409,24 +409,36 @@ stages: timeout 60 adb wait-for-device fi - # Wake screen and dismiss lock - adb -s "$DEVICE_ID" shell input keyevent KEYCODE_WAKEUP 2>/dev/null || true - adb -s "$DEVICE_ID" shell input keyevent KEYCODE_MENU 2>/dev/null || true - sleep 2 + # Dismiss ANR dialogs and wake screen — run twice for reliability + for PASS in 1 2; do + echo "--- Warmup pass $PASS ---" + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_WAKEUP 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_MENU 2>/dev/null || true + sleep 1 - # Force-stop any ANR'd system processes and dismiss dialogs - adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true - # Press ENTER and BACK to dismiss any remaining dialogs - adb -s "$DEVICE_ID" shell input keyevent KEYCODE_ENTER 2>/dev/null || true - adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true - sleep 1 + # Dismiss system dialogs (ANR, crash, etc.) + adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_ENTER 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true + sleep 1 + done + + # Check for lingering ANR in window state + if adb -s "$DEVICE_ID" shell dumpsys window 2>/dev/null | grep -qi "Application Not Responding\|ANR"; then + echo "⚠️ ANR dialog still present — force-dismissing with HOME + BACK" + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_HOME 2>/dev/null || true + sleep 2 + adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true + adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true + sleep 1 + fi # Open and close Settings to exercise the system and confirm responsiveness adb -s "$DEVICE_ID" shell am start -a android.settings.SETTINGS 2>/dev/null || true sleep 3 adb -s "$DEVICE_ID" shell am force-stop com.android.settings 2>/dev/null || true - # Final ANR dialog sweep + # Final dialog sweep adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true From 46911701afaee14bda7b3671fe6623c6996cc54f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:44:31 -0500 Subject: [PATCH 25/26] Fix: Add emulator boot retry loop and step-level retry Emulator sometimes starts but ADB shows offline for >120s on hosted agents (~25% of failed builds). This was a fatal failure with no recovery. Changes: - Wrap emulator launch + adb wait-for-device in a retry loop (2 attempts) - On retry: stop emulator process, restart ADB, relaunch - Add retryCountOnTaskFailure: 1 on the AVD step as belt-and-suspenders - Add ADB key re-authorization every 60s during boot wait (matches Cake) This gives up to 4 chances total to get a working emulator: 2 script-level retries x 2 step-level retries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 58 ++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index f5b17a3c033e..a7a8df3956bb 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -155,19 +155,46 @@ stages: sleep 1 adb start-server - nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 > /tmp/emulator.log 2>&1 & - EMULATOR_PID=$! - echo "Emulator PID: $EMULATOR_PID" - - echo "Waiting for emulator device (adb wait-for-device)..." - timeout 120 adb wait-for-device - if [ $? -ne 0 ]; then - echo "##vso[task.logissue type=error]adb wait-for-device timed out after 120s" + # Retry loop: emulator sometimes fails to connect ADB on first launch + MAX_LAUNCH_ATTEMPTS=2 + EMULATOR_PID="" + for LAUNCH_ATTEMPT in $(seq 1 $MAX_LAUNCH_ATTEMPTS); do + echo "--- Emulator launch attempt $LAUNCH_ATTEMPT of $MAX_LAUNCH_ATTEMPTS ---" + + if [ $LAUNCH_ATTEMPT -gt 1 ]; then + echo "Cleaning up before retry..." + if [ -n "$EMULATOR_PID" ] && kill -0 "$EMULATOR_PID" 2>/dev/null; then + kill "$EMULATOR_PID" 2>/dev/null || true + sleep 2 + kill -0 "$EMULATOR_PID" 2>/dev/null && kill -9 "$EMULATOR_PID" 2>/dev/null || true + fi + sleep 3 + adb kill-server 2>/dev/null || true + sleep 2 + adb start-server + sleep 2 + fi + + nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 > /tmp/emulator.log 2>&1 & + EMULATOR_PID=$! + echo "Emulator PID: $EMULATOR_PID" + + echo "Waiting for emulator device (adb wait-for-device, 120s timeout)..." + timeout 120 adb wait-for-device + if [ $? -eq 0 ]; then + echo "Device detected: $(adb devices -l | grep emulator)" + break + fi + + echo "##vso[task.logissue type=warning]adb wait-for-device timed out (attempt $LAUNCH_ATTEMPT)" adb devices -l - cat /tmp/emulator.log | tail -30 - exit 1 - fi - echo "Device detected: $(adb devices -l | grep emulator)" + tail -30 /tmp/emulator.log + + if [ $LAUNCH_ATTEMPT -eq $MAX_LAUNCH_ATTEMPTS ]; then + echo "##vso[task.logissue type=error]Emulator failed to connect after $MAX_LAUNCH_ATTEMPTS attempts" + exit 1 + fi + done timeout=300 waited=0 @@ -187,6 +214,12 @@ stages: sleep 2 echo "ADB server restarted. Continuing to wait..." fi + # Re-ensure ADB keys every 60s during boot (mirrors android.cake PrepareDevice) + if [ $((waited % 60)) -eq 0 ] && [ $waited -gt 0 ]; then + if [ -f "$ADB_KEY_PUB" ] && [ -d "$AVD_DIR" ]; then + cp "$ADB_KEY_PUB" "$AVD_DIR/adbkey.pub" 2>/dev/null || true + fi + fi done echo "Boot completed, waiting for package manager..." @@ -234,6 +267,7 @@ stages: echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools" echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator" displayName: 'Create AVD and Boot Android Emulator' + retryCountOnTaskFailure: 1 timeoutInMinutes: 15 # Install Node.js and Appium (same as ui-tests-steps.yml) From dc1ed2100fee377953edf971ec91b8866c47305e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:50:57 -0500 Subject: [PATCH 26/26] Increase job timeout to 360 minutes for complex PRs The Copilot reviewer agent on PR #25036 (Nested Flex Layouts) ran 5 try-fix attempts across models and hit the 180-minute timeout during Round 2 cross-pollination. Doubling to 360 minutes to accommodate complex PRs that need multiple fix attempts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/ci-copilot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index a7a8df3956bb..b8c8c4d4399f 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -47,7 +47,7 @@ stages: - job: CopilotReview displayName: 'Run Copilot PR Reviewer Agent' pool: ${{ parameters.pool }} - timeoutInMinutes: 180 + timeoutInMinutes: 360 steps: - checkout: self fetchDepth: 0