pg_walstream 0.6.0

PostgreSQL logical replication protocol library - parse and handle PostgreSQL WAL streaming messages
Documentation
name: CI

on:
  push:
    branches: [ main, develop ]
    tags:
      - 'v*'
  pull_request:
    branches: [ "*" ]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  MIN_COVERAGE: 90

jobs:
  test:
    name: Test Suite
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        rust: [stable, beta]

    steps:
    - uses: actions/checkout@v4

    - name: Install Rust
      uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ matrix.rust }}
        components: rustfmt, clippy

    - name: Install dependencies (Ubuntu)
      if: matrix.os == 'ubuntu-latest'
      run: |
        sudo apt-get update
        sudo apt-get install -y libpq-dev pkg-config libssl-dev

    - name: Cache cargo registry
      uses: actions/cache@v4
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-cargo-

    - name: Cache target directory
      uses: actions/cache@v4
      with:
        path: target
        key: ${{ runner.os }}-${{ matrix.rust }}-target-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-${{ matrix.rust }}-target-

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

    - name: Build
      run: cargo build --verbose

    - name: Run unit tests
      run: cargo test --lib --verbose

  security:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - name: Install cargo-audit
      run: cargo install cargo-audit
    - name: Run security audit
      run: cargo audit

  docs:
    name: Documentation
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable

    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y libpq-dev pkg-config libssl-dev

    - name: Build documentation
      run: cargo doc --no-deps --all-features
      env:
        RUSTDOCFLAGS: "-D warnings"

  coverage:
    name: Code Coverage
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Install Rust
      uses: dtolnay/rust-toolchain@stable
      with:
        components: llvm-tools-preview

    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y libpq-dev pkg-config libssl-dev

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

    - name: Cache cargo registry
      uses: actions/cache@v4
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
        key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-cargo-coverage-

    - name: Generate coverage report (lcov for Codecov)
      run: cargo llvm-cov --lib --lcov --output-path lcov.info

    - name: Generate coverage report (json for threshold check)
      run: cargo llvm-cov --lib --json --output-path coverage.json

    - name: Check coverage threshold (minimum ${{ env.MIN_COVERAGE }}%)
      run: |
        COVERAGE=$(cat coverage.json | python3 -c "
        import json, sys
        data = json.load(sys.stdin)
        totals = data['data'][0]['totals']
        lines = totals['lines']
        pct = lines['percent']
        print(f'{pct:.2f}')
        ")
        echo "Line coverage: ${COVERAGE}%"
        if python3 -c "exit(0 if float('${COVERAGE}') >= ${{ env.MIN_COVERAGE }} else 1)"; then
          echo "Coverage ${COVERAGE}% meets minimum threshold of ${{ env.MIN_COVERAGE }}%"
        else
          echo "Coverage ${COVERAGE}% is below minimum threshold of ${{ env.MIN_COVERAGE }}%"
          exit 1
        fi

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v4
      with:
        files: lcov.info
        fail_ci_if_error: false
        token: ${{ secrets.CODECOV_TOKEN }}

  integration:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: test

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_walstream
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
    - uses: actions/checkout@v4

    - name: Install Rust
      uses: dtolnay/rust-toolchain@stable

    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y libpq-dev pkg-config libssl-dev

    - name: Cache cargo registry
      uses: actions/cache@v4
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
        key: ${{ runner.os }}-cargo-integration-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-cargo-integration-

    - name: Configure PostgreSQL for logical replication
      env:
        PGPASSWORD: postgres
      run: |
        psql -h localhost -U postgres -d test_walstream -c "ALTER SYSTEM SET wal_level = 'logical';"
        psql -h localhost -U postgres -d test_walstream -c "ALTER SYSTEM SET max_replication_slots = 16;"
        psql -h localhost -U postgres -d test_walstream -c "ALTER SYSTEM SET max_wal_senders = 16;"
        # Restart PostgreSQL container to apply wal_level change (requires restart)
        docker restart ${{ job.services.postgres.id }}
        # Wait for PostgreSQL to be ready after restart
        for i in $(seq 1 30); do
          pg_isready -h localhost -p 5432 -U postgres && break
          echo "Waiting for PostgreSQL after restart... ($i/30)"
          sleep 2
        done

    - name: Verify PostgreSQL logical replication config
      env:
        PGPASSWORD: postgres
      run: |
        psql -h localhost -U postgres -d test_walstream -c "SHOW wal_level;"
        psql -h localhost -U postgres -d test_walstream -c "SHOW max_replication_slots;"
        psql -h localhost -U postgres -d test_walstream -c "SHOW max_wal_senders;"

    - name: Run integration tests
      env:
        DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/test_walstream?replication=database"
        DATABASE_URL_REGULAR: "postgresql://postgres:postgres@localhost:5432/test_walstream"
        RUST_BACKTRACE: 1
      run: |
        cargo test --test snapshot_export -- --ignored --nocapture --test-threads=1

        # Clean up any lingering replication slots between test suites
        PGPASSWORD=postgres psql -h localhost -U postgres -d test_walstream \
          -c "SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_type = 'logical';" || true

        cargo test --test rate_limited_streaming -- --ignored --nocapture --test-threads=1

        PGPASSWORD=postgres psql -h localhost -U postgres -d test_walstream \
          -c "SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_type = 'logical';" || true

        cargo test --test safe_transaction_consumer -- --ignored --nocapture --test-threads=1

        PGPASSWORD=postgres psql -h localhost -U postgres -d test_walstream \
          -c "SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_type = 'logical';" || true

        cargo test --test complex_types -- --ignored --nocapture --test-threads=1

  publish:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    needs: [test, integration, coverage]
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Verify tag is on main branch
      run: |
        git fetch origin main
        if git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
          echo "Tag commit is on main branch — proceeding with publish."
        else
          echo "Error: Tag is NOT on the main branch. Publishing is only allowed from main."
          exit 1
        fi

    - name: Install Rust toolchain
      uses: dtolnay/rust-toolchain@stable

    - name: Install system dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y libpq-dev pkg-config libssl-dev

    - name: Extract version from tag
      id: get_version
      run: |
        TAG=${GITHUB_REF#refs/tags/}
        VERSION=${TAG#v}
        echo "version=${VERSION}" >> $GITHUB_OUTPUT
        echo "tag=${TAG}" >> $GITHUB_OUTPUT

    - name: Verify tag matches Cargo.toml version
      run: |
        CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*= "//' | sed 's/".*//')
        TAG_VERSION="${{ steps.get_version.outputs.version }}"
        echo "Cargo.toml version: $CARGO_VERSION"
        echo "Git tag version: $TAG_VERSION"
        if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
          echo "Error: Version mismatch between Cargo.toml ($CARGO_VERSION) and git tag ($TAG_VERSION)"
          exit 1
        fi

    - name: Dry run publish
      run: cargo publish --dry-run
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

    - name: Publish to crates.io
      run: cargo publish
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}