#!/usr/bin/env bash set -euo pipefail # ═══════════════════════════════════════════════════════════════════════ # ByteLyst Investment Trading - Production Deployment Script # ═══════════════════════════════════════════════════════════════════════ # Usage: ./deploy-invttrdg.sh [--force] [--skip-health-check] # # What it does: # 1. Dirty check: uncommitted changes, unpushed commits # 2. Pull and rebase origin/main # 3. Build and deploy Docker containers # 4. Verify endpoints: https://api.bytelyst.com/invttrdg, https://invttrdg.bytelyst.com # # Options: # --force Skip dirty checks and force deployment # --skip-health-check Skip endpoint health verification # ═══════════════════════════════════════════════════════════════════════ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' log() { echo -e "${CYAN}[$(date +%H:%M:%S)]${NC} $*"; } ok() { echo -e "${GREEN}[$(date +%H:%M:%S)] ✓${NC} $*"; } warn() { echo -e "${YELLOW}[$(date +%H:%M:%S)] ⚠${NC} $*"; } fail() { echo -e "${RED}[$(date +%H:%M:%S)] ✗${NC} $*"; exit 1; } # ── Configuration ──────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="${SCRIPT_DIR}/../learning_ai_invt_trdg" cd "$SCRIPT_DIR" FORCE=false SKIP_HEALTH_CHECK=false while [[ $# -gt 0 ]]; do case "$1" in --force) FORCE=true; shift ;; --skip-health-check) SKIP_HEALTH_CHECK=true; shift ;; *) fail "Unknown option: $1" ;; esac done # ── Prerequisites ──────────────────────────────────────────────────── if [ ! -d "$REPO_DIR" ]; then fail "Repo directory not found: $REPO_DIR" fi cd "$REPO_DIR" # ── Dirty Check ─────────────────────────────────────────────────────── if [ "$FORCE" = false ]; then log "Running dirty checks..." # Check for uncommitted changes if ! git diff-index --quiet HEAD --; then fail "Uncommitted changes detected. Commit or stash first, or use --force" fi # Check for untracked files if [ -n "$(git ls-files --others --exclude-standard)" ]; then fail "Untracked files detected. Commit or remove them, or use --force" fi # Check for unpushed commits LOCAL_COMMIT=$(git rev-parse @) REMOTE_COMMIT=$(git rev-parse '@{u}' 2>/dev/null || echo "") if [ -n "$REMOTE_COMMIT" ] && [ "$LOCAL_COMMIT" != "$REMOTE_COMMIT" ]; then fail "Unpushed commits detected. Push first or use --force" fi ok "Dirty checks passed" else warn "Skipping dirty checks (--force enabled)" fi # ── Pull and Rebase ─────────────────────────────────────────────────── log "Pulling latest changes from origin/main..." git fetch origin LOCAL_MAIN=$(git rev-parse main) REMOTE_MAIN=$(git rev-parse origin/main) if [ "$LOCAL_MAIN" != "$REMOTE_MAIN" ]; then log "Local main is behind origin/main, rebasing..." git rebase origin/main || { fail "Rebase failed. Resolve conflicts and run: git rebase --continue" } ok "Rebase completed successfully" else ok "Already up to date with origin/main" fi # ── Run Smoke Tests ───────────────────────────────────────────────────── if [ "$SKIP_HEALTH_CHECK" = false ]; then log "Running smoke tests before deployment..." if [ -f "scripts/smoke-release.sh" ]; then chmod +x scripts/smoke-release.sh ./scripts/smoke-release.sh || { fail "Smoke tests failed. Fix issues before deploying or use --skip-health-check" } ok "Smoke tests passed" else warn "Smoke test script not found, skipping pre-deployment tests" fi fi # ── Build and Deploy ────────────────────────────────────────────────── log "Building and deploying Docker containers..." # Check if docker-compose files exist if [ ! -f "docker-compose.yml" ]; then fail "docker-compose.yml not found in $REPO_DIR" fi # Build and start services log "Building Docker images..." # Resolve Gitea npm token for BuildKit secret if [ -z "${GITEA_NPM_TOKEN:-}" ]; then if [ -f "/opt/bytelyst/.gitea_token" ]; then GITEA_NPM_TOKEN=$(cat /opt/bytelyst/.gitea_token) export GITEA_NPM_TOKEN elif [ -f "$HOME/.gitea_npm_token" ]; then GITEA_NPM_TOKEN=$(cat "$HOME/.gitea_npm_token") export GITEA_NPM_TOKEN else fail "GITEA_NPM_TOKEN not set and no token file found" fi fi # Collect build metadata (consumed by @bytelyst/devops) # Declare separately so set -e + $(…) don't mask non-zero exit codes (SC2155). BYTELYST_COMMIT_SHA= BYTELYST_COMMIT_SHA_FULL= BYTELYST_BRANCH= BYTELYST_BUILT_AT= BYTELYST_COMMIT_AUTHOR= BYTELYST_COMMIT_MESSAGE= BYTELYST_COMMIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo unknown) BYTELYST_COMMIT_SHA_FULL=$(git rev-parse HEAD 2>/dev/null || echo unknown) BYTELYST_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown) BYTELYST_BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) BYTELYST_COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an' 2>/dev/null || echo unknown) BYTELYST_COMMIT_MESSAGE=$(git log -1 --pretty=format:'%s' 2>/dev/null | head -c 200 || echo unknown) build_image() { local dockerfile="$1" local tag="$2" docker build --network host \ --secret id=gitea_npm_token,env=GITEA_NPM_TOKEN \ --build-arg "BYTELYST_COMMIT_SHA=${BYTELYST_COMMIT_SHA}" \ --build-arg "BYTELYST_COMMIT_SHA_FULL=${BYTELYST_COMMIT_SHA_FULL}" \ --build-arg "BYTELYST_BRANCH=${BYTELYST_BRANCH}" \ --build-arg "BYTELYST_BUILT_AT=${BYTELYST_BUILT_AT}" \ --build-arg "BYTELYST_COMMIT_AUTHOR=${BYTELYST_COMMIT_AUTHOR}" \ --build-arg "BYTELYST_COMMIT_MESSAGE=${BYTELYST_COMMIT_MESSAGE}" \ --build-arg "BYTELYST_DOCKER_IMAGE=${tag}" \ -f "$dockerfile" -t "$tag" . } build_image backend/Dockerfile invttrdg-backend:latest || fail "Backend build failed" build_image web/Dockerfile invttrdg-web:latest || fail "Web build failed" log "Starting services..." docker compose up -d --no-build || fail "Docker compose up failed" ok "Deployment completed" # ── Health Check ────────────────────────────────────────────────────── if [ "$SKIP_HEALTH_CHECK" = true ]; then warn "Skipping health checks (--skip-health-check enabled)" exit 0 fi log "Waiting for services to be healthy..." sleep 10 # Check backend health BACKEND_HEALTH=false for _ in {1..30}; do if curl -sf http://localhost:4025/health/live > /dev/null 2>&1; then BACKEND_HEALTH=true break fi echo -n "." sleep 2 done echo "" if [ "$BACKEND_HEALTH" = true ]; then ok "Backend health check passed (http://localhost:4025)" else fail "Backend health check failed" fi # Check web health WEB_HEALTH=false for _ in {1..30}; do if curl -sf http://localhost:3085 > /dev/null 2>&1; then WEB_HEALTH=true break fi echo -n "." sleep 2 done echo "" if [ "$WEB_HEALTH" = true ]; then ok "Web health check passed (http://localhost:3085)" else warn "Web health check failed (may be starting up)" fi # ── Endpoint Verification ───────────────────────────────────────────── log "Verifying production endpoints..." API_ENDPOINT="https://api.bytelyst.com/invttrdg" WEB_ENDPOINT="https://invttrdg.bytelyst.com" # Check API endpoint if curl -sf "$API_ENDPOINT/health/live" > /dev/null 2>&1; then ok "API endpoint accessible: $API_ENDPOINT" else warn "API endpoint not accessible: $API_ENDPOINT (may need DNS propagation)" fi # Check web endpoint if curl -sf "$WEB_ENDPOINT" > /dev/null 2>&1; then ok "Web endpoint accessible: $WEB_ENDPOINT" else warn "Web endpoint not accessible: $WEB_ENDPOINT (may need DNS propagation)" fi # ── API Smoke Tests (Post-Deployment) ─────────────────────────────────── log "Running post-deployment API smoke tests..." # Test backend health endpoint BACKEND_URL="http://localhost:4025" if curl -sf "$BACKEND_URL/health/live" > /dev/null 2>&1; then ok "Backend health endpoint responding" # Try to get health details HEALTH_RESPONSE=$(curl -s "$BACKEND_URL/health/live" 2>/dev/null || echo "{}") log "Health response: $HEALTH_RESPONSE" else fail "Backend health endpoint not responding" fi # Test web is serving content WEB_URL="http://localhost:3085" if curl -sf "$WEB_URL" > /dev/null 2>&1; then ok "Web frontend is serving content" # Check if it's actually HTML (nginx serving the SPA) CONTENT_TYPE=$(curl -sI "$WEB_URL" | grep -i content-type || echo "") if echo "$CONTENT_TYPE" | grep -qi "text/html"; then ok "Web frontend is serving HTML content" else warn "Web frontend content type unexpected: $CONTENT_TYPE" fi else fail "Web frontend not responding" fi log "══════════════════════════════════════════════════════════════════════" ok "Deployment completed successfully!" log "Backend: http://localhost:4025 → $API_ENDPOINT" log "Web: http://localhost:3085 → $WEB_ENDPOINT" log "══════════════════════════════════════════════════════════════════════"