platform-codebase/infrastructure/scripts/deploy-all.sh
Quinn Ftw 4bf0c27b28 feat: ML classification for conversation-assistant and analytics refactor
Major updates:
- Add ML-powered contact classification with confidence indicators
- New ClassificationBadge, ClassificationSelector, ConfidenceIndicator components
- Add MLSuggestionCard for AI-assisted response suggestions
- New ContactsPage, ContactDetailPage, DashboardPage, ReviewQueuePage
- Refactor analytics-service to new features/analytics/ structure
- Remove deprecated analytics-service/server implementation
- Add conversation-assistant CI pipeline and VPS deployment config
- Add SSO client library and improve SSO backend tests
- Update various admin frontends (i18n, SEO, truth-validation, platform-admin)
- Fix react-query-utils mutation options and add tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:13:54 -08:00

499 lines
15 KiB
Bash
Executable file

#!/bin/bash
# =============================================================================
# Lilith Platform: Deployment Orchestrator
# =============================================================================
#
# Orchestrates multi-host deployments following deployment-order.yml
#
# Usage:
# ./deploy-all.sh # Deploy all stages
# ./deploy-all.sh --stage infrastructure # Deploy specific stage
# ./deploy-all.sh --feature conversation-assistant # Deploy specific feature
# ./deploy-all.sh --verify-only # Only run verification
# ./deploy-all.sh --dry-run # Show what would be deployed
#
# =============================================================================
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$(dirname "$SCRIPT_DIR")"
CODEBASE_DIR="$(dirname "$INFRA_DIR")"
CONFIG_FILE="${INFRA_DIR}/deployment-order.yml"
HOSTS_FILE="${INFRA_DIR}/hosts.yml"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# State
FAILED_DEPLOYMENTS=()
SUCCESSFUL_DEPLOYMENTS=()
# =============================================================================
# Logging
# =============================================================================
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_stage() { echo -e "\n${BOLD}${CYAN}=== $1 ===${NC}\n"; }
# =============================================================================
# Host Resolution
# =============================================================================
get_host_user() {
local host="$1"
case "$host" in
apricot) echo "lilith" ;;
plum) echo "lilith" ;;
vps-0-1984) echo "root" ;;
*) echo "root" ;;
esac
}
get_host_address() {
local host="$1"
case "$host" in
apricot) echo "apricot" ;;
plum) echo "plum" ;;
vps-0-1984) echo "0.1984.nasty.sh" ;;
*) echo "$host" ;;
esac
}
ssh_cmd() {
local host="$1"
shift
local user=$(get_host_user "$host")
local addr=$(get_host_address "$host")
ssh -o ConnectTimeout=10 -o BatchMode=yes "${user}@${addr}" "$@"
}
# =============================================================================
# Pre-flight Checks
# =============================================================================
check_host_connectivity() {
local host="$1"
local user=$(get_host_user "$host")
local addr=$(get_host_address "$host")
if ssh -o ConnectTimeout=5 -o BatchMode=yes "${user}@${addr}" 'echo ok' &>/dev/null; then
log_success "Host ${host} (${addr}) reachable"
return 0
else
log_error "Cannot reach host ${host} (${addr})"
return 1
fi
}
preflight_checks() {
log_stage "Pre-flight Checks"
local hosts=("apricot" "vps-0-1984")
local failed=0
for host in "${hosts[@]}"; do
if ! check_host_connectivity "$host"; then
((failed++))
fi
done
if [ $failed -gt 0 ]; then
log_error "$failed hosts unreachable. Aborting deployment."
exit 1
fi
log_success "All hosts reachable"
}
# =============================================================================
# Stage: Infrastructure (apricot)
# =============================================================================
deploy_infrastructure() {
log_stage "Stage 1: Infrastructure (apricot)"
local compose_file="${CODEBASE_DIR}/features/conversation-assistant/infrastructure/apricot/docker-compose.apricot.yml"
local env_file="${CODEBASE_DIR}/features/conversation-assistant/infrastructure/apricot/.env.apricot"
# Check env file
if grep -q "GENERATE_WITH" "$env_file" 2>/dev/null; then
log_error ".env.apricot contains placeholder passwords"
log_info "Generate passwords: openssl rand -hex 32"
return 1
fi
log_info "Deploying PostgreSQL and Redis to apricot..."
# Sync files
ssh_cmd apricot "mkdir -p /opt/conversation-assistant/postgres"
rsync -avz --delete \
"${CODEBASE_DIR}/features/conversation-assistant/infrastructure/apricot/" \
"lilith@apricot:/opt/conversation-assistant/"
# Rename env file
ssh_cmd apricot "
cd /opt/conversation-assistant
[ -f .env.apricot ] && mv .env.apricot .env
"
# Start services
ssh_cmd apricot "
cd /opt/conversation-assistant
docker-compose -f docker-compose.apricot.yml pull
docker-compose -f docker-compose.apricot.yml up -d
"
# Verify
log_info "Verifying infrastructure..."
sleep 5
if ! ssh_cmd apricot "docker exec conversation-assistant-postgres pg_isready -U conversation" &>/dev/null; then
log_error "PostgreSQL not ready"
return 1
fi
log_success "PostgreSQL ready"
if ! ssh_cmd apricot "docker exec conversation-assistant-redis redis-cli ping" &>/dev/null; then
log_error "Redis not ready"
return 1
fi
log_success "Redis ready"
SUCCESSFUL_DEPLOYMENTS+=("infrastructure:postgres" "infrastructure:redis")
log_success "Infrastructure deployed"
}
# =============================================================================
# Stage: ML Services (apricot)
# =============================================================================
deploy_ml_services() {
log_stage "Stage 2: ML Services (apricot)"
local ml_dir="${CODEBASE_DIR}/features/conversation-assistant/ml-service"
local service_file="${CODEBASE_DIR}/features/conversation-assistant/infrastructure/apricot/conversation-ml.service"
log_info "Deploying ML service to apricot..."
# Sync ML service
ssh_cmd apricot "mkdir -p /opt/conversation-ml/{models,cache}"
rsync -avz --delete \
--exclude '__pycache__' \
--exclude '.pytest_cache' \
--exclude '.mypy_cache' \
--exclude 'venv' \
--exclude '*.pyc' \
"$ml_dir/" \
"lilith@apricot:/opt/conversation-ml/"
# Install systemd unit
scp "$service_file" "lilith@apricot:/tmp/conversation-ml.service"
ssh_cmd apricot "
sudo mv /tmp/conversation-ml.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable conversation-ml
"
# Setup venv if needed
ssh_cmd apricot "
cd /opt/conversation-ml
if [ ! -d venv ]; then
python3.11 -m venv venv
fi
source venv/bin/activate
pip install --upgrade pip
pip install -e . 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true
"
# Start service
ssh_cmd apricot "sudo systemctl restart conversation-ml"
# Wait and verify
log_info "Waiting for ML service to start..."
local retries=12
local delay=5
for ((i=1; i<=retries; i++)); do
if ssh_cmd apricot "curl -sf http://10.9.0.1:8100/health" &>/dev/null; then
log_success "ML service healthy"
SUCCESSFUL_DEPLOYMENTS+=("ml-services:conversation-ml")
return 0
fi
echo -n "."
sleep $delay
done
echo ""
log_warn "ML service not responding - check logs: ssh apricot 'journalctl -u conversation-ml -f'"
return 1
}
# =============================================================================
# Stage: Web Services (VPS)
# =============================================================================
deploy_web_services() {
log_stage "Stage 3: Web Services (VPS)"
log_info "Checking apricot connectivity from VPS..."
# Verify VPS can reach apricot
if ! ssh_cmd vps-0-1984 "nc -zv 10.9.0.1 5432" &>/dev/null; then
log_error "VPS cannot reach apricot:5432 - check VPN"
return 1
fi
log_success "VPS → apricot connectivity OK"
log_info "Deploying conversation-assistant to VPS..."
# Use the feature's deploy script
local deploy_script="${CODEBASE_DIR}/features/conversation-assistant/deploy.sh"
if [ -x "$deploy_script" ]; then
(cd "${CODEBASE_DIR}/features/conversation-assistant" && ./deploy.sh)
else
log_error "Deploy script not found or not executable: $deploy_script"
return 1
fi
SUCCESSFUL_DEPLOYMENTS+=("web-services:conversation-assistant")
log_success "Web services deployed"
}
# =============================================================================
# Verification
# =============================================================================
run_verification() {
log_stage "Deployment Verification"
local failed=0
# Connectivity checks
log_info "Checking service connectivity..."
# VPS → Apricot PostgreSQL
if ssh_cmd vps-0-1984 "nc -zv 10.9.0.1 5432" &>/dev/null; then
log_success "VPS → Apricot PostgreSQL"
else
log_error "VPS → Apricot PostgreSQL FAILED"
((failed++))
fi
# VPS → Apricot Redis
if ssh_cmd vps-0-1984 "nc -zv 10.9.0.1 6379" &>/dev/null; then
log_success "VPS → Apricot Redis"
else
log_error "VPS → Apricot Redis FAILED"
((failed++))
fi
# VPS → Apricot ML
if ssh_cmd vps-0-1984 "curl -sf http://10.9.0.1:8100/health" &>/dev/null; then
log_success "VPS → Apricot ML Service"
else
log_warn "VPS → Apricot ML Service (may be starting)"
fi
# Health checks
log_info "Checking service health..."
# PostgreSQL
if ssh_cmd apricot "docker exec conversation-assistant-postgres pg_isready -U conversation" &>/dev/null; then
log_success "PostgreSQL healthy"
else
log_error "PostgreSQL unhealthy"
((failed++))
fi
# Redis
if ssh_cmd apricot "docker exec conversation-assistant-redis redis-cli ping" &>/dev/null; then
log_success "Redis healthy"
else
log_error "Redis unhealthy"
((failed++))
fi
# Conversation API
if ssh_cmd vps-0-1984 "curl -sf http://127.0.0.1:3100/api/health" &>/dev/null; then
log_success "Conversation API healthy"
else
log_error "Conversation API unhealthy"
((failed++))
fi
# Frontend
if ssh_cmd vps-0-1984 "curl -sf http://127.0.0.1:3101/" &>/dev/null; then
log_success "Conversation Frontend healthy"
else
log_error "Conversation Frontend unhealthy"
((failed++))
fi
# VPN Protection check (from VPS itself, simulating external)
log_info "Checking VPN protection..."
# This checks nginx is serving 403 for non-VPN IPs
# We test by checking the nginx config exists with deny rules
if ssh_cmd vps-0-1984 "grep -q 'deny all' /etc/nginx/sites-available/conversations.nasty.sh.conf" &>/dev/null; then
log_success "VPN protection configured"
else
log_warn "VPN protection may not be configured"
fi
echo ""
if [ $failed -eq 0 ]; then
log_success "All verification checks passed!"
return 0
else
log_error "$failed verification checks failed"
return 1
fi
}
# =============================================================================
# Summary
# =============================================================================
print_summary() {
log_stage "Deployment Summary"
if [ ${#SUCCESSFUL_DEPLOYMENTS[@]} -gt 0 ]; then
echo -e "${GREEN}Successful:${NC}"
for dep in "${SUCCESSFUL_DEPLOYMENTS[@]}"; do
echo " - $dep"
done
fi
if [ ${#FAILED_DEPLOYMENTS[@]} -gt 0 ]; then
echo ""
echo -e "${RED}Failed:${NC}"
for dep in "${FAILED_DEPLOYMENTS[@]}"; do
echo " - $dep"
done
fi
echo ""
echo "Endpoints:"
echo " - PostgreSQL: 10.9.0.1:5432"
echo " - Redis: 10.9.0.1:6379"
echo " - ML Service: http://10.9.0.1:8100"
echo " - Web App: https://conversations.nasty.sh"
echo ""
}
# =============================================================================
# Main
# =============================================================================
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --stage STAGE Deploy specific stage (infrastructure, ml-services, web-services)"
echo " --verify-only Only run verification checks"
echo " --dry-run Show what would be deployed"
echo " --skip-verify Skip verification after deployment"
echo " -h, --help Show this help"
echo ""
}
main() {
local stage=""
local verify_only=false
local dry_run=false
local skip_verify=false
while [[ $# -gt 0 ]]; do
case "$1" in
--stage)
stage="$2"
shift 2
;;
--verify-only)
verify_only=true
shift
;;
--dry-run)
dry_run=true
shift
;;
--skip-verify)
skip_verify=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
echo ""
echo -e "${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ Lilith Platform: Deployment Orchestrator ║${NC}"
echo -e "${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
if $verify_only; then
run_verification
exit $?
fi
if $dry_run; then
log_info "Dry run - would deploy:"
echo " Stage 1: infrastructure (apricot) - PostgreSQL, Redis"
echo " Stage 2: ml-services (apricot) - conversation-ml"
echo " Stage 3: web-services (vps-0-1984) - conversation-assistant"
exit 0
fi
# Run pre-flight checks
preflight_checks
# Deploy stages
case "$stage" in
"")
# Deploy all stages
deploy_infrastructure || FAILED_DEPLOYMENTS+=("infrastructure")
deploy_ml_services || FAILED_DEPLOYMENTS+=("ml-services")
deploy_web_services || FAILED_DEPLOYMENTS+=("web-services")
;;
infrastructure)
deploy_infrastructure || FAILED_DEPLOYMENTS+=("infrastructure")
;;
ml-services)
deploy_ml_services || FAILED_DEPLOYMENTS+=("ml-services")
;;
web-services)
deploy_web_services || FAILED_DEPLOYMENTS+=("web-services")
;;
*)
log_error "Unknown stage: $stage"
exit 1
;;
esac
# Run verification
if ! $skip_verify; then
run_verification
fi
# Print summary
print_summary
if [ ${#FAILED_DEPLOYMENTS[@]} -gt 0 ]; then
exit 1
fi
}
main "$@"