dataflow/deploy.sh
Paul Trowbridge 1b34dc830b Fix deploy.sh: don't prompt for systemd service if already installed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 16:13:06 -04:00

410 lines
15 KiB
Bash
Executable File

#!/bin/bash
#
# Dataflow Deploy Script
# First run: full install (database, schema, UI, nginx, systemd)
# Subsequent runs: update functions, UI, and restart service
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ── Helpers ───────────────────────────────────────────────────────────────────
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
RESET='\033[0m'
section() { echo ""; echo -e "${BOLD}── $1 ──${RESET}"; }
step() { printf " %-42s" "$1..."; }
ok() { echo -e "${GREEN}${RESET}"; }
fail() { echo -e "${RED}$1${RESET}"; exit 1; }
info() { echo -e " ${DIM}$1${RESET}"; }
warn() { echo -e " ${YELLOW}$1${RESET}"; }
confirm() {
# confirm "Section name" — prints section header, asks to proceed
# Returns 0 to proceed, 1 to skip
section "$1"
read -p " Proceed? [Y/n]: " _yn
[[ ! "$_yn" =~ ^[Nn]$ ]]
}
# ── Mode detection ────────────────────────────────────────────────────────────
echo ""
echo " This script covers:"
echo " 1. Database — create user/database, deploy schema + functions"
echo " 2. UI — build from source"
echo " 3. Nginx — reverse proxy config + SSL via certbot"
echo " 4. Service — install and enable systemd unit"
echo " 5. Restart — start or restart the API server"
echo ""
echo " Each step will be confirmed before running."
echo " Press Ctrl+C at any time to abort."
section "Mode"
if [ ! -f .env ]; then
echo " No .env found — first-time install"
MODE=install
else
echo " .env found — update mode"
MODE=update
export $(cat .env | grep -v '^#' | xargs)
fi
# ── Phase 1: Collect all config ───────────────────────────────────────────────
if [ "$MODE" = "install" ]; then
section "PostgreSQL Admin"
info "Needed to create the app user and database"
read -p " Admin username [postgres]: " ADMIN_USER; ADMIN_USER=${ADMIN_USER:-postgres}
read -s -p " Admin password: " ADMIN_PASS; echo ""
section "Application Database"
read -p " Host [localhost]: " DB_HOST; DB_HOST=${DB_HOST:-localhost}
read -p " Port [5432]: " DB_PORT; DB_PORT=${DB_PORT:-5432}
read -p " Database [dataflow]: " DB_NAME; DB_NAME=${DB_NAME:-dataflow}
read -p " App user [dataflow]: " DB_USER; DB_USER=${DB_USER:-dataflow}
read -s -p " App password: " DB_PASSWORD; echo ""
section "API"
read -p " Port [3020]: " API_PORT; API_PORT=${API_PORT:-3020}
read -p " Environment [production]:" NODE_ENV; NODE_ENV=${NODE_ENV:-production}
section "Nginx"
read -p " Set up nginx reverse proxy? [Y/n]: " _yn
if [[ ! "$_yn" =~ ^[Nn]$ ]]; then
read -p " Domain (e.g. dataflow.example.com): " NGINX_DOMAIN
fi
DO_DEPS=y; DO_DB=y; DO_SCHEMA=y; DO_FN=y; DO_BUILD=y; DO_SERVICE=y; DO_RESTART=y
else
section "Database"
echo " Current: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
read -p " Change target? [y/N]: " _yn
if [[ "$_yn" =~ ^[Yy]$ ]]; then
read -p " Host [${DB_HOST}]: " _in; DB_HOST=${_in:-$DB_HOST}
read -p " Port [${DB_PORT}]: " _in; DB_PORT=${_in:-$DB_PORT}
read -p " Database [${DB_NAME}]: " _in; DB_NAME=${_in:-$DB_NAME}
read -p " User [${DB_USER}]: " _in; DB_USER=${_in:-$DB_USER}
read -s -p " Password (blank = keep): " _in; echo ""
if [ -n "$_in" ]; then DB_PASSWORD=$_in; fi
CHANGE_DB=y
fi
section "Select steps to run"
read -p " Redeploy SQL functions? [Y/n]: " DO_FN; DO_FN=${DO_FN:-y}
read -p " Rebuild UI? [Y/n]: " DO_BUILD; DO_BUILD=${DO_BUILD:-y}
read -p " Set up / update nginx? [y/N]: " DO_NGINX
read -p " Restart API service? [Y/n]: " DO_RESTART; DO_RESTART=${DO_RESTART:-y}
if [[ "$DO_NGINX" =~ ^[Yy]$ ]]; then
read -p " Nginx domain: " NGINX_DOMAIN
fi
fi
# ── Phase 2: Plan summary ─────────────────────────────────────────────────────
section "Plan"
echo " Mode: $( [ "$MODE" = "install" ] && echo "First-time install" || echo "Update" )"
echo " Database: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo " API port: ${API_PORT:-3020}"
echo ""
echo " Steps:"
if [ "$MODE" = "install" ]; then
echo " • Install Node.js dependencies"
echo " • Test admin connection and create DB user/database"
echo " • Deploy schema and SQL functions"
[ -n "$NGINX_DOMAIN" ] && echo " • Configure nginx → $NGINX_DOMAIN" \
|| echo " • Nginx — skipped (no domain provided)"
echo " • Build UI"
echo " • Install systemd service"
echo " • Start service"
else
[ "${CHANGE_DB}" = "y" ] && echo " • Update .env with new database target"
[[ ! "$DO_FN" =~ ^[Nn]$ ]] && echo " • Redeploy SQL functions" \
|| echo " • SQL functions — skipped"
[[ ! "$DO_BUILD" =~ ^[Nn]$ ]] && echo " • Rebuild UI" \
|| echo " • UI build — skipped"
[ -n "$NGINX_DOMAIN" ] && echo " • Configure nginx → $NGINX_DOMAIN" \
|| echo " • Nginx — skipped"
[[ ! "$DO_RESTART" =~ ^[Nn]$ ]] && echo " • Restart API service" \
|| echo " • Service restart — skipped"
fi
echo ""
read -p " Continue? [Y/n]: " _yn
[[ "$_yn" =~ ^[Nn]$ ]] && echo " Aborted." && exit 0
# ── Phase 3: Execute ──────────────────────────────────────────────────────────
if [ "$MODE" = "install" ]; then
# Dependencies
if confirm "Dependencies"; then
step "API (npm install)"
npm install --omit=dev -q && ok || fail "npm install failed"
step "UI (npm install)"
cd ui && npm install -q && cd .. && ok || fail "ui npm install failed"
else
info "skipped"
fi
# PostgreSQL
if confirm "PostgreSQL"; then
step "Testing admin connection"
export PGPASSWORD="$ADMIN_PASS"
psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -c '\q' 2>/dev/null && ok \
|| fail "Cannot connect as $ADMIN_USER"
step "Creating user '$DB_USER'"
if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -tAc \
"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" 2>/dev/null | grep -q 1; then
echo -e "${DIM}already exists${RESET}"
else
psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \
-c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" > /dev/null && ok \
|| fail "Could not create user"
fi
step "Creating database '$DB_NAME'"
if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -lqt \
| cut -d'|' -f1 | grep -qw "$DB_NAME"; then
echo -e "${DIM}already exists${RESET}"
else
psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \
-c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" > /dev/null && ok \
|| fail "Could not create database"
fi
unset PGPASSWORD
step "Writing .env"
cat > .env << ENVEOF
# Database Configuration
DB_HOST=$DB_HOST
DB_PORT=$DB_PORT
DB_NAME=$DB_NAME
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
# API Configuration
API_PORT=$API_PORT
NODE_ENV=$NODE_ENV
ENVEOF
ok
export PGPASSWORD="$DB_PASSWORD"
step "Deploying schema"
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/schema.sql -q && ok \
|| fail "Schema deploy failed"
step "Deploying functions"
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q && ok \
|| fail "Functions deploy failed"
else
info "skipped"
fi
else
# Update: save .env if DB target changed
section ".env"
if [ "${CHANGE_DB}" = "y" ]; then
step "Writing updated .env"
cat > .env << ENVEOF
# Database Configuration
DB_HOST=$DB_HOST
DB_PORT=$DB_PORT
DB_NAME=$DB_NAME
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
# API Configuration
API_PORT=${API_PORT:-3020}
NODE_ENV=${NODE_ENV:-production}
ENVEOF
ok
else
info "no changes"
fi
# Test connection
section "Database Connection"
step "Testing"
export PGPASSWORD="$DB_PASSWORD"
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -c '\q' 2>/dev/null && ok \
|| fail "Cannot connect — check credentials"
# SQL functions
if [[ ! "$DO_FN" =~ ^[Nn]$ ]]; then
if confirm "SQL Functions"; then
step "Deploying functions"
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q && ok \
|| fail "Functions deploy failed"
else
info "skipped"
fi
else
section "SQL Functions"
info "skipped"
fi
fi
# ── UI ────────────────────────────────────────────────────────────────────────
if [ "$MODE" = "install" ] || [[ ! "$DO_BUILD" =~ ^[Nn]$ ]]; then
if confirm "UI Build"; then
step "Building"
cd ui && npm run build > /dev/null 2>&1 && cd .. && ok \
|| fail "UI build failed"
else
info "skipped"
fi
else
section "UI Build"
info "skipped"
fi
# ── Nginx ─────────────────────────────────────────────────────────────────────
if [ -n "$NGINX_DOMAIN" ]; then
if confirm "Nginx ($NGINX_DOMAIN)"; then
CONF_NAME=$(echo "$NGINX_DOMAIN" | cut -d. -f1)
CONF_PATH="/etc/nginx/sites-enabled/$CONF_NAME"
CERT_PATH="/etc/letsencrypt/live/$NGINX_DOMAIN/fullchain.pem"
TMP_CONF=$(mktemp)
if [ -f "$CERT_PATH" ]; then
cat > "$TMP_CONF" << NGINXEOF
server {
listen 80;
listen [::]:80;
server_name $NGINX_DOMAIN;
location / { return 301 https://\$host\$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name $NGINX_DOMAIN;
ssl_certificate $CERT_PATH;
ssl_certificate_key /etc/letsencrypt/live/$NGINX_DOMAIN/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
keepalive_timeout 70;
sendfile on;
client_max_body_size 80m;
location / {
proxy_pass http://localhost:${API_PORT:-3020};
}
}
NGINXEOF
else
cat > "$TMP_CONF" << NGINXEOF
server {
listen 80;
listen [::]:80;
server_name $NGINX_DOMAIN;
location / {
proxy_pass http://localhost:${API_PORT:-3020};
}
}
NGINXEOF
fi
step "Writing /etc/nginx/sites-enabled/$CONF_NAME"
sudo cp "$TMP_CONF" "$CONF_PATH" && rm "$TMP_CONF" && ok \
|| fail "Could not write nginx config (check sudo)"
step "Testing nginx config"
sudo nginx -t > /dev/null 2>&1 && ok \
|| fail "nginx config invalid — run: sudo nginx -t"
step "Reloading nginx"
sudo systemctl reload nginx && ok \
|| fail "nginx reload failed"
if [ ! -f "$CERT_PATH" ]; then
warn "No SSL cert found for $NGINX_DOMAIN"
read -p " Run certbot now? [Y/n]: " _yn
if [[ ! "$_yn" =~ ^[Nn]$ ]]; then
step "Running certbot"
sudo certbot --nginx -d "$NGINX_DOMAIN" --non-interactive --agree-tos \
--redirect -m "admin@$NGINX_DOMAIN" > /dev/null 2>&1 && ok \
|| fail "certbot failed — run manually: sudo certbot --nginx -d $NGINX_DOMAIN"
fi
fi
else
info "skipped"
fi
else
section "Nginx"
info "skipped"
fi
# ── Systemd service ───────────────────────────────────────────────────────────
SERVICE_FILE="/etc/systemd/system/dataflow.service"
section "Systemd Service"
if [ -f "$SERVICE_FILE" ]; then
info "already installed"
else
read -p " Not installed. Install now? (requires sudo) [y/N]: " _yn
if [[ "$_yn" =~ ^[Yy]$ ]]; then
step "Installing service"
sudo cp "$SCRIPT_DIR/dataflow.service" "$SERVICE_FILE" && ok \
|| fail "Could not install service"
step "Enabling on boot"
sudo systemctl daemon-reload && sudo systemctl enable dataflow > /dev/null 2>&1 && ok \
|| fail "Could not enable service"
else
info "skipped"
fi
fi
# ── API Server ────────────────────────────────────────────────────────────────
if [ "$MODE" = "install" ] || [[ ! "$DO_RESTART" =~ ^[Nn]$ ]]; then
if confirm "API Server"; then
if [ -f "$SERVICE_FILE" ]; then
step "Restarting dataflow service"
sudo systemctl restart dataflow && sleep 1
systemctl is-active --quiet dataflow && ok \
|| fail "Service failed — check: journalctl -u dataflow -n 30"
else
info "systemd service not installed — start manually: node api/server.js"
fi
else
info "skipped"
fi
else
section "API Server"
info "skipped"
fi
# ── Done ──────────────────────────────────────────────────────────────────────
section "Done"
echo " API: http://localhost:${API_PORT:-3020}"
[ -n "$NGINX_DOMAIN" ] && echo " Web: https://$NGINX_DOMAIN"
echo " Logs: journalctl -u dataflow -f"
echo ""