git / moonbite-network / deploy/cutover-droplet.sh · raw

#!/bin/bash# Cut the dashboard over to the staged build, rolling back if it does not serve.set -u# Branch to deploy. Defaults to main now that the rebuild is merged.BRANCH="${1:-main}"REPO=https://moonbite.org/git/moonbite-network.gitOLD=/opt/moonbite-dashboard-oldCUR=/opt/moonbite-dashboardNEW=/opt/moonbite-nextfail() { echo "CUTOVER-FAILED: $*"; }# Stop the service and confirm it is actually down.## `systemctl stop` exits non-zero when its job is superseded — an auto-restart# (Restart=on-failure) or a concurrent systemctl call can cancel the stop job# with "Job canceled", even though the service often stops regardless. Trusting# that exit code aborted the whole cutover on a transient race. Instead, issue# the stop and poll the real state, retrying, and only fail if it is genuinely# still active after ~90s.stop_service() {    local name="moonbite-dashboard" i j state    for i in $(seq 1 6); do        systemctl stop "$name" 2>/dev/null || true        for j in $(seq 1 15); do            state=$(systemctl is-active "$name" 2>/dev/null || true)            case "$state" in                inactive|failed|unknown|"") return 0 ;;            esac            sleep 1        done        # Still active/activating — likely an auto-restart raced the stop. Loop        # and stop again.    done    state=$(systemctl is-active "$name" 2>/dev/null || true)    [ "$state" = "active" ] && return 1 || return 0}echo "step: stage $BRANCH"rm -rf "$NEW"git clone -q --branch "$BRANCH" --single-branch "$REPO" "$NEW" || { fail "clone failed"; exit 1; }(cd "$NEW" && git log --oneline -1 | sed "s/^/  staged: /")echo "step: back up current deployment"BK=/root/backups/moonbite-$(date +%Y%m%d-%H%M%S)mkdir -p "$BK"cp -a "$CUR"/*.db "$BK"/ 2>/dev/null || true(cd "$CUR" && git rev-parse HEAD > "$BK/DEPLOYED_COMMIT.txt" 2>/dev/null) || trueecho "  backup: $BK"echo "step: stop staged test server"pkill -f 'bind 127.0.0.1:8051' 2>/dev/null || truesleep 2echo "step: stop service"stop_service || { fail "service still active after repeated stop attempts"; exit 1; }echo "  stopped"echo "step: swap directories"rm -rf "$OLD"mv "$CUR" "$OLD" || { fail "could not move current aside"; systemctl start moonbite-dashboard; exit 1; }mv "$NEW" "$CUR" || { fail "could not move new into place"; mv "$OLD" "$CUR"; systemctl start moonbite-dashboard; exit 1; }echo "step: databases"# With MOONBITE_DATA_DIR set, durable state lives outside the deploy tree and# a cutover cannot touch it - which is the point. The copy below only applies# to a host still keeping its databases beside the code.DATA_DIR=$(systemctl show -p Environment --value moonbite-dashboard 2>/dev/null | grep -o "MOONBITE_DATA_DIR=[^ ]*" | cut -d= -f2-)if [ -n "${DATA_DIR:-}" ]; then    echo "  held outside the deploy tree in $DATA_DIR - nothing to copy"else    cp -a "$OLD"/*.db "$CUR"/ 2>/dev/null || true    echo "  carried across: $(ls "$CUR"/*.db 2>/dev/null | wc -l) database(s)"fiecho "step: rebuild venv at the new path"# venv scripts hardcode absolute paths, so the directory move breaks them.rm -rf "$CUR/venv"python3 -m venv "$CUR/venv" >/dev/null 2>&1"$CUR/venv/bin/pip" install -q --upgrade pip >/dev/null 2>&1"$CUR/venv/bin/pip" install -q -r "$CUR/requirements.txt" 2>&1 | tail -2"$CUR/venv/bin/pip" install -q gunicorn 2>&1 | tail -1echo "step: import check"if ! (cd "$CUR" && "$CUR/venv/bin/python" -c 'import web_app') 2>/tmp/import_err; then    fail "app does not import after move"    head -5 /tmp/import_err    rm -rf "$CUR"; mv "$OLD" "$CUR"    systemctl start moonbite-dashboard    echo "ROLLED-BACK"    exit 1fiecho "  imports fine"echo "step: restore ownership"# The clone runs as root but the service runs as its own user, and sqlite needs# to write WAL sidecars *into the directory*, not just the .db files. Without# this every write fails with "attempt to write a readonly database".SVC_USER=$(systemctl show -p User --value moonbite-dashboard)SVC_USER=${SVC_USER:-dashboard}chown -R "$SVC_USER":"$SVC_USER" "$CUR"echo "  owned by $SVC_USER"echo "step: start service"systemctl start moonbite-dashboardsleep 12echo "step: verify"CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8050/wallet --max-time 15)echo "  local /wallet -> $CODE"if [ "$CODE" != "200" ]; then    fail "new build does not serve; rolling back"    stop_service    rm -rf "$CUR"; mv "$OLD" "$CUR"    systemctl start moonbite-dashboard    sleep 8    echo -n "  after rollback /wallet -> "    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8050/wallet --max-time 15    echo "ROLLED-BACK"    exit 1fiecho "CUTOVER-OK"cd "$CUR" && git log --oneline -1