#!/usr/bin/env bash set -euo pipefail # ═══════════════════════════════════════════════════════════════════════ # ByteLyst NoteLett - Production Deployment Script # ═══════════════════════════════════════════════════════════════════════ # Usage: ./deploy-notes.sh [option] # # Options (interactive menu when no arguments): # 1 - Normal deployment (with cache, with health checks) # 2 - Force deployment (skip dirty checks, with cache) # 3 - Skip health checks (with cache) # 4 - No-cache build (force rebuild, with health checks) # 5 - Force + No-cache (skip checks, force rebuild) # 6 - Force + Skip health checks (skip both) # 7 - All options: Force + Skip health + No-cache # # Command-line options: # --force Skip dirty checks and force deployment # --skip-health-check Skip endpoint health verification # --no-cache Force rebuild without cache # ═══════════════════════════════════════════════════════════════════════ 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_notes" cd "$SCRIPT_DIR" FORCE=false SKIP_HEALTH_CHECK=false NO_CACHE=false # Parse command-line arguments while [[ $# -gt 0 ]]; do case "$1" in --force) FORCE=true; shift ;; --skip-health-check) SKIP_HEALTH_CHECK=true; shift ;; --no-cache) NO_CACHE=true; shift ;; *) fail "Unknown option: $1";; esac done # ── Interactive Menu ───────────────────────────────────────────────── if [ "$FORCE" = false ] && [ "$SKIP_HEALTH_CHECK" = false ] && [ "$NO_CACHE" = false ]; then echo "" echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║${NC} ByteLyst NoteLett - Deployment Options ${CYAN}║${NC}" echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e " ${GREEN}1${NC} - Normal deployment (with cache, with health checks)" echo -e " ${GREEN}2${NC} - Force deployment (skip dirty checks, with cache)" echo -e " ${GREEN}3${NC} - Skip health checks (with cache)" echo -e " ${GREEN}4${NC} - No-cache build (force rebuild, with health checks)" echo -e " ${GREEN}5${NC} - Force + No-cache (skip checks, force rebuild)" echo -e " ${GREEN}6${NC} - Force + Skip health checks (skip both)" echo -e " ${GREEN}7${NC} - All options: Force + Skip health + No-cache" echo "" read -r -p "Select option (1-7): " choice case $choice in 1) ;; 2) FORCE=true ;; 3) SKIP_HEALTH_CHECK=true ;; 4) NO_CACHE=true ;; 5) FORCE=true; NO_CACHE=true ;; 6) FORCE=true; SKIP_HEALTH_CHECK=true ;; 7) FORCE=true; SKIP_HEALTH_CHECK=true; NO_CACHE=true ;; *) fail "Invalid option. Please select 1-7." ;; esac fi # ── 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 # ── Package Publication Check ─────────────────────────────────────────── log "Checking @bytelyst package publication status..." # Read Gitea token from file unset GITEA_NPM_TOKEN if [ -f "/opt/bytelyst/.gitea_token" ]; then GITEA_NPM_TOKEN="$(< /opt/bytelyst/.gitea_token)" elif [ -f "$HOME/.gitea_npm_token" ]; then GITEA_NPM_TOKEN="$(< "$HOME/.gitea_npm_token")" fi if [ -z "$GITEA_NPM_TOKEN" ]; then warn "Gitea token not found, skipping package publication check" else GITEA_REGISTRY="http://localhost:3300/api/packages/ByteLyst/npm/" CRITICAL_PACKAGES=( "@bytelyst/config|@bytelyst%2fconfig" "@bytelyst/cosmos|@bytelyst%2fcosmos" "@bytelyst/errors|@bytelyst%2ferrors" "@bytelyst/fastify-core|@bytelyst%2ffastify-core" ) MISSING_PACKAGES=() for entry in "${CRITICAL_PACKAGES[@]}"; do package="${entry%%|*}" encoded="${entry##*|}" if ! curl -sf -H "Authorization: token ${GITEA_NPM_TOKEN}" "${GITEA_REGISTRY}${encoded}" > /dev/null 2>&1; then MISSING_PACKAGES+=("$package") fi done if [ "${#MISSING_PACKAGES[@]}" -eq 0 ]; then ok "All critical @bytelyst packages are published" else warn "Some @bytelyst packages may not be published: ${MISSING_PACKAGES[*]}" read -r -p "Continue anyway? (y/N): " continue_anyway if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then fail "Deployment cancelled" fi 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 BUILD_ARGS=() if [ "$NO_CACHE" = true ]; then BUILD_ARGS+=(--no-cache) log "Building Docker images without cache..." else log "Building Docker images..." fi docker compose build "${BUILD_ARGS[@]}" || fail "Docker build failed" log "Starting services..." docker compose up -d || 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:4016/health > /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:4016)" else fail "Backend health check failed" fi # Check web health WEB_HEALTH=false for _ in {1..30}; do if curl -sf http://localhost:3045 > /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:3045)" else warn "Web health check failed (may be starting up)" fi # ── Endpoint Verification ───────────────────────────────────────────── log "Verifying production endpoints..." API_ENDPOINT="https://api.bytelyst.com/notelett" WEB_ENDPOINT="https://notes.bytelyst.com" # Check API endpoint if curl -sf "$API_ENDPOINT/health" > /dev/null 2>&1; then ok "API endpoint accessible: $API_ENDPOINT" else warn "API endpoint not accessible: $API_ENDPOINT (may need DNS propagation or routing)" 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 be starting up)" fi # ── API Smoke Tests (Post-Deployment) ─────────────────────────────────── log "Running post-deployment API smoke tests..." # Test backend health endpoint BACKEND_URL="http://localhost:4016" if curl -sf "$BACKEND_URL/health" > /dev/null 2>&1; then ok "Backend health endpoint responding" # Try to get health details HEALTH_RESPONSE=$(curl -s "$BACKEND_URL/health" 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:3045" if curl -sf "$WEB_URL" > /dev/null 2>&1; then ok "Web frontend is serving content" # Check if it's actually HTML 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:4016 → $API_ENDPOINT" log "Web: http://localhost:3045 → $WEB_ENDPOINT" log "══════════════════════════════════════════════════════════════════════"