ci(install-e2e): one screen-recording mechanism on every runner, Xvfb for headless linux

The composite action .github/actions/e2e-screen-record owns setup and
lifecycle on all three OSes: ffmpeg via apt/brew-verify/winget+cache,
capture via x11grab/gdigrab/avfoundation, mkv at 15fps stopped by 'q'
on live stdin with kill fallback. Linux runners have no display, so
start brings up a dedicated Xvfb :99 and exports DISPLAY - one display
serves both the recorder and any app a later step launches.

Recording moves out of the GUI driver into workflow infrastructure -
that is what makes it uniform - and a missing ffmpeg or a zero-frame
file now FAILS the leg instead of skipping silently: the graceful-skip
path is how the windows leg shipped no recording.mkv while green.

Lifecycle proven locally: start against lavfi testsrc, q-stop, ffprobe
duration check (record-start.sh/record-stop.sh under nix ffmpeg).
This commit is contained in:
ethernet
2026-08-12 04:15:24 -04:00
parent 239523414e
commit cdaf0cf091
9 changed files with 381 additions and 64 deletions

View File

@@ -0,0 +1,102 @@
name: E2E screen recording
description: >
One screen-recording mechanism for every install-e2e runner. mode=start
installs what the OS needs (ffmpeg everywhere; Xvfb on headless linux),
starts the recorder, and exports DISPLAY for later steps. mode=stop
finalizes the recording and fails on a zero-frame file. A missing ffmpeg
is a hard error - a graceful skip makes the missing tool invisible and
the artifact silently loses its recording.
inputs:
mode:
description: start | stop
required: true
output:
description: Path of the recording (mkv).
required: true
runs:
using: composite
steps:
# ---- setup + start ------------------------------------------------------
- name: Install ffmpeg + Xvfb (linux)
if: inputs.mode == 'start' && runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v Xvfb >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends ffmpeg xvfb
fi
- name: Start Xvfb (linux, headless)
if: inputs.mode == 'start' && runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
# A dedicated display rather than xvfb-run-wrapping each command, so
# ONE display serves both the app under test and the recorder.
if [ -z "${DISPLAY:-}" ]; then
Xvfb :99 -screen 0 1920x1080x24 &
echo "$!" > "$RUNNER_TEMP/xvfb.pid"
echo "DISPLAY=:99" >> "$GITHUB_ENV"
export DISPLAY=:99
fi
# Wait until the display accepts connections; xdpyinfo may not be
# installed, so probe with the X socket.
for _ in $(seq 1 50); do
[ -S "/tmp/.X11-unix/X99" ] && break
sleep 0.2
done
[ -S "/tmp/.X11-unix/X99" ] || { echo "Xvfb :99 did not come up" >&2; exit 1; }
- name: Verify ffmpeg (macos)
if: inputs.mode == 'start' && runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
# Do not trust "preinstalled" claims - verify, install on miss.
command -v ffmpeg >/dev/null 2>&1 || brew install --quiet ffmpeg
- name: Restore cached ffmpeg (windows)
if: inputs.mode == 'start' && runner.os == 'Windows'
id: ffmpeg-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ runner.temp }}\test-bins\ffmpeg
key: e2e-ffmpeg-${{ runner.os }}-v1
- name: Install ffmpeg (windows)
if: inputs.mode == 'start' && runner.os == 'Windows' && steps.ffmpeg-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
$bins = "$env:RUNNER_TEMP\test-bins\ffmpeg"
New-Item -ItemType Directory -Path $bins -Force | Out-Null
winget install -e --id Gyan.FFmpeg --silent --accept-source-agreements --accept-package-agreements --disable-interactivity --location "$env:RUNNER_TEMP\ffmpeg_dir"
Copy-Item -Path "$env:RUNNER_TEMP\ffmpeg_dir\*\*" -Destination $bins -Recurse -Force
- name: Add ffmpeg to PATH (windows)
if: inputs.mode == 'start' && runner.os == 'Windows'
shell: pwsh
run: Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\test-bins\ffmpeg\bin"
- name: Start recording (posix)
if: inputs.mode == 'start' && runner.os != 'Windows'
shell: bash
run: bash "$GITHUB_ACTION_PATH/../../../tests/install/e2e-assets/record-start.sh" '${{ inputs.output }}'
- name: Start recording (windows)
if: inputs.mode == 'start' && runner.os == 'Windows'
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File "$env:GITHUB_ACTION_PATH\..\..\..\tests\install\e2e-assets\record-start.ps1" -OutFile "${{ inputs.output }}"
# ---- stop ---------------------------------------------------------------
- name: Stop recording (posix)
if: inputs.mode == 'stop' && runner.os != 'Windows'
shell: bash
run: bash "$GITHUB_ACTION_PATH/../../../tests/install/e2e-assets/record-stop.sh" '${{ inputs.output }}'
- name: Stop recording (windows)
if: inputs.mode == 'stop' && runner.os == 'Windows'
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File "$env:GITHUB_ACTION_PATH\..\..\..\tests\install\e2e-assets\record-stop.ps1" -OutFile "${{ inputs.output }}"

