Merge pull request #297 from BASE-Laboratory/claude/issue-289-mlmg-eb… #459
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # .github/workflows/build-test.yml | |
| name: Build and Test OpenImpala | |
| # Define workflow triggers | |
| on: | |
| push: | |
| # Trigger on pushes to these specific branches | |
| branches: | |
| - master | |
| - working | |
| - build | |
| - 'makefile' # Include makefile branch trigger | |
| pull_request: | |
| # Trigger on Pull Requests targeting these branches | |
| branches: [ master, working ] | |
| workflow_dispatch: # Allows manual triggering | |
| jobs: | |
| format-check: | |
| name: C++ Format Check (clang-format) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Install clang-format | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang-format | |
| - name: Run clang-format check | |
| run: | | |
| find src/ tests/ python/bindings/ -type f \( -name "*.cpp" -o -name "*.H" -o -name "*.h" -o -name "*.hpp" -o -name "*.cc" -o -name "*.cxx" -o -name "*.c" \) \ | |
| -exec clang-format --dry-run --Werror {} + | |
| - name: Check @file/@brief documentation in headers | |
| run: | | |
| MISSING=0 | |
| for f in $(find src/ -type f -name "*.H" -o -name "*.h" | sort); do | |
| if ! head -5 "$f" | grep -q "@file"; then | |
| echo "::warning file=$f::Missing @file doxygen tag" | |
| MISSING=$((MISSING + 1)) | |
| fi | |
| done | |
| if [ "$MISSING" -gt 0 ]; then | |
| echo "" | |
| echo "WARNING: $MISSING header(s) missing @file/@brief documentation." | |
| echo "Add a doxygen block before the include guard:" | |
| echo ' /** @file MyHeader.H' | |
| echo ' * @brief One-line description of the file.' | |
| echo ' */' | |
| fi | |
| static-analysis: | |
| name: Static Analysis (clang-tidy) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Apptainer | |
| uses: eWaterCycle/setup-apptainer@v2 | |
| with: | |
| apptainer-version: 1.2.5 | |
| - name: Restore Dependency SIF Cache | |
| id: cache-restore-sif-tidy | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: dependency_image.sif | |
| key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }} | |
| restore-keys: | | |
| ${{ runner.os }}-apptainer-sif- | |
| - name: Build Dependency SIF Image (if cache miss) | |
| id: build_sif_tidy | |
| if: steps.cache-restore-sif-tidy.outputs.cache-hit != 'true' | |
| run: | | |
| sudo apptainer build --force dependency_image.sif containers/Singularity.deps.def | |
| - name: Run clang-tidy | |
| run: | | |
| sudo apptainer exec --writable-tmpfs --bind $PWD:/src ./dependency_image.sif \ | |
| bash -c ' | |
| set -e | |
| source /opt/rh/gcc-toolset-11/enable | |
| # Rocky 8 mirrors occasionally 404 on repodata for a few minutes | |
| # before AlmaLinux mirrors catch up. Retry the install with | |
| # backoff up to 3 times — typical transient outages recover | |
| # well within that window — then fail clearly with an | |
| # infrastructure-error message rather than silently leaving | |
| # clang-tidy unavailable. | |
| install_ok=0 | |
| for attempt in 1 2 3; do | |
| if dnf install -y clang-tools-extra 2>&1 | tail -3 && \ | |
| command -v clang-tidy >/dev/null 2>&1; then | |
| install_ok=1 | |
| break | |
| fi | |
| echo "clang-tidy install attempt $attempt failed; sleeping ${attempt}0s..." | |
| sleep $((attempt * 10)) | |
| dnf clean all >/dev/null 2>&1 || true | |
| done | |
| if [ $install_ok -ne 1 ] || ! command -v clang-tidy >/dev/null 2>&1; then | |
| echo "::error::clang-tidy could not be installed (Rocky 8 mirror 404). This is an infrastructure issue, not a code issue. Re-run the job." | |
| exit 2 | |
| fi | |
| echo "clang-tidy version: $(clang-tidy --version | head -1)" | |
| cd /src | |
| # Fetch nlohmann/json header (used by ResultsJSON.H, normally via CMake FetchContent) | |
| NLOHMANN_VERSION="v3.11.3" | |
| mkdir -p /tmp/nlohmann_include/nlohmann | |
| curl -sL "https://github.com/nlohmann/json/releases/download/${NLOHMANN_VERSION}/json.hpp" \ | |
| -o /tmp/nlohmann_include/nlohmann/json.hpp | |
| echo "nlohmann/json ${NLOHMANN_VERSION} downloaded for clang-tidy" | |
| ERRORS=0 | |
| # Note: python/bindings/ excluded — pybind11 + Python.h not available | |
| # in the dependency container; those files are checked by clang-format | |
| for f in $(find src/ tests/ -path tests/unit -prune -o -type f -name "*.cpp" -print); do | |
| echo "Analyzing $f..." | |
| clang-tidy "$f" \ | |
| -warnings-as-errors="*" \ | |
| -header-filter="src/.*" \ | |
| -- -std=c++17 -fopenmp -DOMPI_SKIP_MPICXX \ | |
| -Isrc -Isrc/io -Isrc/props \ | |
| -I/opt/amrex/25.03/include \ | |
| -I/opt/hypre/v2.32.0/include \ | |
| -I/opt/hdf5/1.12.3/include \ | |
| -I/opt/libtiff/4.6.0/include \ | |
| -I/tmp/nlohmann_include || ERRORS=$((ERRORS+1)) | |
| done | |
| if [ $ERRORS -ne 0 ]; then | |
| echo "::error::clang-tidy found issues in $ERRORS file(s)." | |
| exit 1 | |
| fi | |
| echo "clang-tidy passed on all files." | |
| ' | |
| build-and-test-openimpala: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Apptainer | |
| uses: eWaterCycle/setup-apptainer@v2 | |
| with: | |
| apptainer-version: 1.2.5 | |
| - name: Restore Dependency SIF Cache | |
| id: cache-restore-sif | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: dependency_image.sif | |
| key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }} | |
| - name: Build Dependency SIF Image (if cache miss) | |
| if: steps.cache-restore-sif.outputs.cache-hit != 'true' | |
| run: sudo apptainer build --force dependency_image.sif containers/Singularity.deps.def | |
| - name: Save Dependency SIF Image Cache | |
| if: steps.cache-restore-sif.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: dependency_image.sif | |
| key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }} | |
| - name: Build and Test OpenImpala C++ | |
| run: | | |
| sudo apptainer exec \ | |
| --bind $PWD:/src \ | |
| ./dependency_image.sif \ | |
| bash -c ' | |
| source /opt/rh/gcc-toolset-11/enable | |
| cd /src | |
| rm -rf cmake_build && mkdir cmake_build && cd cmake_build | |
| # Notice we can use standard cmake now! | |
| cmake .. \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_C_COMPILER=$(which mpicc) \ | |
| -DCMAKE_CXX_COMPILER=$(which mpicxx) \ | |
| -DCMAKE_PREFIX_PATH="/opt/amrex/25.03;/opt/hypre/v2.32.0;/opt/hdf5/1.12.3;/opt/libtiff/4.6.0" \ | |
| -DBUILD_TESTING=ON | |
| make -j$(nproc) | |
| ctest --output-on-failure --timeout 300 | |
| ' | |
| python-bindings-test: | |
| name: Python Bindings Test | |
| runs-on: ubuntu-latest | |
| needs: build-and-test-openimpala | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Apptainer | |
| uses: eWaterCycle/setup-apptainer@v2 | |
| with: | |
| apptainer-version: 1.2.5 | |
| - name: Restore Dependency SIF Cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: dependency_image.sif | |
| key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }} | |
| - name: Build and Test Python Bindings | |
| run: | | |
| sudo apptainer exec \ | |
| --bind $PWD:/src \ | |
| ./dependency_image.sif \ | |
| bash -c ' | |
| source /opt/rh/gcc-toolset-11/enable | |
| cd /src | |
| rm -rf cmake_build_py && mkdir cmake_build_py && cd cmake_build_py | |
| # 1. Ask Python exactly where the pybind11 CMake files are | |
| PYBIND11_DIR=$(python3.11 -m pybind11 --cmakedir) | |
| echo "Found pybind11 CMake dir at: $PYBIND11_DIR" | |
| # 2. Pass it to CMake using -Dpybind11_DIR | |
| cmake .. \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_C_COMPILER=$(which mpicc) \ | |
| -DCMAKE_CXX_COMPILER=$(which mpicxx) \ | |
| -DPython_EXECUTABLE=/usr/bin/python3.11 \ | |
| -Dpybind11_DIR=${PYBIND11_DIR} \ | |
| -DCMAKE_PREFIX_PATH="/opt/amrex/25.03;/opt/hypre/v2.32.0;/opt/hdf5/1.12.3;/opt/libtiff/4.6.0" \ | |
| -DBUILD_TESTING=ON \ | |
| -DOPENIMPALA_PYTHON=ON | |
| make -j$(nproc) | |
| # pytest and pyAMReX are native to the container now! | |
| ctest --output-on-failure --timeout 120 -L python | |
| ' | |
| coverage: | |
| name: Code Coverage | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Apptainer | |
| uses: eWaterCycle/setup-apptainer@v2 | |
| with: | |
| apptainer-version: 1.2.5 | |
| - name: Restore Dependency SIF Cache | |
| id: cache-restore-sif-cov | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: dependency_image.sif | |
| key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }} | |
| restore-keys: | | |
| ${{ runner.os }}-apptainer-sif- | |
| - name: Build Dependency SIF Image (if cache miss) | |
| if: steps.cache-restore-sif-cov.outputs.cache-hit != 'true' | |
| run: | | |
| sudo apptainer build --force dependency_image.sif containers/Singularity.deps.def | |
| - name: Build with coverage and run tests | |
| run: | | |
| sudo apptainer exec \ | |
| --writable-tmpfs \ | |
| --bind $PWD:/src \ | |
| ./dependency_image.sif \ | |
| bash -c ' | |
| source /opt/rh/gcc-toolset-11/enable | |
| cd /src | |
| rm -rf cmake_build_cov && mkdir cmake_build_cov && cd cmake_build_cov | |
| # Changed cmake3 to cmake | |
| cmake .. \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCMAKE_C_COMPILER=$(which mpicc) \ | |
| -DCMAKE_CXX_COMPILER=$(which mpicxx) \ | |
| -DCMAKE_PREFIX_PATH="/opt/amrex/25.03;/opt/hypre/v2.32.0;/opt/hdf5/1.12.3;/opt/libtiff/4.6.0" \ | |
| -DBUILD_TESTING=ON \ | |
| -DENABLE_COVERAGE=ON | |
| make -j$(nproc) | |
| ctest --output-on-failure --timeout 300 || true | |
| ' | |
| - name: Generate coverage report | |
| run: | | |
| sudo apptainer exec \ | |
| --writable-tmpfs \ | |
| --bind $PWD:/src \ | |
| ./dependency_image.sif \ | |
| bash -c ' | |
| source /opt/rh/gcc-toolset-11/enable | |
| # Dropped the dnf install and used native python3.11 pip! | |
| python3.11 -m pip install gcovr | |
| cd /src | |
| gcovr --root . \ | |
| --filter "src/" \ | |
| --print-summary \ | |
| --gcov-ignore-parse-errors=suspicious_hits.warn_once_per_file \ | |
| --xml coverage.xml \ | |
| --txt coverage.txt \ | |
| cmake_build_cov/ | |
| ' | |
| - name: Generate summary | |
| if: always() | |
| run: | | |
| if [ -f coverage.txt ]; then | |
| echo "## Code Coverage Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| cat coverage.txt >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Upload coverage artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report-${{ github.run_id }} | |
| path: | | |
| coverage.xml | |
| coverage.txt | |
| retention-days: 30 | |
| if-no-files-found: warn | |
| - name: Upload to Codecov | |
| if: always() | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: coverage.xml | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Post coverage summary to PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let coverageText = ''; | |
| try { | |
| coverageText = fs.readFileSync('coverage.txt', 'utf8'); | |
| } catch (e) { | |
| coverageText = 'Coverage report not available.'; | |
| } | |
| const marker = '<!-- coverage-report -->'; | |
| const body = [ | |
| marker, | |
| '## Code Coverage Report', | |
| '', | |
| '```', | |
| coverageText, | |
| '```', | |
| '', | |
| '---', | |
| '*Generated by CI — coverage data from [gcovr](https://gcovr.com/)*', | |
| ].join('\n'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: body, | |
| }); | |
| } |