Files
AzerothCore-RealmMaster/deploy.sh
2025-10-28 20:36:31 -04:00

423 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# High-level orchestrator for module-aware deployments.
# 1. Ensures AzerothCore source repo is present
# 2. Runs ac-modules to sync/clean module checkout and configs
# 3. Rebuilds source images when C++ modules demand it
# 4. Stages target compose profile and optionally tails worldserver logs
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
ENV_PATH="$ROOT_DIR/.env"
TARGET_PROFILE=""
WATCH_LOGS=1
KEEP_RUNNING=0
WORLD_LOG_SINCE=""
ASSUME_YES=0
COMPILE_MODULE_VARS=(
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE
MODULE_TRANSMOG MODULE_NPC_BUFFER MODULE_DYNAMIC_XP MODULE_SOLO_LFG MODULE_1V1_ARENA MODULE_PHASED_DUELS
MODULE_BREAKING_NEWS MODULE_BOSS_ANNOUNCER MODULE_ACCOUNT_ACHIEVEMENTS MODULE_AUTO_REVIVE MODULE_GAIN_HONOR_GUARD
MODULE_TIME_IS_TIME MODULE_POCKET_PORTAL MODULE_RANDOM_ENCHANTS MODULE_SOLOCRAFT MODULE_PVP_TITLES MODULE_NPC_BEASTMASTER
MODULE_NPC_ENCHANTER MODULE_INSTANCE_RESET MODULE_LEVEL_GRANT MODULE_ARAC MODULE_ASSISTANT MODULE_REAGENT_BANK
MODULE_CHALLENGE_MODES MODULE_OLLAMA_CHAT MODULE_PLAYER_BOT_LEVEL_BRACKETS MODULE_STATBOOSTER MODULE_DUNGEON_RESPAWN
MODULE_SKELETON_MODULE MODULE_BG_SLAVERYVALLEY MODULE_AZEROTHSHARD MODULE_WORGOBLIN
)
BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
info(){ printf '%b\n' "${BLUE} $*${NC}"; }
ok(){ printf '%b\n' "${GREEN}$*${NC}"; }
warn(){ printf '%b\n' "${YELLOW}⚠️ $*${NC}"; }
err(){ printf '%b\n' "${RED}$*${NC}"; }
show_deployment_header(){
printf '\n%b\n' "${BLUE}⚔️ AZEROTHCORE REALM DEPLOYMENT ⚔️${NC}"
printf '%b\n' "${BLUE}═══════════════════════════════════════${NC}"
printf '%b\n\n' "${BLUE}🏰 Bringing Your Realm Online 🏰${NC}"
}
show_step(){
local step="$1" total="$2" message="$3"
printf '%b\n' "${YELLOW}🔧 Step ${step}/${total}: ${message}...${NC}"
}
show_realm_ready(){
printf '\n%b\n' "${GREEN}⚔️ The realm has been forged! ⚔️${NC}"
printf '%b\n' "${GREEN}🏰 Adventurers may now enter your world${NC}"
printf '%b\n\n' "${GREEN}🗡️ May your server bring epic adventures!${NC}"
}
usage(){
cat <<EOF
Usage: $(basename "$0") [options]
Options:
--profile {standard|playerbots|modules} Force target profile (default: auto-detect)
--no-watch Do not tail worldserver logs after staging
--keep-running Do not pre-stop runtime stack
--yes, -y Auto-confirm deployment prompts
--watch-logs Tail worldserver logs even if --no-watch was set earlier
--log-tail LINES Override WORLD_LOG_TAIL (number of log lines to show)
--once Run status checks once (alias for --no-watch)
-h, --help Show this help
This command automates deployment: sync modules, stage the correct compose profile,
and optionally watch worldserver logs.
Image Requirements:
This script assumes Docker images are already built. If you have custom modules:
• Run './build.sh' first to build custom images
• Standard AzerothCore images will be pulled automatically
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) TARGET_PROFILE="$2"; shift 2;;
--no-watch) WATCH_LOGS=0; shift;;
--keep-running) KEEP_RUNNING=1; shift;;
--yes|-y) ASSUME_YES=1; shift;;
-h|--help) usage; exit 0;;
*) err "Unknown option: $1"; usage; exit 1;;
esac
done
require_cmd(){
command -v "$1" >/dev/null 2>&1 || { err "Missing required command: $1"; exit 1; }
}
require_cmd docker
read_env(){
local key="$1" default="${2:-}"
local value=""
if [ -f "$ENV_PATH" ]; then
value="$(grep -E "^${key}=" "$ENV_PATH" | tail -n1 | cut -d'=' -f2- | tr -d '\r')"
fi
if [ -z "$value" ]; then
value="$default"
fi
echo "$value"
}
resolve_project_name(){
local raw_name="$(read_env COMPOSE_PROJECT_NAME "acore-compose")"
local sanitized
sanitized="$(echo "$raw_name" | tr '[:upper:]' '[:lower:]')"
sanitized="${sanitized// /-}"
sanitized="$(echo "$sanitized" | tr -cd 'a-z0-9_-')"
if [[ -z "$sanitized" ]]; then
sanitized="acore-compose"
elif [[ ! "$sanitized" =~ ^[a-z0-9] ]]; then
sanitized="ac${sanitized}"
fi
echo "$sanitized"
}
filter_empty_lines(){
awk '
/^[[:space:]]*$/ {
empty_count++
if (empty_count <= 1) print
}
/[^[:space:]]/ {
empty_count = 0
print
}
'
}
compose(){
local project_name
project_name="$(resolve_project_name)"
# Add --quiet for less verbose output, filter excessive empty lines
docker compose --project-name "$project_name" -f "$COMPOSE_FILE" "$@" | filter_empty_lines
}
# Build detection logic
detect_build_needed(){
local reasons=()
# Check sentinel file
if modules_need_rebuild; then
reasons+=("Module changes detected (sentinel file present)")
fi
# Check if any C++ modules are enabled but modules-latest images don't exist
local any_cxx_modules=0
local var
for var in "${COMPILE_MODULE_VARS[@]}"; do
if [ "$(read_env "$var" "0")" = "1" ]; then
any_cxx_modules=1
break
fi
done
if [ "$any_cxx_modules" = "1" ]; then
local authserver_modules_image
local worldserver_modules_image
authserver_modules_image="$(read_env AC_AUTHSERVER_IMAGE_MODULES "uprightbass360/azerothcore-wotlk-playerbots:authserver-modules-latest")"
worldserver_modules_image="$(read_env AC_WORLDSERVER_IMAGE_MODULES "uprightbass360/azerothcore-wotlk-playerbots:worldserver-modules-latest")"
if ! docker image inspect "$authserver_modules_image" >/dev/null 2>&1; then
reasons+=("C++ modules enabled but authserver modules image $authserver_modules_image is missing")
fi
if ! docker image inspect "$worldserver_modules_image" >/dev/null 2>&1; then
reasons+=("C++ modules enabled but worldserver modules image $worldserver_modules_image is missing")
fi
fi
printf '%s\n' "${reasons[@]}"
}
stop_runtime_stack(){
info "Stopping runtime stack to avoid container name conflicts"
compose \
--profile services-standard \
--profile services-playerbots \
--profile services-modules \
--profile db \
--profile client-data \
--profile client-data-bots \
--profile modules \
down 2>/dev/null || true
}
# Deployment sentinel management
mark_deployment_complete(){
local storage_path
storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
if [[ "$storage_path" != /* ]]; then
# Remove leading ./ if present
storage_path="${storage_path#./}"
storage_path="$ROOT_DIR/$storage_path"
fi
local sentinel="$storage_path/modules/.last_deployed"
if ! mkdir -p "$(dirname "$sentinel")" 2>/dev/null; then
warn "Cannot create local-storage directory. Deployment tracking may not work properly."
return 0
fi
date > "$sentinel"
}
modules_need_rebuild(){
local storage_path
storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
if [[ "$storage_path" != /* ]]; then
# Remove leading ./ if present
storage_path="${storage_path#./}"
storage_path="$ROOT_DIR/$storage_path"
fi
local sentinel="$storage_path/modules/.requires_rebuild"
[[ -f "$sentinel" ]]
}
# Build prompting logic
prompt_build_if_needed(){
local build_reasons_output
build_reasons_output=$(detect_build_needed)
if [ -z "$build_reasons_output" ]; then
return 0 # No build needed
fi
local build_reasons
readarray -t build_reasons <<< "$build_reasons_output"
# Check if auto-rebuild is enabled
local auto_rebuild
auto_rebuild="$(read_env AUTO_REBUILD_ON_DEPLOY "0")"
if [ "$auto_rebuild" = "1" ]; then
warn "Auto-rebuild enabled, running build process..."
if (cd "$ROOT_DIR" && ./build.sh --yes); then
ok "Build completed successfully"
return 0
else
err "Build failed"
return 1
fi
fi
# Interactive prompt
echo
warn "Build appears to be required:"
local reason
for reason in "${build_reasons[@]}"; do
warn "$reason"
done
echo
if [ -t 0 ]; then
local reply
read -r -p "Run build now? [y/N]: " reply
reply="${reply:-n}"
case "$reply" in
[Yy]*)
if (cd "$ROOT_DIR" && ./build.sh --yes); then
ok "Build completed successfully"
return 0
else
err "Build failed"
return 1
fi
;;
*)
err "Build required but declined. Run './build.sh' manually before deploying or re-run this script."
return 1
;;
esac
else
err "Build required but running non-interactively. Run './build.sh' manually before deploying or re-run this script."
return 1
fi
}
determine_profile(){
if [ -n "$TARGET_PROFILE" ]; then
echo "$TARGET_PROFILE"
return
fi
local module_playerbots
local playerbot_enabled
module_playerbots="$(read_env MODULE_PLAYERBOTS "0")"
playerbot_enabled="$(read_env PLAYERBOT_ENABLED "0")"
if [ "$module_playerbots" = "1" ] || [ "$playerbot_enabled" = "1" ]; then
echo "playerbots"
return
fi
local var
for var in "${COMPILE_MODULE_VARS[@]}"; do
if [ "$(read_env "$var" "0")" = "1" ]; then
echo "modules"
return
fi
done
echo "standard"
}
stage_runtime(){
local args=(--yes)
if [ -n "$TARGET_PROFILE" ]; then
args+=("$TARGET_PROFILE")
fi
info "Staging runtime environment via stage-modules.sh ${args[*]}"
(cd "$ROOT_DIR" && ./scripts/stage-modules.sh "${args[@]}")
}
tail_world_logs(){
info "Tailing worldserver logs (Ctrl+C to stop)"
local args=(--follow)
if [ -n "$WORLD_LOG_SINCE" ]; then
args+=(--since "$WORLD_LOG_SINCE")
fi
local tail_opt="${WORLD_LOG_TAIL:-0}"
args+=(--tail "$tail_opt")
if ! docker logs "${args[@]}" ac-worldserver; then
warn "Worldserver logs unavailable; container may not be running."
fi
}
wait_for_worldserver_ready(){
local timeout="${WORLD_READY_TIMEOUT:-180}" start
start="$(date +%s)"
info "Waiting for worldserver to become ready (timeout: ${timeout}s)"
info "First deployment may take 10-15 minutes while client-data is extracted"
while true; do
if ! docker ps --format '{{.Names}}' | grep -qx "ac-worldserver"; then
info "Worldserver container is not running yet; retrying..."
else
local health
health="$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' ac-worldserver 2>/dev/null || echo none)"
case "$health" in
healthy)
WORLD_LOG_SINCE="$(docker inspect --format='{{.State.StartedAt}}' ac-worldserver 2>/dev/null)"
ok "Worldserver reported healthy"
return 0
;;
none)
if docker inspect --format='{{.State.Status}}' ac-worldserver 2>/dev/null | grep -q '^running$'; then
WORLD_LOG_SINCE="$(docker inspect --format='{{.State.StartedAt}}' ac-worldserver 2>/dev/null)"
ok "Worldserver running (no healthcheck configured)"
return 0
fi
;;
unhealthy)
info "Worldserver starting up - waiting for client-data to complete..."
info "This may take several minutes on first deployment while data files are extracted"
;;
esac
fi
if [ $(( $(date +%s) - start )) -ge "$timeout" ]; then
info "Worldserver is still starting up after ${timeout}s. This is normal for first deployments."
info "Client-data extraction can take 10-15 minutes. Check progress with './status.sh' or container logs."
return 1
fi
sleep 3
done
}
main(){
if [ "$ASSUME_YES" -ne 1 ]; then
if [ -t 0 ]; then
read -r -p "Proceed with AzerothCore deployment? [y/N]: " reply
reply="${reply:-n}"
else
warn "No --yes flag provided and standard input is not interactive; aborting deployment."
exit 1
fi
case "$reply" in
[Yy]*) info "Deployment confirmed."; ;;
*) err "Deployment cancelled."; exit 1 ;;
esac
else
info "Auto-confirming deployment (--yes supplied)."
fi
show_deployment_header
local resolved_profile
resolved_profile="$(determine_profile)"
show_step 1 4 "Checking build requirements"
if ! prompt_build_if_needed; then
err "Build required but not completed. Deployment cancelled."
exit 1
fi
if [ "$KEEP_RUNNING" -ne 1 ]; then
show_step 2 4 "Stopping runtime stack"
stop_runtime_stack
fi
show_step 3 4 "Bringing your realm online"
info "Pulling images and waiting for containers to become healthy; this may take a few minutes on first deploy."
stage_runtime
show_step 4 4 "Finalizing deployment"
mark_deployment_complete
show_realm_ready
if [ "$WATCH_LOGS" -eq 1 ]; then
if wait_for_worldserver_ready; then
info "Watching your realm come to life (Ctrl+C to stop watching)"
tail_world_logs
else
info "Worldserver still initializing. Client-data extraction may still be in progress."
info "Use './status.sh' to monitor progress or 'docker logs ac-worldserver' to view startup logs."
fi
else
ok "Realm deployment completed. Use './status.sh' to monitor your realm."
fi
}
main