pulsehive-db 0.5.0

Embedded database for agentic AI systems — collective memory for multi-agent coordination
Documentation
name: CI

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

# Cancel in-progress runs when new commits are pushed to the same PR
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: 0

jobs:
  # ─────────────────────────────────────────────
  # Job 1: Format & Lint (fast-fail gate)
  # ─────────────────────────────────────────────
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - uses: Swatinem/rust-cache@v2
        with:
          prefix-key: lint

      - name: Check formatting
        run: cargo fmt --check

      - name: Clippy (default features)
        run: cargo clippy -- -D warnings

      - name: Clippy (builtin-embeddings)
        run: cargo clippy --features builtin-embeddings -- -D warnings

      - name: Clippy (sync)
        run: cargo clippy --features sync -- -D warnings

      - name: Clippy (sync-http)
        run: cargo clippy --features sync-http -- -D warnings

  # ─────────────────────────────────────────────
  # Job 2: Test matrix (3 OS x 2 feature sets)
  # ─────────────────────────────────────────────
  test:
    name: Test (${{ matrix.name }})
    needs: lint
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            features: ""
            name: Linux / default
          - os: ubuntu-latest
            features: "--features builtin-embeddings"
            name: Linux / builtin-embeddings
          - os: ubuntu-latest
            features: "--features sync"
            name: Linux / sync
          - os: ubuntu-latest
            features: "--features sync-http"
            name: Linux / sync-http
          - os: macos-latest
            features: ""
            name: macOS / default
          - os: macos-latest
            features: "--features builtin-embeddings"
            name: macOS / builtin-embeddings
          - os: windows-latest
            features: ""
            name: Windows / default
          - os: windows-latest
            features: "--features builtin-embeddings"
            name: Windows / builtin-embeddings
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2
        with:
          prefix-key: "test-${{ matrix.os }}"

      - name: Build
        run: cargo build ${{ matrix.features }}

      - name: Run tests
        run: cargo test ${{ matrix.features }}

  # ─────────────────────────────────────────────
  # Job 3: MSRV verification (Rust 1.89)
  # ─────────────────────────────────────────────
  test-msrv:
    name: MSRV (Rust 1.89)
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.89"

      - uses: Swatinem/rust-cache@v2
        with:
          prefix-key: msrv

      - name: Build (default features)
        run: cargo build

      - name: Test (default features)
        run: cargo test

      - name: Build (builtin-embeddings)
        run: cargo build --features builtin-embeddings

      - name: Test (builtin-embeddings)
        run: cargo test --features builtin-embeddings

  # ─────────────────────────────────────────────
  # Job 4: Code coverage
  # ─────────────────────────────────────────────
  coverage:
    name: Coverage
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2
        with:
          prefix-key: coverage

      - name: Install cargo-tarpaulin
        uses: taiki-e/install-action@cargo-tarpaulin

      - name: Generate coverage
        run: cargo tarpaulin --fail-under 80 --out xml --out html

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: |
            cobertura.xml
            tarpaulin-report.html
        if: always()

  # ─────────────────────────────────────────────
  # Job 5: Security audit
  # ─────────────────────────────────────────────
  security:
    name: Security Audit
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install cargo-audit
        uses: taiki-e/install-action@cargo-audit

      - name: Run audit
        run: cargo audit

  # ─────────────────────────────────────────────
  # Job 6: Benchmarks
  # ─────────────────────────────────────────────
  bench:
    name: Benchmarks
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2
        with:
          prefix-key: bench

      - name: Install critcmp
        run: cargo install critcmp

      # Restore baseline from main branch (PRs only)
      - name: Restore baseline
        if: github.event_name == 'pull_request'
        uses: actions/cache/restore@v4
        with:
          path: target/criterion-baseline/
          key: criterion-baseline-main

      - name: Run benchmarks
        # The NFR-018 1M P99 bench is a local/manual source-of-truth: it builds a
        # persisted, seed-stamped ~4 GB fixture once (~76 min) and reuses it (D5).
        # CI has no persisted fixture, so running it at the literal default N=1M
        # would rebuild a 4 GB fixture every run. Cap the fixture size on CI so the
        # bench is a fast smoke; the literal-1M P99 gate is measured locally (#19).
        env:
          NFR018_N: "2000"
          NFR018_WARMUP: "20"
          NFR018_SAMPLES: "200"
        run: cargo bench

      # Compare against baseline and report regressions (PRs only, advisory)
      - name: Compare with baseline (10% threshold)
        if: github.event_name == 'pull_request'
        continue-on-error: true
        run: |
          if [ -d target/criterion-baseline ]; then
            critcmp target/criterion-baseline target/criterion --threshold 10
          else
            echo "No baseline found — skipping comparison"
          fi

      # Save current results as baseline (main branch only)
      - name: Save baseline
        if: github.ref == 'refs/heads/main'
        run: |
          rm -rf target/criterion-baseline
          cp -r target/criterion target/criterion-baseline

      - name: Cache baseline
        if: github.ref == 'refs/heads/main'
        uses: actions/cache/save@v4
        with:
          path: target/criterion-baseline/
          key: criterion-baseline-main

      - name: Upload benchmark results
        uses: actions/upload-artifact@v4
        with:
          name: benchmark-results
          path: target/criterion/

  # ─────────────────────────────────────────────
  # Job 7: Publish to crates.io (on v* tags only)
  # ─────────────────────────────────────────────
  publish:
    name: Publish to crates.io
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [lint, test, test-msrv, coverage, security]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Verify version matches tag
        run: |
          CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          TAG_VERSION=${GITHUB_REF#refs/tags/v}
          if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
            echo "ERROR: Cargo.toml version ($CARGO_VERSION) does not match tag (v$TAG_VERSION)"
            exit 1
          fi
          echo "Version verified: $CARGO_VERSION"

      - name: Publish
        run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true