Skip to content

Merge pull request #297 from BASE-Laboratory/claude/issue-289-mlmg-eb… #459

Merge pull request #297 from BASE-Laboratory/claude/issue-289-mlmg-eb…

Merge pull request #297 from BASE-Laboratory/claude/issue-289-mlmg-eb… #459

Workflow file for this run

# .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,
});
}