name: Dashboard CI description: | This workflow runs tests and builds for the AzerothCore dashboard. It includes testing of bash scripts and integration testing of the AzerothCore server. Do not remove this if something is broken here and you don't know how to fix it, ping Yehonal instead. on: push: branches: - 'master' - 'test-staging' pull_request: types: - opened - reopened - synchronize workflow_dispatch: concurrency: # One concurrency group per workflow + ref. # # - PRs use `refs/pull//merge`, so new commits cancel older # in-progress runs for the same PR. # - A merge does NOT cancel the PR's own runs: this group is keyed on # refs/pull//merge and the master push on refs/heads/master, so they # never share a group. # - Branch pushes are isolated by ref. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: actions: write contents: read env: CONTINUOUS_INTEGRATION: true MYSQL_ROOT_PASSWORD: root jobs: test-bash-scripts: name: Test Bash Scripts strategy: fail-fast: true matrix: include: - os: ubuntu-24.04 - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install requirements run: | sudo apt-get update # Install bats-core >= 1.5.0 to support bats_require_minimum_version sudo apt-get install -y git curl git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core sudo /tmp/bats-core/install.sh /usr/local bats --version ./acore.sh install-deps - name: Run bash script tests for ${{ matrix.test-module }} env: TERM: xterm-256color run: | ./acore.sh test bash --tap --all build-and-test: name: Build and Integration Test strategy: fail-fast: true matrix: include: - os: ubuntu-24.04 - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install ccache shell: bash run: | sudo apt-get update sudo apt-get install -y ccache ccache --version # Detect the compilers that acore.sh / CMake will end up using. # We record both the binary name and a short version tag for the cache key. - name: Detect compiler id: detect shell: bash run: | set -euo pipefail CC_BIN="${CC:-}" CXX_BIN="${CXX:-}" [[ -z "$CC_BIN" ]] && CC_BIN="$(command -v clang || command -v gcc)" [[ -z "$CXX_BIN" ]] && CXX_BIN="$(command -v clang++ || command -v g++)" make_ver_id() { local bin="$1"; local base="$(basename "$bin")" case "$base" in clang) maj="$("$bin" -dumpversion 2>/dev/null | cut -d. -f1)"; [[ -z "$maj" ]] && maj="$( "$bin" --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1 )" echo "clang-${maj:-unknown}" ;; clang++) maj="$("$bin" -dumpversion 2>/dev/null | cut -d. -f1)"; [[ -z "$maj" ]] && maj="$( "$bin" --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1 )" echo "clang++-${maj:-unknown}" ;; gcc) maj="$("$bin" -dumpfullversion -dumpversion 2>/dev/null || "$bin" -dumpversion 2>/dev/null)"; maj="${maj%%.*}" echo "gcc-${maj:-unknown}" ;; g++) maj="$("$bin" -dumpfullversion -dumpversion 2>/dev/null || "$bin" -dumpversion 2>/dev/null)"; maj="${maj%%.*}" echo "g++-${maj:-unknown}" ;; *) echo "$base" ;; esac } echo "cc_id=$(make_ver_id "$CC_BIN")" >> "$GITHUB_OUTPUT" echo "cxx_id=$(make_ver_id "$CXX_BIN")" >> "$GITHUB_OUTPUT" echo "Detected: $CC_BIN, $CXX_BIN" - name: Prepare ccache dir shell: bash run: mkdir -p "${{ github.workspace }}/var/ccache" - name: Echo cache key shell: bash run: echo "Cache key -> ccache:${{ matrix.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:${{ github.ref_name }}-${{ github.run_id }}" # The primary key must be unique per run. A static key hits on itself from # the second run onward and the save is then skipped, freezing the cache # at its first-run contents. It is keyed on matrix.os rather than # runner.os because both legs report Linux and share a run_id, so only the # detected compiler would separate them. The first restore-key picks up # the most recent cache for this leg on this ref; the second the most # recent on any ref the run can reach, which is its own and the base # branch, and is how a first run inherits master's. Both must use # matrix.os to stay in the key's namespace. # The runner.os entries below reach the linux-build action's caches, but # only on the 24.04 leg: on 26.04 this workflow detects clang-21 while # linux-build is passed clang-20, so nothing there lines up. - name: Restore ccache id: restore_ccache uses: actions/cache/restore@v4 with: path: ${{ github.workspace }}/var/ccache key: ccache:${{ matrix.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:${{ github.ref_name }}-${{ github.run_id }} restore-keys: | ccache:${{ matrix.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:${{ github.ref_name }}- ccache:${{ matrix.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:true:pch=false: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:false:pch=false: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:true:pch=true: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:false:pch=true: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:true: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:false: ccache:${{ runner.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}: - name: Setup ccache env shell: bash env: CCACHE_DIR: ${{ github.workspace }}/var/ccache run: | mkdir -p "$CCACHE_DIR" cat <> "$GITHUB_ENV" CCACHE_BASEDIR=${{ github.workspace }} CCACHE_DIR=${{ github.workspace }}/var/ccache CCACHE_HASHDIR=1 CCACHE_MAXSIZE=5G CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime CCACHE_COMPRESS=1 CCACHE_COMPRESSLEVEL=3 CCACHE_COMPILERCHECK=content CCACHE_LOGFILE=${{ github.workspace }}/var/ccache/cache.debug CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache EOF - name: ccache snapshot (before) shell: bash run: | echo "==== Effective ccache configuration ====" ccache -p | egrep 'base_dir|hash_dir|compiler_check|sloppiness|max_size' || true echo echo "==== Previous cache stats ====" ccache -s || true echo echo "==== Top cache results (from prior runs) ====" grep -o 'result: .*' "${{ github.workspace }}/var/ccache/cache.debug" 2>/dev/null | sort | uniq -c | sort -nr | head || true - name: Reset ccache stats shell: bash run: ccache -z || true - name: Configure AzerothCore settings run: | touch conf/config.sh echo 'MTHREADS=4' >> conf/config.sh echo 'CBUILD_TESTING=ON' >> conf/config.sh echo 'AC_ENABLE_ROOT_CMAKE_INSTALL=1' >> conf/config.sh echo 'export AC_CONFIG_POLICY=$AC_CONFIG_POLICY_PRESET_ZERO_CONF' >> conf/config.sh echo 'AC_ENABLE_CONF_COPY_ON_INSTALL=0' >> conf/config.sh cat conf/config.sh # debug content of AC_CONFIG_POLICY ./acore.sh config show AC_CONFIG_POLICY - name: Test module commands run: | ./acore.sh module install mod-autobalance ./acore.sh module install mod-duel-reset ./acore.sh module list ./acore.sh module install --all ./acore.sh module update mod-autobalance ./acore.sh module update --all - name: Run complete installation (deps, compile, database, client-data) run: | # This runs: install-deps, compile, database setup, client-data download ./acore.sh init sudo npm install -g pm2 timeout-minutes: 120 - name: Test module removal run: | ./acore.sh module remove mod-autobalance ./acore.sh module list ./acore.sh module remove mod-duel-reset ./acore.sh module list - name: Run core tests run: | ./acore.sh test core # This job runs zero-conf, so the core falls back to console-only logging and # writes no Errors.log, which is what the build workflows scrape. The grep is a # second net in case a later change stops the updater from failing the run on a # bad import. - name: Test authserver dry-run run: | source ./acore.sh config load cd env/dist/bin timeout 5m ./authserver -dry-run 2>&1 | tee "$RUNNER_TEMP/authserver-dry-run.log" status=${PIPESTATUS[0]} [ "$status" -eq 0 ] || exit "$status" if grep -qE "Applying of file .+ failed!" "$RUNNER_TEMP/authserver-dry-run.log"; then echo "::error::A SQL update failed to apply during the authserver dry run" exit 1 fi continue-on-error: false - name: Test worldserver dry-run run: | source ./acore.sh config load cd env/dist/bin timeout 5m ./worldserver -dry-run 2>&1 | tee "$RUNNER_TEMP/worldserver-dry-run.log" status=${PIPESTATUS[0]} [ "$status" -eq 0 ] || exit "$status" if grep -qE "Applying of file .+ failed!" "$RUNNER_TEMP/worldserver-dry-run.log"; then echo "::error::A SQL update failed to apply during the worldserver dry run" exit 1 fi continue-on-error: false - name: Test worldserver with startup scripts run: | ./acore.sh sm create world worldserver --bin-path ./env/dist/bin --provider pm2 ./acore.sh sm show-config worldserver ./acore.sh sm start worldserver ./acore.sh sm wait-uptime worldserver 10 300 ./acore.sh sm send worldserver "account create tester password 3" ./acore.sh sm send worldserver "account set gm tester 3" ./acore.sh sm send worldserver "account set addon tester 1" ./acore.sh sm wait-uptime worldserver 10 300 ./acore.sh sm stop worldserver ./acore.sh sm delete worldserver timeout-minutes: 30 continue-on-error: false - name: Test authserver with startup scripts run: | ./acore.sh sm create auth authserver --bin-path ./env/dist/bin --provider pm2 ./acore.sh sm show-config authserver ./acore.sh sm start authserver ./acore.sh sm wait-uptime authserver 10 300 ./acore.sh sm stop authserver ./acore.sh sm delete authserver timeout-minutes: 30 continue-on-error: false # Proves the dry-run steps above actually fail on a bad import. - name: Verify a broken SQL update fails the dry run run: | # Absolute: the trap fires after the cd below. probe="$PWD/data/sql/updates/pending_db_world/rev_9999999999999999999.sql" trap 'rm -f "$probe"' EXIT cat > "$probe" <<'EOF' DELETE FROM `ci_gate_probe_table_that_does_not_exist` WHERE `id` = 1; EOF source ./acore.sh config load cd env/dist/bin # Without this the step fails on a healthy tree as soon as it gains a # `shell: bash`: that turns pipefail on, so errexit aborts on the exit 1 the # assertions below are here to check for. set +o pipefail timeout 5m ./worldserver -dry-run 2>&1 | tee "$RUNNER_TEMP/broken-update-dry-run.log" status=${PIPESTATUS[0]} # Exactly 1, so a hang (timeout returns 124) does not pass as a detection. if [ "$status" -ne 1 ]; then echo "::error::expected worldserver -dry-run to exit 1 on a broken SQL update, got $status" exit 1 fi # 1 is also the generic startup failure code, so require the updater to be the one # that failed the run, on the probe file specifically. if ! grep -qE "Applying of file .*rev_9999999999999999999\.sql.* failed!" "$RUNNER_TEMP/broken-update-dry-run.log"; then echo "::error::worldserver -dry-run exited 1 without reporting the broken probe update" exit 1 fi if ! grep -q "Failed Database Update" "$RUNNER_TEMP/broken-update-dry-run.log"; then echo "::error::the dry run did not terminate on the failed update count" exit 1 fi timeout-minutes: 10 # The old "only save on a cache miss" guard is dropped: the per-run key # only ever collides on a re-run, since github.run_id is stable across # attempts, and there the save is a no-op anyway. # A failed build still compiled most of the tree and that work is worth # keeping. A cancelled one is skipped so a superseded run cannot publish # a half-populated cache as the newest for the ref, and a run that died # before the restore has no compiler id and no ccache dir to save. - name: Save ccache if: ${{ !cancelled() && steps.restore_ccache.outcome == 'success' }} uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/var/ccache key: ccache:${{ matrix.os }}:${{ steps.detect.outputs.cc_id }}_${{ steps.detect.outputs.cxx_id }}:${{ github.ref_name }}-${{ github.run_id }} - name: ccache stats (after) shell: bash run: ccache -s || true