prospector/deploy/deploy-server.sh
Natalie c56f2dfcf6
Some checks are pending
CI / verify (push) Waiting to run
fix(deploy): target com.uvlava.ct.services + DO managed-PG CA cert
SERVER_HOST default → 138.197.120.105 (old lime IP released). Ship DO CA cert
+ NODE_EXTRA_CA_CERTS in the unit (managed PG = self-signed chain → node
SELF_SIGNED_CERT_IN_CHAIN). Box must be a DB trusted-source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 02:44:18 -04:00

148 lines
7.3 KiB
Bash
Executable file

#!/bin/bash
set -euo pipefail
# Deploy the prospector backend (NestJS) to lime (com.uvlava.ct.services).
#
# Build locally (nest build -> dist/), ship dist/ + runtime package files +
# migrations/ + the built PWA, ensure node20 + psql-16, install a systemd unit,
# provision the app's .env (dotenv — NOT a systemd EnvironmentFile; ConfigModule
# reads envFilePath ['.env.local','.env'] and dotenv does NOT override
# process.env, so EnvironmentFile silently shadows it), run pending SQL
# migrations, and (re)start. Binds 0.0.0.0:3210 but lime exposes no public app
# port (DO firewall = WG + SSH only) — reachable only over the mesh / VPC.
#
# DATABASE (one-time, secret-bearing — NOT done here):
# doctl databases db create <cluster> prospector
# doctl databases user create <cluster> prospector # generates pw
# # as doadmin, on the prospector DB:
# ALTER DATABASE prospector OWNER TO prospector;
# GRANT ALL ON SCHEMA public TO prospector; ALTER SCHEMA public OWNER TO prospector;
# then fill the PROSPECTOR_DB_* lines in /opt/prospector/.env on lime.
# This script runs migrations + starts only once .env has real DB creds.
#
# Usage:
# ./deploy-server.sh # build + ship + migrate + restart
# ./deploy-server.sh --skip-build # ship the current dist/ as-is
# SERVER_HOST=10.9.0.5 ./deploy-server.sh # over the wg mesh
SERVER_HOST="${SERVER_HOST:-138.197.120.105}" # com.uvlava.ct.services; 10.9.0.5 = mesh
# NOTE: the box must be a TRUSTED SOURCE on the lilith-store-pg managed cluster
# (DO console / databases firewall) or migrations + the app's DB connect time out.
REMOTE_DIR="/opt/prospector"
SERVICE_NAME="prospector"
PORT="3210"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SKIP_BUILD=false
for a in "$@"; do [ "$a" = "--skip-build" ] && SKIP_BUILD=true; done
SSH="ssh -o StrictHostKeyChecking=accept-new"
[ -f "$HOME/.ssh/id_ed25519_1984" ] && SSH="$SSH -i $HOME/.ssh/id_ed25519_1984"
RSH="$SSH"
R="root@$SERVER_HOST"
say() { printf '\033[0;32m▸\033[0m %s\n' "$1"; }
die() { printf '\033[0;31m✗ %s\033[0m\n' "$1" >&2; exit 1; }
say "Checking SSH to $SERVER_HOST"
$SSH "$R" 'echo ok' >/dev/null 2>&1 || die "cannot reach $R"
if [ "$SKIP_BUILD" = false ]; then
say "Building backend (nest build) + PWA (vite build) locally"
( cd "$APP_ROOT" && npm run build ) || die "backend build failed"
[ -f "$APP_ROOT/web/package.json" ] && { ( cd "$APP_ROOT/web" && npm run build ) || die "web build failed"; }
fi
[ -d "$APP_ROOT/dist" ] || die "no dist/ — build first (drop --skip-build)"
say "Ensuring node20 + psql-16 on the droplet"
$SSH "$R" 'command -v node >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1 && apt-get install -y nodejs >/dev/null 2>&1; }
command -v psql >/dev/null 2>&1 && psql --version >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; apt-get install -y postgresql-client-16 >/dev/null 2>&1; }
node -v' >/dev/null || die "node/psql provisioning failed"
say "Shipping dist/ + package files + migrations/ (+ built PWA if present)"
$SSH "$R" "mkdir -p $REMOTE_DIR"
rsync -az --delete -e "$RSH" "$APP_ROOT/dist/" "$R:$REMOTE_DIR/dist/"
rsync -az -e "$RSH" "$APP_ROOT/package.json" "$R:$REMOTE_DIR/package.json"
[ -f "$APP_ROOT/package-lock.json" ] && rsync -az -e "$RSH" "$APP_ROOT/package-lock.json" "$R:$REMOTE_DIR/"
[ -d "$APP_ROOT/migrations" ] && rsync -az -e "$RSH" "$APP_ROOT/migrations/" "$R:$REMOTE_DIR/migrations/"
for wd in "$APP_ROOT/web/dist" "$APP_ROOT/dist/web"; do
[ -d "$wd" ] && { rsync -az --delete -e "$RSH" "$wd/" "$R:$REMOTE_DIR/web-dist/"; break; }
done
say "Installing runtime deps (npm ci --omit=dev)"
$SSH "$R" "cd $REMOTE_DIR && (npm ci --omit=dev || npm install --omit=dev)" >/dev/null
say "Provisioning $REMOTE_DIR/.env (dotenv; preserves existing secrets)"
$SSH "$R" "test -f $REMOTE_DIR/.env" 2>/dev/null && say ".env exists — preserving" || \
$SSH "$R" "gen() { openssl rand -hex \"\$1\" 2>/dev/null || head -c \"\$1\" /dev/urandom | xxd -p | tr -d '\n'; }
cat > $REMOTE_DIR/.env <<EOF
PROSPECTOR_API_PORT=$PORT
NODE_ENV=production
PROSPECTOR_WEB_DIST=$REMOTE_DIR/web-dist
# Fill these from the prospector role on lilith-store-pg (see header).
PROSPECTOR_DB_HOST=__SET_ME__
PROSPECTOR_DB_PORT=25060
PROSPECTOR_DB_NAME=prospector
PROSPECTOR_DB_USER=prospector
PROSPECTOR_DB_PASSWORD=__SET_ME__
PROSPECTOR_DB_SSL=true
PROSPECTOR_SERVICE_TOKEN=\$(gen 32)
# deps (HTTP) — placeholders until people/mac-sync/mr-number deploy
PEOPLE_BASE_URL=http://10.9.0.5:3061
PEOPLE_SERVICE_TOKEN=\$(gen 24)
MACSYNC_BASE_URL=http://10.9.0.5:3201
MACSYNC_DEVICE_ID=lime
MACSYNC_SERVICE_TOKEN=\$(gen 24)
MRNUMBER_BASE_URL=http://10.9.0.6:8787
MRNUMBER_SERVICE_TOKEN=\$(gen 24)
EOF
chmod 600 $REMOTE_DIR/.env"
say "Ensuring DO managed-PG CA cert on the droplet (NODE_EXTRA_CA_CERTS)"
$SSH "$R" "test -f $REMOTE_DIR/do-ca.crt" 2>/dev/null || {
_CID=ef22022e-de47-4a4d-8303-0166dbf891d6
curl -s -H "Authorization: Bearer $(cat "$HOME/.vault/do-pat-ct.token")" "https://api.digitalocean.com/v2/databases/$_CID/ca" \
| python3 -c 'import sys,json,base64;sys.stdout.write(base64.b64decode(json.load(sys.stdin)["ca"]["certificate"]).decode())' > /tmp/_do-ca.crt
rsync -az -e "$RSH" /tmp/_do-ca.crt "$R:$REMOTE_DIR/do-ca.crt" && rm -f /tmp/_do-ca.crt
}
say "Installing systemd unit $SERVICE_NAME (reads its own .env; no EnvironmentFile)"
$SSH "$R" "cat > /etc/systemd/system/$SERVICE_NAME.service <<EOF
[Unit]
Description=prospector backend (NestJS) — AFK auto-send + operator PWA
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=$REMOTE_DIR
Environment=NODE_EXTRA_CA_CERTS=$REMOTE_DIR/do-ca.crt
ExecStart=/usr/bin/node $REMOTE_DIR/dist/main.js
Restart=always
RestartSec=5
StandardOutput=append:/var/log/$SERVICE_NAME.log
StandardError=append:/var/log/$SERVICE_NAME.log
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable $SERVICE_NAME >/dev/null 2>&1 || true"
if $SSH "$R" "grep -q __SET_ME__ $REMOTE_DIR/.env" 2>/dev/null; then
printf '\033[1;33m⚠ PROSPECTOR_DB_* not set in %s:%s/.env — created DB+role, fill creds, then re-run.\033[0m\n' "$SERVER_HOST" "$REMOTE_DIR"
exit 0
fi
say "Applying pending SQL migrations (ledger-tracked, sslmode=require)"
$SSH "$R" 'set -e; . '"$REMOTE_DIR"'/.env
URI="postgres://$PROSPECTOR_DB_USER:$PROSPECTOR_DB_PASSWORD@$PROSPECTOR_DB_HOST:$PROSPECTOR_DB_PORT/$PROSPECTOR_DB_NAME?sslmode=require"
psql -v ON_ERROR_STOP=1 -q -d "$URI" -c "CREATE TABLE IF NOT EXISTS _prospector_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())" >/dev/null
for f in '"$REMOTE_DIR"'/migrations/*.sql; do b=$(basename "$f")
[ "$(psql -tAq -d "$URI" -c "SELECT 1 FROM _prospector_migrations WHERE filename='"'"'$b'"'"'")" = "1" ] && continue
psql -v ON_ERROR_STOP=1 -q -d "$URI" -f "$f" >/dev/null
psql -q -d "$URI" -c "INSERT INTO _prospector_migrations(filename) VALUES ('"'"'$b'"'"')" >/dev/null
echo " applied $b"; done'
say "Restarting $SERVICE_NAME"
$SSH "$R" "systemctl restart $SERVICE_NAME"; sleep 4
$SSH "$R" "curl -fsS http://127.0.0.1:$PORT/ >/dev/null 2>&1" \
&& say "prospector up on :$PORT (mesh-only)" || printf '\033[1;33m⚠ started but / didnt answer yet; check /var/log/%s.log\033[0m\n' "$SERVICE_NAME"