name: Coverage Gate on: pull_request: workflow_dispatch: permissions: contents: read pull-requests: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} cancel-in-progress: true defaults: run: # The CI container's default `sh` is dash, which rejects `set -o pipefail` # and bashisms like `mapfile`. Force bash for every `run:` step. shell: bash jobs: frontend-coverage: name: Frontend Coverage (Vitest) runs-on: ubuntu-22.04 container: image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 1 - name: Cache pnpm store uses: actions/cache@v5 with: path: ~/.local/share/pnpm/store key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | pnpm-store-${{ runner.os }}- - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run Vitest with coverage run: pnpm test:coverage working-directory: app env: NODE_ENV: test - name: Normalize lcov source paths to repo root # Vitest writes paths relative to app/ (the Vite root). diff-cover # resolves SF: paths against the repo root, so prefix them with `app/` # to match how `git diff` names the files. run: | set -euo pipefail test -f app/coverage/lcov.info sed -i -E 's#^SF:(src/)#SF:app/\1#' app/coverage/lcov.info sed -i -E 's#^SF:(\./src/)#SF:app/src/#' app/coverage/lcov.info - name: Upload frontend lcov uses: actions/upload-artifact@v5 with: name: lcov-frontend path: app/coverage/lcov.info retention-days: 7 if-no-files-found: error rust-core-coverage: name: Rust Core Coverage (cargo-llvm-cov) runs-on: ubuntu-22.04 # See test.yml `rust-core-tests` — same shared-singleton flake risk in # the lib suite. Coverage instrumentation roughly doubles wall time vs. # the plain test runs; give it enough headroom for cold/warm cache # variance while still surfacing deadlocks with logs. timeout-minutes: 45 container: image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 env: CARGO_INCREMENTAL: '0' # sccache is incompatible with `-C instrument-coverage` profiles, so we # skip it for coverage runs and rely on Swatinem/rust-cache for warmup. steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 1 submodules: recursive - name: Cache Rust build artifacts uses: Swatinem/rust-cache@v2 with: workspaces: . -> target cache-on-failure: true key: core-coverage - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Run cargo llvm-cov for openhuman core run: cargo llvm-cov -p openhuman --lcov --output-path lcov-core.info - name: Upload core lcov uses: actions/upload-artifact@v5 with: name: lcov-rust-core path: lcov-core.info retention-days: 7 if-no-files-found: error rust-tauri-coverage: name: Rust Tauri Coverage (cargo-llvm-cov) runs-on: ubuntu-22.04 container: image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 env: CARGO_INCREMENTAL: '0' steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 1 submodules: recursive - name: Cache Rust build artifacts uses: Swatinem/rust-cache@v2 with: workspaces: | . -> target app/src-tauri -> target cache-on-failure: true key: tauri-coverage - name: Cache CEF binary distribution uses: actions/cache@v5 with: path: ~/.cache/tauri-cef key: cef-ubuntu-22.04-${{ hashFiles('app/src-tauri/Cargo.toml') }} restore-keys: | cef-ubuntu-22.04- - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Run cargo llvm-cov for Tauri shell run: cargo llvm-cov --manifest-path app/src-tauri/Cargo.toml --lcov --output-path lcov-tauri.info - name: Upload tauri lcov uses: actions/upload-artifact@v5 with: name: lcov-rust-tauri path: lcov-tauri.info retention-days: 7 if-no-files-found: error coverage-gate: name: Coverage Gate (diff-cover ≥ 80%) needs: [frontend-coverage, rust-core-coverage, rust-tauri-coverage] runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@v5 with: # diff-cover needs full history for the merge-base with the PR base. fetch-depth: 0 - name: Fetch PR base branch run: git fetch origin "${{ github.base_ref }}" --depth=200 - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install diff-cover run: pip install 'diff-cover>=9.2.0' - name: Download all lcov artifacts uses: actions/download-artifact@v5 with: path: lcov-artifacts pattern: lcov-* merge-multiple: false - name: List collected lcov files run: | set -euo pipefail find lcov-artifacts -type f -name '*.info' -print - name: Enforce ≥ 80% coverage on changed lines # diff-cover accepts multiple lcov inputs and computes coverage on # *changed lines only*, scoped to files present in the lcov report. # Test files are excluded from the lcov reports themselves (Vitest # `coverage.exclude`, cargo-llvm-cov's `#[cfg(test)]` filtering), # so changed test lines are simply not measured and do not skew the # ratio. run: | set -euo pipefail mapfile -t LCOV_FILES < <(find lcov-artifacts -type f -name '*.info' | sort) if [ "${#LCOV_FILES[@]}" -eq 0 ]; then echo "::error::No lcov files found — coverage gate cannot run" exit 1 fi diff-cover "${LCOV_FILES[@]}" \ --compare-branch="origin/${{ github.base_ref }}" \ --fail-under=80 \ --html-report diff-coverage.html \ --markdown-report diff-coverage.md - name: Upload diff-cover report if: always() uses: actions/upload-artifact@v5 with: name: diff-coverage-report path: | diff-coverage.html diff-coverage.md retention-days: 14 if-no-files-found: warn