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 }}