thermograph/deploy/deploy.sh
Emi Griffith 1e89bc71c9 Apply accounts-DB migrations on deploy (#219)
The deploy scripts pulled code and restarted but never applied the
hand-written SQL migrations in deploy/migrations/, so column additions to
the long-lived accounts DB had to be run by hand.

Add deploy/migrate-db.py, an idempotent runner that resolves the accounts
DB the same way the app does (THERMOGRAPH_ACCOUNTS_DB or
<repo>/data/accounts.sqlite), applies any files not yet recorded in a
schema_migrations table, and:
  - backs the DB up before touching it;
  - baselines a fresh DB (no file, or no user table) rather than ALTERing
    a table create_all() will build with the current schema on next start;
  - tolerates an already-present column/index (hand-applied or model-built)
    by recording it instead of failing.

Wire it into deploy.sh (prod) and deploy-dev.sh (dev) just before the
service restarts, so new code never queries a column an older DB lacks.
No env or systemd changes: the migration runs as the deploy user, who owns
the data dir.

Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 06:09:15 +00:00

74 lines
2.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# Pull the latest code and restart Thermograph. Run on the VPS — the GitHub
# Actions workflow invokes this over SSH, and you can run it by hand too.
#
# ssh deploy@vps '/opt/thermograph/deploy/deploy.sh'
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/thermograph}"
BRANCH="${BRANCH:-main}"
cd "$APP_DIR"
# Pre-warm the ~750 city-page archives so /climate pages serve from cache and a
# search-engine crawl never bursts the archive API quota. Detached + backgrounded
# so it never blocks the deploy or health check; idempotent (skips already-cached
# cells), so it's cheap on every deploy after the first full warm.
warm_city_archives() {
mkdir -p "$APP_DIR/logs"
echo "==> Warming city-page archives in the background (logs/warm-cities.log)"
setsid nohup bash -c "cd '$APP_DIR/backend' && exec '$APP_DIR/.venv/bin/python' warm_cities.py --pace 2" \
</dev/null >"$APP_DIR/logs/warm-cities.log" 2>&1 &
}
# Notify IndexNow (Bing / DuckDuckGo / Yandex) of the site's URLs, but only when
# the set of pages actually changed (a new/removed city) — code-only deploys skip,
# so we don't re-blast ~14k URLs every push. Best-effort: never fails the deploy.
# Sourcing the env matches the key the running service serves at /{key}.txt.
ping_indexnow() {
echo "==> Pinging IndexNow (only if the URL set changed)"
( set -a; . /etc/thermograph.env 2>/dev/null || true; set +a
base="${THERMOGRAPH_BASE_URL:-https://thermograph.org}"
cd "$APP_DIR/backend" && "$APP_DIR/.venv/bin/python" indexnow.py --if-changed "$base"
) || echo "!! IndexNow ping failed (non-fatal)" >&2
}
echo "==> Fetching $BRANCH"
git fetch --prune origin "$BRANCH"
git reset --hard "origin/$BRANCH"
echo "==> Installing dependencies"
if [ ! -d .venv ]; then
python3 -m venv .venv
fi
.venv/bin/pip install --upgrade pip -q
.venv/bin/pip install -r backend/requirements.txt -q
# Apply any pending accounts-DB schema migrations before the new code starts, so
# it never queries a column an older database lacks. Idempotent and tracked, so
# every deploy can run it; sourcing the env picks up a THERMOGRAPH_ACCOUNTS_DB
# override if one is set. Runs as the deploy user, who owns the data dir.
echo "==> Applying database migrations"
( set -a; . /etc/thermograph.env 2>/dev/null || true; set +a
"$APP_DIR/.venv/bin/python" "$APP_DIR/deploy/migrate-db.py"
)
echo "==> Restarting service"
sudo systemctl restart thermograph
echo "==> Health check"
PORT="$(sed -n 's/^PORT=//p' /etc/thermograph.env)"; PORT="${PORT:-8137}"
BASE="$(sed -n 's/^THERMOGRAPH_BASE=//p' /etc/thermograph.env)"; BASE="${BASE:-/}"
# Normalize: no double slash, allow root.
url="http://127.0.0.1:${PORT}${BASE%/}/"
for i in $(seq 1 15); do
if curl -fsS -o /dev/null "$url"; then
echo "==> OK: $url is serving"
warm_city_archives
ping_indexnow
exit 0
fi
sleep 1
done
echo "!! Health check failed for $url" >&2
sudo systemctl status thermograph --no-pager -l || true
exit 1