From 86b9d069b129f4ad96a3b192d14396d7e1d9ac9f Mon Sep 17 00:00:00 2001 From: Timmy Date: Tue, 19 May 2026 22:46:28 +0100 Subject: [PATCH] script/local-release: restore build + hot-restart workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1145 narrowed local-release to install-only (binary + codesign-heal wrapper) and removed the cargo build + gateway hot-restart steps that the script used to do. That broke the "rebuild the gateway" muscle memory: running script/local-release no longer rebuilt or restarted anything, just re-installed the same binary. Restore the build + restart logic while keeping 1145's wrapper: - `cargo build --release --bin huskies` before install - Snapshot the prior binary to ~/bin/huskies-bin.prev for rollback - Print PREV → NEW version delta after install - Detect a running `huskies .*--gateway` process and SSH-safe-restart it (kill descendants depth-first, then nohup the wrapper from the detached subshell) - Wait up to 10s for the new gateway PID to appear; on timeout, roll back to the previous binary and try to relaunch it - Refuse to restart when more than one --gateway process matches, so we don't kill the wrong tree - `--skip-check` bypasses script/check for already-verified changes Co-Authored-By: Claude Opus 4.7 (1M context) --- script/local-release | 154 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 16 deletions(-) mode change 100644 => 100755 script/local-release diff --git a/script/local-release b/script/local-release old mode 100644 new mode 100755 index 836b2f0a..5f89f3b4 --- a/script/local-release +++ b/script/local-release @@ -1,43 +1,165 @@ #!/usr/bin/env bash -# Install huskies locally on macOS: the underlying binary + a codesign-heal wrapper. +# Build huskies, install (codesign-heal wrapper + underlying binary), and if a +# gateway is running on this host, hot-restart it detached from the current shell +# so SSH disconnect — e.g. when redeploying from a phone — doesn't kill it. +# +# Skips the restart silently if no gateway is running. Errors loudly if more +# than one matches, so we don't restart the wrong one. +# +# Pass --skip-check to bypass `script/check` (useful for docs / build-script +# changes you've already verified). +# +# On relaunch failure the previous binary is restored from +# ~/bin/huskies-bin.prev and re-launched, so a bad deploy doesn't leave the +# host without a working gateway. # # After a `cp` or download the binary loses its ad-hoc signature and macOS -# SIGKILLs it silently on Apple Silicon. This script installs the binary as -# ~/bin/huskies-bin and installs a thin wrapper at ~/bin/huskies that -# re-signs the underlying binary whenever codesign validation fails, then -# execs it. Normal launches (already signed) are silent and zero-overhead. +# SIGKILLs it silently on Apple Silicon. The wrapper at ~/bin/huskies re-signs +# the underlying binary at ~/bin/huskies-bin whenever codesign validation +# fails, then execs it. Normal launches (already signed) are zero-overhead. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -BINARY_PATH="${SCRIPT_DIR}/target/release/huskies" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +LOG_DIR="${HUSKIES_LOG_DIR:-$PROJECT_ROOT/logs}" +GATEWAY_PATTERN='huskies .*--gateway' BIN_DIR="${HOME}/bin" UNDERLYING="${BIN_DIR}/huskies-bin" WRAPPER="${BIN_DIR}/huskies" +PREV_BIN="${BIN_DIR}/huskies-bin.prev" +NEW_BIN="${PROJECT_ROOT}/target/release/huskies" -if [ ! -f "${BINARY_PATH}" ]; then - echo "Error: binary not found at ${BINARY_PATH}" - echo "Run: cargo build --release" - exit 1 +SKIP_CHECK=0 +for arg in "$@"; do + case "$arg" in + --skip-check) SKIP_CHECK=1 ;; + -h|--help) sed -n '2,17p' "$0"; exit 0 ;; + *) echo "Unknown arg: $arg (use --help)" >&2; exit 2 ;; + esac +done + +if [ "$SKIP_CHECK" -eq 0 ] && [ -x "$SCRIPT_DIR/check" ]; then + echo "=== Running script/check ===" + "$SCRIPT_DIR/check" fi -mkdir -p "${BIN_DIR}" +echo "=== Building release binary ===" +cd "$PROJECT_ROOT" +cargo build --release --bin huskies -cp "${BINARY_PATH}" "${UNDERLYING}" -chmod +x "${UNDERLYING}" +mkdir -p "$BIN_DIR" + +# Snapshot current binary so we can roll back if the relaunch fails. +PREV_VERSION="" +if [ -x "$UNDERLYING" ]; then + PREV_VERSION="$("$UNDERLYING" --version 2>/dev/null || echo unknown)" + cp "$UNDERLYING" "$PREV_BIN" +fi + +cp "$NEW_BIN" "$UNDERLYING" +chmod +x "$UNDERLYING" +codesign -s - -f "$UNDERLYING" 2>/dev/null +NEW_VERSION="$("$UNDERLYING" --version 2>/dev/null || echo unknown)" echo "==> Installed binary: ${UNDERLYING}" +if [ -n "$PREV_VERSION" ]; then + echo " version: $PREV_VERSION → $NEW_VERSION" +else + echo " version: $NEW_VERSION (no prior install)" +fi cat > "${WRAPPER}" << 'WRAPPER_EOF' #!/usr/bin/env bash # Codesign-heal wrapper — re-signs ~/bin/huskies-bin if the signature is # missing or invalid, then execs the binary. Logs only when it re-signs. BIN="${HOME}/bin/huskies-bin" - if ! codesign --verify --quiet "${BIN}" 2>/dev/null; then codesign -s - "${BIN}" echo "[codesign-heal] re-signed ~/bin/huskies-bin" >&2 fi - exec "${BIN}" "$@" WRAPPER_EOF chmod +x "${WRAPPER}" echo "==> Installed wrapper: ${WRAPPER}" + +# ── Hot-restart gateway if one is running ───────────────────────────── +collect_descendants() { + local pid="$1" kid + for kid in $(pgrep -P "$pid" 2>/dev/null); do + collect_descendants "$kid" + printf '%s\n' "$kid" + done +} + +GATEWAY_PIDS="$(pgrep -f "$GATEWAY_PATTERN" || true)" +if [ -z "$GATEWAY_PIDS" ]; then + echo "==> No running gateway found; install complete." + exit 0 +fi + +if [ "$(echo "$GATEWAY_PIDS" | wc -l)" -gt 1 ]; then + echo "Error: multiple gateway processes match '${GATEWAY_PATTERN}':" >&2 + ps -p $GATEWAY_PIDS -o pid,args >&2 || true + echo "Refusing to guess which to restart." >&2 + exit 3 +fi + +GATEWAY_PID="$GATEWAY_PIDS" +GATEWAY_ARGS="$(ps -p "$GATEWAY_PID" -o args= | sed -E 's@^[^ ]*huskies[^ ]* @@')" +GATEWAY_CWD="$(lsof -p "$GATEWAY_PID" 2>/dev/null | awk '$4=="cwd"{print $9; exit}')" +if [ -z "$GATEWAY_CWD" ]; then GATEWAY_CWD="$PWD"; fi + +LOG_FILE="$LOG_DIR/gateway-$(date +%Y%m%d-%H%M%S).log" +mkdir -p "$LOG_DIR" + +DESCENDANTS="$(collect_descendants "$GATEWAY_PID" | tr '\n' ' ')" +echo "==> Stopping gateway tree (pids: $GATEWAY_PID $DESCENDANTS)" +# Kill descendants depth-first so PTY children die before the gateway, then the gateway. +for pid in $DESCENDANTS $GATEWAY_PID; do + kill "$pid" 2>/dev/null || true +done +sleep 2 + +echo "==> Restarting gateway" +echo " log: $LOG_FILE" +( + cd "$GATEWAY_CWD" + nohup "$WRAPPER" $GATEWAY_ARGS >> "$LOG_FILE" 2>&1 < /dev/null & + disown +) + +# Wait up to 10s for the new gateway to appear AND be a different PID. +NEW_PID="" +for _ in 1 2 3 4 5 6 7 8 9 10; do + sleep 1 + candidate="$(pgrep -f "$GATEWAY_PATTERN" 2>/dev/null || true)" + if [ -n "$candidate" ] && [ "$candidate" != "$GATEWAY_PID" ]; then + NEW_PID="$candidate" + break + fi +done + +if [ -n "$NEW_PID" ]; then + echo "==> Gateway restarted as pid $NEW_PID" + exit 0 +fi + +# ── Rollback ────────────────────────────────────────────────────────── +echo "Error: new gateway failed to come up within 10s; rolling back" >&2 +if [ -x "$PREV_BIN" ]; then + cp "$PREV_BIN" "$UNDERLYING" + chmod +x "$UNDERLYING" + codesign -s - -f "$UNDERLYING" 2>/dev/null + echo "==> Restored previous binary" + ( + cd "$GATEWAY_CWD" + nohup "$WRAPPER" $GATEWAY_ARGS >> "$LOG_FILE" 2>&1 < /dev/null & + disown + ) + sleep 2 + if pgrep -f "$GATEWAY_PATTERN" >/dev/null 2>&1; then + echo "==> Gateway restored to previous version" + exit 1 + fi +fi +echo "Error: rollback failed; gateway is DOWN. Inspect $LOG_FILE." >&2 +exit 1