#!/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" # Source common library for standardized logging if ! source "$SCRIPT_DIR/scripts/bash/lib/common.sh" 2>/dev/null; then echo "❌ FATAL: Cannot load $SCRIPT_DIR/scripts/bash/lib/common.sh" >&2 exit 1 fi # 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}" # Specialized logging with timestamp for changelog context log() { info "[$(date '+%H:%M:%S')] $*"; } success() { ok "$*"; } # warn() function already provided by lib/common.sh 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 "$@"