feat(install-e2e): cover non-app update methods after a dmg install

A dmg user can also update from the terminal (hermes update) or by
re-running the install one-liner; the dmg arm only routed the two
app-button methods, leaving three declared cells as permanent TODOs.
Port the POSIX driver's method blocks into the macos driver's update
phase (hermes-update with the --yes probe, installer re-run with per-ref
flag probing, the +desktop built-app assert) and open the workflow gate.

The dmg bootstrap driver also learns to recover instead of waiting out
its bound when a stage fails: the bootstrap parks on an error screen
with a Retry button (seen live: HTTP 429 downloading install.sh under
full-matrix runner load), so the driver watches the bootstrap log for
real failure shapes, requires two consecutive error probes before
retargeting the click at Retry's measured position, unlatches when the
log goes healthy, and gives up with the true cause after three retries.

Driver rule learned three times in this suite (lsof +D, git show and
find piped to grep -q): under set -euo pipefail, never feed grep -q
from a pipe... grep exits at first match, the producer takes SIGPIPE,
and a TRUE condition reads as failure. Capture to a variable or test
paths directly.

Verified end to end: run 33506177130, macos slice 13/13 green.
This commit is contained in:
yoniebans
2026-09-01 14:48:46 +02:00
parent 15ebb81184
commit d18ad46eee
3 changed files with 119 additions and 5 deletions

View File

@@ -82,7 +82,7 @@ jobs:
name: Hermes-Setup.dmg
if: >-
inputs.install-method == 'desktop-installer@latest' && inputs.tag-has-desktop
&& contains(fromJSON('["open-app-update", "hermes-desktop-app-update"]'), inputs.update-method)
&& contains(fromJSON('["open-app-update", "hermes-desktop-app-update", "hermes-update", "installer-script", "installer-script+desktop"]'), inputs.update-method)
runs-on: macos-latest
timeout-minutes: ${{ inputs.timeout-minutes }}

View File

