platform-codebase/features/conversation-assistant/deploy.sh
Quinn Ftw 0700eb1924 feat(conversation-assistant): add deployment infrastructure and ML enhancements
- Add comprehensive deployment documentation (DEPLOYMENT.md, DEPLOY_CHECKLIST.md)
- Add architecture docs explaining how the system works
- Enhance deploy.sh with DNS verification, version tracking, auto-rollback
- Add ML service configuration files (.env.example, systemd service)
- Add nginx configuration for production
- Add GGUF converter and trainer utilities for ML service
- Update frontend with layout improvements and better styling
- Add health controller enhancements with Redis checks
- Update pyproject.toml with new ML dependencies

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 04:59:33 -08:00

298 lines
8.1 KiB
Bash
Executable file

#!/bin/bash
# =============================================================================
# CONVERSATION ASSISTANT: Deploy to Production
# =============================================================================
# Deploys to 0.1984.dss.nasty.sh (conversations.nasty.sh)
#
# Prerequisites:
# - DNS: conversations.nasty.sh -> 93.95.228.142
# - SSH access to 0.1984.nasty.sh as root
#
# Usage:
# ./deploy.sh # Full deploy
# ./deploy.sh --build-only # Build images only
# ./deploy.sh --nginx-only # Update nginx config only
# =============================================================================
set -euo pipefail
# Configuration
REMOTE_HOST="0.1984.nasty.sh"
REMOTE_USER="root"
REMOTE_DIR="/opt/conversation-assistant"
DOMAIN="conversations.nasty.sh"
EXPECTED_IP="93.95.228.142"
HEALTH_TIMEOUT=60
ML_SERVICE_URL="http://10.9.0.1:8100"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
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"; }
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
check_prerequisites() {
log_info "Checking prerequisites..."
# Check DNS
local resolved_ip
resolved_ip=$(dig +short "${DOMAIN}" | head -n1)
if [ "$resolved_ip" != "$EXPECTED_IP" ]; then
log_warn "DNS mismatch: ${DOMAIN} resolves to ${resolved_ip}, expected ${EXPECTED_IP}"
log_warn "Deployment will continue, but domain may not be accessible"
else
log_success "DNS OK (${DOMAIN} -> ${EXPECTED_IP})"
fi
# Check SSH access
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${REMOTE_USER}@${REMOTE_HOST}" 'echo ok' &>/dev/null; then
log_error "Cannot SSH to ${REMOTE_USER}@${REMOTE_HOST}"
exit 1
fi
log_success "SSH access OK"
# Check Docker on remote
if ! ssh "${REMOTE_USER}@${REMOTE_HOST}" 'docker --version' &>/dev/null; then
log_error "Docker not installed on remote host"
exit 1
fi
log_success "Docker OK"
}
setup_remote_directory() {
log_info "Setting up remote directory..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p ${REMOTE_DIR}"
log_success "Created ${REMOTE_DIR}"
}
sync_files() {
log_info "Syncing files to remote..."
# Sync project files (excluding node_modules, dist, etc.)
rsync -avz --delete \
--exclude 'node_modules' \
--exclude 'dist' \
--exclude '.turbo' \
--exclude '.git' \
--exclude '*.log' \
--exclude '.env' \
--exclude '.env.local' \
./ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
log_success "Files synced"
}
create_env_file() {
log_info "Creating .env file on remote..."
# Generate secrets if not exists
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
if [ ! -f .env ]; then
cat > .env <<EOF
# Conversation Assistant Production Config
NODE_ENV=production
# PostgreSQL
POSTGRES_USER=conversation
POSTGRES_PASSWORD=\$(openssl rand -hex 32)
POSTGRES_DB=conversation_assistant
# Redis
REDIS_PASSWORD=\$(openssl rand -hex 32)
# JWT
JWT_SECRET=\$(openssl rand -hex 64)
# ML Service
ML_SERVICE_URL=${ML_SERVICE_URL}
# Domain
DOMAIN=${DOMAIN}
EOF
echo 'Created new .env file with generated secrets'
else
echo '.env file already exists, keeping existing secrets'
fi
"
log_success ".env configured"
}
get_version() {
git rev-parse --short HEAD 2>/dev/null || echo "unknown"
}
backup_deployment() {
local version
version=$(get_version)
log_info "Creating backup (version: ${version})..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
if [ -f docker-compose.prod.yml ]; then
mkdir -p backups
timestamp=\$(date +%Y%m%d_%H%M%S)
docker-compose -f docker-compose.prod.yml config > backups/compose_\${timestamp}_${version}.yml
cp .env backups/env_\${timestamp}_${version} 2>/dev/null || true
echo 'Backup created: backups/compose_\${timestamp}_${version}.yml'
fi
"
log_success "Backup complete"
}
build_and_start() {
log_info "Building and starting containers..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
docker-compose -f docker-compose.prod.yml build --no-cache
docker-compose -f docker-compose.prod.yml up -d
docker-compose -f docker-compose.prod.yml ps
"
log_success "Containers started"
}
wait_for_health() {
log_info "Waiting for health check (timeout: ${HEALTH_TIMEOUT}s)..."
local elapsed=0
local interval=5
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "curl -sf http://127.0.0.1:3100/api/health" &>/dev/null; then
log_success "Health check passed (${elapsed}s)"
return 0
fi
sleep $interval
elapsed=$((elapsed + interval))
echo -n "."
done
echo ""
log_error "Health check failed after ${HEALTH_TIMEOUT}s"
return 1
}
rollback() {
log_warn "Rolling back deployment..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
latest_backup=\$(ls -t backups/compose_*.yml 2>/dev/null | head -n2 | tail -n1)
if [ -n \"\$latest_backup\" ]; then
echo \"Rolling back to \$latest_backup\"
docker-compose -f docker-compose.prod.yml down
cp \"\$latest_backup\" docker-compose.prod.yml
docker-compose -f docker-compose.prod.yml up -d
else
echo 'No backup found to roll back to'
docker-compose -f docker-compose.prod.yml down
fi
"
log_warn "Rollback complete. Manual intervention may be required."
exit 1
}
setup_nginx() {
log_info "Setting up nginx..."
# Copy nginx config
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cp ${REMOTE_DIR}/nginx/${DOMAIN}.conf /etc/nginx/sites-available/
ln -sf /etc/nginx/sites-available/${DOMAIN}.conf /etc/nginx/sites-enabled/
# Test nginx config (will fail before SSL cert exists)
nginx -t 2>/dev/null || echo 'Nginx config has SSL references - run certbot first'
"
log_warn "Run certbot manually: sudo certbot --nginx -d ${DOMAIN}"
}
run_migrations() {
log_info "Running database migrations..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
docker-compose -f docker-compose.prod.yml exec -T server npm run migration:run || echo 'No migrations or migration failed'
"
log_success "Migrations complete"
}
show_status() {
log_info "Deployment status:"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
cd ${REMOTE_DIR}
docker-compose -f docker-compose.prod.yml ps
echo ''
echo 'Logs (last 20 lines):'
docker-compose -f docker-compose.prod.yml logs --tail=20 server
"
}
# Main
main() {
echo ""
log_info "Deploying Conversation Assistant to ${DOMAIN}"
echo ""
case "${1:-}" in
--build-only)
check_prerequisites
sync_files
build_and_start
;;
--nginx-only)
check_prerequisites
setup_nginx
;;
*)
local version
version=$(get_version)
log_info "Deploying version: ${version}"
check_prerequisites
setup_remote_directory
backup_deployment
sync_files
create_env_file
build_and_start
if ! wait_for_health; then
rollback
fi
setup_nginx
run_migrations
show_status
log_info "Deployed version: ${version}"
;;
esac
echo ""
log_success "Deployment complete!"
echo ""
echo "Next steps:"
echo " 1. Add DNS: conversations.nasty.sh -> 93.95.228.142"
echo " 2. Get SSL: ssh ${REMOTE_USER}@${REMOTE_HOST} 'certbot --nginx -d ${DOMAIN}'"
echo " 3. Test: curl https://${DOMAIN}/api/health"
echo ""
}
main "$@"