name: CI
on:
push:
branches: ["main", "dev"]
paths-ignore:
- "**.md"
- "docs/**"
- ".gitignore"
- "*.service"
pull_request:
branches: ["main"]
paths-ignore:
- "**.md"
- "docs/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
should-run:
name: Check if CI should run
runs-on: ubuntu-latest
outputs:
run: ${{ steps.check.outputs.run }}
steps:
- name: Check commit message prefix
id: check
env:
MSG: ${{ github.event.head_commit.message }}
run: |
if echo "$MSG" | grep -qE '^(docs|style|chore)(\(.+\))?:'; then
echo "run=false" >> "$GITHUB_OUTPUT"
echo "Skipping CI for docs/style/chore commit"
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi
lint:
name: Lint (fmt + clippy)
needs: should-run
if: needs.should-run.outputs.run == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust stable + tools
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo registry & build
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-lint-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-lint-
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy (deny warnings)
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test (${{ matrix.os }} / Rust ${{ matrix.rust }})
needs: should-run
if: needs.should-run.outputs.run == 'true'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: [stable, beta]
exclude:
- os: windows-latest
rust: beta
- os: macos-latest
rust: beta
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-test-${{ matrix.rust }}-
- name: Build
run: cargo build --all-targets
- name: Run tests
run: cargo test --all -- --nocapture
coverage:
name: Coverage
needs: should-run
if: needs.should-run.outputs.run == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-tarpaulin
uses: taiki-e/install-action@v2
with:
tool: cargo-tarpaulin
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-cov-${{ hashFiles('**/Cargo.lock') }}
- name: Run coverage
run: |
cargo tarpaulin \
--out Xml \
--output-dir coverage \
--timeout 120 \
--exclude-files "src/main.rs" \
--ignore-tests
- name: Upload to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage/cobertura.xml
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
audit:
name: Security audit
needs: should-run
if: needs.should-run.outputs.run == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit
- name: Audit dependencies
run: cargo audit
ci-success:
name: CI passed
needs: [lint, test, coverage, audit]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all jobs passed
run: |
results="${{ join(needs.*.result, ' ') }}"
for r in $results; do
if [ "$r" != "success" ] && [ "$r" != "skipped" ]; then
echo "Job failed: $r"
exit 1
fi
done
echo "All CI jobs passed ✓"