Vite inlines import.meta.env.VITE_DEV_AUTH_BYPASS at build time. The build container wasn't getting it, so even with DEV_AUTH_BYPASS=true on the backend the SPA still rendered the MSAL login gate. Resolve VITE_DEV_AUTH_BYPASS, falling back to DEV_AUTH_BYPASS if it's not explicitly set, and pass it through `docker run -e` to the build. A single DEV_AUTH_BYPASS=true in .env now controls both halves.
302 lines
10 KiB
Bash
Executable file
302 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# OLIVER Sales Ops Platform — deploy script.
|
|
#
|
|
# Idempotent. Safe to re-run on the dev server.
|
|
# Public URL: https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
|
|
#
|
|
# Server layout (mirrors /gsb/, /barclays-banner-builder/, etc.):
|
|
# /opt/oliver-sales-ops-platform/ — repo + docker-compose
|
|
# /var/www/html/oliver-sales-ops-platform/ — built SPA, served by Apache
|
|
#
|
|
# What it does:
|
|
# 1. Sanity (.env, docker, git on PATH).
|
|
# 2. Auto-pick free host ports:
|
|
# - prefers OSOP_DB_PORT (default 5435)
|
|
# - prefers OSOP_REDIS_PORT (default 6380)
|
|
# - prefers OSOP_BACKEND_PORT (default 8003)
|
|
# If any preferred port is taken on the host, scans upward in its
|
|
# range for the next free port and persists the chosen value to
|
|
# .env so subsequent deploys keep using it.
|
|
# 3. Render deploy/apache-osop.conf from .tmpl with the chosen
|
|
# backend port.
|
|
# 4. git pull (--no-pull to skip).
|
|
# 5. docker compose build && up -d (--no-build to skip).
|
|
# 6. Build frontend SPA (Vite) inside a one-shot node container and
|
|
# sync dist/ to /var/www/html/oliver-sales-ops-platform/
|
|
# (--no-frontend to skip).
|
|
# 7. Poll /api/health until ready.
|
|
# 8. Print URLs + admin email + apache-reload reminder.
|
|
#
|
|
# Usage:
|
|
# ./deploy/deploy.sh # full deploy
|
|
# ./deploy/deploy.sh --no-pull # skip git pull
|
|
# ./deploy/deploy.sh --no-build # skip docker rebuild
|
|
# ./deploy/deploy.sh --no-frontend # skip SPA build/copy
|
|
# ./deploy/deploy.sh --logs # tail backend logs after deploy
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
COMPOSE_PROJECT="oliver-sales-ops-platform"
|
|
URL_PATH="/oliver-sales-ops-platform"
|
|
WEB_ROOT="/var/www/html/oliver-sales-ops-platform"
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
log() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; }
|
|
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; }
|
|
ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*"; }
|
|
|
|
DO_PULL=1
|
|
DO_BUILD=1
|
|
DO_FRONTEND=1
|
|
TAIL_LOGS=0
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--no-pull) DO_PULL=0 ;;
|
|
--no-build) DO_BUILD=0 ;;
|
|
--no-frontend) DO_FRONTEND=0 ;;
|
|
--logs) TAIL_LOGS=1 ;;
|
|
--help|-h)
|
|
sed -n '2,/^set/p' "$0" | grep -E '^# ' | sed 's/^# //'
|
|
exit 0
|
|
;;
|
|
*)
|
|
err "Unknown flag: $arg (try --help)"
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# 1. Sanity
|
|
[[ -f docker-compose.yml ]] || { err "docker-compose.yml not found in $REPO_ROOT"; exit 1; }
|
|
if [[ ! -f .env ]]; then
|
|
err ".env not found. Copy .env.example and fill it in:"
|
|
err " cp .env.example .env && \$EDITOR .env"
|
|
exit 1
|
|
fi
|
|
command -v docker >/dev/null 2>&1 || { err "docker not on PATH"; exit 1; }
|
|
command -v git >/dev/null 2>&1 || { err "git not on PATH"; exit 1; }
|
|
|
|
# Avoid the dev profile (Vite container) on the deploy server. Production
|
|
# serves the SPA off the filesystem; the Vite container is local-dev-only.
|
|
unset COMPOSE_PROFILES
|
|
|
|
# ---------- helpers ----------
|
|
|
|
# True (0) if something else is listening on $1 on the host.
|
|
port_in_use() {
|
|
local port=$1
|
|
local pid=""
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
pid=$( { lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true; } | awk 'NR>1 {print $2}' | head -1 )
|
|
else
|
|
pid=$( { ss -ltnp "sport = :$port" 2>/dev/null || true; } | awk -F'pid=' 'NR>1 {print $2}' | cut -d, -f1 | head -1 )
|
|
fi
|
|
[[ -n "$pid" ]]
|
|
}
|
|
|
|
# Find the first free port. Tries $preferred first, then scans [start..end].
|
|
find_free_port() {
|
|
local preferred=$1
|
|
local start=$2
|
|
local end=$3
|
|
if ! port_in_use "$preferred"; then
|
|
printf '%s' "$preferred"
|
|
return 0
|
|
fi
|
|
local p
|
|
for ((p=start; p<=end; p++)); do
|
|
if ! port_in_use "$p"; then
|
|
printf '%s' "$p"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Idempotent KEY=VALUE upsert into .env.
|
|
set_env_var() {
|
|
local key=$1
|
|
local value=$2
|
|
local file="${REPO_ROOT}/.env"
|
|
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
|
sed -i.bak "s#^${key}=.*#${key}=${value}#" "$file"
|
|
rm -f "${file}.bak"
|
|
else
|
|
printf '%s=%s\n' "$key" "$value" >> "$file"
|
|
fi
|
|
}
|
|
|
|
# Echo current value of KEY from .env (empty if absent).
|
|
get_env_var() {
|
|
grep -E "^${1}=" "${REPO_ROOT}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' || true
|
|
}
|
|
|
|
# ---------- 2. Pick ports ----------
|
|
|
|
DEFAULT_DB_PORT=5435
|
|
DEFAULT_REDIS_PORT=6380
|
|
DEFAULT_BACKEND_PORT=8003
|
|
|
|
DB_PORT=$(get_env_var OSOP_DB_PORT); DB_PORT=${DB_PORT:-$DEFAULT_DB_PORT}
|
|
REDIS_PORT=$(get_env_var OSOP_REDIS_PORT); REDIS_PORT=${REDIS_PORT:-$DEFAULT_REDIS_PORT}
|
|
BACKEND_PORT=$(get_env_var OSOP_BACKEND_PORT); BACKEND_PORT=${BACKEND_PORT:-$DEFAULT_BACKEND_PORT}
|
|
|
|
PREV_BACKEND_PORT="$BACKEND_PORT"
|
|
|
|
log "Resolving host ports (preferred: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT)…"
|
|
|
|
RUNNING=$(docker compose ps -q 2>/dev/null | wc -l | tr -d ' ')
|
|
if [[ "$RUNNING" -gt 0 ]]; then
|
|
ok "Project '$COMPOSE_PROJECT' already has $RUNNING containers running — keeping current port assignment."
|
|
else
|
|
NEW_DB_PORT=$(find_free_port "$DB_PORT" 5435 5499) || NEW_DB_PORT=""
|
|
NEW_REDIS_PORT=$(find_free_port "$REDIS_PORT" 6380 6399) || NEW_REDIS_PORT=""
|
|
NEW_BACKEND_PORT=$(find_free_port "$BACKEND_PORT" 8003 8099) || NEW_BACKEND_PORT=""
|
|
if [[ -z "$NEW_DB_PORT" || -z "$NEW_REDIS_PORT" || -z "$NEW_BACKEND_PORT" ]]; then
|
|
err "Could not find a free port in the configured ranges."
|
|
err " db desired=$DB_PORT scanned=5435-5499"
|
|
err " redis desired=$REDIS_PORT scanned=6380-6399"
|
|
err " backend desired=$BACKEND_PORT scanned=8003-8099"
|
|
exit 1
|
|
fi
|
|
|
|
[[ "$NEW_DB_PORT" != "$DB_PORT" ]] && warn "db port $DB_PORT busy → using $NEW_DB_PORT"
|
|
[[ "$NEW_REDIS_PORT" != "$REDIS_PORT" ]] && warn "redis port $REDIS_PORT busy → using $NEW_REDIS_PORT"
|
|
[[ "$NEW_BACKEND_PORT" != "$BACKEND_PORT" ]] && warn "backend port $BACKEND_PORT busy → using $NEW_BACKEND_PORT"
|
|
|
|
DB_PORT=$NEW_DB_PORT
|
|
REDIS_PORT=$NEW_REDIS_PORT
|
|
BACKEND_PORT=$NEW_BACKEND_PORT
|
|
|
|
set_env_var OSOP_DB_PORT "$DB_PORT"
|
|
set_env_var OSOP_REDIS_PORT "$REDIS_PORT"
|
|
set_env_var OSOP_BACKEND_PORT "$BACKEND_PORT"
|
|
|
|
ok "Ports: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT (persisted to .env)"
|
|
fi
|
|
|
|
# ---------- 3. Render apache-osop.conf from template ----------
|
|
|
|
APACHE_TMPL="$REPO_ROOT/deploy/apache-osop.conf.tmpl"
|
|
APACHE_CONF="$REPO_ROOT/deploy/apache-osop.conf"
|
|
if [[ -f "$APACHE_TMPL" ]]; then
|
|
sed "s#__BACKEND_PORT__#${BACKEND_PORT}#g" "$APACHE_TMPL" > "$APACHE_CONF"
|
|
ok "Rendered apache-osop.conf with backend port $BACKEND_PORT"
|
|
else
|
|
warn "apache-osop.conf.tmpl missing — leaving deploy/apache-osop.conf untouched."
|
|
fi
|
|
|
|
# ---------- 4. git pull ----------
|
|
|
|
if (( DO_PULL )); then
|
|
log "git pull origin main"
|
|
git pull --ff-only origin main
|
|
fi
|
|
|
|
# ---------- 5. Backend build + up ----------
|
|
|
|
if (( DO_BUILD )); then
|
|
log "docker compose build"
|
|
docker compose build
|
|
fi
|
|
|
|
log "docker compose up -d (db + redis + backend)"
|
|
docker compose up -d
|
|
|
|
# ---------- 6. Frontend build + sync ----------
|
|
|
|
if (( DO_FRONTEND )); then
|
|
# VITE_DEV_AUTH_BYPASS controls whether the SPA renders the MSAL login
|
|
# gate (false → MSAL, true → straight to the app). Mirror DEV_AUTH_BYPASS
|
|
# so a single env var decides both sides.
|
|
VITE_BYPASS=$(get_env_var VITE_DEV_AUTH_BYPASS)
|
|
if [[ -z "$VITE_BYPASS" ]]; then
|
|
VITE_BYPASS=$(get_env_var DEV_AUTH_BYPASS)
|
|
fi
|
|
[[ -z "$VITE_BYPASS" ]] && VITE_BYPASS="false"
|
|
log "Building Vite SPA (VITE_DEV_AUTH_BYPASS=${VITE_BYPASS}) in a one-shot node:20 container…"
|
|
docker run --rm \
|
|
-v "$REPO_ROOT/frontend:/app" \
|
|
-w /app \
|
|
-e VITE_DEV_AUTH_BYPASS="$VITE_BYPASS" \
|
|
node:20-alpine \
|
|
sh -c "npm install --silent && npm run build"
|
|
|
|
if [[ ! -d "$REPO_ROOT/frontend/dist" ]]; then
|
|
err "Vite build did not produce frontend/dist — aborting frontend sync."
|
|
exit 1
|
|
fi
|
|
|
|
log "Syncing frontend/dist/ → $WEB_ROOT/"
|
|
if [[ ! -d "$WEB_ROOT" ]]; then
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
sudo mkdir -p "$WEB_ROOT"
|
|
else
|
|
mkdir -p "$WEB_ROOT"
|
|
fi
|
|
fi
|
|
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
if [[ -w "$WEB_ROOT" ]]; then
|
|
rsync -a --delete "$REPO_ROOT/frontend/dist/" "$WEB_ROOT/"
|
|
else
|
|
sudo rsync -a --delete "$REPO_ROOT/frontend/dist/" "$WEB_ROOT/"
|
|
fi
|
|
else
|
|
if [[ -w "$WEB_ROOT" ]]; then
|
|
rm -rf "$WEB_ROOT"/*
|
|
cp -a "$REPO_ROOT/frontend/dist/." "$WEB_ROOT/"
|
|
else
|
|
sudo rm -rf "$WEB_ROOT"/*
|
|
sudo cp -a "$REPO_ROOT/frontend/dist/." "$WEB_ROOT/"
|
|
fi
|
|
fi
|
|
ok "SPA synced to $WEB_ROOT"
|
|
fi
|
|
|
|
# ---------- 7. Health poll ----------
|
|
|
|
log "Waiting for backend /api/health on :$BACKEND_PORT (max 60s)…"
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null 2>&1; then
|
|
ok "Backend healthy"
|
|
break
|
|
fi
|
|
sleep 2
|
|
if (( i == 30 )); then
|
|
err "Backend did not become healthy within 60s. Recent logs:"
|
|
docker compose logs backend --tail 40 || true
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ---------- 8. Report ----------
|
|
|
|
ADMIN_EMAIL=$(get_env_var DEV_AUTH_EMAIL); ADMIN_EMAIL=${ADMIN_EMAIL:-admin@oliver.agency}
|
|
PUBLIC_URL=$(get_env_var APP_PUBLIC_URL)
|
|
ok "Deploy complete."
|
|
echo
|
|
echo " Backend (local): http://127.0.0.1:${BACKEND_PORT}/api/health"
|
|
[[ -n "$PUBLIC_URL" ]] && echo " Public URL: ${PUBLIC_URL%/}${URL_PATH}/"
|
|
[[ -d "$WEB_ROOT" ]] && echo " SPA on disk: $WEB_ROOT"
|
|
echo " Admin user: $ADMIN_EMAIL (role=admin, no password — DEV_AUTH_BYPASS=true)"
|
|
echo " Ports: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT"
|
|
echo
|
|
echo " Apache include line for the merged vhost:"
|
|
echo " Include $REPO_ROOT/deploy/apache-osop.conf"
|
|
if [[ "$BACKEND_PORT" != "$PREV_BACKEND_PORT" ]] || ! grep -qF "$REPO_ROOT/deploy/apache-osop.conf" /etc/apache2/sites-enabled/*.conf 2>/dev/null; then
|
|
echo
|
|
warn "Backend port changed (or first deploy). Reload Apache to pick up the new ProxyPass:"
|
|
echo " sudo apachectl configtest && sudo systemctl reload apache2"
|
|
fi
|
|
echo
|
|
|
|
if (( TAIL_LOGS )); then
|
|
log "Tailing backend logs (Ctrl-C to stop)…"
|
|
docker compose logs -f backend
|
|
fi
|