name: CI
concurrency:
group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }}
cancel-in-progress: true
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
permissions:
contents: read
checks: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RUSTFLAGS: '-C debuginfo=line-tables-only -C incremental=false'
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.changes.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@master
id: changes
with:
predicate-quantifier: 'every'
filters: |
code:
- '**'
- '!**.md'
- '!docs/**'
- '!.claude/**'
- '!.gitignore'
- '!.justfile'
- '!LICENSE'
- '!.github/workflows/pr.yml'
- '!.github/workflows/release.yml'
- '!.github/workflows/security.yml'
- '!.github/dependabot.yml'
- '!.github/labeler.yml'
check:
name: Check
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.code == 'true'
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Rust nightly (for formatting)
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Check formatting
run: cargo +nightly fmt -- --check
- name: Run clippy
run: cargo +stable clippy --profile ci --all-features --all-targets -- -D warnings
test-unit:
name: Unit Tests
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.code == 'true'
strategy:
matrix:
features:
- "test-utils"
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.job }}-${{ strategy.job-index }}
- name: Run unit tests
run: |
cargo test --lib --profile ci -F ${{ matrix.features }} -- --nocapture --show-output
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.code == 'true'
strategy:
matrix:
features:
- "test-utils"
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.job }}-${{ strategy.job-index }}
- name: Run integration tests
run: |
cargo test --profile ci -F ${{ matrix.features }} --test "e2e" -- --nocapture --show-output
coverage:
name: Code Coverage
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.code == 'true'
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate code coverage
run: |
cargo llvm-cov clean --workspace
cargo llvm-cov --lcov --no-report --ignore-filename-regex "(examples).*" -F test-utils
cargo llvm-cov report --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: true
disable_telem: true
ci-success:
name: CI Success
needs: [changes, check, test-unit, test-integration, coverage]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check if all jobs succeeded
run: |
# If only docs changed, that's success
if [[ "${{ needs.changes.outputs.code }}" == "false" ]]; then
echo "Documentation-only change ✅"
exit 0
fi
# Otherwise check that all CI jobs succeeded
if [[ "${{ needs.check.result }}" != "success" ||
"${{ needs.test-unit.result }}" != "success" ||
"${{ needs.test-integration.result }}" != "success" ||
"${{ needs.coverage.result }}" != "success" ]]; then
echo "One or more jobs failed ❌"
exit 1
fi
echo "All CI jobs passed successfully! ✅"