@@ -76,8 +76,16 @@ click_install() {
# 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).
if [ -n "$LAST_ERR" ]; then
# Error screen: Retry install sits left of center at ~59% height
# (measured: button x 359-492, y 402-441 in the 880x620 window).
local cx=$((x + wd * 48 / 100))
local cy=$((y + ht * 59 / 100))
cliclick "c:${cx},${cy}" 2>&1 || true
echo "clicked retry ${cx},${cy} (window ${x},${y} ${wd}x${ht})"
return 0
fi
# First-run screen: button center sits at ~65% of window height.
local cx=$((x + wd / 2))
local cy=$((y + ht * 65 / 100))
cliclick "c:${cx},${cy}" 2>&1 || true
@@ -101,6 +109,26 @@ installed_app() {
install_complete() {
[ -d "$INSTALL_DIR/.git" ] && [ -x "$HERMES_BIN" ] && installed_app
}
# The bootstrap parks on an error screen instead of exiting when a stage
# fails (e.g. a transient 429 downloading install.sh), with a Retry button
# in the same button zone the install click hits. Its log names the cause;
# watch for new failure lines, let the regular click drive the retry, and
# give up after a few so a persistent failure reports the real error
# instead of burning the whole install timeout.
BOOTSTRAP_LOG="$HOME/.hermes/logs/bootstrap-installer.log"
bootstrap_error() {
[ -f "$BOOTSTRAP_LOG" ] || return 0
# Match REAL failure shapes only: the structured stage log's state=Failed,
# or the error screen's own message. Healthy INFO lines carry the literal
# field error=None, so a bare error/failed substring match false-positives
# on every stage transition.
tail -5 "$BOOTSTRAP_LOG" 2>/dev/null \
| grep -E 'state=Failed|install script failed|didn.t finish|ERROR ' | tail -1 || true
}
RETRIES=0
MAX_RETRIES=3
LAST_ERR=""
PENDING_ERR_COUNT=0
DEADLINE=$((SECONDS + INSTALL_TIMEOUT_SECS))
FIRST_SHOT=0
CLICKS=0
@@ -119,6 +147,28 @@ while :; do
log "Hermes-Setup exited (pid $SETUP_PID) before the install landed"
exit 1
fi
err="$(bootstrap_error)"
if [ -n "$err" ]; then
PENDING_ERR_COUNT=$((PENDING_ERR_COUNT + 1))
else
PENDING_ERR_COUNT=0
fi
# Two consecutive error probes before switching click targets: a single
# log-parse glitch must never redirect the clicker.
if [ "$PENDING_ERR_COUNT" -ge 2 ] && [ "$err" != "$LAST_ERR" ]; then
LAST_ERR="$err"
RETRIES=$((RETRIES + 1))
log "bootstrap error (retry $RETRIES/$MAX_RETRIES): $err"
shot "ERROR-bootstrap-attempt-$RETRIES"
if [ "$RETRIES" -gt "$MAX_RETRIES" ]; then
log "bootstrap failing persistently; giving up"
exit 1
fi
fi
if [ -z "$err" ] && [ -n "$LAST_ERR" ]; then
log "bootstrap error cleared; resuming install-button targeting"
LAST_ERR=""
fi
if [ "$FIRST_SHOT" -eq 0 ] && [ "$SECONDS" -gt 10 ]; then
shot "00-setup-window"
FIRST_SHOT=1

View File

@@ -21,6 +21,8 @@
# hermes-desktop-app-update capture `hermes desktop`'s spawn,
# launch the spec under Playwright,
# click Update now
# hermes-update CLI update from the installed venv
# installer-script[+desktop] re-run the current install one-liner
#
# Usage:
# tests/install/macos-desktop-e2e.sh --phase stage|install|update|all
@@ -61,8 +63,8 @@ while [ "$#" -gt 0 ]; do
esac
done
case "$UPDATE_METHOD" in
open-app-update|hermes-desktop-app-update) ;;
*) echo "error: --update-method must be open-app-update or hermes-desktop-app-update, got '$UPDATE_METHOD'" >&2; exit 1 ;;
open-app-update|hermes-desktop-app-update|hermes-update|installer-script|installer-script+desktop) ;;
*) echo "error: unsupported --update-method '$UPDATE_METHOD'" >&2; exit 1 ;;
esac
[ "$(uname -s)" = "Darwin" ] || { echo "error: this driver runs on macOS only" >&2; exit 1; }
@@ -266,6 +268,39 @@ ensure_playwright() {
printf '%s' "$pw_dir"
}
installer_supports() {
# $1: ref; $2: flag. Installer flags must match the installer being run,
# not this checkout's: older releases reject options added later.
# Capture before grepping: a `git show | grep -q` pipe takes SIGPIPE
# under pipefail when grep exits at first match, so a supported flag
# would read as unsupported.
local text
text="$(git -C "$REPO_ROOT" show "$1:scripts/install.sh")"
grep -qF -- "$2" <<< "$text"
}
run_installer() {
# $1: ref whose scripts/install.sh to run; $2: log name; $3: "desktop" to
# opt the desktop stage in (--include-desktop). Mirrors the POSIX driver.
local script="$WORK_ROOT/install-$2.sh"
git -C "$REPO_ROOT" show "$1:scripts/install.sh" > "$script"
chmod +x "$script"
local flags=(--skip-setup)
if installer_supports "$1" "--skip-browser"; then
flags+=(--skip-browser)
fi
if [ "${3:-}" = "desktop" ]; then
installer_supports "$1" "--include-desktop" \
|| fail "ref $1 does not support --include-desktop; this leg cannot mean what it claims"
flags+=(--include-desktop)
fi
# </dev/null: EOF makes every prompt take its default.
local rc=0
bash "$script" "${flags[@]}" < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/install-$2.log" || rc=$?
log_group "installer ($2) transcript" "$LOG_DIR/install-$2.log"
[ "$rc" -eq 0 ] || fail "installer ($2) exited $rc; transcript above"
}
run_playwright_update() {
# $1: spec file to launch from.
local spec="$1"
@@ -300,6 +335,35 @@ phase_update() {
mock_start "$WORK_ROOT"
trap mock_stop EXIT
case "$UPDATE_METHOD" in
hermes-update)
# The CLI route a dmg user takes from a terminal. `--yes` reaches the
# update subcommand only in later releases; ask the installed hermes.
local hermes="$INSTALL_DIR/venv/bin/hermes"
local update_cmd=("$hermes" update)
if "$hermes" update --help 2>&1 | grep -qF -- --yes; then
update_cmd=("$hermes" update --yes)
fi
local rc=0
(cd "$INSTALL_DIR" && "${update_cmd[@]}" < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/update.log") || rc=$?
log_group "hermes update transcript" "$LOG_DIR/update.log"
[ "$rc" -eq 0 ] || fail "hermes update exited $rc; transcript above"
;;
installer-script)
# A dmg user re-running today's install one-liner.
run_installer "$HEAD_SHA" head
;;
installer-script+desktop)
run_installer "$HEAD_SHA" head desktop
# The desktop stage is this leg's claim: the rebuilt app must exist.
head_app=""
for cand in \
"$INSTALL_DIR/apps/desktop/release/mac-arm64/Hermes.app" \
"$INSTALL_DIR/apps/desktop/release/mac/Hermes.app"; do
[ -d "$cand" ] && { head_app="$cand"; break; }
done
[ -n "$head_app" ] || fail "no built Hermes.app under the checkout after the +desktop update"
ok "rebuilt app present: $head_app"
;;
open-app-update)
# The installed app IS the user surface here (double-click the .app);
# hand-build the spec Playwright launches from. Env: the redirect set,