feat(tenant): setup CLI, CD glue, bundled compose, docs + security fix
Tier 2 productization for single-tenant instances, plus fixes from an adversarial review pass. New: - scripts/setup.mjs: 'pnpm setup' CLI (doctor/provision/create-admin/all), idempotent, creates the first admin via the Supabase Admin API (email_confirm), flag > env > .env.local precedence, masked interactive prompts, --yes/--force. - scripts/apply-migrations.sh + deploy/deploy.sh: reproducible, idempotent CD glue (previously only on the prod droplet), tracked via public._migrations_applied. - docker-compose.full.yml + .env.full.example: one-command app + self-hosted Supabase bundle (compose config validated). - docs/SINGLE_TENANT.md (deploy + CLI guide) and docs/CUSTOMIZE.md (the documented lib/tenant.ts config pattern for white-labeling); README pointer. - __tests__/tenant.test.ts: 19 unit tests for resolveTenantConfig + isMarketingPath. Security (found by review, pre-existing): - Drop the stale handle_space_signup() auth trigger, which derived member role and space from client-controlled raw_user_meta_data and could self-insert an admin. Membership is created only by the vetted server actions. Migration scripts/056_drop_stale_signup_trigger.sql + folded into schema.sql. Fixes: - setup.mjs masked-password prompt hung (callback passed to readline/promises question); use the awaited form. - notifications-enqueue.test: base-URL default is now localhost (platform-domain fallback removed), not hackerspace.sh. - docs accuracy (logo/BrandMark, apply-migrations does not restart PostgREST, --yes). Gates: tsc 0, lint 0 errors, 664 unit tests, build ok.
This commit is contained in:
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/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 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"
|
||||
Reference in New Issue
Block a user