OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / scripts / remote-deploy.sh

75 lines · bash ref:
#!/usr/bin/env bash
# Hardened in-place deploy for a pgh app. Runs ON the target host.
#
# Fixes the 2026-07-03 outage: a detached `stop -> install -> start` stranded the daemon
# when `install` stalled AFTER the stop (network topic hang), and overlapping deploys
# thrashed it. Guarantees, by construction:
#   - SERIALIZED     — flock; a second deploy aborts instead of overlapping.
#   - BOUNDED INSTALL— install can't hang forever (timeout); on failure we don't proceed blind.
#   - NEVER STRANDED — the daemon is ALWAYS (re)started afterwards, even if install fails.
#   - VERIFIED       — polls for running:true AND an HTTP health check before declaring success.
#   - ROLLBACK       — if the new tree won't come up healthy, restore the previous tree.
#
# Usage (on the host):  remote-deploy.sh <app> [port] [--check]
#   --check : report current tree/status/health only; make NO changes.

set -uo pipefail
APP="${1:?usage: remote-deploy.sh <app> [port] [--check]}"
PORT="${2:-3399}"
MODE="${3:-}"
LOCK="/tmp/deploy-$APP.lock"

health() { curl -sf --max-time 6 "http://127.0.0.1:$PORT/xrpc/com.atproto.server.describeServer" >/dev/null; }
current_tree() { pgh app current-tree "$APP" 2>/dev/null | grep -oE '[0-9a-f]{64}' | head -1; }
status_line() { pgh app status "$APP" 2>/dev/null | grep -E 'running:|version:'; }

if [ "$MODE" = "--check" ]; then
  echo "[check] $APP tree=$(current_tree)"
  status_line
  if health; then echo "[check] HEALTHY"; else echo "[check] UNHEALTHY"; fi
  exit 0
fi

# Serialize: never let two deploys overlap (the other half of the outage).
exec 9>"$LOCK"
if ! flock -n 9; then echo "[deploy] another deploy of $APP in progress — aborting"; exit 1; fi

PREV="$(current_tree)"
echo "[deploy] $APP prev-tree=${PREV:-none}"

pgh app stop "$APP" || true

if timeout 180 pgh app install "$APP"; then
  echo "[deploy] install ok"
else
  echo "[deploy] install FAILED/timed out"
  if [ -n "$PREV" ]; then echo "[deploy] restoring previous tree $PREV"; pgh app install-tree "$APP" --tree "$PREV" || true; fi
fi

# ALWAYS start — the daemon must never be left stopped, whatever happened above.
pgh app start "$APP" || true

ok=""
for _ in $(seq 1 20); do
  pgh app status "$APP" 2>/dev/null | grep -q "running: true" && { ok=1; break; }
  sleep 3
done
if [ -z "$ok" ]; then echo "[deploy] not running yet — forcing start"; pgh app start "$APP" || true; sleep 5; fi

if health; then
  echo "[deploy] HEALTHY — $(status_line)"
  exit 0
fi

echo "[deploy] UNHEALTHY after start"
if [ -n "$PREV" ]; then
  echo "[deploy] rolling back to $PREV"
  pgh app stop "$APP" || true
  pgh app install-tree "$APP" --tree "$PREV" || true
  pgh app start "$APP" || true
  sleep 5
  if health; then echo "[deploy] ROLLED BACK — HEALTHY"; exit 0; fi
fi
echo "[deploy] STILL UNHEALTHY — MANUAL INTERVENTION NEEDED"
exit 1