Improve deploy.sh: TOC/plan summary, per-section confirm, nginx setup

- Collect all config upfront then print a plan showing every step
  (active or skipped) before doing any work
- Prompt "Proceed? [Y/n]" at each section for granular control
- Add nginx reverse proxy setup with certbot SSL support
- Add overall "Continue?" confirmation after plan is shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-05 16:10:17 -04:00
parent 619e83acb6
commit fde9e31b14

419
deploy.sh
View File

@ -1,8 +1,8 @@
#!/bin/bash
#
# Dataflow Deploy Script
# First run: creates DB user/database, deploys schema, builds UI, installs service
# Subsequent runs: updates functions, rebuilds UI, restarts service
# First run: full install (database, schema, UI, nginx, systemd)
# Subsequent runs: update functions, UI, and restart service
#
set -e
@ -10,74 +10,174 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "Dataflow Deploy"
echo "==============="
echo ""
# ── Helpers ───────────────────────────────────────────────────────────────────
# ── First-time setup ──────────────────────────────────────────────────────────
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 ────────────────────────────────────────────────────────────
section "Mode"
if [ ! -f .env ]; then
echo "No .env found — running first-time setup."
echo ""
echo " No .env found — first-time install"
MODE=install
else
echo " .env found — update mode"
MODE=update
export $(cat .env | grep -v '^#' | xargs)
fi
# Admin credentials (needed to create user/database)
echo "PostgreSQL Admin Credentials"
# ── 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 ""
echo ""
# App credentials
echo "Application Database"
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 name [dataflow]: " DB_NAME; DB_NAME=${DB_NAME:-dataflow}
read -p " App username [dataflow]: " DB_USER; DB_USER=${DB_USER:-dataflow}
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 ""
echo ""
# API config
echo "API Configuration"
section "API"
read -p " Port [3020]: " API_PORT; API_PORT=${API_PORT:-3020}
read -p " Environment [production]:" NODE_ENV; NODE_ENV=${NODE_ENV:-production}
echo ""
# Install Node dependencies
echo "Installing Node.js dependencies..."
npm install --prefix . --omit=dev -q
cd ui && npm install -q && cd ..
echo "✓ Dependencies installed"
echo ""
# Test admin connection
echo "Testing admin connection..."
export PGPASSWORD="$ADMIN_PASS"
if ! psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -c '\q' 2>/dev/null; then
echo "✗ Cannot connect as $ADMIN_USER — check credentials"
exit 1
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
echo "✓ Admin connection OK"
# Create app user
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'" | grep -q 1; then
echo "✓ User '$DB_USER' already exists"
"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
echo "✓ User '$DB_USER' created"
-c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" > /dev/null && ok \
|| fail "Could not create user"
fi
# Create database
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 "✓ Database '$DB_NAME' already exists"
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
echo "✓ Database '$DB_NAME' created"
-c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" > /dev/null && ok \
|| fail "Could not create database"
fi
unset PGPASSWORD
# Write .env
step "Writing .env"
cat > .env << ENVEOF
# Database Configuration
DB_HOST=$DB_HOST
@ -90,35 +190,27 @@ DB_PASSWORD=$DB_PASSWORD
API_PORT=$API_PORT
NODE_ENV=$NODE_ENV
ENVEOF
echo "✓ .env written"
echo ""
ok
# Deploy schema (first time only)
export PGPASSWORD="$DB_PASSWORD"
echo "Deploying schema..."
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/schema.sql -q
echo "✓ Schema deployed"
DEPLOY_FN=y
BUILD_UI=y
INSTALL_SERVICE=y
RESTART=y
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"
# ── Update mode ───────────────────────────────────────────────────────────────
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
export $(cat .env | grep -v '^#' | xargs)
info "skipped"
fi
echo "Current database: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
read -p "Change database target? [y/N]: " IN
if [[ "$IN" =~ ^[Yy]$ ]]; then
echo ""
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 name [${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 current): " IN; echo ""
if [ -n "$IN" ]; then DB_PASSWORD=$IN; 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
@ -131,69 +223,174 @@ DB_PASSWORD=$DB_PASSWORD
API_PORT=${API_PORT:-3020}
NODE_ENV=${NODE_ENV:-production}
ENVEOF
echo "✓ .env updated"
ok
else
info "no changes"
fi
echo ""
# Test connection
section "Database Connection"
step "Testing"
export PGPASSWORD="$DB_PASSWORD"
echo "Testing database connection..."
if ! psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -c '\q' 2>/dev/null; then
echo "✗ Cannot connect — check credentials"
exit 1
fi
echo "✓ Connected to ${DB_NAME} on ${DB_HOST}:${DB_PORT}"
echo ""
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -c '\q' 2>/dev/null && ok \
|| fail "Cannot connect — check credentials"
read -p "Redeploy SQL functions? [Y/n]: " DEPLOY_FN
read -p "Rebuild UI? [Y/n]: " BUILD_UI
read -p "Restart API server? [Y/n]: " RESTART
INSTALL_SERVICE=n
# 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
# ── Shared steps ──────────────────────────────────────────────────────────────
# ── UI ────────────────────────────────────────────────────────────────────────
if [[ ! "$DEPLOY_FN" =~ ^[Nn]$ ]]; then
echo ""
echo "Deploying SQL functions..."
export PGPASSWORD="$DB_PASSWORD"
psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q
echo "✓ Functions deployed"
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
if [[ ! "$BUILD_UI" =~ ^[Nn]$ ]]; then
echo ""
echo "Building UI..."
cd ui && npm run build && cd ..
echo "✓ UI built"
# ── 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
# Systemd service
echo ""
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"
if [ ! -f "$SERVICE_FILE" ] && [[ "$INSTALL_SERVICE" =~ ^[Yy]$ ]]; then
read -p "Install systemd service? (requires sudo) [Y/n]: " IN
if [[ ! "$IN" =~ ^[Nn]$ ]]; then
sudo cp "$SCRIPT_DIR/dataflow.service" "$SERVICE_FILE"
sudo systemctl daemon-reload
sudo systemctl enable dataflow
echo "✓ Service installed and enabled"
fi
fi
if [[ ! "$RESTART" =~ ^[Nn]$ ]]; then
echo ""
if confirm "Systemd Service"; then
if [ -f "$SERVICE_FILE" ]; then
sudo systemctl restart dataflow
sleep 1
if systemctl is-active --quiet dataflow; then
echo "✓ Service restarted and running"
info "already installed"
else
echo "✗ Service failed — check: journalctl -u dataflow -n 30"
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"
fi
else
echo " Systemd service not installed. Start manually: node api/server.js"
fi
info "skipped"
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 ""
echo "✓ Done — http://localhost:${API_PORT}"