View File

@@ -91,6 +91,14 @@ jobs:
with:
fetch-depth: 0
# One recording mechanism on every OS (Xvfb gives headless linux a
# display; the same display serves any app the driver launches).
- name: Start screen recording
uses: ./.github/actions/e2e-screen-record
with:
mode: start
output: ${{ runner.temp }}/e2e-logs/recording.mkv
- name: Run install + update E2E
run: |
set -euo pipefail
@@ -103,6 +111,13 @@ jobs:
# would trip the driver's own dirty-tree guard.
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
- name: Stop screen recording
if: always()
uses: ./.github/actions/e2e-screen-record
with:
mode: stop
output: ${{ runner.temp }}/e2e-logs/recording.mkv
# Artifact names cannot contain '/', and install-ref may be a full ref
# like refs/heads/main. GitHub Actions expressions have no string-replace
# function, so build the safe name here. Runs even on failure -- that is

View File

@@ -119,31 +119,15 @@ jobs:
with:
fetch-depth: 0
# ffmpeg records the screen for the whole run so a failed click is
# diagnosable from the artifact instead of by guesswork. NOT
# preinstalled on windows-latest; the driver skips recording
# gracefully when it's absent, so this step is what makes the
# recording actually happen. Cached — winget's ffmpeg download is
# the slow part.
- name: Restore cached ffmpeg
id: ffmpeg-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
# One recording mechanism on every OS: the composite action installs
# ffmpeg (cached - winget's download is the slow part), starts the
# capture, and record-stop fails on a zero-frame file so a silently
# missing recording cannot go green.
- name: Start screen recording
uses: ./.github/actions/e2e-screen-record
with:
path: ${{ runner.temp }}\test-bins\ffmpeg
key: e2e-ffmpeg-${{ runner.os }}-v1
- name: Install ffmpeg
if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
$bins = "$env:RUNNER_TEMP\test-bins\ffmpeg"
New-Item -ItemType Directory -Path $bins -Force | Out-Null
winget install -e --id Gyan.FFmpeg --silent --accept-source-agreements --accept-package-agreements --disable-interactivity --location "$env:RUNNER_TEMP\ffmpeg_dir"
Copy-Item -Path "$env:RUNNER_TEMP\ffmpeg_dir\*\*" -Destination $bins -Recurse -Force
- name: Add ffmpeg to PATH
shell: pwsh
run: Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\test-bins\ffmpeg\bin"
mode: start
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
- name: Stage serve repo (main -> ${{ inputs.install-ref }})
shell: powershell
@@ -157,6 +141,13 @@ jobs:
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-desktop-gui-e2e.ps1 -Phase update-gui -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
- name: Stop screen recording
if: always()
uses: ./.github/actions/e2e-screen-record
with:
mode: stop
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
- name: Collect proof + logs
if: always()
shell: powershell
@@ -208,12 +199,25 @@ jobs:
with:
fetch-depth: 0
- name: Start screen recording
uses: ./.github/actions/e2e-screen-record
with:
mode: start
output: ${{ runner.temp }}\e2e-logs\recording.mkv
- name: Run install + update E2E
shell: powershell
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-installer-script-e2e.ps1 -InstallMethod "${{ inputs.install-method }}" -UpdateMethod "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}"
env:
HERMES_E2E_LOG_DIR: ${{ runner.temp }}\e2e-logs
- name: Stop screen recording
if: always()
uses: ./.github/actions/e2e-screen-record
with:
mode: stop
output: ${{ runner.temp }}\e2e-logs\recording.mkv
- name: Upload installer logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

