130 lines
6.2 KiB
Bash
Executable File
130 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh — production deploy hook, run on the Droplet by .github/workflows/deploy.yml.
|
|
#
|
|
# The GitHub Actions job SSHes into the Droplet on every push to main and invokes the
|
|
# deployed copy of this script (conventionally /opt/hackerspace-ops/deploy.sh). It pulls
|
|
# the new code, installs deps, applies any pending DB migrations, refreshes PostgREST's
|
|
# schema cache, rebuilds, restarts the app, and finally health-probes it.
|
|
#
|
|
# Every step is defensive and driven by environment variables with sane defaults, so a
|
|
# fresh instance can adopt it by setting at most a couple of vars. It fails fast: any
|
|
# step that errors aborts the whole deploy (set -e) and the workflow reports failure.
|
|
#
|
|
# Environment variables (all optional; defaults match docs/DEPLOY_DO_SELFHOSTED.md):
|
|
# APP_DIR app checkout on the Droplet (default /opt/hackerspace)
|
|
# DEPLOY_BRANCH branch to deploy (default main)
|
|
# SUPABASE_DIR self-hosted Supabase compose dir (default /opt/supabase/docker)
|
|
# SUPABASE_DB_CONTAINER Postgres compose service for migrations (default db)
|
|
# DATABASE_URL alternative to the two vars above; if set, migrations use it
|
|
# directly and Supabase-container steps are skipped
|
|
# APP_SERVICE docker compose service name for the app (default app)
|
|
# SYSTEMD_APP_UNIT systemd unit name for a non-Docker app deploy (default hackerspace-app)
|
|
# SYSTEMD_REST_UNIT systemd unit for PostgREST when not on Docker (default postgrest)
|
|
# HEALTH_URL local health endpoint to probe (default http://localhost:3000/api/health)
|
|
|
|
set -euo pipefail
|
|
|
|
APP_DIR="${APP_DIR:-/opt/hackerspace}"
|
|
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
|
|
SUPABASE_DIR="${SUPABASE_DIR:-/opt/supabase/docker}"
|
|
SUPABASE_DB_CONTAINER="${SUPABASE_DB_CONTAINER:-db}"
|
|
APP_SERVICE="${APP_SERVICE:-app}"
|
|
SYSTEMD_APP_UNIT="${SYSTEMD_APP_UNIT:-hackerspace-app}"
|
|
SYSTEMD_REST_UNIT="${SYSTEMD_REST_UNIT:-postgrest}"
|
|
HEALTH_URL="${HEALTH_URL:-http://localhost:3000/api/health}"
|
|
|
|
log() { printf '[deploy %s] %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Enter the app checkout.
|
|
# ---------------------------------------------------------------------------
|
|
log "cd to app dir: $APP_DIR"
|
|
cd "$APP_DIR"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Fetch and hard-reset to the deployed branch. reset --hard (not pull) so a
|
|
# dirty working tree or a force-push never leaves the checkout in a half state.
|
|
# ---------------------------------------------------------------------------
|
|
log "git fetch + reset to origin/$DEPLOY_BRANCH"
|
|
git fetch --prune origin "$DEPLOY_BRANCH"
|
|
git reset --hard "origin/$DEPLOY_BRANCH"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Install dependencies exactly as locked. --frozen-lockfile fails if the
|
|
# lockfile is out of sync rather than silently resolving new versions.
|
|
# ---------------------------------------------------------------------------
|
|
log "pnpm install --frozen-lockfile"
|
|
pnpm install --frozen-lockfile
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Apply pending DB migrations. apply-migrations.sh is idempotent; it reads
|
|
# DATABASE_URL if set, otherwise talks to the Supabase Postgres container.
|
|
# ---------------------------------------------------------------------------
|
|
log "applying database migrations"
|
|
if [[ -n "${DATABASE_URL:-}" ]]; then
|
|
DATABASE_URL="$DATABASE_URL" bash "$APP_DIR/scripts/apply-migrations.sh"
|
|
else
|
|
SUPABASE_DIR="$SUPABASE_DIR" SUPABASE_DB_CONTAINER="$SUPABASE_DB_CONTAINER" \
|
|
bash "$APP_DIR/scripts/apply-migrations.sh"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Restart PostgREST so it refreshes its cached schema after any DDL.
|
|
# Documented both ways: the self-hosted Docker Supabase stack exposes the
|
|
# PostgREST service as "rest"; a systemd-managed PostgREST is restarted by unit.
|
|
# ---------------------------------------------------------------------------
|
|
log "restarting PostgREST to refresh its schema cache"
|
|
if [[ -f "$SUPABASE_DIR/docker-compose.yml" ]]; then
|
|
docker compose -f "$SUPABASE_DIR/docker-compose.yml" restart rest
|
|
elif command -v systemctl >/dev/null 2>&1; then
|
|
# Non-Docker alternative: a systemd-managed PostgREST unit.
|
|
systemctl restart "$SYSTEMD_REST_UNIT"
|
|
else
|
|
log "warning: no $SUPABASE_DIR/docker-compose.yml and no systemctl; skipping PostgREST restart"
|
|
log " restart PostgREST manually so it picks up schema changes"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Build the app.
|
|
# ---------------------------------------------------------------------------
|
|
log "pnpm build"
|
|
pnpm build
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Restart the app. Prefer a Docker Compose rebuild when this is a
|
|
# Dockerfile-based deploy; otherwise fall back to the systemd unit.
|
|
# ---------------------------------------------------------------------------
|
|
log "restarting the app"
|
|
if [[ -f "$APP_DIR/Dockerfile" ]] && command -v docker >/dev/null 2>&1; then
|
|
docker compose -f docker-compose.app.yml up -d --build "$APP_SERVICE"
|
|
elif command -v systemctl >/dev/null 2>&1; then
|
|
systemctl restart "$SYSTEMD_APP_UNIT"
|
|
else
|
|
log "error: cannot restart the app (no Dockerfile-based deploy and no systemctl)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Health probe. Give the freshly restarted app a few seconds to come up,
|
|
# then require a 200 from the local health endpoint.
|
|
# ---------------------------------------------------------------------------
|
|
log "probing health at $HEALTH_URL"
|
|
health_code=""
|
|
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
|
health_code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$HEALTH_URL" || true)"
|
|
if [[ "$health_code" == "200" ]]; then
|
|
break
|
|
fi
|
|
log " attempt $attempt: got '${health_code:-no response}', retrying in 3s"
|
|
sleep 3
|
|
done
|
|
|
|
if [[ "$health_code" != "200" ]]; then
|
|
log "error: health check failed (last status: '${health_code:-no response}')" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "health check passed (200)"
|
|
log "deploy complete"
|