name: Backend CI
on:
push:
branches: [ "main", "debug/*", "feature/*", "claude/*" ]
pull_request:
branches: [ main ]
schedule:
# Weekly release binary validation (Sundays at 2 AM UTC)
- cron: '0 2 * * 0'
# Security: Explicit permissions following principle of least privilege
permissions:
contents: read
actions: read
env:
CARGO_TERM_COLOR: always
jobs:
# Clippy linting with zero tolerance (includes cognitive complexity)
clippy:
name: Clippy Linting
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
with:
components: rustfmt, clippy
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-clippy-1.92.0-${{ hashFiles('**/Cargo.lock') }}
- name: Check formatting
run: cargo fmt --all -- --check
- name: Lint with Clippy (Zero Tolerance + Cognitive Complexity)
run: cargo clippy --all-targets --all-features -- -D warnings -D clippy::all -D clippy::pedantic -D clippy::nursery -W clippy::cognitive_complexity
backend-tests:
name: Backend Tests (SQLite)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false # Keep Docker for potential future use
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-1.92.0-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny
- name: Security Audit (via deny.toml)
run: cargo deny check
continue-on-error: true
- name: Install ripgrep (for architectural validation)
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Architectural Validation (Custom Patterns)
run: ./scripts/architectural-validation.sh
- name: Secret Pattern Validation
run: ./scripts/validate-no-secrets.sh
- name: Validate no ignored doctests
run: |
echo "🔍 Checking for ignored doctests..."
DOCTEST_OUTPUT=$(cargo test --doc 2>&1 || true)
IGNORED_COUNT=$(echo "$DOCTEST_OUTPUT" | grep -E "test result:.*ignored" | grep -oE "[0-9]+ ignored" | grep -oE "[0-9]+" | head -1 || echo "0")
if [ "${IGNORED_COUNT:-0}" -gt 0 ]; then
echo "❌ ERROR: Found $IGNORED_COUNT ignored doctests!"
echo "Doctests marked with 'ignore' are not compiled or tested."
echo "Use 'no_run' if code should compile but not execute,"
echo "or 'text' for documentation-only examples."
echo ""
echo "To find ignored doctests, run: cargo test --doc 2>&1 | grep -i ignore"
exit 1
fi
echo "✅ No ignored doctests found"
env:
DATABASE_URL: "sqlite::memory:"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8080/auth/strava/callback"
- name: Setup Node.js (for SDK E2E tests runtime)
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Bun (for SDK build)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build MCP SDK (required for E2E tests)
working-directory: sdk
run: |
bun install --frozen-lockfile
bun run build
- name: Run all backend tests (SQLite)
# Tests marked with #[serial] (concurrent/stress tests) run in isolation
# Using --test-threads=4 for parallelism - see issue #36 for serial_test approach
# Coverage is generated by dedicated coverage.yml workflow
run: cargo test -- --test-threads=4
env:
DATABASE_URL: "sqlite::memory:"
ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8080/auth/strava/callback"
postgres-tests:
name: Database Tests (PostgreSQL)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: pierre
POSTGRES_PASSWORD: ci_test_password
POSTGRES_DB: pierre_mcp_server
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false # Keep Docker - PostgreSQL runs as container
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-postgres-1.92.0-${{ hashFiles('**/Cargo.lock') }}
- name: Wait for PostgreSQL
run: |
echo "🐘 Waiting for PostgreSQL to be ready..."
timeout 60 bash -c 'until pg_isready -h localhost -p 5432 -U pierre; do sleep 2; done'
echo "✅ PostgreSQL is ready!"
- name: Verify PostgreSQL connection
run: |
echo "🔧 Testing PostgreSQL connection..."
PGPASSWORD=ci_test_password psql -h localhost -U pierre -d pierre_mcp_server -c "SELECT version();"
- name: Run PostgreSQL tests (scoped on branches, full on main)
# Main branch: full test suite validates complete PostgreSQL compatibility
# Feature branches: scoped tests for faster iteration
# Using --test-threads=4 - concurrent tests use #[serial] for isolation (issue #36)
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "🐘 Running FULL test suite on main branch..."
cargo test --features postgresql -- --test-threads=4
else
echo "🐘 Running SCOPED database tests on feature branch..."
cargo test --features postgresql database_plugins -- --test-threads=4
fi
timeout-minutes: 30
env:
DATABASE_URL: "postgresql://pierre:ci_test_password@localhost:5432/pierre_mcp_server"
ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8080/auth/strava/callback"
POSTGRES_MAX_CONNECTIONS: "3"
POSTGRES_MIN_CONNECTIONS: "1"
POSTGRES_ACQUIRE_TIMEOUT: "20"
RUST_LOG: "info"
redis-tests:
name: Cache Tests (Redis)
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-redis-1.92.0-${{ hashFiles('**/Cargo.lock') }}
- name: Install Redis tools
run: sudo apt-get update && sudo apt-get install -y redis-tools
- name: Wait for Redis
run: |
echo "🔴 Waiting for Redis to be ready..."
timeout 60 bash -c 'until redis-cli -h localhost -p 6379 ping | grep -q PONG; do sleep 2; done'
echo "✅ Redis is ready!"
- name: Verify Redis connection
run: |
echo "🔧 Testing Redis connection..."
redis-cli -h localhost -p 6379 INFO server | head -10
- name: Run Redis cache tests
# Coverage provided by SQLite backend tests - Redis tests validate integration only
run: cargo test --test cache_redis_test -- --test-threads=1
env:
REDIS_URL: "redis://localhost:6379"
DATABASE_URL: "sqlite::memory:"
ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8080/auth/strava/callback"
RUST_LOG: "info"
# Release binary validation (runs on push/PR and weekly schedule)
release-binary:
name: Release Binary Validation
runs-on: ubuntu-latest
needs: clippy
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-release-1.92.0-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-release-1.92.0-
# SDK build removed - release binary validation focuses on Rust binary only
# Full test suite with SDK is covered by backend-tests job
- name: Build release binary
run: cargo build --release --verbose
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C opt-level=3"
- name: Verify release binary exists
run: |
if [ -f "target/release/pierre-mcp-server" ]; then
echo "✅ Release binary built successfully"
ls -lh target/release/pierre-mcp-server
else
echo "❌ Release binary not found"
exit 1
fi
- name: Check release binary size
run: |
BINARY_SIZE_BYTES=$(ls -l target/release/pierre-mcp-server | awk '{print $5}')
BINARY_SIZE_MB=$((BINARY_SIZE_BYTES / 1024 / 1024))
MAX_SIZE_MB=50
echo "Binary size: ${BINARY_SIZE_MB}MB"
if [ "$BINARY_SIZE_MB" -le "$MAX_SIZE_MB" ]; then
echo "✅ Binary size (${BINARY_SIZE_MB}MB) within limit (<${MAX_SIZE_MB}MB)"
else
echo "❌ Binary size (${BINARY_SIZE_MB}MB) exceeds limit (${MAX_SIZE_MB}MB)"
exit 1
fi
# Full test suite removed - SQLite backend-tests job provides comprehensive coverage
# Release binary validation focuses on build success and binary size
- name: Basic smoke test of release binary
run: |
timeout 5 target/release/pierre-mcp-server --version || true
echo "✅ Release binary executed successfully"
- name: Upload release binary as artifact
uses: actions/upload-artifact@v4
with:
name: pierre-mcp-server-release-ubuntu
path: target/release/pierre-mcp-server
retention-days: 7