Skip to content

Add cross-platform Coverage CI #3

Add cross-platform Coverage CI

Add cross-platform Coverage CI #3

Workflow file for this run

name: Coverage
# Runs clang source-based coverage on every PR (advisory only —
# never gates) and on a nightly schedule. The output is a per-line
# union of all platforms in the matrix, exported as the
# `coverage-merged` artifact (containing both .json and .md), and
# consumed by `coverage-comment.yml` (a separate workflow with the
# write token) to post the report on PRs / update the tracking
# issue.
#
# This workflow mirrors the regular ctest invocation across the CI
# matrix, so the coverage it reports is exactly what every-PR CI
# exercises.
on:
pull_request:
branches: [ main ]
schedule:
# Nightly at 04:00 UTC; cheapest free-runner slot.
- cron: '0 4 * * *'
workflow_dispatch:
# Default token; the build does not push, comment, or modify any
# resource. The follow-on `coverage-comment.yml` is the only writer.
permissions: read-all
concurrency:
group: coverage-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
# ============================================================================
# Linux + macOS host builds via reusable-cmake-build.yml.
# ============================================================================
build:
name: coverage / ${{ matrix.os }}${{ matrix.label-suffix }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
label-suffix: ''
label: ubuntu-24.04
dependencies: 'sudo apt install -y ninja-build clang-19 llvm-19'
extra-cmake-flags: '-DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19'
self-host: false
coverage-mode: tests
- os: macos-14
label-suffix: ''
label: macos-14
# macOS runners have AppleClang preinstalled but `llvm-cov`
# / `llvm-profdata` are NOT on PATH (they live behind
# `xcrun`). The coverage target's find_program(LLVM_COV ...)
# only looks for unversioned/-19/-15 names, so we install
# llvm@19 via brew and pass the explicit binary paths.
#
# SDKROOT is exported in the dependencies step so brew
# clang treats the Apple SDK as a system header path,
# which suppresses -Wundef on Apple's _STDC_VERSION_
# check inside <sys/cdefs.h>.
dependencies: |
brew install --quiet ninja llvm@19
echo "SDKROOT=$(xcrun --show-sdk-path)" >> "$GITHUB_ENV"
extra-cmake-flags: >-
-DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm@19/bin/clang++
-DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm@19/bin/clang
-DLLVM_COV=/opt/homebrew/opt/llvm@19/bin/llvm-cov
-DLLVM_PROFDATA=/opt/homebrew/opt/llvm@19/bin/llvm-profdata
self-host: false
coverage-mode: tests
# ------------------------------------------------------------------
# Self-host coverage legs. The shim is preloaded into ninja during
# a clean rebuild; profraws are written by the shim's child
# processes and merged into selfhost.json.
#
# Two legs because shim and shim-checks exercise different
# mitigations: shim-checks adds bounds-checked memcpy +
# SNMALLOC_CHECK_LOADS, which is a distinct coverage surface.
# ------------------------------------------------------------------
- os: ubuntu-24.04
label-suffix: ' / self-host shim'
label: linux-self-host-shim
dependencies: 'sudo apt install -y ninja-build clang-19 llvm-19'
extra-cmake-flags: '-DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19'
self-host: true
coverage-mode: tests+selfhost
- os: ubuntu-24.04
label-suffix: ' / self-host shim-checks'
label: linux-self-host-shim-checks
dependencies: 'sudo apt install -y ninja-build clang-19 llvm-19'
extra-cmake-flags: '-DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 -DSNMALLOC_MEMCPY_BOUNDS=ON -DSNMALLOC_CHECK_LOADS=ON'
self-host: true
coverage-mode: tests+selfhost
uses: ./.github/workflows/reusable-cmake-build.yml
with:
os: ${{ matrix.os }}
# build-type is overridden to Debug by the reusable workflow
# whenever coverage-mode != 'off', but the input is required.
build-type: Debug
cmake-config: '-G Ninja'
extra-cmake-flags: ${{ matrix.extra-cmake-flags }}
dependencies: ${{ matrix.dependencies }}
self-host: ${{ matrix.self-host }}
coverage-mode: ${{ matrix.coverage-mode }}
coverage-artifact-name: ${{ matrix.label }}
# ============================================================================
# FreeBSD / NetBSD via reusable-vm-build.yml.
#
# llvm-profdata / llvm-cov must be installed in the VM by the
# `dependencies:` script. The reusable workflow forces
# copyback: true when coverage-mode != 'off' so coverage.json
# makes it back to the host runner.
# ============================================================================
build-vm:
name: coverage / ${{ matrix.label }}
strategy:
fail-fast: false
matrix:
include:
- label: freebsd-14
vm-type: freebsd
vm-version: '14.1'
# FreeBSD's llvm19 port installs versioned binaries
# (clang19, clang++19, llvm-cov19, llvm-profdata19)
# directly under /usr/local/bin/ — not under a
# /usr/local/llvm19/bin/ subtree. Pass absolute paths so
# find_program is preset and doesn't depend on PATH.
dependencies: 'pkg install -y cmake ninja llvm19'
cmake-flags: >-
-DCMAKE_CXX_COMPILER=/usr/local/bin/clang++19
-DCMAKE_C_COMPILER=/usr/local/bin/clang19
-DLLVM_COV=/usr/local/bin/llvm-cov19
-DLLVM_PROFDATA=/usr/local/bin/llvm-profdata19
- label: netbsd-10
vm-type: netbsd
vm-version: '10.0'
# NetBSD pkgsrc installs unversioned clang/llvm-cov/
# llvm-profdata under /usr/pkg/bin. The default system
# compiler is gcc, so we still must select clang explicitly
# because SNMALLOC_COVERAGE rejects non-Clang compilers.
dependencies: '/usr/sbin/pkg_add cmake ninja-build llvm clang'
cmake-flags: >-
-DCMAKE_CXX_COMPILER=clang++
-DCMAKE_C_COMPILER=clang
uses: ./.github/workflows/reusable-vm-build.yml
with:
vm-type: ${{ matrix.vm-type }}
vm-version: ${{ matrix.vm-version }}
build-type: Debug
dependencies: ${{ matrix.dependencies }}
cmake-flags: ${{ matrix.cmake-flags }}
coverage-mode: tests
coverage-artifact-name: ${{ matrix.label }}
# ============================================================================
# Windows clang-cl pre-gate. Configure + build only — coverage
# instrumentation on Windows is unverified upstream, so this leg
# validates that SNMALLOC_COVERAGE=ON compiles and links cleanly
# before we try to extract a coverage.json from clang-cl. If this
# is green for a sustained period, a follow-up promotes it to a
# full coverage leg.
# ============================================================================
windows-pregate:
name: coverage / windows clang-cl pre-gate
uses: ./.github/workflows/reusable-cmake-build.yml
with:
os: windows-2022
build-type: Debug
cmake-config: '-G Ninja'
extra-cmake-flags: '-DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl'
dependencies: ''
build-only: true
coverage-mode: tests
coverage-artifact-name: windows-2022-pregate
# ============================================================================
# Merge per-line union across every leg that produced
# coverage.json. The Windows pre-gate intentionally does NOT
# upload — `build-only: true` skips both the coverage target and
# the upload step.
# ============================================================================
merge:
name: merge coverage
needs: [ build, build-vm ]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Download coverage artifacts
uses: actions/download-artifact@v4
with:
path: coverage-artifacts
pattern: coverage-*
merge-multiple: false
- name: Inventory artifacts
run: |
set -euo pipefail
echo "Downloaded artifacts:"
find coverage-artifacts -mindepth 1 -maxdepth 1 -type d -printf ' %f\n'
echo
echo "JSON files:"
find coverage-artifacts -name '*.json' -printf ' %p (%s bytes)\n'
- name: Build merge inputs
id: inputs
run: |
set -euo pipefail
# Each artifact directory is named coverage-<label>; the
# merger takes "label=path" pairs. Self-host runs that
# produced selfhost.json get a separate label suffixed
# "-selfhost" so the merger treats them as distinct
# platforms in the per-platform breakdown.
# The directory list is sorted for deterministic merge
# input order (otherwise the per-platform table order in
# the rendered markdown depends on filesystem readdir).
inputs=()
while IFS= read -r d; do
label="${d##*/coverage-}"
label="${label%/}"
if [ -f "$d/coverage.json" ]; then
inputs+=("$label=$d/coverage.json")
fi
if [ -f "$d/selfhost.json" ]; then
inputs+=("$label-selfhost=$d/selfhost.json")
fi
done < <(find coverage-artifacts -mindepth 1 -maxdepth 1 -type d -name 'coverage-*' | sort)
if [ ${#inputs[@]} -eq 0 ]; then
echo "::error::no coverage JSON artifacts found"
exit 1
fi
printf 'merge inputs:\n'
printf ' %s\n' "${inputs[@]}"
# Hand off via env (newline-delimited).
{
echo 'INPUTS<<EOF'
printf '%s\n' "${inputs[@]}"
echo 'EOF'
} >> "$GITHUB_ENV"
- name: Merge per-line union
run: |
set -euo pipefail
mapfile -t args < <(printf '%s\n' "$INPUTS")
python3 .github/scripts/merge_coverage.py \
--output-json coverage-merged.json \
--output-md coverage-merged.md \
"${args[@]}"
echo
echo "=== merged coverage (markdown preview) ==="
head -40 coverage-merged.md
echo
echo "=== merged coverage (JSON totals) ==="
python3 -c "import json; m=json.load(open('coverage-merged.json')); print('files:', len(m['files'])); print('totals:', m['totals']); print('platforms:', list(m['platforms'].keys()))"
- name: Upload merged coverage
uses: actions/upload-artifact@v4
with:
# Artifact name is consumed by coverage-comment.yml. If
# this name changes, update coverage-comment.yml's
# download-artifact step in lockstep.
name: coverage-merged
path: |
coverage-merged.json
coverage-merged.md
if-no-files-found: error
retention-days: 30