feat(F-049): completed feature
This commit is contained in:
348
project/scripts/monolith.sh
Executable file
348
project/scripts/monolith.sh
Executable file
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env bash
|
||||
# Manage the complete MercadoDeVida monolith on one development/LAN host.
|
||||
# Usage: ./scripts/monolith.sh <dev|prod> <start|restart|status|stop|logs|urls>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
MODE="${1:-}"
|
||||
ACTION="${2:-}"
|
||||
RUNTIME_ROOT="${MDV_RUNTIME_DIR:-$PROJECT_DIR/.runtime}"
|
||||
RUNTIME_DIR="$RUNTIME_ROOT/$MODE"
|
||||
BACKEND_PORT="${BACKEND_PORT:-3000}"
|
||||
FRONTEND_PORT="${FRONTEND_PORT:-3003}"
|
||||
ADMIN_PORT="${ADMIN_PORT:-3004}"
|
||||
STOREFRONT_PORT="${STOREFRONT_PORT:-3005}"
|
||||
START_TIMEOUT="${START_TIMEOUT:-90}"
|
||||
|
||||
SERVICES=(backend frontend admin storefront)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/monolith.sh <dev|prod> <command>
|
||||
|
||||
Commands:
|
||||
start Install/build when needed, migrate, and start every service
|
||||
restart Stop managed processes, then start every service
|
||||
status Show PID, process state, HTTP status, and URLs
|
||||
stop Gracefully stop every managed HTTP process
|
||||
logs Follow all service logs (Ctrl-C exits without stopping services)
|
||||
urls Print localhost and LAN URLs
|
||||
|
||||
Environment overrides:
|
||||
LAN_IP, BACKEND_PORT, FRONTEND_PORT, ADMIN_PORT, STOREFRONT_PORT
|
||||
MDV_RUNTIME_DIR, START_TIMEOUT
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "$MODE" != "dev" && "$MODE" != "prod" ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$ACTION" in
|
||||
start|restart|status|stop|logs|urls) ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
|
||||
lan_ip() {
|
||||
if [[ -n "${LAN_IP:-}" ]]; then
|
||||
printf '%s\n' "$LAN_IP"
|
||||
return
|
||||
fi
|
||||
|
||||
local iface ip
|
||||
if command -v route >/dev/null 2>&1 && command -v ipconfig >/dev/null 2>&1; then
|
||||
iface="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')"
|
||||
if [[ -n "$iface" ]]; then
|
||||
ip="$(ipconfig getifaddr "$iface" 2>/dev/null || true)"
|
||||
if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
|
||||
fi
|
||||
fi
|
||||
if command -v hostname >/dev/null 2>&1; then
|
||||
ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
||||
if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
|
||||
fi
|
||||
printf '127.0.0.1\n'
|
||||
}
|
||||
|
||||
LAN_ADDRESS="$(lan_ip)"
|
||||
API_PUBLIC_URL="${API_PUBLIC_URL:-http://$LAN_ADDRESS:$BACKEND_PORT}"
|
||||
|
||||
service_port() {
|
||||
case "$1" in
|
||||
backend) echo "$BACKEND_PORT" ;;
|
||||
frontend) echo "$FRONTEND_PORT" ;;
|
||||
admin) echo "$ADMIN_PORT" ;;
|
||||
storefront) echo "$STOREFRONT_PORT" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
service_path() {
|
||||
case "$1" in
|
||||
backend) echo "/health" ;;
|
||||
*) echo "/" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
service_url() {
|
||||
local service="$1" host="${2:-127.0.0.1}"
|
||||
printf 'http://%s:%s%s\n' "$host" "$(service_port "$service")" "$(service_path "$service")"
|
||||
}
|
||||
|
||||
pid_file() { printf '%s/%s.pid\n' "$RUNTIME_DIR" "$1"; }
|
||||
log_file() { printf '%s/%s.log\n' "$RUNTIME_DIR" "$1"; }
|
||||
|
||||
read_pid() {
|
||||
local file
|
||||
file="$(pid_file "$1")"
|
||||
[[ -f "$file" ]] && tr -dc '0-9' < "$file" || true
|
||||
}
|
||||
|
||||
is_running() {
|
||||
local pid
|
||||
pid="$(read_pid "$1")"
|
||||
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
port_pid() {
|
||||
local port="$1"
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
assert_port_available() {
|
||||
local service="$1" port existing managed
|
||||
port="$(service_port "$service")"
|
||||
existing="$(port_pid "$port")"
|
||||
managed="$(read_pid "$service")"
|
||||
if [[ -n "$existing" && "$existing" != "$managed" ]]; then
|
||||
echo "[FAIL] $service port $port is already used by unmanaged PID $existing" >&2
|
||||
echo " Stop it explicitly before retrying; this script never broad-kills processes." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_http() {
|
||||
local service="$1" url deadline code
|
||||
url="$(service_url "$service")"
|
||||
deadline=$((SECONDS + START_TIMEOUT))
|
||||
while (( SECONDS < deadline )); do
|
||||
if ! is_running "$service"; then
|
||||
echo "[FAIL] $service exited during startup. Last log lines:" >&2
|
||||
tail -40 "$(log_file "$service")" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
||||
if [[ "$code" =~ ^[23] ]]; then
|
||||
echo "[OK] $service ready ($code) — $url"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "[FAIL] $service did not become healthy at $url in ${START_TIMEOUT}s" >&2
|
||||
tail -40 "$(log_file "$service")" >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_env() {
|
||||
if [[ ! -f "$PROJECT_DIR/.env" ]]; then
|
||||
cat > "$PROJECT_DIR/.env" <<'EOF'
|
||||
DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida
|
||||
REDIS_URL=redis://localhost:6379
|
||||
COOKIE_SECURE=false
|
||||
EOF
|
||||
echo "[INFO] Created project/.env for local development"
|
||||
fi
|
||||
}
|
||||
|
||||
start_infrastructure() {
|
||||
command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; }
|
||||
echo '[INFO] Starting PostgreSQL and Redis...'
|
||||
|
||||
local service container
|
||||
for service in postgres redis; do
|
||||
container="mdv-dev-$service"
|
||||
if docker inspect "$container" >/dev/null 2>&1; then
|
||||
docker start "$container" >/dev/null
|
||||
else
|
||||
(cd "$PROJECT_DIR" && docker compose up -d "$service")
|
||||
fi
|
||||
done
|
||||
|
||||
local deadline=$((SECONDS + 60))
|
||||
until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do
|
||||
(( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
local install_command=(npm install)
|
||||
[[ "$MODE" == "prod" ]] && install_command=(npm ci)
|
||||
for dir in "$PROJECT_DIR" "$PROJECT_DIR/apps/admin" "$PROJECT_DIR/frontend" "$PROJECT_DIR/storefront"; do
|
||||
echo "[INFO] ${install_command[*]} — ${dir#$PROJECT_DIR/}"
|
||||
(cd "$dir" && "${install_command[@]}")
|
||||
done
|
||||
}
|
||||
|
||||
migrate() {
|
||||
echo '[INFO] Applying database migrations...'
|
||||
(cd "$PROJECT_DIR" && node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations)
|
||||
}
|
||||
|
||||
build_prod() {
|
||||
echo '[INFO] Building backend...'
|
||||
(cd "$PROJECT_DIR" && npm run build)
|
||||
echo '[INFO] Building admin...'
|
||||
(cd "$PROJECT_DIR/apps/admin" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
||||
echo '[INFO] Building customer frontend...'
|
||||
(cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
||||
echo '[INFO] Building SEO storefront...'
|
||||
(cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
|
||||
}
|
||||
|
||||
spawn_service() {
|
||||
local service="$1" dir log pidfile port
|
||||
dir="$PROJECT_DIR"
|
||||
log="$(log_file "$service")"
|
||||
pidfile="$(pid_file "$service")"
|
||||
port="$(service_port "$service")"
|
||||
: > "$log"
|
||||
|
||||
case "$MODE:$service" in
|
||||
dev:backend)
|
||||
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
prod:backend)
|
||||
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
dev:admin)
|
||||
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
prod:admin)
|
||||
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
dev:frontend)
|
||||
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
prod:frontend)
|
||||
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
dev:storefront)
|
||||
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
prod:storefront)
|
||||
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
|
||||
;;
|
||||
esac
|
||||
sleep 1
|
||||
wait_http "$service"
|
||||
local listener_pid
|
||||
listener_pid="$(port_pid "$port")"
|
||||
if [[ -n "$listener_pid" ]]; then
|
||||
echo "$listener_pid" > "$pidfile"
|
||||
fi
|
||||
}
|
||||
|
||||
start_all() {
|
||||
ensure_env
|
||||
for service in "${SERVICES[@]}"; do
|
||||
if is_running "$service"; then
|
||||
echo "[FAIL] $service is already managed in $MODE mode; use restart" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$(pid_file "$service")"
|
||||
assert_port_available "$service"
|
||||
done
|
||||
start_infrastructure
|
||||
install_dependencies
|
||||
migrate
|
||||
[[ "$MODE" == "prod" ]] && build_prod
|
||||
for service in "${SERVICES[@]}"; do spawn_service "$service"; done
|
||||
echo
|
||||
print_urls
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
local service="$1" pid deadline
|
||||
pid="$(read_pid "$service")"
|
||||
if [[ -z "$pid" ]]; then
|
||||
rm -f "$(pid_file "$service")"
|
||||
return
|
||||
fi
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
rm -f "$(pid_file "$service")"
|
||||
return
|
||||
fi
|
||||
echo "[INFO] Stopping $service (PID $pid)..."
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
deadline=$((SECONDS + 15))
|
||||
while kill -0 "$pid" 2>/dev/null && (( SECONDS < deadline )); do sleep 1; done
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "[WARN] $service did not stop gracefully; sending KILL"
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$(pid_file "$service")"
|
||||
}
|
||||
|
||||
stop_all() {
|
||||
local i
|
||||
for ((i=${#SERVICES[@]}-1; i>=0; i--)); do stop_service "${SERVICES[$i]}"; done
|
||||
}
|
||||
|
||||
status_all() {
|
||||
local service pid state code url lan_url failed=0
|
||||
printf '%-11s %-8s %-10s %-6s %s\n' SERVICE PID PROCESS HTTP URL
|
||||
for service in "${SERVICES[@]}"; do
|
||||
pid="$(read_pid "$service")"
|
||||
state='stopped'
|
||||
[[ -n "$pid" ]] || pid='-'
|
||||
if [[ "$pid" != '-' ]] && kill -0 "$pid" 2>/dev/null; then state='running'; fi
|
||||
url="$(service_url "$service")"
|
||||
code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
||||
[[ -n "$code" && "$code" != '000' ]] || code='-'
|
||||
lan_url="$(service_url "$service" "$LAN_ADDRESS")"
|
||||
printf '%-11s %-8s %-10s %-6s %s\n' "$service" "$pid" "$state" "$code" "$lan_url"
|
||||
[[ "$state" == 'running' && "$code" =~ ^[23] ]] || failed=1
|
||||
done
|
||||
echo
|
||||
(cd "$PROJECT_DIR" && docker compose ps) || true
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
print_urls() {
|
||||
cat <<EOF
|
||||
Mode: $MODE
|
||||
LAN IP: $LAN_ADDRESS
|
||||
|
||||
Customer frontend: http://$LAN_ADDRESS:$FRONTEND_PORT/
|
||||
Admin backoffice: http://$LAN_ADDRESS:$ADMIN_PORT/
|
||||
SEO storefront: http://$LAN_ADDRESS:$STOREFRONT_PORT/
|
||||
Backend health: http://$LAN_ADDRESS:$BACKEND_PORT/health
|
||||
Swagger/OpenAPI: http://$LAN_ADDRESS:$BACKEND_PORT/docs
|
||||
|
||||
Local equivalents use 127.0.0.1 with the same ports.
|
||||
EOF
|
||||
}
|
||||
|
||||
follow_logs() {
|
||||
local files=() service
|
||||
for service in "${SERVICES[@]}"; do
|
||||
touch "$(log_file "$service")"
|
||||
files+=("$(log_file "$service")")
|
||||
done
|
||||
tail -n 100 -F "${files[@]}"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start) start_all ;;
|
||||
restart) stop_all; start_all ;;
|
||||
status) status_all ;;
|
||||
stop) stop_all ;;
|
||||
logs) follow_logs ;;
|
||||
urls) print_urls ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user