diff --git a/.gitignore b/.gitignore index 8d608bf..9e01291 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ database-import/*.sql database-import/*.sql.gz +database-import/*/ database-import/ImportBackup*/ source/* local-data-tools/ +changelogs/ storage/ local-storage/ .claude/ @@ -13,4 +15,4 @@ scripts/__pycache__/ .env package-lock.json package.json -todo.md +todo.md \ No newline at end of file diff --git a/changelog.sh b/changelog.sh new file mode 100755 index 0000000..53144e4 --- /dev/null +++ b/changelog.sh @@ -0,0 +1,532 @@ +#!/bin/bash +# Dynamic changelog generator for AzerothCore source repositories and modules +# Uses existing project configuration to automatically detect and track changes +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" + +# Load environment configuration (available on deployed servers) +if [ -f ".env" ]; then + set -a + source .env + set +a +fi + +# Default configuration +LOCAL_STORAGE_ROOT="${STORAGE_PATH_LOCAL:-./local-storage}" +OUTPUT_DIR="${CHANGELOG_OUTPUT_DIR:-./changelogs}" +DAYS_BACK="${CHANGELOG_DAYS_BACK:-7}" +FORMAT="${CHANGELOG_FORMAT:-markdown}" + +# Colors for output +GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[1;33m'; NC='\033[0m' +log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $*" >&2; } +success() { echo -e "${GREEN}✅${NC} $*" >&2; } +warn() { echo -e "${YELLOW}⚠️${NC} $*" >&2; } + +usage() { + cat <&2; exit 1;; + esac +done + +# Get last build time from container metadata +get_last_build_time() { + local containers=("ac-worldserver" "ac-authserver") + local images=("azerothcore-stack:worldserver-playerbots" "azerothcore-stack:authserver-playerbots") + local latest_date="" + + # Try to get build timestamp from containers and images + local sources=() + for container in "${containers[@]}"; do + sources+=("container:$container") + done + for image in "${images[@]}"; do + sources+=("image:$image") + done + + for source in "${sources[@]}"; do + local type="${source%%:*}" + local name="${source#*:}" + local build_date="" + + # Try build.source_date first, then build.timestamp as fallback + build_date=$(docker inspect "$name" --format='{{index .Config.Labels "build.source_date"}}' 2>/dev/null || echo "") + + if [[ -z "$build_date" || "$build_date" == "unknown" ]]; then + build_date=$(docker inspect "$name" --format='{{index .Config.Labels "build.timestamp"}}' 2>/dev/null || echo "") + fi + + if [[ -n "$build_date" && "$build_date" != "unknown" ]]; then + # Convert ISO date to YYYY-MM-DD format + build_date=$(echo "$build_date" | cut -d'T' -f1) + + # Keep the latest date + if [[ -z "$latest_date" ]] || [[ "$build_date" > "$latest_date" ]]; then + latest_date="$build_date" + fi + fi + done + + echo "$latest_date" +} + +# Determine date range +if [[ -n "$SINCE_DATE" ]]; then + SINCE_OPTION="--since=$SINCE_DATE" + DATE_DESC="since $SINCE_DATE" +else + # Try to use last build time as default + LAST_BUILD_DATE=$(get_last_build_time) + + if [[ -n "$LAST_BUILD_DATE" ]]; then + SINCE_OPTION="--since=$LAST_BUILD_DATE" + DATE_DESC="since last build ($LAST_BUILD_DATE)" + $VERBOSE && log "Using last build date: $LAST_BUILD_DATE" + else + SINCE_OPTION="--since=$(date -d "$DAYS_BACK days ago" +%Y-%m-%d)" + DATE_DESC="last $DAYS_BACK days (no build metadata found)" + $VERBOSE && warn "No build metadata found in containers, falling back to $DAYS_BACK days" + fi +fi + +# Auto-detect source variant and configuration +detect_source_config() { + local variant="core" + + # Check environment variables for playerbots mode + if [[ "${MODULE_PLAYERBOTS:-0}" == "1" ]] || [[ "${PLAYERBOT_ENABLED:-0}" == "1" ]] || [[ "${STACK_SOURCE_VARIANT:-}" == "playerbots" ]]; then + variant="playerbots" + fi + + # Also check which source directory actually exists (resolve relative paths) + local playerbots_path="$LOCAL_STORAGE_ROOT/source/azerothcore-playerbots" + local standard_path="$LOCAL_STORAGE_ROOT/source/azerothcore" + + # Convert to absolute paths if needed + if [[ "$playerbots_path" != /* ]]; then + if [[ -d "$PROJECT_ROOT/$playerbots_path" ]]; then + playerbots_path="$(realpath "$PROJECT_ROOT/$playerbots_path")" + else + playerbots_path="$PROJECT_ROOT/$playerbots_path" + fi + fi + if [[ "$standard_path" != /* ]]; then + if [[ -d "$PROJECT_ROOT/$standard_path" ]]; then + standard_path="$(realpath "$PROJECT_ROOT/$standard_path")" + else + standard_path="$PROJECT_ROOT/$standard_path" + fi + fi + + $VERBOSE && log "Checking absolute paths: playerbots=$playerbots_path, standard=$standard_path" >&2 + $VERBOSE && log "Playerbots exists: $([[ -d "$playerbots_path/.git" ]] && echo "yes" || echo "no")" >&2 + $VERBOSE && log "Standard exists: $([[ -d "$standard_path/.git" ]] && echo "yes" || echo "no")" >&2 + + if [[ "$variant" == "core" && -d "$playerbots_path/.git" && ! -d "$standard_path/.git" ]]; then + variant="playerbots" + $VERBOSE && log "Switched to playerbots variant" >&2 + fi + + # Repository URLs from environment or defaults + local standard_repo="${ACORE_REPO_STANDARD:-https://github.com/azerothcore/azerothcore-wotlk.git}" + local standard_branch="${ACORE_BRANCH_STANDARD:-master}" + local playerbots_repo="${ACORE_REPO_PLAYERBOTS:-https://github.com/mod-playerbots/azerothcore-wotlk.git}" + local playerbots_branch="${ACORE_BRANCH_PLAYERBOTS:-Playerbot}" + + if [[ "$variant" == "playerbots" ]]; then + echo "$playerbots_repo|$playerbots_branch|$LOCAL_STORAGE_ROOT/source/azerothcore-playerbots" + else + echo "$standard_repo|$standard_branch|$LOCAL_STORAGE_ROOT/source/azerothcore" + fi +} + +# Get enabled modules from project configuration +get_enabled_modules() { + local modules_file="$LOCAL_STORAGE_ROOT/modules/.modules-meta/modules-enabled.txt" + local modules_state="$LOCAL_STORAGE_ROOT/modules/.modules_state" + + if [[ -f "$modules_file" ]]; then + cat "$modules_file" 2>/dev/null || true + elif [[ -f "$modules_state" ]]; then + # Parse modules state format: MODULE_NAME=1|MODULE_NAME2=0|... + grep -o 'MODULE_[^=]*=1' "$modules_state" 2>/dev/null | sed 's/MODULE_//; s/=1//' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' || true + fi +} + +# Get module repository info using existing module management +get_module_repos() { + local python_helper="$PROJECT_ROOT/scripts/python/modules.py" + + if [[ -x "$python_helper" ]]; then + # Use existing module helper if available + python3 "$python_helper" list --format=repo 2>/dev/null || true + else + # Fallback: scan module directories for git repos + find "$LOCAL_STORAGE_ROOT/modules" -name ".git" -type d 2>/dev/null | while read -r git_dir; do + local module_dir="$(dirname "$git_dir")" + local module_name="$(basename "$module_dir")" + local repo_url="" + + if [[ -f "$git_dir/config" ]]; then + repo_url=$(grep -A1 '\[remote "origin"\]' "$git_dir/config" 2>/dev/null | grep -E '^\s*url\s*=' | sed 's/.*url\s*=\s*//' | tr -d '\r\n' || true) + fi + + if [[ -n "$repo_url" && -n "$module_name" ]]; then + echo "$repo_url|master|$module_dir" + fi + done + fi +} + +# Format changelog entry based on output format +format_changelog() { + local repo_name="$1" + local repo_url="$2" + local commit_count="$3" + local commits="$4" + + case "$FORMAT" in + json) + cat <&2 + return + fi + + # Handle git ownership issues on deployed servers + local original_dir="$PWD" + cd "$repo_path" + + # Ensure git can access this repository + git config --global --add safe.directory "$repo_path" 2>/dev/null || true + + # Get remote URL + local repo_url + repo_url=$(git remote get-url origin 2>/dev/null || echo "local") + + # Get commits in the specified timeframe + local commits + commits=$(git log --oneline --no-merges $SINCE_OPTION 2>/dev/null | head -50 || echo "") + + cd "$original_dir" + + if [[ -z "$commits" ]]; then + $VERBOSE && log "No commits found in $repo_name for $DATE_DESC" >&2 + return + fi + + local commit_count=$(echo "$commits" | wc -l) + $VERBOSE && log "Found $commit_count commits in $repo_name" >&2 + + # For summary format, just return the count + if [[ "$FORMAT" == "summary" ]]; then + echo "$commit_count" + return + fi + + # Format commits based on output format + local formatted_commits + case "$FORMAT" in + json) + formatted_commits=$(echo "$commits" | sed 's/^\([^ ]*\) \(.*\)$/ {"hash": "\1", "message": "\2"}/') + ;; + markdown) + formatted_commits=$(echo "$commits" | sed 's/^\([^ ]*\) \(.*\)$/- **\1**: \2/') + ;; + text) + formatted_commits=$(echo "$commits" | sed 's/^/ /') + ;; + esac + + format_changelog "$repo_name" "$repo_url" "$commit_count" "$formatted_commits" +} + +# Main execution +main() { + $VERBOSE && log "Generating changelog for $DATE_DESC" + + # Determine output destination + local output_file="" + local use_stdout=true + + if [[ "$SAVE_TO_FILE" == "true" ]]; then + use_stdout=false + mkdir -p "$OUTPUT_DIR" + local timestamp=$(date +"%Y%m%d_%H%M%S") + output_file="$OUTPUT_DIR/changelog_${timestamp}.${FORMAT}" + fi + + # Function to write output (either to file or stdout) + write_output() { + if [[ "$use_stdout" == "true" ]]; then + echo "$1" + else + echo "$1" >> "$output_file" + fi + } + + # Start output + case "$FORMAT" in + json) + if [[ "$use_stdout" == "false" ]]; then + echo "{" > "$output_file" + else + echo "{" + fi + write_output " \"generated\": \"$(date -Iseconds)\"," + write_output " \"period\": \"$DATE_DESC\"," + write_output " \"repositories\": [" + ;; + markdown) + write_output "# AzerothCore Changelog" + write_output "" + write_output "**Generated:** $(date)" + write_output "**Period:** $DATE_DESC" + write_output "" + ;; + text) + write_output "AzerothCore Changelog" + write_output "Generated: $(date)" + write_output "Period: $DATE_DESC" + write_output "$(printf '=%.0s' {1..50})" + ;; + summary) + # Summary format will be handled after collecting data + ;; + esac + + local first_repo=true + + # Collect all output in a variable first for console display + local changelog_content="" + local main_commits=0 + local module_commits=0 + local total_repos=0 + + # Function to collect output + collect_output() { + changelog_content+="$1"$'\n' + } + + # Main source repository + if ! $MAIN_ONLY; then + $VERBOSE && log "Processing main source repository..." + local source_config + source_config="$(detect_source_config)" + $VERBOSE && log "Source config: $source_config" >&2 + IFS='|' read -r repo_url branch repo_path <<< "$source_config" + + local repo_output + repo_output=$(get_repo_commits "$repo_path") + if [[ -n "$repo_output" ]]; then + if [[ "$FORMAT" == "summary" ]]; then + main_commits="$repo_output" + total_repos=$((total_repos + 1)) + else + if [[ "$FORMAT" == "json" && ! $first_repo ]]; then + collect_output "," + fi + collect_output "$repo_output" + first_repo=false + fi + fi + fi + + # Module repositories + if [[ "$INCLUDE_MODULES" != "false" && ! $MAIN_ONLY ]]; then + # Auto-detect if modules should be included + if [[ "$INCLUDE_MODULES" == "auto" ]]; then + local enabled_modules=$(get_enabled_modules) + if [[ -n "$enabled_modules" ]]; then + INCLUDE_MODULES=true + else + INCLUDE_MODULES=false + fi + fi + + if [[ "$INCLUDE_MODULES" == "true" ]]; then + $VERBOSE && log "Processing module repositories..." + + while IFS='|' read -r repo_url branch repo_path; do + [[ -z "$repo_path" ]] && continue + + local repo_output + repo_output=$(get_repo_commits "$repo_path") + if [[ -n "$repo_output" ]]; then + if [[ "$FORMAT" == "summary" ]]; then + module_commits=$((module_commits + repo_output)) + total_repos=$((total_repos + 1)) + else + if [[ "$FORMAT" == "json" && ! $first_repo ]]; then + collect_output "," + fi + collect_output "$repo_output" + first_repo=false + fi + fi + done < <(get_module_repos) + fi + fi + + # Handle different output formats + case "$FORMAT" in + summary) + local total_commits=$((main_commits + module_commits)) + if [[ $total_commits -eq 0 ]]; then + write_output "No changes since last build" + else + write_output "Changes since last build: ${total_commits} commits" + if [[ $main_commits -gt 0 ]]; then + write_output " Core: ${main_commits} commits" + fi + if [[ $module_commits -gt 0 ]]; then + write_output " Modules: ${module_commits} commits" + fi + write_output " Repositories: ${total_repos}" + fi + ;; + *) + # Output collected content (if any) + if [[ -n "$changelog_content" ]]; then + write_output "$changelog_content" + fi + + # Close output + case "$FORMAT" in + json) + write_output " ]" + write_output "}" + ;; + esac + ;; + esac + + # Show completion message + if [[ "$use_stdout" == "false" ]]; then + success "Changelog generated: $output_file" + if $VERBOSE; then + log "File size: $(du -h "$output_file" | cut -f1)" + fi + fi + + # Show summary if verbose + if $VERBOSE; then + local repo_count=0 + case "$FORMAT" in + json) + repo_count=$(echo "$changelog_content" | grep -c '"repository":' 2>/dev/null || echo "0") + ;; + *) + repo_count=$(echo "$changelog_content" | grep -E '^(## |=== )' | wc -l) + ;; + esac + log "Repositories processed: $repo_count" + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/scripts/bash/rebuild-with-modules.sh b/scripts/bash/rebuild-with-modules.sh index 6b60a3e..1598d15 100755 --- a/scripts/bash/rebuild-with-modules.sh +++ b/scripts/bash/rebuild-with-modules.sh @@ -330,9 +330,16 @@ else fi echo "🚀 Building AzerothCore with modules..." -docker compose build --no-cache -echo "🔖 Tagging modules-latest images" +# Generate build metadata +BUILD_TIMESTAMP=$(date -Iseconds) +BUILD_SOURCE_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown") +BUILD_SOURCE_DATE=$(git log -1 --format=%cd --date=iso-strict 2>/dev/null || echo "unknown") + +echo "📝 Build metadata:" +echo " • Timestamp: $BUILD_TIMESTAMP" +echo " • Source commit: $BUILD_SOURCE_COMMIT" +echo " • Source date: $BUILD_SOURCE_DATE" # Get image names and tags from .env.template TEMPLATE_FILE="$PROJECT_DIR/.env.template" @@ -360,6 +367,30 @@ strip_tag(){ fi } +add_build_labels(){ + local image="$1" + local timestamp="$2" + local commit="$3" + local date="$4" + + if [ -z "$image" ] || ! docker image inspect "$image" >/dev/null 2>&1; then + return 1 + fi + + # Create a temporary container to add labels + local temp_id + temp_id=$(docker create "$image") + docker commit \ + --change "LABEL build.timestamp=\"$timestamp\"" \ + --change "LABEL build.source_commit=\"$commit\"" \ + --change "LABEL build.source_date=\"$date\"" \ + "$temp_id" "$image" + docker rm "$temp_id" >/dev/null 2>&1 + + echo "📝 Added build metadata to $image" + return 0 +} + tag_if_exists(){ local source_image="$1" local target_image="$2" @@ -390,6 +421,15 @@ BUILT_WORLDSERVER_IMAGE="$WORLDSERVER_BASE_REPO:$SOURCE_IMAGE_TAG" BUILT_DB_IMPORT_IMAGE="$DB_IMPORT_BASE_REPO:$SOURCE_IMAGE_TAG" BUILT_CLIENT_DATA_IMAGE="$CLIENT_DATA_BASE_REPO:$SOURCE_IMAGE_TAG" +# Build images normally +docker compose build --no-cache + +echo "📝 Adding build metadata to images..." +add_build_labels "$BUILT_AUTHSERVER_IMAGE" "$BUILD_TIMESTAMP" "$BUILD_SOURCE_COMMIT" "$BUILD_SOURCE_DATE" || true +add_build_labels "$BUILT_WORLDSERVER_IMAGE" "$BUILD_TIMESTAMP" "$BUILD_SOURCE_COMMIT" "$BUILD_SOURCE_DATE" || true +add_build_labels "$BUILT_DB_IMPORT_IMAGE" "$BUILD_TIMESTAMP" "$BUILD_SOURCE_COMMIT" "$BUILD_SOURCE_DATE" || true +add_build_labels "$BUILT_CLIENT_DATA_IMAGE" "$BUILD_TIMESTAMP" "$BUILD_SOURCE_COMMIT" "$BUILD_SOURCE_DATE" || true + TARGET_AUTHSERVER_IMAGE="$(read_env AC_AUTHSERVER_IMAGE_MODULES "$(get_template_value "AC_AUTHSERVER_IMAGE_MODULES")")" TARGET_WORLDSERVER_IMAGE="$(read_env AC_WORLDSERVER_IMAGE_MODULES "$(get_template_value "AC_WORLDSERVER_IMAGE_MODULES")")" PLAYERBOTS_AUTHSERVER_IMAGE="$(read_env AC_AUTHSERVER_IMAGE_PLAYERBOTS "$(get_template_value "AC_AUTHSERVER_IMAGE_PLAYERBOTS")")"