script/local-release: restore build + hot-restart workflow

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) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-19 22:46:28 +01:00
parent f6ee90e169
commit 86b9d069b1
Regular → Executable
+138 -16
View File
@@ -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