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.
This commit is contained in:
yoniebans
2026-08-31 17:48:59 +02:00
parent 29203d0859
commit 15ebb81184
3 changed files with 172 additions and 12 deletions
@@ -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 <path> --install-dir <path> \
# [--install-timeout-secs 2700] [--proof-dir <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
+22 -9
View File
@@ -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 \