#!/bin/bash set -euo pipefail # Manage deployment releases # Usage: ./manage.sh [--dry-run] COMMAND [ARGS] # Source common functions SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)" source "${SCRIPT_DIR}/common-lib.sh" # Global variables DRY_RUN=false COMMAND="" ARGS=() # Parse global options while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true shift ;; -*) echo "Error: Unknown option: $1" echo "Usage: $0 [--dry-run] COMMAND [ARGS]" exit 1 ;; *) # First non-option argument is the command if [ -z "$COMMAND" ]; then COMMAND="$1" else # Rest are command arguments ARGS+=("$1") fi shift ;; esac done if [ -z "$COMMAND" ]; then echo "Error: No command specified" echo "Usage: $0 [--dry-run] COMMAND [ARGS]" echo "Commands:" indent_output echo "status - Show deployment status" indent_output echo "list - List all releases" indent_output echo "version - Show current release" indent_output echo "switch VERSION - Switch to a specific release version" indent_output echo "cleanup [KEEP] - Clean up old releases (default: keep 5)" echo "" echo "Global options:" indent_output echo "--dry-run - Show what would happen without making changes" exit 1 fi REPO_PROJECT_PATH="$(realpath "${SCRIPT_DIR}/../../../")" CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" RELEASES_PATH="${REPO_PROJECT_PATH}/releases" # Announce dry-run mode if active if [ "$DRY_RUN" = true ]; then echo "🔍 DRY RUN MODE - No changes will be made" echo "" fi case "$COMMAND" in status) echo "🔍 Deployment Status" echo "====================" # Check if deployment is downgraded if [ -d "$RELEASES_PATH" ] && [ -L "$CURRENT_LINK_PATH" ]; then CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") # Find the latest release by modification time. LATEST_RELEASE_DIR_NAME=$(find "$RELEASES_PATH" -maxdepth 1 -mindepth 1 -type d -printf '%T@ %f\n' | sort -nr | head -n 1 | cut -d' ' -f2-) if [ -n "$LATEST_RELEASE_DIR_NAME" ]; then if [ "$CURRENT_RELEASE_DIR_NAME" == "$LATEST_RELEASE_DIR_NAME" ]; then echo "✅ Deployment is on the latest release (${LATEST_RELEASE_DIR_NAME})." else echo "âš ī¸ Deployment is downgraded." indent_output echo "Current: ${CURRENT_RELEASE_DIR_NAME}" indent_output echo "Latest: ${LATEST_RELEASE_DIR_NAME}" fi else # This case happens if RELEASES_PATH is empty echo "â„šī¸ No releases found in ${RELEASES_PATH}." fi elif [ ! -L "$CURRENT_LINK_PATH" ]; then echo "â„šī¸ No current deployment symlink found." else # RELEASES_PATH does not exist echo "â„šī¸ Releases directory not found at ${RELEASES_PATH}." fi echo "" # Add a newline for spacing # Get current state CURRENT_STATE=$(get_deployment_state) if [ "$CURRENT_STATE" = "both" ]; then echo "🟡 Deployment State: both" elif [ "$CURRENT_STATE" = "blue" ]; then echo "đŸ”ĩ Deployment State: blue" elif [ "$CURRENT_STATE" = "green" ]; then echo "đŸŸĸ Deployment State: green" else indent_output echo "Deployment State: none" fi echo "âš™ī¸ Core Containers:" indent_output docker ps --filter 'label=deployment.core=true' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' # Show containers by color with image info echo "đŸ”ĩ Blue Containers:" BLUE_COUNT=$(count_containers "--filter label=deployment.color=blue") # make sure BLUE_COUNT is a number BLUE_COUNT=$(echo "$BLUE_COUNT" | tr -d '\n') if [ "$BLUE_COUNT" -gt 0 ]; then BLUE_IMAGE=$(get_deployment_image_tag "blue") indent_output echo "Image: ${BLUE_IMAGE}" indent_output docker ps --filter 'label=deployment.color=blue' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' else indent_output echo "No blue containers running" fi echo "đŸŸĸ Green Containers:" GREEN_COUNT=$(count_containers "--filter label=deployment.color=green") if [ "$GREEN_COUNT" -gt 0 ]; then GREEN_IMAGE=$(get_deployment_image_tag "green") indent_output echo "Image: ${GREEN_IMAGE}" indent_output docker ps --filter 'label=deployment.color=green' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' else indent_output echo "No green containers running" fi list_releases "${REPO_PROJECT_PATH}" # Health check summary echo "â¤ī¸ Health Check Summary:" case "$CURRENT_STATE" in "both") indent_output echo "âš ī¸ WARNING: Both blue and green containers are running!" indent_output echo "This might indicate an incomplete deployment." ;; "none") indent_output echo "âš ī¸ WARNING: No web containers are running!" ;; *) if [ "$CURRENT_STATE" = "blue" ]; then indent_output echo "đŸ”ĩ System is running on blue deployment" else indent_output echo "đŸŸĸ System is running on green deployment" fi indent_output echo "â¤ī¸ Overall Healthcheck:" indent_output " " get_health_check_status ;; esac # Show resource usage echo "📊 Resource Usage:" indent_output docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}' # Show deployment images echo "đŸ“Ļ Deployment Images:" indent_output docker images 'badbl0cks/pkmntrade-club' --format 'table {{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' | grep -E '^TAG|sha-.{7} ' || indent_output echo "No deployment images found" ;; list) list_releases "${REPO_PROJECT_PATH}" ;; version) if [ -L "$CURRENT_LINK_PATH" ]; then current_version=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") echo "📌 Current version: ${current_version}" else echo "❌ No current deployment found" fi ;; switch) if [ ${#ARGS[@]} -lt 1 ]; then echo "Error: VERSION required for switch" echo "Usage: $0 [--dry-run] switch VERSION" exit 1 fi TARGET_VERSION="${ARGS[0]}" TARGET_PATH="${RELEASES_PATH}/${TARGET_VERSION}" # Validate target version exists if [ ! -d "$TARGET_PATH" ]; then echo "❌ Error: Version ${TARGET_VERSION} not found" echo "Available releases:" list_releases "${REPO_PROJECT_PATH}" exit 1 fi # Get current version if exists CURRENT_VERSION="none" CURRENT_VERSION_PATH="" if [ -L "$CURRENT_LINK_PATH" ]; then CURRENT_VERSION_PATH=$(readlink -f "$CURRENT_LINK_PATH") CURRENT_VERSION=$(basename "$CURRENT_VERSION_PATH") fi # Edge case: trying to switch to the same version if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then echo "✅ Already on version ${TARGET_VERSION}. No action taken." exit 0 fi CURRENT_COLOR=$(get_current_color) NEW_COLOR=$(switch_color "$CURRENT_COLOR") echo "🔄 Switch Plan:" indent_output echo "Current version: ${CURRENT_VERSION}" if [ "$CURRENT_VERSION" != "none" ]; then indent_output echo "Current path: ${CURRENT_VERSION_PATH}" indent_output echo "Current color: ${CURRENT_COLOR}" fi indent_output echo "Target version: ${TARGET_VERSION}" indent_output echo "Target path: ${TARGET_PATH}" indent_output echo "Target color: ${NEW_COLOR}" # Verify target release has necessary files echo "📋 Checking target release integrity..." MISSING_FILES=() for file in "docker-compose_web.yml" "docker-compose_core.yml" ".env"; do if [ ! -f "${TARGET_PATH}/${file}" ]; then MISSING_FILES+=("$file") fi done if [ ${#MISSING_FILES[@]} -gt 0 ]; then echo "❌ Error: Target release is missing required files:" printf " - %s\n" "${MISSING_FILES[@]}" exit 1 fi # Get compose files based on environment COMPOSE_FILES=$(get_compose_files) echo "🛑 Stopping current containers..." if [ -d "$CURRENT_VERSION_PATH" ]; then ( cd "$CURRENT_VERSION_PATH" || exit 1 WEB_PROJECT_NAME=$(get_project_name "$CURRENT_COLOR") indent_output echo "Stopping web containers for project: ${WEB_PROJECT_NAME}..." execute_or_warn "stop web containers" docker compose ${COMPOSE_FILES} -p "${WEB_PROJECT_NAME}" down indent_output echo "Stopping core services for project: ${CORE_PROJECT_NAME}..." execute_or_warn "stop core services" docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" down ) else indent_output echo "No current deployment to stop" fi echo "📝 Updating deployment metadata for ${TARGET_VERSION}..." execute_or_fail "update .deployment_color to ${NEW_COLOR}" \ bash -c "echo '${NEW_COLOR}' > '${TARGET_PATH}/.deployment_color'" execute_or_fail "update DEPLOYMENT_COLOR in .env" \ bash -c "sed -i 's/^DEPLOYMENT_COLOR=.*/DEPLOYMENT_COLOR=${NEW_COLOR}/' '${TARGET_PATH}/.env'" # Update symlink echo "🔗 Updating deployment symlink..." execute_or_fail "update symlink from $CURRENT_LINK_PATH to $TARGET_PATH" \ ln -sfn "$TARGET_PATH" "$CURRENT_LINK_PATH" # Start containers echo "🚀 Starting containers from ${TARGET_VERSION}..." ( cd "$TARGET_PATH" || exit 1 TARGET_WEB_PROJECT_NAME=$(get_project_name "$NEW_COLOR") indent_output echo "Starting core services for project: ${CORE_PROJECT_NAME}..." execute_or_fail "start core services" \ docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" up -d indent_output echo "Starting web containers for project: ${TARGET_WEB_PROJECT_NAME}..." execute_or_fail "start web containers" \ docker compose ${COMPOSE_FILES} -p "${TARGET_WEB_PROJECT_NAME}" up -d ) if [ "$DRY_RUN" = true ]; then echo "" echo "✅ Dry run completed - no changes made" else echo "✅ Switch completed to version: ${TARGET_VERSION}" echo "Run '$0 status' to verify deployment health" fi ;; cleanup) # Parse cleanup arguments KEEP_COUNT=5 for arg in "${ARGS[@]}"; do if [[ "$arg" =~ ^[0-9]+$ ]]; then KEEP_COUNT="$arg" else echo "Error: Invalid argument for cleanup: $arg" echo "Usage: $0 [--dry-run] cleanup [KEEP_COUNT]" exit 1 fi done echo "đŸ—‘ī¸ Cleaning up old releases (keeping last ${KEEP_COUNT} and current)" if [ ! -L "$CURRENT_LINK_PATH" ]; then echo "❌ No current deployment symlink found. Aborting cleanup." exit 1 fi CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") echo "📌 Current release: ${CURRENT_RELEASE_DIR_NAME}" if [ -d "$RELEASES_PATH" ]; then cd "$RELEASES_PATH" # Get a list of inactive release directories, sorted by modification time (newest first). INACTIVE_RELEASES=$(find . -maxdepth 1 -mindepth 1 -type d \ -not -name "$CURRENT_RELEASE_DIR_NAME" \ -printf '%T@ %f\n' | sort -nr | cut -d' ' -f2-) if [ -z "$INACTIVE_RELEASES" ]; then echo "No inactive releases found to clean up." exit 0 fi # Count total inactive releases TOTAL_INACTIVE=$(echo "$INACTIVE_RELEASES" | wc -l | xargs) echo "📊 Found ${TOTAL_INACTIVE} inactive release(s)" # Identify releases to delete by skipping the KEEP_COUNT newest ones. RELEASES_TO_DELETE=$(echo "$INACTIVE_RELEASES" | tail -n +$((KEEP_COUNT + 1))) if [ -n "$RELEASES_TO_DELETE" ]; then DELETE_COUNT=$(echo "$RELEASES_TO_DELETE" | wc -l | xargs) echo "đŸ—‘ī¸ The following ${DELETE_COUNT} old release(s) will be deleted:" # Show releases with their sizes while IFS= read -r release; do if [ -d "$release" ]; then SIZE=$(du -sh "$release" 2>/dev/null | cut -f1) indent_output echo "- $release (Size: $SIZE)" fi done <<< "$RELEASES_TO_DELETE" # Delete the releases echo "" while IFS= read -r release; do execute_if_not_dry "delete release $release" rm -rf "$release" done <<< "$RELEASES_TO_DELETE" if [ "$DRY_RUN" = true ]; then echo "" echo "✅ Dry run completed - no releases were deleted" else echo "✅ Cleanup completed - deleted ${DELETE_COUNT} release(s)" fi else KEPT_COUNT=$(echo "$INACTIVE_RELEASES" | wc -l | tr -d '\n') echo "No old releases to delete. Found ${KEPT_COUNT} inactive release(s), which is within the retention count of ${KEEP_COUNT}." fi else echo "No releases directory found" fi ;; *) echo "Error: Unknown command: $COMMAND" exit 1 ;; esac