From 15ebb8118422b529f065dad5b2f026ba76c5b889 Mon Sep 17 00:00:00 2001 From: yoniebans Date: Mon, 31 Aug 2026 17:48:59 +0200 Subject: [PATCH] fix(install-e2e): drive the dmg bootstrap GUI so the macos desktop-installer legs run Hermes-Setup is a Tauri app that boots to a setup-choice screen and waits for a click on Install Hermes before any install work starts; run bare it blocked until the 120-minute job cap (both dmg legs, every run). Launch it in the background with the driver's env, read the window geometry via System Events (position and size need no assistive grant), post a real CGEvent click with cliclick at the button's measured position (65% of window height; System Events' own click needs assistive access the runners deny), then wait for the full install to land: checkout, venv console script, AND the built Hermes.app, since the cleanup trap would otherwise kill the installer before its desktop-build stage. Bounded at 45 minutes with desktop screenshots on every phase and failure. Adds a macos-desktop dispatch route so this arm iterates without the full matrix. Verified end to end: run 33407401698, both dmg legs green (first ever), macos slice 10/10. --- .github/workflows/install-e2e.yml | 6 +- tests/install/e2e-assets/drive-dmg-install.sh | 147 ++++++++++++++++++ tests/install/macos-desktop-e2e.sh | 31 ++-- 3 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 tests/install/e2e-assets/drive-dmg-install.sh diff --git a/.github/workflows/install-e2e.yml b/.github/workflows/install-e2e.yml index 6320a848dc..8ca7875642 100644 --- a/.github/workflows/install-e2e.yml +++ b/.github/workflows/install-e2e.yml @@ -47,11 +47,11 @@ on: workflow_dispatch: inputs: route: - description: 'Which combinations to run. all = every OS; both/update/installer = the linux legs; windows-desktop = the windows legs.' + description: 'Which combinations to run. all = every OS; both/update/installer = the linux legs; windows-desktop = the windows legs; macos-desktop = the macos legs.' required: false type: choice default: all - options: [all, both, update, installer, windows-desktop] + options: [all, both, update, installer, windows-desktop, macos-desktop] tag-count: description: 'How many release tags to sample (newest, oldest, and a spread between).' required: false @@ -179,7 +179,7 @@ jobs: macos: name: ${{ matrix.name }} - if: github.event_name != 'workflow_dispatch' || inputs.route == 'all' + if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "macos-desktop"]'), inputs.route) needs: generate-matrix strategy: fail-fast: false diff --git a/tests/install/e2e-assets/drive-dmg-install.sh b/tests/install/e2e-assets/drive-dmg-install.sh new file mode 100644 index 0000000000..60ddd1e35f --- /dev/null +++ b/tests/install/e2e-assets/drive-dmg-install.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Drive the Hermes-Setup dmg bootstrap through its first-run GUI. +# +# The Setup app is Tauri (Rust + system webview), so Playwright/Electron +# attach never works. Launch the binary bare in the background (it inherits +# the redirect env), click "Install Hermes ->" with native input, then watch +# the install land on disk: checkout + venv console script. +# +# Usage: +# drive-dmg-install.sh --app-bin --install-dir \ +# [--install-timeout-secs 2700] [--proof-dir ] +# +# Exit 0: install landed. Non-zero: click never registered or the install +# never landed; desktop screenshots in --proof-dir say which. +set -euo pipefail + +APP_BIN="" +INSTALL_DIR="" +INSTALL_TIMEOUT_SECS=2700 +PROOF_DIR="." +while [ "$#" -gt 0 ]; do + case "$1" in + --app-bin) APP_BIN="$2"; shift 2 ;; + --install-dir) INSTALL_DIR="$2"; shift 2 ;; + --install-timeout-secs) INSTALL_TIMEOUT_SECS="$2"; shift 2 ;; + --proof-dir) PROOF_DIR="$2"; shift 2 ;; + *) echo "error: unknown argument: $1" >&2; exit 1 ;; + esac +done +[ -n "$APP_BIN" ] && [ -n "$INSTALL_DIR" ] || { echo 'error: --app-bin and --install-dir are required' >&2; exit 1; } +mkdir -p "$PROOF_DIR" + +log() { echo "[drive-dmg-install] $*"; } +shot() { + # screencapture works regardless of app internals; -x mutes the shutter. + screencapture -x "$PROOF_DIR/$1.png" 2>/dev/null || true + log "screenshot: $PROOF_DIR/$1.png" +} + +"$APP_BIN" & +SETUP_PID=$! +log "launched $APP_BIN (pid $SETUP_PID)" +cleanup() { kill "$SETUP_PID" 2>/dev/null || true; } +trap cleanup EXIT + +# Wait for the installer window, then click the install button. The webview's +# button is not a native control and System Events is not permitted assistive +# access on the runners (error -25208), so post real CGEvents with cliclick. +# The AppleScript half only READS window geometry (position/size need no +# assistive grant). Re-click every probe until the install starts: a click +# that lands before the webview finishes painting is swallowed, and +# re-clicking a started install hits a progress screen (harmless). +command -v cliclick >/dev/null 2>&1 || brew install --quiet cliclick + +window_geometry() { + osascript <<'OSA' 2>/dev/null +tell application "System Events" + set procs to (every process whose name contains "Hermes") + if (count of procs) = 0 then return "no-process" + set p to item 1 of procs + if (count of windows of p) = 0 then return "no-window" + set w to window 1 of p + set {x, y} to position of w + set {wd, ht} to size of w + return (x as text) & " " & (y as text) & " " & (wd as text) & " " & (ht as text) +end tell +OSA +} + +click_install() { + local geo + geo="$(window_geometry)" + case "$geo" in + no-process|no-window|'') echo "$geo"; return 0 ;; + esac + # shellcheck disable=SC2086 + set -- $geo + local x=$1 y=$2 wd=$3 ht=$4 + # Button center sits at ~65% of window height (measured from the + # installer's first-run screen at its fixed 880x620 window size). + local cx=$((x + wd / 2)) + local cy=$((y + ht * 65 / 100)) + cliclick "c:${cx},${cy}" 2>&1 || true + echo "clicked ${cx},${cy} (window ${x},${y} ${wd}x${ht})" +} + +HERMES_BIN="$INSTALL_DIR/venv/bin/hermes" +# The bootstrap runs 11 stages; checkout + venv land in the first few and +# the desktop app build is near the end, so success requires all three or +# the EXIT trap kills the installer mid-build. +installed_app() { + local cand + for cand in \ + "$INSTALL_DIR/apps/desktop/release/mac-arm64/Hermes.app" \ + "$INSTALL_DIR/apps/desktop/release/mac/Hermes.app" \ + "/Applications/Hermes.app"; do + [ -d "$cand" ] && return 0 + done + return 1 +} +install_complete() { + [ -d "$INSTALL_DIR/.git" ] && [ -x "$HERMES_BIN" ] && installed_app +} +DEADLINE=$((SECONDS + INSTALL_TIMEOUT_SECS)) +FIRST_SHOT=0 +CLICKS=0 +while :; do + if install_complete; then + log "install landed: checkout + venv console script + Hermes.app present" + shot "02-install-landed" + break + fi + if ! kill -0 "$SETUP_PID" 2>/dev/null; then + # The bootstrap relaunches the desktop and exits on success; only fail + # if it died without the install landing (give the FS one last look). + sleep 5 + install_complete && continue + shot "ERROR-setup-exited" + log "Hermes-Setup exited (pid $SETUP_PID) before the install landed" + exit 1 + fi + if [ "$FIRST_SHOT" -eq 0 ] && [ "$SECONDS" -gt 10 ]; then + shot "00-setup-window" + FIRST_SHOT=1 + fi + # Keep clicking until the install shows up on disk; count for the log. + result="$(click_install || true)" + CLICKS=$((CLICKS + 1)) + [ $((CLICKS % 6)) -eq 1 ] && log "click attempt $CLICKS: $result" + if [ "$CLICKS" -eq 3 ]; then shot "01-after-first-clicks"; fi + if [ "$SECONDS" -ge "$DEADLINE" ]; then + shot "ERROR-install-timeout" + log "install did not land within ${INSTALL_TIMEOUT_SECS}s ($CLICKS clicks attempted)" + exit 1 + fi + sleep 10 +done + +# Success: the bootstrap owns its own exit (it relaunches the desktop). +# Leave it a grace window, then stop it if it lingers; the caller's asserts +# take over from here. +for _ in 1 2 3 4 5 6; do + kill -0 "$SETUP_PID" 2>/dev/null || break + sleep 5 +done +log "done" +exit 0 diff --git a/tests/install/macos-desktop-e2e.sh b/tests/install/macos-desktop-e2e.sh index ccf36a4f93..c623431d35 100755 --- a/tests/install/macos-desktop-e2e.sh +++ b/tests/install/macos-desktop-e2e.sh @@ -226,12 +226,17 @@ phase_install() { app_bin="$(find "$app/Contents/MacOS" -type f -perm +111 | head -1)" [ -n "$app_bin" ] || fail "no executable inside $app/Contents/MacOS" - # Run the installer binary DIRECTLY: `open` launches via launchd, which - # inherits NONE of the redirect env (GIT_CONFIG_GLOBAL, HOME) - the whole - # isolation would silently evaporate. Direct exec is the same binary and - # the same first-launch flow. + # The Setup app is Tauri (Rust + system webview): Playwright/Electron + # attach never works, and run bare it waits forever on its setup-choice + # screen. Launch it in the background with our env (direct exec, not + # `open`: launchd inherits NONE of the redirect env) and drive the + # "Install Hermes" button with native input. local rc=0 - "$app_bin" 2>&1 | ts_prefix > "$LOG_DIR/bootstrap-install.log" || rc=$? + bash "$ASSETS/drive-dmg-install.sh" \ + --app-bin "$app_bin" \ + --install-dir "$INSTALL_DIR" \ + --proof-dir "$LOG_DIR" 2>&1 \ + | ts_prefix > "$LOG_DIR/bootstrap-install.log" || rc=$? log_group "Hermes-Setup (dmg bootstrap) transcript" "$LOG_DIR/bootstrap-install.log" hdiutil detach "$mount" >/dev/null 2>&1 || true [ "$rc" -eq 0 ] || fail "dmg bootstrap exited $rc; transcript above" @@ -249,15 +254,23 @@ phase_install() { ok "installed app: $(find_installed_app)" } -run_playwright_update() { - # $1: spec file to launch from. Installs the driver's OWN pinned - # @playwright/test into a scratch dir (never the installed tree's copy). - local spec="$1" +ensure_playwright() { + # Install the driver's OWN pinned @playwright/test into a scratch dir + # (never the installed tree's copy). Idempotent across phases. local pw_dir="$WORK_ROOT/playwright" + [ -d "$pw_dir/node_modules/@playwright/test" ] && { printf '%s' "$pw_dir"; return 0; } mkdir -p "$pw_dir" (cd "$pw_dir" && npm install --no-save --no-audit --no-fund \ "@playwright/test@$PLAYWRIGHT_VERSION" 2>&1 | ts_prefix > "$LOG_DIR/playwright-install.log") \ || { log_group "playwright install transcript" "$LOG_DIR/playwright-install.log"; fail "playwright install failed"; } + printf '%s' "$pw_dir" +} + +run_playwright_update() { + # $1: spec file to launch from. + local spec="$1" + local pw_dir + pw_dir="$(ensure_playwright)" cp "$ASSETS/launch-from-spec.mjs" "$pw_dir/" local rc=0 (cd "$pw_dir" && node launch-from-spec.mjs \