glmm 0.3.2

Standalone f64 GLMM fit kernels (OLS, GLM, LMM, GLMM) in pure Rust on faer — the validation-pinned numerics from the MCPower engine.
Documentation
name: CI

# Lint, the full #[test] suite across feature configs, plus the three gates a
# standalone publishable crate needs: an MSRV floor check, a wasm compile (the
# crate ships a cfg(wasm32) faer split), and a publish dry-run so `main` is always
# in a releasable state.
on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main]
  # Let release.yml reuse these exact gates before publishing (single source of truth).
  workflow_call:

# Newer pushes supersede in-flight runs on the same ref.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  # ── fmt + clippy + the regex-free-kernel guard, on stable ─────────────────
  # Split out from the #[test] suite so the four feature-variant test runs can
  # go wide in a matrix (below) instead of recompiling serially behind clippy.
  lint:
    name: lint
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@v2
      - name: fmt
        run: cargo fmt --all -- --check
      - name: clippy
        # The crate's workspace lints set clippy::all = deny, so a plain clippy
        # run is the correctness gate — no extra `-- -D warnings` needed.
        run: cargo clippy --all-targets
      - name: guard — the default-off kernel links no regex
        # `formula` is on by default (semver-covered surface), so the kernel's
        # regex-free property is no longer structural — this assertion IS the
        # guarantee.
        run: |
          if cargo tree -e normal -p glmm --no-default-features | grep -q regex; then
            echo "::error::default-off kernel pulls in regex — the formula feature is leaking"
            cargo tree -e normal -p glmm --no-default-features | grep -B5 regex
            exit 1
          fi
          echo "Kernel is regex-free with default features off."

  # ── the full #[test] suite across all four feature configs, in parallel ───
  test:
    name: test
    runs-on: ubuntu-latest
    timeout-minutes: 15
    env:
      # The dhat bounded-allocation gates assert exact alloc counts; faer/rayon
      # multi-thread jitter perturbs them, so pin the pool to one thread.
      RAYON_NUM_THREADS: 1
    strategy:
      # A failing feature config must not cancel the others — we want every
      # leg's result on one push, not just the first to break.
      fail-fast: false
      matrix:
        # Each entry is one recompile-and-test on its own runner:
        #   default        — the semver-covered surface.
        #   loop_advanced  — unstable scratch-explicit surface, off by default;
        #                    must still compile+pass so the hot-path consumer
        #                    never breaks silently.
        #   parallel       — rayon in-fit parallelism (AGQ cluster loop), off by
        #                    default; includes the bit-identity test pinning it
        #                    to the serial path's exact output.
        #   no-default     — the parse-once/fit-many formula-free kernel; the
        #                    formula tests are #![cfg(feature = "formula")] and
        #                    skip themselves.
        #   counters       — dev-only evaluation/PIRLS counters, off by default
        #                    and cfg'd out entirely without the feature, so no
        #                    other leg even compiles this code; fit output is
        #                    pinned bit-identical with the feature on.
        include:
          - { name: default, flags: "" }
          - { name: loop_advanced, flags: "--features loop_advanced" }
          - { name: parallel, flags: "--features parallel" }
          - { name: no-default, flags: "--no-default-features" }
          - { name: counters, flags: "--features counters" }
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: test (${{ matrix.name }})
        run: cargo test ${{ matrix.flags }}

  # ── MSRV floor: the bobyqa dep pins rust-version = 1.85 ───────────────────
  msrv:
    name: msrv (1.85)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.85
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-targets

  # ── wasm: keep the cfg(wasm32) faer config honest ─────────────────────────
  wasm:
    name: wasm
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --target wasm32-unknown-unknown
      - name: check (parallel feature, must be inert on wasm)
        # rayon is target-dep-excluded on wasm32, so the feature must compile to
        # a no-op here, never pull rayon in or break the build.
        run: cargo check --target wasm32-unknown-unknown --features parallel

  # ── package: gate the publishable state on every push ─────────────────────
  package:
    name: publish dry-run
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo publish --dry-run

  # ── python: build the glmm wheel, install it, test + lint ───────
  python:
    name: python (glmm wheel)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Build wheel (abi3, Linux x86_64)
        uses: PyO3/maturin-action@v1
        with:
          working-directory: python
          command: build
          args: --release --out dist
      - name: Install the built wheel (non-editable, catches packaging mistakes)
        run: pip install --find-links python/dist glmm
      - name: Install test + lint deps
        # ruff pinned so a new release's rule/formatter changes can't break CI
        # mid-release (0.16.0 did exactly that); bump deliberately.
        run: pip install pytest ruff==0.16.0
      - name: pytest against the installed wheel, run outside the source tree
        run: |
          cd "$RUNNER_TEMP"
          python -m pytest "$GITHUB_WORKSPACE/python/tests" -v
      - name: ruff check
        run: ruff check python/
      - name: ruff format --check
        run: ruff format --check python/