View File

@@ -79,4 +79,4 @@ gh workflow run install-e2e.yml --ref <branch> -f route=both -f tag-count=2
## Artifacts
Each leg uploads its logs as an artifact. The windows GUI leg also uploads screenshots, a screen recording, and the update result file. Get them with `gh run download <run-id>`.
Each leg uploads its logs as an artifact. Every leg also records the screen for its whole run: the composite action `.github/actions/e2e-screen-record` installs ffmpeg, records with the OS's capture backend (x11grab on linux, gdigrab on windows, avfoundation on macos), and fails the leg if the recording is missing or has zero frames. Linux runners have no display, so the action starts `Xvfb :99` first and exports `DISPLAY` for every later step — the app under test and the recorder share that display. The windows GUI leg also uploads screenshots and the update result file. Get them with `gh run download <run-id>`.

View File

@@ -0,0 +1,62 @@
# Start a continuous ffmpeg screen recording in the background (windows).
#
# Usage: powershell -File record-start.ps1 -OutFile recording.mkv
#
# The graceful stop is the character 'q' on ffmpeg's LIVE stdin, which only
# System.Diagnostics.Process exposes (Start-Process -RedirectStandardInput
# hands ffmpeg a file handle already at EOF). This script therefore spawns a
# detached HOLDER powershell that owns the ffmpeg process and its stdin pipe,
# and stops it when a STOP marker file appears; record-stop.ps1 writes the
# marker. mkv on purpose: it stays playable even unfinalized. A missing
# ffmpeg is a HARD error - a graceful skip makes the missing tool invisible
# and the artifact silently loses its recording.
#Requires -Version 5.1
param(
[Parameter(Mandatory = $true)][string]$OutFile
)
$ErrorActionPreference = "Stop"
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
Write-Host "record-start: ffmpeg not on PATH (the workflow must install it)"
exit 1
}
$outDir = Split-Path -Parent $OutFile
if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
$stopMarker = "$OutFile.stop"
$stateFile = "$OutFile.state"
Remove-Item -LiteralPath $stopMarker, $stateFile -Force -ErrorAction SilentlyContinue
$holder = "$OutFile.holder.ps1"
@'
param([string]$OutFile, [string]$StopMarker)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "ffmpeg"
$psi.Arguments = "-y -f gdigrab -framerate 15 -i desktop " +
"-hide_banner -loglevel error " +
"-c:v libx264 -preset ultrafast -pix_fmt yuv420p `"$OutFile`""
$psi.RedirectStandardInput = $true
$psi.UseShellExecute = $false
$proc = [System.Diagnostics.Process]::Start($psi)
Set-Content -LiteralPath "$OutFile.ffpid" -Value $proc.Id
while (-not $proc.HasExited) {
if (Test-Path -LiteralPath $StopMarker) {
try {
$proc.StandardInput.Write("q")
$proc.StandardInput.Close()
} catch {}
if (-not $proc.WaitForExit(15000)) { try { $proc.Kill() } catch {} }
break
}
Start-Sleep -Milliseconds 500
}
'@ | Set-Content -LiteralPath $holder -Encoding UTF8
$holderProc = Start-Process -FilePath "powershell.exe" `
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $holder, "-OutFile", $OutFile, "-StopMarker", $stopMarker `
-WindowStyle Hidden -PassThru
Set-Content -LiteralPath $stateFile -Value "$($holderProc.Id) $stopMarker"
Write-Host "record-start: recording to $OutFile (holder pid $($holderProc.Id))"

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Start a continuous ffmpeg screen recording in the background.
#
# Usage: record-start.sh OUTPUT.mkv [INPUT_ARGS...]
# OUTPUT.mkv where to record (mkv: stays playable even unfinalized)
# INPUT_ARGS optional ffmpeg input override; default picks per-OS:
# linux -f x11grab -i "$DISPLAY" (Xvfb or real)
# macos -f avfoundation -i <first screen index>
#
# Writes state (pid + control fifo path) next to OUTPUT as OUTPUT.state so
# record-stop.sh can finalize gracefully: ffmpeg stops cleanly on the
# character 'q' on live stdin, so stdin is a fifo we hold open. A missing
# ffmpeg is a HARD error - a graceful skip makes the missing tool invisible
# and the artifact silently loses its recording.
set -euo pipefail
OUT="${1:?usage: record-start.sh OUTPUT.mkv [input args...]}"
shift || true
command -v ffmpeg >/dev/null 2>&1 || {
echo "record-start: ffmpeg not on PATH (the workflow must install it)" >&2
exit 1
}
INPUT=("$@")
if [ "${#INPUT[@]}" -eq 0 ]; then
case "$(uname -s)" in
Linux)
: "${DISPLAY:?record-start: DISPLAY not set (start Xvfb first on headless runners)}"
INPUT=(-f x11grab -framerate 15 -i "$DISPLAY")
;;
Darwin)
# avfoundation lists devices on stderr; the first "Capture screen"
# index is the whole display. Parse it rather than hardcoding: the
# index shifts with attached cameras.
screen_idx="$(ffmpeg -f avfoundation -list_devices true -i "" 2>&1 \
| sed -n 's/^\[AVFoundation[^]]*\] \[\([0-9]*\)\] Capture screen.*/\1/p' | head -1)"
[ -n "$screen_idx" ] || { echo "record-start: no capture screen device found" >&2; exit 1; }
INPUT=(-f avfoundation -framerate 15 -capture_cursor 1 -i "${screen_idx}:none")
;;
*)
echo "record-start: unsupported OS $(uname -s) (windows uses record-start.ps1)" >&2
exit 1
;;
esac
fi
mkdir -p "$(dirname "$OUT")"
FIFO="$OUT.ctl"
STATE="$OUT.state"
rm -f "$FIFO" "$STATE"
mkfifo "$FIFO"
# Hold the fifo's write end open in a shepherd process; ffmpeg reads its
# stdin from the fifo. record-stop.sh writes 'q' into the fifo.
ffmpeg -hide_banner -loglevel error "${INPUT[@]}" \
-pix_fmt yuv420p -c:v libx264 -preset ultrafast "$OUT" < "$FIFO" &
FFMPEG_PID=$!
# Open a persistent write fd so the fifo doesn't EOF before stop.
exec 9> "$FIFO"
# Hand the fd to a shepherd that outlives this script.
(
exec 9>&9
while kill -0 "$FFMPEG_PID" 2>/dev/null; do sleep 1; done
) &
SHEPHERD_PID=$!
disown "$SHEPHERD_PID" 2>/dev/null || true
printf '%s %s %s\n' "$FFMPEG_PID" "$FIFO" "$SHEPHERD_PID" > "$STATE"
echo "record-start: recording to $OUT (ffmpeg pid $FFMPEG_PID)"

View File

@@ -0,0 +1,53 @@
# Stop a recording started by record-start.ps1 and verify the file is real.
#
# Usage: powershell -File record-stop.ps1 -OutFile recording.mkv
#
# Drops the STOP marker the holder watches for (it writes 'q' to ffmpeg's
# live stdin), waits for the holder to exit, then asserts the output exists
# and has a decodable duration - a zero-frame recording is the classic
# silent failure.
#Requires -Version 5.1
param(
[Parameter(Mandatory = $true)][string]$OutFile
)
$ErrorActionPreference = "Stop"
$stateFile = "$OutFile.state"
if (-not (Test-Path -LiteralPath $stateFile)) {
Write-Host "record-stop: no state at $stateFile (was record-start run?)"
exit 1
}
$state = (Get-Content -LiteralPath $stateFile -Raw).Trim() -split " ", 2
$holderPid = [int]$state[0]
$stopMarker = $state[1]
Set-Content -LiteralPath $stopMarker -Value "stop"
try {
$holder = Get-Process -Id $holderPid -ErrorAction SilentlyContinue
if ($holder) { $holder.WaitForExit(20000) | Out-Null }
} catch {}
# Belt and braces: if ffmpeg outlived the holder, kill it directly.
$ffpidFile = "$OutFile.ffpid"
if (Test-Path -LiteralPath $ffpidFile) {
$ffpid = [int](Get-Content -LiteralPath $ffpidFile -Raw).Trim()
try { Stop-Process -Id $ffpid -Force -ErrorAction SilentlyContinue } catch {}
}
Remove-Item -LiteralPath $stateFile, $stopMarker, $ffpidFile, "$OutFile.holder.ps1" -Force -ErrorAction SilentlyContinue
if (-not (Test-Path -LiteralPath $OutFile) -or (Get-Item -LiteralPath $OutFile).Length -eq 0) {
Write-Host "record-stop: $OutFile missing or empty"
exit 1
}
if (Get-Command ffprobe -ErrorAction SilentlyContinue) {
$prevEap = $ErrorActionPreference; $ErrorActionPreference = "Continue"
$dur = (& ffprobe -v error -show_entries format=duration -of csv=p=0 $OutFile 2>&1 | Out-String).Trim()
$ErrorActionPreference = $prevEap
if (-not $dur -or $dur -eq "0" -or $dur -like "0.0*") {
Write-Host "record-stop: $OutFile has no duration (zero-frame recording)"
exit 1
}
Write-Host "record-stop: $OutFile finalized (${dur}s)"
} else {
Write-Host "record-stop: $OutFile finalized (ffprobe absent; size $((Get-Item -LiteralPath $OutFile).Length) bytes)"
}

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Stop a recording started by record-start.sh and verify the file is real.
#
# Usage: record-stop.sh OUTPUT.mkv
#
# Graceful stop: the character 'q' on ffmpeg's live stdin (the control
# fifo). Falls back to SIGINT (also a clean finalize for ffmpeg), then
# SIGKILL. Fails if the output is missing or has no decodable duration -
# a zero-frame recording is the classic silent failure.
set -euo pipefail
OUT="${1:?usage: record-stop.sh OUTPUT.mkv}"
STATE="$OUT.state"
[ -f "$STATE" ] || { echo "record-stop: no state at $STATE (was record-start run?)" >&2; exit 1; }
read -r FFMPEG_PID FIFO _SHEPHERD < "$STATE"
if kill -0 "$FFMPEG_PID" 2>/dev/null; then
# Write the quit key; don't hang if the reader is already gone.
{ printf 'q' > "$FIFO"; } 2>/dev/null &
WRITER=$!
for _ in $(seq 1 50); do
kill -0 "$FFMPEG_PID" 2>/dev/null || break
sleep 0.2
done
kill "$WRITER" 2>/dev/null || true
if kill -0 "$FFMPEG_PID" 2>/dev/null; then
echo "record-stop: q did not stop ffmpeg; SIGINT" >&2
kill -INT "$FFMPEG_PID" 2>/dev/null || true
for _ in $(seq 1 25); do
kill -0 "$FFMPEG_PID" 2>/dev/null || break
sleep 0.2
done
kill -9 "$FFMPEG_PID" 2>/dev/null || true
fi
fi
rm -f "$FIFO" "$STATE"
[ -s "$OUT" ] || { echo "record-stop: $OUT missing or empty" >&2; exit 1; }
if command -v ffprobe >/dev/null 2>&1; then
dur="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUT" || echo 0)"
case "$dur" in
''|0|0.*) echo "record-stop: $OUT has no duration (zero-frame recording)" >&2; exit 1 ;;
esac
echo "record-stop: $OUT finalized (${dur}s)"
else
echo "record-stop: $OUT finalized (ffprobe absent; size $(wc -c < "$OUT") bytes)"
fi

View File

@@ -225,41 +225,6 @@ function Save-DesktopScreenshot([string]$OutFile) {
}
}
function Start-ScreenRecording([string]$OutFile) {
# Continuous ffmpeg screen capture (gdigrab, 15fps). ffmpeg ships on the
# windows-latest runner image; skip gracefully elsewhere. mkv on purpose:
# it stays playable even if the process dies without finalizing.
#
# ffmpeg must be started, fed, and stopped from THIS process: the
# graceful stop is the character 'q' on its LIVE stdin pipe, which only
# System.Diagnostics.Process exposes (Start-Process
# -RedirectStandardInput hands it a file handle already at EOF).
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
Write-Host " (ffmpeg not on PATH; skipping screen recording)"
return $null
}
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "ffmpeg"
$psi.Arguments = "-y -f gdigrab -framerate 15 -i desktop " +
"-hide_banner -loglevel error " +
"-c:v libx264 -preset ultrafast -pix_fmt yuv420p `"$OutFile`""
$psi.RedirectStandardInput = $true
$psi.UseShellExecute = $false
$proc = [System.Diagnostics.Process]::Start($psi)
Write-Host " screen recording started (pid $($proc.Id)) -> $OutFile"
return $proc
}
function Stop-ScreenRecording($proc) {
if ($proc -and -not $proc.HasExited) {
try {
$proc.StandardInput.Write("q")
$proc.StandardInput.Close()
} catch {}
if (-not $proc.WaitForExit(15000)) { try { $proc.Kill() } catch {} }
}
}
function Start-DesktopRecorder([string]$OutDir) {
# Rolling desktop capture: one PNG every 3s from a detached PowerShell,
# capped at 800 frames (~40 min). Proof that survives any step failure.
@@ -419,7 +384,6 @@ function Invoke-PhaseInstallGui {
New-Item -ItemType Directory -Path $HermesHome -Force | Out-Null
$recorder = Start-DesktopRecorder (Join-Path $proof "desktop-frames")
$recording = Start-ScreenRecording (Join-Path $proof "recording.mkv")
$ahkLog = Join-Path $proof "ahk.log"
try {
Save-DesktopScreenshot (Join-Path $proof "00-before-installer.png")
@@ -458,7 +422,6 @@ function Invoke-PhaseInstallGui {
Assert-True $installer.HasExited "Hermes-Setup.exe exited after Launch"
}
finally {
Stop-ScreenRecording $recording
Stop-DesktopRecorder $recorder (Join-Path $proof "desktop-frames")
# Surface the installer's own log win or lose, full and folded.
$bootLog = Join-Path $HermesHome "logs\bootstrap-installer.log"
@@ -549,7 +512,6 @@ function Invoke-GuiUpdateDesktopRoute([string]$TargetSha) {
Assert-True ($npmExit -eq 0) "npm install @playwright/test@$PlaywrightVersion into the driver dir"
$recorder = Start-DesktopRecorder (Join-Path $proof "desktop-frames")
$recording = Start-ScreenRecording (Join-Path $proof "recording.mkv")
try {
# Launch the installed app and click through Settings -> About ->
# Update now. Exit 0 = the app quit for the updater hand-off.
@@ -658,7 +620,6 @@ function Invoke-GuiUpdateDesktopRoute([string]$TargetSha) {
Save-DesktopScreenshot (Join-Path $proof "99-relaunched-desktop.png")
}
finally {
Stop-ScreenRecording $recording
Stop-DesktopRecorder $recorder (Join-Path $proof "desktop-frames")
$handoffLog = Join-Path $HermesHome "logs\desktop-update-handoff.log"
if (Test-Path -LiteralPath $handoffLog) {