#!/usr/bin/env bash set -euo pipefail # ═══════════════════════════════════════════════════════════════════════ # ByteLyst Production Deployment - All Repos # ═══════════════════════════════════════════════════════════════════════ # Usage: ./deploy-all.sh [--force] [--skip-health-check] [repo1 repo2 ...] # # Deploys all production repos or specific repos: # - learning_ai_invt_trdg (Trading) # - learning_ai_common_plat (Platform Services) # - learning_ai_clock (ChronoMind) # - learning_ai_notes (NoteLett) # # 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)" REPOS_BASE_DIR="${SCRIPT_DIR}/.." cd "$SCRIPT_DIR" PRODUCTION_REPOS=( "learning_ai_invt_trdg" "learning_ai_common_plat" "learning_ai_clock" "learning_ai_notes" ) FORCE=false SKIP_HEALTH_CHECK=false SPECIFIC_REPOS=() while [[ $# -gt 0 ]]; do case "$1" in --force) FORCE=true; shift ;; --skip-health-check) SKIP_HEALTH_CHECK=true; shift ;; -*) fail "Unknown option: $1" ;; *) SPECIFIC_REPOS+=("$1"); shift ;; esac done # If specific repos provided, validate them if [ ${#SPECIFIC_REPOS[@]} -gt 0 ]; then for repo in "${SPECIFIC_REPOS[@]}"; do valid=false for valid_repo in "${PRODUCTION_REPOS[@]}"; do if [ "$repo" = "$valid_repo" ]; then valid=true break fi done if [ "$valid" = false ]; then fail "Unknown repo: $repo. Valid repos: ${PRODUCTION_REPOS[*]}" fi done REPOS_TO_DEPLOY=("${SPECIFIC_REPOS[@]}") else REPOS_TO_DEPLOY=("${PRODUCTION_REPOS[@]}") fi # ── Deploy Function ─────────────────────────────────────────────────── deploy_repo() { local repo="$1" local repo_dir="${REPOS_BASE_DIR}/${repo}" log "══════════════════════════════════════════════════════════════════════" log "Deploying: $repo" log "══════════════════════════════════════════════════════════════════════" 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 for $repo..." if ! git diff-index --quiet HEAD --; then fail "Uncommitted changes in $repo. Commit or stash first, or use --force" fi if [ -n "$(git ls-files --others --exclude-standard)" ]; then fail "Untracked files in $repo. Commit or remove them, or use --force" fi 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 in $repo. Push first or use --force" fi ok "Dirty checks passed for $repo" else warn "Skipping dirty checks for $repo (--force enabled)" fi # ── Pull and Rebase ─────────────────────────────────────────────── log "Pulling latest changes for $repo..." 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 $repo..." git rebase origin/main || { fail "Rebase failed for $repo. Resolve conflicts and run: git rebase --continue" } ok "Rebase completed for $repo" else ok "$repo is already up to date" fi # ── Build and Deploy ─────────────────────────────────────────────── if [ -f "docker-compose.yml" ]; then log "Building and deploying $repo..." # Pull latest base images log "Pulling latest base images for $repo..." docker pull bytelyst-common-base-backend:latest || warn "Failed to pull backend base image for $repo, using local cache" docker pull bytelyst-common-base-web:latest || warn "Failed to pull web base image for $repo, using local cache" docker compose build || fail "Docker build failed for $repo" docker compose up -d || fail "Docker compose up failed for $repo" ok "Deployment completed for $repo" else warn "No docker-compose.yml found in $repo, skipping Docker deployment" fi cd "$REPOS_BASE_DIR" } # ── Deploy All Repos ─────────────────────────────────────────────────── FAILED_REPOS=() SUCCESSFUL_REPOS=() for repo in "${REPOS_TO_DEPLOY[@]}"; do if deploy_repo "$repo"; then SUCCESSFUL_REPOS+=("$repo") else FAILED_REPOS+=("$repo") fi done # ── Summary ───────────────────────────────────────────────────────────── log "══════════════════════════════════════════════════════════════════════" log "Deployment Summary" log "══════════════════════════════════════════════════════════════════════" if [ ${#SUCCESSFUL_REPOS[@]} -gt 0 ]; then ok "Successfully deployed: ${SUCCESSFUL_REPOS[*]}" fi if [ ${#FAILED_REPOS[@]} -gt 0 ]; then fail "Failed to deploy: ${FAILED_REPOS[*]}" fi # ── Health Checks (if not skipped) ─────────────────────────────────────── if [ "$SKIP_HEALTH_CHECK" = false ]; then log "Running health checks..." # Define health endpoints for each service declare -A HEALTH_ENDPOINTS=( ["learning_ai_invt_trdg"]="http://localhost:4025/health/live" ["learning_ai_common_plat"]="http://localhost:4003/health" ["learning_ai_clock"]="http://localhost:4011/health" ["learning_ai_notes"]="http://localhost:4016/health" ) for repo in "${SUCCESSFUL_REPOS[@]}"; do endpoint="${HEALTH_ENDPOINTS[$repo]:-}" if [ -n "$endpoint" ]; then log "Checking $repo at $endpoint" if curl -sf "$endpoint" > /dev/null 2>&1; then ok "$repo is healthy" else warn "$repo health check failed (may still be starting)" fi fi done else warn "Skipping health checks (--skip-health-check enabled)" fi log "══════════════════════════════════════════════════════════════════════" ok "All deployments completed successfully!" log "══════════════════════════════════════════════════════════════════════"