#!/usr/bin/env bash # Build whatever branch is currently checked out and deploy it to a single # instance on the self-hosted router box (versioned, side-by-side deploys — # see docs/instance-router.md and CLAUDE.local.md "Deploying"). Health-checked: # `lchat deploy` itself verifies /api/version's build id after restarting and # auto-rolls back on a mismatch, so a bad build never stays live. Also ensures # the instance has its default bot sidecar (Claude/Eleven) running — # deterministic-only (no ANTHROPIC_API_KEY) unless you pass one in — so Eleven # always welcomes new members and answers slash commands out of the box. # # Usage: ./deploy/go-router.sh [host] # ./deploy/go-router.sh # host defaults to team.elevenmessenger.com # ./deploy/go-router.sh test.example.com # SKIP_BOT=1 ./deploy/go-router.sh # skip the bot-ensure step # ANTHROPIC_API_KEY=sk-ant-... ./deploy/go-router.sh # AI-enabled instead # # Never deploys the whole fleet (`--all`) — always one explicit host, so this # script can never affect an instance other than the one you asked for. set -euo pipefail HOST="${1:-team.elevenmessenger.com}" # No user in the default: operator SSH rides the WireGuard tunnel and the # username comes from ~/.ssh/config (root SSH is bootstrap-only — see # docs/instance-router.md § Hardening). Privileged remote commands go through # `sudo -n` (the committed allowlist, deploy/sudoers-operators); root can # sudo too, so a bootstrap-era root session still works unchanged. BOX="${LCHAT_BOX:-89.167.122.227}" SKIP_BOT="${SKIP_BOT:-}" ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" REPO="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO" BRANCH="$(git rev-parse --abbrev-ref HEAD)" VER="$(git rev-parse --short HEAD)" if ! git diff --quiet || ! git diff --cached --quiet; then VER="${VER}-dirty" echo "Warning: working tree has uncommitted changes — building anyway (version tagged '$VER')." >&2 fi OUT="$(mktemp -t lchat-linux-amd64.XXXXXX)" trap 'rm -f "$OUT"' EXIT echo "Building branch '$BRANCH' ($VER) for linux/amd64..." OUT="$OUT" ./deploy/build-linux.sh "$VER" # /opt/lchat/incoming is a DIRECTORY now (the auto-updater downloads into it # too — see updater.go); upload as a file inside it, staged then removed. echo "Uploading to $BOX:/opt/lchat/incoming/lchat-upload..." # No sudo: the dir exists group-writable (lchat-operators) after operator # setup, and mkdir -p on an existing dir succeeds without write access. ssh "$BOX" 'mkdir -p /opt/lchat/incoming' scp -q "$OUT" "$BOX:/opt/lchat/incoming/lchat-upload" echo "Staging on the box..." STAGE_OUT="$(ssh "$BOX" 'sudo -n /opt/lchat/lchat stage -file /opt/lchat/incoming/lchat-upload && rm -f /opt/lchat/incoming/lchat-upload')" echo " $STAGE_OUT" STAGED_VER="$(echo "$STAGE_OUT" | awk '{print $2}')" if [ "$STAGED_VER" != "$VER" ]; then echo "go-router: staged version '$STAGED_VER' doesn't match built version '$VER' — aborting" >&2 exit 1 fi echo "Deploying $STAGED_VER to $HOST (health-checked; auto-rolls back on a build mismatch)..." ssh "$BOX" "sudo -n /opt/lchat/lchat deploy '$HOST' --version '$STAGED_VER'" if [ -n "$SKIP_BOT" ]; then echo "SKIP_BOT set — leaving the bot sidecar alone." elif ssh "$BOX" "sudo -n test -f /data/${HOST}-bot.env"; then echo "Bot sidecar already provisioned for $HOST — leaving it alone (add-bot mints fresh, one-time invites, so re-running it would orphan the existing Claude/Eleven identities)." else echo "No bot sidecar yet for $HOST — adding one (deterministic-only unless \$ANTHROPIC_API_KEY is set)..." # Work around a known bug in the box's current control binary: add-bot's # MkdirAll leaves the session dir root-owned, but the bot unit runs as the # unprivileged `lchat` user (post-hardening) and can't write into it — every # freshly-added bot crash-loops with EACCES, burning its one-time Claude # invite before anyone can intervene (hit this firsthand adding Eleven here). # Fixed upstream in 8e20692, not yet deployed to /opt/lchat/lchat's control # binary on the box. Pre-creating the dir with the right owner is a no-op # once that binary is updated, so this is safe to leave in permanently. ssh "$BOX" "sudo -n mkdir -p /data/${HOST}-bot && sudo -n chown lchat:lchat /data/${HOST}-bot && sudo -n chmod 750 /data/${HOST}-bot" if [ -n "$ANTHROPIC_API_KEY" ]; then ssh "$BOX" "sudo -n ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY' /opt/lchat/lchat add-bot '$HOST'" else ssh "$BOX" "sudo -n /opt/lchat/lchat add-bot '$HOST'" fi fi echo echo "✅ $HOST is now running $STAGED_VER (branch '$BRANCH')" echo " Recover the admin link anytime: ssh $BOX sudo -n /opt/lchat/lchat admin-link $HOST" echo " Step one instance back: ssh $BOX sudo -n /opt/lchat/lchat rollback $HOST"