Subpath-aware deploy for /adeo-maturity/ behind the shared optical-dev vhost. Auto-picks a free host port (prefers 3102, scans 3102-3199) and persists it to .env so re-deploys are idempotent. Renders the Apache conf from a template on each run. - script.js: detect URL prefix at load time and prepend it to all /api/ calls, so the same code works at the root locally and under a sub-path behind Apache. - Dockerfile: fix broken package.json copy (lives at repo root, not server/) and install python3 + reportlab + openpyxl for the sync/ import/PDF endpoints that shell out. - docker-compose: pin top-level name (per global docker policy), configurable host port, bind 127.0.0.1 only. - deploy/: new deploy.sh + apache-adeo.conf.tmpl. Old root deploy.sh removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
218 lines
6.5 KiB
Bash
Executable file
218 lines
6.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# adeo-maturity-tool — deploy script.
|
|
#
|
|
# Idempotent. Safe to re-run on the dev server.
|
|
# Public URL: https://optical-dev.oliver.solutions/adeo-maturity/
|
|
#
|
|
# Server layout (mirrors /opt/oliver-sales-ops-platform/, etc.):
|
|
# /opt/adeo-maturity-tool/ — repo + docker-compose
|
|
#
|
|
# What it does:
|
|
# 1. Sanity (docker, git on PATH).
|
|
# 2. Auto-pick a free host port:
|
|
# - prefers ADEO_PORT (default 3102)
|
|
# - if taken, scans upward in 3102-3199 for the next free port
|
|
# - persists the chosen value to .env so subsequent deploys keep it
|
|
# 3. Render deploy/apache-adeo.conf from .tmpl with the chosen port.
|
|
# 4. git pull (--no-pull to skip).
|
|
# 5. docker compose build && up -d (--no-build to skip).
|
|
# 6. Poll /api/health until ready.
|
|
# 7. Print URLs + 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 --logs # tail app logs after deploy
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
COMPOSE_PROJECT="adeo-maturity-tool"
|
|
URL_PATH="/adeo-maturity"
|
|
|
|
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
|
|
TAIL_LOGS=0
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--no-pull) DO_PULL=0 ;;
|
|
--no-build) DO_BUILD=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; }
|
|
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; }
|
|
docker compose version >/dev/null 2>&1 || { err "docker compose v2 not found"; exit 1; }
|
|
|
|
# Avoid stale profile env from outer shell.
|
|
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 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 port ----------
|
|
|
|
DEFAULT_PORT=3102
|
|
|
|
PORT=$(get_env_var ADEO_PORT); PORT=${PORT:-$DEFAULT_PORT}
|
|
PREV_PORT="$PORT"
|
|
|
|
log "Resolving host port (preferred: app=$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 container(s) running — keeping current port assignment."
|
|
else
|
|
NEW_PORT=$(find_free_port "$PORT" 3102 3199) || NEW_PORT=""
|
|
if [[ -z "$NEW_PORT" ]]; then
|
|
err "Could not find a free port in 3102-3199."
|
|
exit 1
|
|
fi
|
|
[[ "$NEW_PORT" != "$PORT" ]] && warn "app port $PORT busy → using $NEW_PORT"
|
|
PORT=$NEW_PORT
|
|
set_env_var ADEO_PORT "$PORT"
|
|
ok "Port: app=$PORT (persisted to .env)"
|
|
fi
|
|
|
|
# ---------- 3. Render apache-adeo.conf from template ----------
|
|
|
|
APACHE_TMPL="$REPO_ROOT/deploy/apache-adeo.conf.tmpl"
|
|
APACHE_CONF="$REPO_ROOT/deploy/apache-adeo.conf"
|
|
if [[ -f "$APACHE_TMPL" ]]; then
|
|
sed "s#__APP_PORT__#${PORT}#g" "$APACHE_TMPL" > "$APACHE_CONF"
|
|
ok "Rendered apache-adeo.conf with app port $PORT"
|
|
else
|
|
warn "apache-adeo.conf.tmpl missing — leaving deploy/apache-adeo.conf untouched."
|
|
fi
|
|
|
|
# ---------- 4. git pull ----------
|
|
|
|
if (( DO_PULL )); then
|
|
log "git pull origin main"
|
|
git pull --ff-only origin main
|
|
fi
|
|
|
|
# ---------- 5. Docker build + up ----------
|
|
|
|
if (( DO_BUILD )); then
|
|
log "docker compose build"
|
|
docker compose build
|
|
fi
|
|
|
|
log "docker compose up -d"
|
|
docker compose up -d
|
|
|
|
# ---------- 6. Health poll ----------
|
|
|
|
log "Waiting for app /api/health on :$PORT (max 60s)…"
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then
|
|
ok "App healthy"
|
|
break
|
|
fi
|
|
sleep 2
|
|
if (( i == 30 )); then
|
|
err "App did not become healthy within 60s. Recent logs:"
|
|
docker compose logs app --tail 40 || true
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ---------- 7. Report ----------
|
|
|
|
ok "Deploy complete."
|
|
echo
|
|
echo " App (local): http://127.0.0.1:${PORT}/api/health"
|
|
echo " Public URL: https://optical-dev.oliver.solutions${URL_PATH}/"
|
|
echo " Port: app=$PORT"
|
|
echo
|
|
echo " Apache include line for the merged vhost:"
|
|
echo " Include $REPO_ROOT/deploy/apache-adeo.conf"
|
|
if [[ "$PORT" != "$PREV_PORT" ]] || ! grep -qF "$REPO_ROOT/deploy/apache-adeo.conf" /etc/apache2/sites-enabled/*.conf 2>/dev/null; then
|
|
echo
|
|
warn "Port changed (or first deploy). Reload Apache to pick up the new ProxyPass:"
|
|
echo " sudo apachectl configtest && sudo systemctl reload apache2"
|
|
fi
|
|
echo
|
|
|
|
FIRST_RUN_FLAG="${REPO_ROOT}/.deployed"
|
|
if [[ ! -f "$FIRST_RUN_FLAG" ]]; then
|
|
touch "$FIRST_RUN_FLAG"
|
|
warn "First deploy. Remember: clients/adeo/data.json is NOT in git."
|
|
echo " Upload it to ${REPO_ROOT}/clients/adeo/data.json after first deploy."
|
|
echo
|
|
fi
|
|
|
|
if (( TAIL_LOGS )); then
|
|
log "Tailing app logs (Ctrl-C to stop)…"
|
|
docker compose logs -f app
|
|
fi
|