name: Clang-Tidy on: workflow_call: workflow_dispatch: permissions: contents: read jobs: clang_tidy: name: Clang-Tidy Checks runs-on: ubuntu-24.04 steps: - name: Shallow checkout (no submodules) uses: actions/checkout@v7 with: submodules: false fetch-depth: 0 - name: Collect changed C/C++ files id: changed_files uses: tj-actions/changed-files@v47 with: files: | **/*.c **/*.cc **/*.cpp **/*.cxx **/*.h **/*.hpp files_ignore: | thirdparty/** build/** src/db/sqlengine/antlr/gen/** src/db/index/column/fts_column/gen/** - name: No C/C++ files changed - skip if: steps.changed_files.outputs.any_changed != 'true' run: echo "No C/C++ files changed. Skipping clang-tidy." - name: Checkout submodules if: steps.changed_files.outputs.any_changed == 'true' run: git submodule update --init --recursive --depth 1 - name: Install dependencies if: steps.changed_files.outputs.any_changed == 'true' run: | sudo apt-get update sudo apt-get install -y clang-tidy=1:18.0-59~exp2 cmake ninja-build libomp-dev libaio-dev - name: Setup ccache if: steps.changed_files.outputs.any_changed == 'true' uses: hendrikmuhs/ccache-action@v1.2 with: key: clang-tidy max-size: 500M - name: Configure CMake and export compile commands if: steps.changed_files.outputs.any_changed == 'true' run: | cmake -S . -B build -G Ninja \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DBUILD_TOOLS=ON \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Filter changed files against compile_commands.json id: tidy_files if: steps.changed_files.outputs.any_changed == 'true' run: | python3 - <<'PY' import json import os from pathlib import Path raw = os.environ.get("ALL_CHANGED_FILES", "") changed = [f for f in raw.split() if f] compile_db_path = Path("build/compile_commands.json") with compile_db_path.open("r", encoding="utf-8") as fh: compile_db = json.load(fh) compile_entries = set() for entry in compile_db: file_field = entry.get("file", "") if file_field: compile_entries.add(os.path.normpath(file_field)) cwd = Path.cwd().resolve() selected = [] skipped = [] for rel_path in changed: abs_path = os.path.normpath(str((cwd / rel_path).resolve())) if abs_path in compile_entries: selected.append(rel_path) elif not Path(rel_path).is_file(): skipped.append(f"{rel_path} (file not found)") else: skipped.append(f"{rel_path} (not in compile_commands.json)") github_output = os.environ["GITHUB_OUTPUT"] with open(github_output, "a", encoding="utf-8") as out: out.write(f"any_tidy_files={'true' if selected else 'false'}\n") out.write("all_tidy_files<> "$GITHUB_OUTPUT" - name: Restore generated headers cache id: cache_headers if: steps.changed_files.outputs.any_changed == 'true' uses: actions/cache/restore@v6 with: path: | build/external build/thirdparty build/src/db/proto key: clang-tidy-headers-${{ runner.os }}-${{ hashFiles('thirdparty/**/*.cmake', 'thirdparty/**/CMakeLists.txt', 'thirdparty/**/*.patch', 'src/db/proto/*.proto') }}-${{ steps.submodule_hash.outputs.value }} - name: Build generated headers only if: steps.tidy_files.outputs.any_tidy_files == 'true' && steps.cache_headers.outputs.cache-hit != 'true' run: | ninja -C build clang_tidy_deps - name: Save generated headers cache if: steps.tidy_files.outputs.any_tidy_files == 'true' && steps.cache_headers.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: path: | build/external build/thirdparty build/src/db/proto key: clang-tidy-headers-${{ runner.os }}-${{ hashFiles('thirdparty/**/*.cmake', 'thirdparty/**/CMakeLists.txt', 'thirdparty/**/*.patch', 'src/db/proto/*.proto') }}-${{ steps.submodule_hash.outputs.value }} - name: Run clang-tidy on changed files (parallel) if: steps.tidy_files.outputs.any_tidy_files == 'true' run: | mapfile -t files_to_check <<'TIDY_EOF' ${{ steps.tidy_files.outputs.all_tidy_files }} TIDY_EOF log_dir=$(mktemp -d) printf '%s\n' "${files_to_check[@]}" \ | grep -v '^\s*$' \ | xargs -I{} -P "$(nproc)" sh -c ' file="{}" if [ -f "$file" ]; then log="'"$log_dir"'/$$.log" echo "$file" > "$log" if clang-tidy -p build --quiet --warnings-as-errors="*" "$file" >> "$log" 2>&1; then echo "PASS: $file" rm -f "$log" else echo "FAIL: $file" fi fi' failed=0 for f in "$log_dir"/*.log; do [ -e "$f" ] || break failed=1 src=$(head -1 "$f") echo "" echo "::group::clang-tidy errors: $src" tail -n +2 "$f" echo "::endgroup::" done rm -rf "$log_dir" if [ "$failed" -eq 1 ]; then echo "::error::clang-tidy found issues in one or more files" exit 1 fi - name: No files to analyse if: steps.changed_files.outputs.any_changed == 'true' && steps.tidy_files.outputs.any_tidy_files != 'true' run: echo "Changed C/C++ files not in compile_commands.json. Nothing to analyse."