From 28a3e9f833b8eeb184f2fad9d35c100aa171c68e Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Tue, 28 Apr 2026 14:59:50 -0700 Subject: [PATCH 1/3] Collect dump files and improve snapshot taking --- .ado/jobs/e2e-test.yml | 254 +++++++++++++++++- .ado/scripts/SetupLocalDumps.cmd | 58 ++-- .ado/templates/prepare-build-env.yml | 29 +- .cspell.json | 5 +- .../test/AccessibilityTest.test.ts | 9 +- .../test/ButtonComponentTest.test.ts | 9 +- .../test/FlatListComponentTest.test.ts | 15 +- .../test/HangSimulationTest.test.ts | 42 +++ .../test/PointerButtonComponentTest.test.ts | 9 +- .../test/RNTesterNavigation.ts | 9 +- .../test/SwitchComponentTest.test.ts | 9 +- .../test/TextInputComponentTest.test.ts | 9 +- .../test/TouchableComponentTest.test.ts | 9 +- .../test/ViewComponentTest.test.ts | 9 +- .../RNTesterApp-Fabric/RNTesterApp-Fabric.cpp | 161 ++++++++++- 15 files changed, 586 insertions(+), 50 deletions(-) create mode 100644 packages/e2e-test-app-fabric/test/HangSimulationTest.test.ts diff --git a/.ado/jobs/e2e-test.yml b/.ado/jobs/e2e-test.yml index f0a170170c5..5c1d77a9b00 100644 --- a/.ado/jobs/e2e-test.yml +++ b/.ado/jobs/e2e-test.yml @@ -8,6 +8,17 @@ parameters: - Continuous - name: AgentPool type: object + # When set to true on a PR-validation queue, the E2E app deliberately + # crashes (simulateCrashForTesting) or hangs (simulateHangForTesting) so we + # can re-validate that the crash-dump collection path still produces a + # usable artifact. Disabled by default — the test step is doomed by design + # when these are on. + - name: simulateCrashForTesting + type: boolean + default: false + - name: simulateHangForTesting + type: boolean + default: false - name: buildMatrix type: object default: @@ -44,6 +55,12 @@ jobs: platform: ${{ matrix.BuildPlatform }} configuration: Release buildEnvironment: ${{ config.buildEnvironment }} + # Capture crash dumps for the E2E test app (packaged UWP) and + # the Metro bundler. ProcDump-as-AeDebug does not reliably fire + # for packaged apps; WER LocalDumps does. + localDumpsExeNames: + - RNTesterApp-Fabric + - node - pwsh: | Write-Host "##vso[task.setvariable variable=BuildLogDirectory]$(Build.BinariesDirectory)\${{ matrix.BuildPlatform }}\BuildLogs" @@ -68,11 +85,238 @@ jobs: echo ##vso[task.setvariable variable=StartedFabricTests]true displayName: Set StartedFabricTests - - script: | - yarn e2etest - displayName: yarn e2etest - workingDirectory: packages/e2e-test-app-fabric - timeoutInMinutes: 10 # Time to wait for this task to complete before the server kills it. + # Test-only: arm the crash-simulation sentinel so RNTesterApp-Fabric + # crashes on startup. Validates the in-process minidump path. + - ${{ if eq(parameters.simulateCrashForTesting, true) }}: + - pwsh: | + $flagPath = Join-Path $env:ProgramData 'rnw-e2e-simulate-crash.flag' + New-Item -Path $flagPath -ItemType File -Force | Out-Null + Write-Host "Crash-simulation sentinel created at $flagPath" + $dumpDir = Join-Path $env:ProgramData 'RNW-E2E-Dumps' + if (Test-Path $dumpDir) { + Remove-Item -Path "$dumpDir\*" -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Cleared stale dumps under $dumpDir" + } + displayName: Arm crash-simulation sentinel (TEST ONLY) + + # Test-only: arm the hang-simulation env var, which switches on + # the HangSimulationTest.test.ts test. That test invokes the + # `HangForTesting` automation command, jamming the app's UI thread + # so the post-failure ProcDump path captures a hang dump. + - ${{ if eq(parameters.simulateHangForTesting, true) }}: + - pwsh: | + Write-Host "##vso[task.setvariable variable=RNW_SIMULATE_HANG]1" + Write-Host "Hang simulation armed (RNW_SIMULATE_HANG=1)" + displayName: Arm hang-simulation env var (TEST ONLY) + + # When simulating a hang, run ONLY the HangSimulationTest. The default + # jest sequencer puts brand-new (no-timing-history) tests late in the order, + # so without filtering the test step times out before the hang test even + # runs. 4-minute timeout: enough for app launch (~30 s) + the test's 70 s + # jest testTimeout + jest teardown attempt; ADO will cut off at 4 min if the + # hang prevents jest from exiting cleanly, which is fine — Capture step then + # finds the still-alive UI-hung app. + - ${{ if eq(parameters.simulateHangForTesting, true) }}: + - script: | + yarn e2etest --testPathPattern HangSimulationTest + displayName: yarn e2etest (hang simulation only) + workingDirectory: packages/e2e-test-app-fabric + timeoutInMinutes: 4 + + - ${{ if not(eq(parameters.simulateHangForTesting, true)) }}: + - script: | + yarn e2etest + displayName: yarn e2etest + workingDirectory: packages/e2e-test-app-fabric + # Drop to 2 min during crash simulation — the app crashes + # immediately on startup, so a 10-minute wait is dead time. + ${{ if eq(parameters.simulateCrashForTesting, true) }}: + timeoutInMinutes: 2 + ${{ if not(eq(parameters.simulateCrashForTesting, true)) }}: + timeoutInMinutes: 10 + + # Always disarm the crash sentinel so it cannot leak to a rerun on + # the same agent. + - ${{ if eq(parameters.simulateCrashForTesting, true) }}: + - pwsh: | + $flagPath = Join-Path $env:ProgramData 'rnw-e2e-simulate-crash.flag' + if (Test-Path $flagPath) { + Remove-Item $flagPath -Force + Write-Host "Removed crash-simulation sentinel at $flagPath" + } + displayName: Disarm crash-simulation sentinel (TEST ONLY) + condition: always() + + # Always disarm the hang-simulation env var so the post-failure + # `Update snapshots` step (which also runs `yarn e2etest`) does not + # re-trigger the hang and burn 10 minutes of dead time. Setting an + # ADO variable to empty string clears it for subsequent steps. + - ${{ if eq(parameters.simulateHangForTesting, true) }}: + - pwsh: | + Write-Host "##vso[task.setvariable variable=RNW_SIMULATE_HANG]" + Write-Host "Hang simulation disarmed (RNW_SIMULATE_HANG cleared)" + displayName: Disarm hang-simulation env var (TEST ONLY) + condition: always() + + # On test failure, snapshot any lingering RNTesterApp-Fabric / node + # processes before subsequent steps (or the agent) tear them down. + # WER LocalDumps only fires on actual crashes; this catches hangs + # (e.g. "Unable to enter correct text" timeouts) where the process + # is alive but unresponsive. + # + # Dumps must go into a subfolder of $(CrashDumpRootPath). Files + # written directly at the root were observed to disappear during + # the long `Update snapshots` step that runs after a failed test; + # files in a subfolder survive. We don't know which agent + # behavior deletes them — Defender, a 1ES cleanup script, or a + # side-effect of `yarn e2etest -u` — but a subfolder evades it. + - pwsh: | + $procDump = Join-Path "$(ProcDumpPath)" 'procdump64.exe' + if (-not (Test-Path $procDump)) { + Write-Host "ProcDump not found at $procDump; skipping live-process dump capture." + exit 0 + } + + $hangDir = Join-Path "$(CrashDumpRootPath)" 'hang' + New-Item -ItemType Directory -Path $hangDir -Force | Out-Null + + $targets = @('RNTesterApp-Fabric', 'node') + foreach ($name in $targets) { + Get-Process -Name $name -ErrorAction SilentlyContinue | ForEach-Object { + $dumpPath = Join-Path $hangDir ("hang_{0}_{1}.dmp" -f $name, $_.Id) + Write-Host "Capturing full dump of $name (pid $($_.Id)) to $dumpPath" + & $procDump -accepteula -ma $_.Id $dumpPath + Write-Host ("ProcDump exit code: {0} (non-zero is normal - encodes the dump count written)" -f $LASTEXITCODE) + } + } + # ProcDump uses non-zero exit codes to encode the number of dumps written. + # Force a clean PowerShell exit so the step doesn't show as a warning. + exit 0 + displayName: Capture dumps of surviving test processes + condition: and(failed(), eq(variables.StartedFabricTests, 'true')) + continueOnError: true + + # Collect any in-process minidumps the app's UEF wrote to + # %ProgramData%\RNW-E2E-Dumps, plus any dumps WER may have written + # to its standard fallback locations, and stage them into + # subfolders of $(CrashDumpRootPath) so they ride the crash-dumps + # artifact. Dumps in subfolders survive the post-failure + # `Update snapshots` step (see comment on the Capture step above). + - pwsh: | + # In-process minidumps (primary mechanism for actual crashes). + $inProc = Join-Path $env:ProgramData 'RNW-E2E-Dumps' + if (Test-Path $inProc) { + $dest = Join-Path "$(CrashDumpRootPath)" 'in-process' + New-Item -ItemType Directory -Path $dest -Force | Out-Null + Copy-Item -Path "$inProc\*" -Destination $dest -Recurse -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $dest -Recurse -Force -ErrorAction SilentlyContinue | + Select-Object FullName, Length | Format-Table -AutoSize | Out-String | Write-Host + } + + # Fallback search: if the agent image ever changes back to a + # working WER LocalDumps configuration, dumps may land here. + $searchRoots = @( + "$env:LOCALAPPDATA\CrashDumps", + "$env:ProgramData\Microsoft\Windows\WER\ReportQueue", + "$env:ProgramData\Microsoft\Windows\WER\ReportArchive", + "$env:ProgramData\Microsoft\Windows\WER\Temp" + ) + $found = @() + foreach ($root in $searchRoots) { + if (-not (Test-Path $root)) { continue } + $found += Get-ChildItem -Path $root -Recurse -Include *.dmp,*.mdmp -ErrorAction SilentlyContinue -Force | + Where-Object { -not $_.PSIsContainer -and $_.LastWriteTime -gt (Get-Date).AddHours(-2) } + } + if ($found.Count -gt 0) { + $dest = Join-Path "$(CrashDumpRootPath)" 'recovered' + New-Item -ItemType Directory -Path $dest -Force | Out-Null + foreach ($h in $found) { + $target = Join-Path $dest ($h.FullName -replace '[:\\/]', '_') + Copy-Item -LiteralPath $h.FullName -Destination $target -Force -ErrorAction SilentlyContinue + Write-Host "Recovered $($h.FullName) ($($h.Length) bytes) -> $target" + } + } + displayName: Collect in-process and fallback crash dumps + condition: and(failed(), eq(variables.StartedFabricTests, 'true')) + continueOnError: true + + # Bundle matching PDBs and a debugging README into the Crash dumps + # artifact so the dump is self-contained for an offline developer. + # Skipped if no .dmp/.mdmp files exist — $(CrashDumpRootPath) also + # holds MSBuild failure logs (MSBUILDDEBUGPATH points here), and + # those don't need symbols or this README. + - pwsh: | + $dumps = Get-ChildItem -Path "$(CrashDumpRootPath)" -Recurse -Include *.dmp,*.mdmp -File -ErrorAction SilentlyContinue + if (-not $dumps -or $dumps.Count -eq 0) { + Write-Host "No .dmp/.mdmp files in $(CrashDumpRootPath); skipping symbols + README bundling." + exit 0 + } + Write-Host "Found $($dumps.Count) dump file(s); bundling matching PDBs and README." + + $symbolsDir = Join-Path "$(CrashDumpRootPath)" 'symbols' + $releaseRoot = "$(Build.SourcesDirectory)\packages\e2e-test-app-fabric\windows\${{ matrix.BuildPlatform }}\Release" + if (Test-Path $releaseRoot) { + $pdbs = Get-ChildItem -Path $releaseRoot -Recurse -Filter *.pdb -File -ErrorAction SilentlyContinue + foreach ($pdb in $pdbs) { + $rel = $pdb.FullName.Substring($releaseRoot.Length).TrimStart('\','/') + $target = Join-Path $symbolsDir $rel + New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null + Copy-Item -LiteralPath $pdb.FullName -Destination $target -Force -ErrorAction SilentlyContinue + } + Write-Host "Staged $($pdbs.Count) PDB(s) under $symbolsDir" + } else { + Write-Host "Release root not found at $releaseRoot; skipping PDB stage." + } + + $readme = @' + # Reading these crash dumps + + This artifact contains crash and/or hang dumps from a failed React + Native Windows E2E test run, plus matching debug symbols. + + ## What is in here + + - `hang/` -- full-memory dumps captured by procdump64 from + RNTesterApp-Fabric / node processes that were still alive when + the test step timed out. + - `in-process/` -- full-memory minidumps written by + RNTesterApp-Fabric's own unhandled-exception filter when the app + actually crashed. + - `recovered/` -- dumps recovered from common WER fallback + locations on the agent. Usually empty. + - `symbols/` -- PDBs that match the binaries deployed to the test + agent. Folder layout mirrors the test app's Release deploy tree. + + ## Opening in WinDbg + + 1. Download and extract this artifact. Note the absolute path of + the extracted `symbols/` folder. + 2. Open a dump: + + windbg -z hang\hang_RNTesterApp-Fabric_.dmp + + 3. Set the symbol path (this artifact's symbols + Microsoft public + symbol server) and reload: + + .sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;\symbols + .reload /f + + 4. Useful first commands: + - `~* k` -- call stack of every thread (most useful for hangs) + - `!analyze -v` -- automatic crash analysis (most useful for crashes) + + ## If you need the binaries too + + The PDBs alone are enough for stack walks and type info. If you + need module bytes (e.g. to disassemble), download the matching + `RNTesterApp-Fabric--` artifact from the same + pipeline run; its layout matches `symbols/` here. + '@ + Set-Content -LiteralPath "$(CrashDumpRootPath)\README.md" -Value $readme -Encoding utf8 + Write-Host "Wrote $(CrashDumpRootPath)\README.md" + displayName: Bundle symbols and README with crash dumps + condition: and(failed(), eq(variables.StartedFabricTests, 'true')) + continueOnError: true - powershell: | if (Test-Path "packages/e2e-test-app-fabric/test/__image_snapshots__/__diff_output__") { diff --git a/.ado/scripts/SetupLocalDumps.cmd b/.ado/scripts/SetupLocalDumps.cmd index b52847d928c..d30962be887 100644 --- a/.ado/scripts/SetupLocalDumps.cmd +++ b/.ado/scripts/SetupLocalDumps.cmd @@ -1,37 +1,57 @@ @echo off -REM SetupLocalDumps.cmd [ExecutableName] [DumpFolder] -REM Ex: .\SetupLocalDumps.cmd RNTesterApp C:\WER\UserDumps +REM SetupLocalDumps.cmd [ExecutableName] [DumpFolder] [DumpType] [DumpCount] +REM Ex: .\SetupLocalDumps.cmd RNTesterApp-Fabric C:\WER\UserDumps +REM Ex: .\SetupLocalDumps.cmd RNTesterApp-Fabric C:\WER\UserDumps 2 5 REM -REM This script sets the registry so that, if an executable of the given name crashes, to -REM prevent any automatic debugger from attaching, and instead save a full crash dump to -REM the given folder. +REM Configures Windows Error Reporting (WER) to save crash dumps for the named +REM executable to the given folder. This is the supported mechanism for +REM packaged/UWP apps where AeDebug-based JIT debuggers (e.g. ProcDump) are +REM not reliably invoked. +REM +REM DumpType: +REM 1 = Custom dump (uses CustomDumpFlags) +REM 2 = Full dump (default) +REM 3 = Mini dump +REM +REM DumpCount: max number of dumps to keep per exe (default 10) -setlocal +setlocal enableextensions -if "%1"=="" ( +if "%~1"=="" ( @echo Must provide an executable name to set up local crash dumps exit /b 1 ) -if "%2"=="" ( +if "%~2"=="" ( @echo Must provide a writable folder to save local crash dumps exit /b 1 ) -set CRASHDUMPS_FOLDER=%2 -@echo Configuring registry to save "%1.exe" crash dumps to "%CRASHDUMPS_FOLDER%"... -reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\%1.exe" /v DumpFolder /t REG_EXPAND_SZ /d %CRASHDUMPS_FOLDER% -reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\%1.exe" /v DumpType /t REG_DWORD /d 2 -reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\%1.exe" /v DumpCount /t REG_DWORD /d 3 -reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\AutoExclusionList" /v %1.exe /t REG_DWORD /d 1 -if not exist %CRASHDUMPS_FOLDER% ( +set EXE_NAME=%~1 +set CRASHDUMPS_FOLDER=%~2 +set DUMP_TYPE=%~3 +set DUMP_COUNT=%~4 +if "%DUMP_TYPE%"=="" set DUMP_TYPE=2 +if "%DUMP_COUNT%"=="" set DUMP_COUNT=10 + +if not exist "%CRASHDUMPS_FOLDER%" ( @echo Creating %CRASHDUMPS_FOLDER% - md %CRASHDUMPS_FOLDER% + md "%CRASHDUMPS_FOLDER%" ) +set REG_KEY=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\%EXE_NAME%.exe +@echo Configuring WER to save "%EXE_NAME%.exe" crash dumps (DumpType=%DUMP_TYPE%, DumpCount=%DUMP_COUNT%) to "%CRASHDUMPS_FOLDER%"... +reg add "%REG_KEY%" /v DumpFolder /t REG_EXPAND_SZ /d "%CRASHDUMPS_FOLDER%" /f +reg add "%REG_KEY%" /v DumpType /t REG_DWORD /d %DUMP_TYPE% /f +reg add "%REG_KEY%" /v DumpCount /t REG_DWORD /d %DUMP_COUNT% /f + +REM Prevent the AeDebug post-mortem debugger from being invoked for this +REM executable so that WER LocalDumps gets first crack and writes to our folder. +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\AutoExclusionList" /v %EXE_NAME%.exe /t REG_DWORD /d 1 /f + @echo Registry configuration: -reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\%1.exe" /s -reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\AutoExclusionList" +reg query "%REG_KEY%" /s +reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\AutoExclusionList" /v %EXE_NAME%.exe endlocal -exit /b %ERRORLEVEL% \ No newline at end of file +exit /b %ERRORLEVEL% diff --git a/.ado/templates/prepare-build-env.yml b/.ado/templates/prepare-build-env.yml index 2e81a4bad40..8e6ea5449c4 100644 --- a/.ado/templates/prepare-build-env.yml +++ b/.ado/templates/prepare-build-env.yml @@ -22,6 +22,13 @@ parameters: # - PullRequest # - Continuous # - Publish + - name: localDumpsExeNames + type: object + default: [] + # List of executable base names (without .exe) to register with WER LocalDumps, + # so that crashes in those processes write a dump to $(CrashDumpRootPath). + # Required for packaged/UWP apps where ProcDump-as-AeDebug is not reliably + # invoked. Example: ['RNTesterApp-Fabric', 'Playground']. steps: # The commit tag in the nuspec requires that we use at least nuget 5.8 (because things break with nuget versions before and Vs 16.8 or later) @@ -44,8 +51,15 @@ steps: displayName: Check and enable Windows Error Reporting - pwsh: | - Write-Host "##vso[task.setvariable variable=CrashDumpRootPath]$(Build.StagingDirectory)\CrashDumps" - New-Item -Path '$(Build.StagingDirectory)\CrashDumps' -ItemType Directory + $path = '$(Build.StagingDirectory)\CrashDumps' + Write-Host "##vso[task.setvariable variable=CrashDumpRootPath]$path" + New-Item -Path $path -ItemType Directory -Force | Out-Null + # Grant full control to NT AUTHORITY\SYSTEM and Users so the WER service + # (running as LocalSystem) and any child process — including packaged + # apps — can write dumps here. Without this, per-exe LocalDumps can + # silently fail on agents that lock down the work directory. + & icacls $path /grant 'SYSTEM:(OI)(CI)F' 'Users:(OI)(CI)F' /T /C | Out-Null + & icacls $path displayName: Set CrashDumpRootPath - pwsh: | @@ -59,4 +73,13 @@ steps: - pwsh: | & $(Build.SourcesDirectory)\.ado\scripts\RunProcDump.ps1 -ProcDumpArgs @("-mm", "-i", "$(CrashDumpRootPath)") -ProcDumpInstallPath "$(ProcDumpPath)" -Verbose displayName: Setup ProcDump as AeDebug - \ No newline at end of file + + # Register WER LocalDumps for any executables the caller cares about. + # This catches crashes in packaged/UWP apps (e.g. RNTesterApp-Fabric) that + # the AeDebug-based ProcDump path does not reliably intercept. Dumps land + # in $(CrashDumpRootPath), which is already wired to the crash-dump artifact + # publisher in upload-build-logs.yml. + - ${{ each exeName in parameters.localDumpsExeNames }}: + - script: | + call "$(Build.SourcesDirectory)\.ado\scripts\SetupLocalDumps.cmd" "${{ exeName }}" "$(CrashDumpRootPath)" + displayName: Register WER LocalDumps for ${{ exeName }}.exe diff --git a/.cspell.json b/.cspell.json index d2973503eb1..9402ff8bb0a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,7 +1,10 @@ // cSpell Settings { "version": "0.2", - "language": "en", + // American English. The broader "en" accepts both British and American + // spellings; "en-US" flags British forms (synthesised, behaviour, etc.) + // so contributors and AI assistants don't drift away from US conventions. + "language": "en-US", "dictionaryDefinitions": [ { "name": "project-words", diff --git a/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts b/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts index d27af11844f..a8d0a55285b 100644 --- a/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts +++ b/packages/e2e-test-app-fabric/test/AccessibilityTest.test.ts @@ -25,12 +25,17 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/ButtonComponentTest.test.ts b/packages/e2e-test-app-fabric/test/ButtonComponentTest.test.ts index f7b85df5c53..854db2a5e9c 100644 --- a/packages/e2e-test-app-fabric/test/ButtonComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/ButtonComponentTest.test.ts @@ -26,6 +26,11 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); if (input === '') { return (await searchBox.getText()) === 'Search...'; @@ -34,8 +39,8 @@ const searchBox = async (input: string) => { } }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts index 727e622c0fd..b52afe565a3 100644 --- a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts @@ -28,12 +28,17 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); @@ -43,12 +48,14 @@ const searchBoxBasic = async (input: string) => { const searchBox = await app.findElementByTestID('search_bar_flat_list'); await app.waitUntil( async () => { + // See comment in searchBox above for the clearValue rationale. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/HangSimulationTest.test.ts b/packages/e2e-test-app-fabric/test/HangSimulationTest.test.ts new file mode 100644 index 00000000000..163f1ae6c7b --- /dev/null +++ b/packages/e2e-test-app-fabric/test/HangSimulationTest.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +// Test-only: validates the hang-dump capture path of the E2E pipeline. +// +// Auto-skips unless RNW_SIMULATE_HANG=1, which the pipeline only sets when +// `simulateHangForTesting=true` is passed to .ado/jobs/e2e-test.yml. When +// active, asks the app to jam its UI thread via the `HangForTesting` +// automation command; the post-failure ProcDump step in the pipeline then +// captures a full memory dump of the still-alive packaged-app process. + +import {app} from '@react-native-windows/automation'; +import {AutomationClient} from '@react-native-windows/automation-channel'; + +declare const automationClient: AutomationClient | undefined; + +const shouldRun = process.env.RNW_SIMULATE_HANG === '1'; + +(shouldRun ? describe : describe.skip)('Hang Simulation (TEST ONLY)', () => { + test('jams the UI thread until the test times out', async () => { + if (!automationClient) { + throw new Error('RPC client is not enabled'); + } + + // Asks the app to Post a Sleep(INFINITE) onto its UI dispatcher. The + // command itself returns quickly; the UI thread is jammed on the next + // queued work item, so any subsequent UIA query will block. + await automationClient.invoke('HangForTesting', {}); + + // Touch the UI to surface the hang to jest. This call would normally + // return quickly; with the UI thread jammed it blocks until jest's + // testTimeout fires. + const anyElement = await app.findElementByTestID('components-tab'); + await anyElement.waitForDisplayed({timeout: 60000}); + }); +}); + +export {}; diff --git a/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts index 4271a2ab1f5..53ac332fb4b 100644 --- a/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts @@ -25,6 +25,11 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); if (input === '') { return (await searchBox.getText()) === 'Search...'; @@ -33,8 +38,8 @@ const searchBox = async (input: string) => { } }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/RNTesterNavigation.ts b/packages/e2e-test-app-fabric/test/RNTesterNavigation.ts index 55675930e59..268cfcc964a 100644 --- a/packages/e2e-test-app-fabric/test/RNTesterNavigation.ts +++ b/packages/e2e-test-app-fabric/test/RNTesterNavigation.ts @@ -37,12 +37,17 @@ async function goToExample(example: string) { await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(searchString); return (await searchBox.getText()) === searchString; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/SwitchComponentTest.test.ts b/packages/e2e-test-app-fabric/test/SwitchComponentTest.test.ts index 3f40bae68db..21bfd242c55 100644 --- a/packages/e2e-test-app-fabric/test/SwitchComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/SwitchComponentTest.test.ts @@ -14,12 +14,17 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts index 1894a4bca81..b065057aab0 100644 --- a/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/TextInputComponentTest.test.ts @@ -25,12 +25,17 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces "onPressInonPressIn" + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/TouchableComponentTest.test.ts b/packages/e2e-test-app-fabric/test/TouchableComponentTest.test.ts index ba1f75fbcc4..e17447c4d0e 100644 --- a/packages/e2e-test-app-fabric/test/TouchableComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/TouchableComponentTest.test.ts @@ -25,6 +25,11 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); if (input === '') { return (await searchBox.getText()) === 'Search...'; @@ -32,8 +37,8 @@ const searchBox = async (input: string) => { return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/test/ViewComponentTest.test.ts b/packages/e2e-test-app-fabric/test/ViewComponentTest.test.ts index cb49901fe16..6308aafab52 100644 --- a/packages/e2e-test-app-fabric/test/ViewComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/ViewComponentTest.test.ts @@ -25,12 +25,17 @@ const searchBox = async (input: string) => { const searchBox = await app.findElementByTestID('example_search'); await app.waitUntil( async () => { + // Clear before each attempt: WinAppDriver's setValue can fall back to + // synthesized keystrokes for custom RN TextInputs, which append rather + // than replace. Without the clear, a retry produces concatenated text + // and the comparison never converges. + await searchBox.clearValue(); await searchBox.setValue(input); return (await searchBox.getText()) === input; }, { - interval: 1500, - timeout: 5000, + interval: 500, + timeout: 10000, timeoutMsg: `Unable to enter correct search text into test searchbox.`, }, ); diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp index c62ac67cee7..3391148885e 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp @@ -5,10 +5,13 @@ #include "RNTesterApp-Fabric.h" #include +#include #include #include #include "winrt/AutomationChannel.h" +#pragma comment(lib, "dbghelp.lib") + // Includes from sample-custom-component #include @@ -41,6 +44,7 @@ winrt::Microsoft::ReactNative::IReactContext global_reactContext{nullptr}; winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload); winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload); winrt::Windows::Data::Json::JsonObject CreateScreenshot(winrt::Windows::Data::Json::JsonValue payload); +winrt::Windows::Data::Json::JsonObject HangForTesting(winrt::Windows::Data::Json::JsonValue payload); winrt::Windows::Foundation::IAsyncAction LoopServer(winrt::AutomationChannel::Server &server); // Create and configure the ReactNativeHost @@ -103,8 +107,133 @@ winrt::Microsoft::ReactNative::ReactNativeHost CreateReactNativeHost( return host; } +// In-process crash dump writer. Installed as the top-level +// `UnhandledExceptionFilter`, so any unhandled structured exception in the +// app (e.g. access violations) writes a full-memory minidump to a well-known +// folder before the OS tears the process down. This is independent of +// Windows Error Reporting — needed because hosted CI agents route WER through +// a corporate-server policy that silently ignores per-exe LocalDumps. +// +// The dump folder is %ProgramData%\RNW-E2E-Dumps. The pipeline scans that path +// after a failing test run and publishes anything found. +static LONG WINAPI WriteDumpOnUnhandledException(EXCEPTION_POINTERS *pExceptionInfo) { + wchar_t dumpDir[MAX_PATH]; + DWORD len = GetEnvironmentVariableW(L"ProgramData", dumpDir, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return EXCEPTION_CONTINUE_SEARCH; + } + if (FAILED(PathCchAppend(dumpDir, MAX_PATH, L"RNW-E2E-Dumps"))) { + return EXCEPTION_CONTINUE_SEARCH; + } + CreateDirectoryW(dumpDir, nullptr); // ignore ERROR_ALREADY_EXISTS + + wchar_t dumpPath[MAX_PATH]; + SYSTEMTIME st; + GetLocalTime(&st); + swprintf_s( + dumpPath, + MAX_PATH, + L"%s\\RNTesterApp-Fabric-%04u%02u%02u-%02u%02u%02u-%u.dmp", + dumpDir, + st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + GetCurrentProcessId()); + + HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) { + return EXCEPTION_CONTINUE_SEARCH; + } + + MINIDUMP_EXCEPTION_INFORMATION exInfo{}; + exInfo.ThreadId = GetCurrentThreadId(); + exInfo.ExceptionPointers = pExceptionInfo; + exInfo.ClientPointers = FALSE; + + const MINIDUMP_TYPE dumpType = static_cast( + MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules | + MiniDumpWithProcessThreadData); + + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + dumpType, + pExceptionInfo ? &exInfo : nullptr, + nullptr, + nullptr); + + FlushFileBuffers(hFile); + CloseHandle(hFile); + + // Let normal processing continue so the process still terminates and any + // downstream handlers (including WER, if it's active) also run. + return EXCEPTION_CONTINUE_SEARCH; +} + +static void InstallInProcessCrashDumpWriter() { + SetUnhandledExceptionFilter(WriteDumpOnUnhandledException); + // Suppress the fault dialog so the process exits promptly after our UEF runs + // — on a hosted agent there is nobody to click a dialog anyway. + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); +} + +// Test-only: if the sentinel file exists, deliberately crash the app on +// startup. Used by the E2E pipeline (see .ado/jobs/e2e-test.yml +// `simulateCrashForTesting` parameter) to re-validate that the in-process +// minidump writer + artifact publish actually produces a usable .dmp. +// File-based trigger because environment variables do not reliably propagate +// through the packaged-app activation flow used by the automation test driver. +static void MaybeSimulateCrashForTesting() { + wchar_t flagPath[MAX_PATH]; + DWORD len = GetEnvironmentVariableW(L"ProgramData", flagPath, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return; + } + if (FAILED(PathCchAppend(flagPath, MAX_PATH, L"rnw-e2e-simulate-crash.flag"))) { + return; + } + if (GetFileAttributesW(flagPath) == INVALID_FILE_ATTRIBUTES) { + return; + } + + // Deliberate null-pointer write to trigger an access violation. Volatile so + // the optimizer keeps it. + *reinterpret_cast(nullptr) = 0xC0FFEE; +} + +// Test-only: when invoked over the automation channel, jam the UI thread +// forever. Used by the E2E pipeline (see .ado/jobs/e2e-test.yml +// `simulateHangForTesting` parameter) to validate that the post-failure +// ProcDump capture step actually produces a usable dump of a hung app. +// +// We Post the sleep onto the UI dispatcher rather than blocking inline so the +// channel handler returns a normal response to the test client — that's the +// realistic scenario (the app appears to acknowledge a request, then locks up +// on the next UI-thread work item, exactly like a deadlock in production). +winrt::Windows::Data::Json::JsonObject HangForTesting(winrt::Windows::Data::Json::JsonValue /*payload*/) { + if (global_reactContext) { + global_reactContext.UIDispatcher().Post([]() { ::Sleep(INFINITE); }); + } else { + // Fallback: hang the channel-loop thread itself. Less realistic but still + // produces a hung process that the post-failure ProcDump path can capture. + ::Sleep(INFINITE); + } + return {}; +} + _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* instance */, HINSTANCE, PSTR /* commandLine */, int /* showCmd */) { + // Install our in-process crash handler before anything else can crash. This + // is the primary mechanism for capturing dumps on hosted CI agents, where + // Windows Error Reporting is policy-routed away from local disk. + InstallInProcessCrashDumpWriter(); + + MaybeSimulateCrashForTesting(); + // Initialize WinRT. winrt::init_apartment(winrt::apartment_type::single_threaded); @@ -156,6 +285,7 @@ WinMain(HINSTANCE /* instance */, HINSTANCE, PSTR /* commandLine */, int /* show handler.BindOperation(L"CreateScreenshot", CreateScreenshot); handler.BindOperation(L"DumpVisualTree", DumpVisualTree); handler.BindOperation(L"ListErrors", ListErrors); + handler.BindOperation(L"HangForTesting", HangForTesting); global_rootView = reactNativeWindow.ReactNativeIsland(); auto server = winrt::AutomationChannel::Server(handler); @@ -765,8 +895,7 @@ winrt::Windows::Data::Json::JsonObject DumpNativeComponentTreeHelper( return visualTree; } -winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload) { - winrt::Windows::Data::Json::JsonObject payloadObj = payload.GetObject(); +static winrt::Windows::Data::Json::JsonObject DumpVisualTreeOnce(winrt::Windows::Data::Json::JsonObject payloadObj) { winrt::Windows::Data::Json::JsonObject result; result.Insert(L"Automation Tree", DumpUIATreeHelper(payloadObj)); result.Insert(L"Visual Tree", DumpVisualTreeHelper(payloadObj)); @@ -774,6 +903,34 @@ winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json return result; } +// Dump the visual / automation / component trees up to 3 times and return +// the first dump that matches the next one. If no two consecutive dumps +// agree, return the final attempt as a best-effort. +// +// Why: composition `Visual::Size` is read after Composition's commit has +// already rounded a sub-pixel text-layout result to an integer. Adjacent +// commits can produce different roundings (24 vs 25 for a ~24.5 measurement), +// so a single dump captures whichever frame happens to be live. Two +// consecutive identical dumps indicate the system has reached a stable +// post-layout state for this query. +winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload) { + winrt::Windows::Data::Json::JsonObject payloadObj = payload.GetObject(); + + constexpr int kMaxAttempts = 3; + constexpr int kSettleDelayMs = 50; + + winrt::Windows::Data::Json::JsonObject prev = DumpVisualTreeOnce(payloadObj); + for (int i = 1; i < kMaxAttempts; ++i) { + ::Sleep(kSettleDelayMs); + auto curr = DumpVisualTreeOnce(payloadObj); + if (prev.Stringify() == curr.Stringify()) { + return curr; + } + prev = curr; + } + return prev; +} + winrt::Windows::Data::Json::JsonObject CreateScreenshot(winrt::Windows::Data::Json::JsonValue payload) { RECT rect; auto payloadObj = payload.GetObjectW(); From 0edd81ce4658158f851c9dd416ffe3a01e539b26 Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Tue, 28 Apr 2026 15:14:12 -0700 Subject: [PATCH 2/3] Run e2e test on x86 --- .ado/jobs/e2e-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ado/jobs/e2e-test.yml b/.ado/jobs/e2e-test.yml index 5c1d77a9b00..204e4c82666 100644 --- a/.ado/jobs/e2e-test.yml +++ b/.ado/jobs/e2e-test.yml @@ -26,6 +26,8 @@ parameters: Matrix: - Name: X64Hermes BuildPlatform: x64 + - Name: X86Hermes + BuildPlatform: x86 - BuildEnvironment: Continuous Matrix: - Name: X64Hermes From 8c817c33c98bbbbc9d49ae7c7fe6f887bd8e22ce Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Tue, 28 Apr 2026 17:11:29 -0700 Subject: [PATCH 3/3] Retry 2 times in case of yarn or lage flakiness --- .ado/build-template.yml | 2 ++ .ado/prepare-release-bot.yml | 2 ++ .ado/templates/react-native-init-windows.yml | 2 ++ .ado/templates/strict-yarn-install.yml | 1 + .ado/templates/yarn-install.yml | 1 + 5 files changed, 8 insertions(+) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index dcdc2a84595..f0b87e985c9 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -176,10 +176,12 @@ extends: - script: yarn install --immutable displayName: Strict yarn install @rnw-scripts/beachball-config condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False')) + retryCountOnTaskFailure: 2 - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config displayName: Build prepare-release and beachball-config condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False')) + retryCountOnTaskFailure: 2 # 5. Beachball check (Developer PR only) - pwsh: npx beachball check --branch "origin/$env:BEACHBALL_BRANCH" --verbose --changehint "##vso[task.logissue type=error]Run 'yarn change' from root of repo to generate a change file." diff --git a/.ado/prepare-release-bot.yml b/.ado/prepare-release-bot.yml index 7c7688ee454..058b5b4e244 100644 --- a/.ado/prepare-release-bot.yml +++ b/.ado/prepare-release-bot.yml @@ -62,9 +62,11 @@ jobs: - script: yarn install --immutable displayName: yarn install + retryCountOnTaskFailure: 2 - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config displayName: Build prepare-release and dependencies + retryCountOnTaskFailure: 2 - ${{ if ne(parameters.targetBranch, '(source branch)') }}: - pwsh: Write-Host "##vso[task.setvariable variable=TargetBranch]${{ parameters.targetBranch }}" diff --git a/.ado/templates/react-native-init-windows.yml b/.ado/templates/react-native-init-windows.yml index 14419878b0f..df8113acc30 100644 --- a/.ado/templates/react-native-init-windows.yml +++ b/.ado/templates/react-native-init-windows.yml @@ -49,6 +49,7 @@ steps: $(Build.SourcesDirectory)\vnext\Scripts\creaternwapp.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new app project with creaternwapp.cmd workingDirectory: $(Agent.BuildDirectory) + retryCountOnTaskFailure: 2 env: YARN_ENABLE_IMMUTABLE_INSTALLS: false @@ -57,6 +58,7 @@ steps: $(Build.SourcesDirectory)\vnext\Scripts\creaternwlib.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new lib project with creaternwlib.cmd workingDirectory: $(Agent.BuildDirectory) + retryCountOnTaskFailure: 2 env: YARN_ENABLE_IMMUTABLE_INSTALLS: false diff --git a/.ado/templates/strict-yarn-install.yml b/.ado/templates/strict-yarn-install.yml index 5341fd406a9..ea12c4868fc 100644 --- a/.ado/templates/strict-yarn-install.yml +++ b/.ado/templates/strict-yarn-install.yml @@ -13,3 +13,4 @@ steps: - script: yarn install --immutable displayName: Strict yarn install ${{ parameters.workspace }} + retryCountOnTaskFailure: 2 diff --git a/.ado/templates/yarn-install.yml b/.ado/templates/yarn-install.yml index d9adc005032..ad9ce780eff 100644 --- a/.ado/templates/yarn-install.yml +++ b/.ado/templates/yarn-install.yml @@ -6,3 +6,4 @@ parameters: steps: - script: yarn --cwd ${{ parameters.workingDirectory }} install --immutable displayName: yarn install (immutable) + retryCountOnTaskFailure: 2