diff --git a/.github/workflows/install-e2e-macos-run.yml b/.github/workflows/install-e2e-macos-run.yml index de90b863b2..1599e566c4 100644 --- a/.github/workflows/install-e2e-macos-run.yml +++ b/.github/workflows/install-e2e-macos-run.yml @@ -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 }} diff --git a/tests/install/e2e-assets/drive-dmg-install.sh b/tests/install/e2e-assets/drive-dmg-install.sh index 60ddd1e35f..1335379bd6 100644 --- a/tests/install/e2e-assets/drive-dmg-install.sh +++ b/tests/install/e2e-assets/drive-dmg-install.sh @@ -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 diff --git a/tests/install/macos-desktop-e2e.sh b/tests/install/macos-desktop-e2e.sh index c623431d35..3467b0b811 100755 --- a/tests/install/macos-desktop-e2e.sh +++ b/tests/install/macos-desktop-e2e.sh @@ -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 + # &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,