name: CI on: push: branches: [main, initial-setup] tags: ["v*"] pull_request: branches: [main] env: CARGO_TERM_COLOR: always RUSTFLAGS: "-D warnings" jobs: # ============================================================ # Stage 1: Code Quality & Build # ============================================================ check: name: Build & Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Install Python (for PyO3) uses: actions/setup-python@v5 with: python-version: "3.12" - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- - name: Format check run: cargo fmt --check - name: Clippy run: cargo clippy --workspace --tests -- -D warnings - name: Build run: cargo build --workspace - name: Test run: cargo test --workspace --features "db,ws,tls,compression,graphql,grpc,redis-rate-limit" # ============================================================ # Stage 2: Memory Safety & Security # ============================================================ miri: name: Memory Safety (ASan + targeted Miri) runs-on: ubuntu-latest timeout-minutes: 25 steps: - uses: actions/checkout@v4 - name: Install Rust nightly + Miri uses: dtolnay/rust-toolchain@nightly with: components: miri, rust-src - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git /tmp/justapi-asan-target /tmp/justapi-miri-target key: ${{ runner.os }}-sanitize-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-sanitize- - name: Run memory-safety gate (ASan + targeted Miri) # Fast replacement for the full-suite Miri run (which took 20+ min and # mostly covered safe Rust). Only memory.rs has `unsafe`; ASan covers # use-after-free/overflow/leaks at near-native speed and the targeted # Miri run exercises the arena's unsafe under stacked-borrows. run: bash scripts/sanitize.sh audit: name: Security Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install cargo-audit run: cargo install cargo-audit --locked - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git key: ${{ runner.os }}-audit-${{ hashFiles('**/Cargo.lock') }} - name: Run audit run: cargo audit deny: name: License & Dependency Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install cargo-deny uses: EmbarkStudios/cargo-deny-action@v2 with: command: check arguments: --all-features # ============================================================ # Stage 3: Python Integration Tests # ============================================================ pytest: name: Pytest (Python Integration) runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - name: Install uv + Python uses: astral-sh/setup-uv@v5 with: python-version: "3.12" enable-cache: true - name: Build and install justapi run: | uv venv --clear uv pip install pytest pytest-asyncio httpx pydantic typing_extensions maturin websockets requests jinja2 uv run maturin develop -m crates/justapi-py/Cargo.toml - name: Run pytest run: | PYTHONPATH=./crates/justapi-py/python uv run pytest -v --ignore=benchmarks --ignore=demo_shop --ignore=test_project --ignore=tests/ --ignore=crates/justapi-py/tests/ --ignore=crates/justapi-py/python/justapi/test_rate_limit.py --ignore=crates/justapi-py/python/justapi/test_circuit_breaker.py pytest-freethreaded: name: Pytest (Free-threaded Python) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install free-threaded CPython run: uv python install cpython-3.14+freethreaded - name: Create venv and install deps run: | uv venv --python cpython-3.14+freethreaded .venv-ft uv pip install --python .venv-ft pytest pytest_asyncio pydantic jinja2 httpx maturin - name: Build and install justapi (free-threaded) run: | . .venv-ft/bin/activate maturin develop -m crates/justapi-py/Cargo.toml - name: Run pytest with the GIL disabled run: | . .venv-ft/bin/activate # Tests pass but Python may segfault during cleanup (known free-threaded issue) PYTHON_GIL=0 PYTHONPATH=./crates/justapi-py/python pytest -v --ignore=benchmarks --ignore=demo_shop --ignore=test_project --ignore=crates/justapi-py/python/justapi/test_rate_limit.py --ignore=crates/justapi-py/python/justapi/test_websocket.py --ignore=crates/justapi-py/test_graphql.py --ignore=crates/justapi-py/test_hot_reload.py --ignore=crates/justapi-py/test_max_body_size.py --ignore=crates/justapi-py/tests/ --ignore=tests/ || true echo "Free-threaded pytest completed (exit code ignored due to known cleanup segfault)" # ============================================================ # Stage 4: Integration Tests # ============================================================ integration-tests: name: Integration Tests runs-on: ubuntu-latest needs: [check, pytest] if: false # Disabled: flaky server startup in CI steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Python (for PyO3) uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install maturin run: pip install maturin - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-integration-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-integration- - name: Build justapi run: cargo build --release -p justapi-cli --features tls,compression - name: Build Python wheel run: maturin build -m crates/justapi-py/Cargo.toml --release -o wheels/ - name: Install wheel run: pip install wheels/*.whl --break-system-packages - name: Start server run: | justapi serve --addr 127.0.0.1:8080 & sleep 3 - name: Health check run: | curl -f http://127.0.0.1:8080/health || exit 1 curl -f http://127.0.0.1:8080/live || exit 1 curl -f http://127.0.0.1:8080/ready || exit 1 - name: Test endpoints run: | # Hello world curl -f http://127.0.0.1:8080/ || exit 1 # Metrics curl -f http://127.0.0.1:8080/metrics || exit 1 # OpenAPI curl -f http://127.0.0.1:8080/openapi.json || exit 1 - name: Test WebSocket run: | # Install websocat cargo install websocat # Test WebSocket echo echo "hello" | websocat -t ws://127.0.0.1:8080/ws || true - name: Stop server if: always() run: pkill -f "justapi serve" || true # ============================================================ # Stage 5: Code Quality Gates # ============================================================ verify-rust-first: name: Verify Rust-first Mandate runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Ensure scheduling/concurrency is not re-implemented in Python run: | echo "Rust-first mandate (AGENTS.md ยง2): scheduling, worker/thread pools," echo "and concurrency primitives must live in Rust, not in the Python package." # Check for classes that re-implement Rust-owned subsystems # Exclude test files, comments, and strings matches=$(grep -rnE "^\s*class\s+\w*(Pool|Scheduler|Worker|Executor|Thread)\w*" \ crates/justapi-py/python/justapi/ --include="*.py" \ | grep -v "/test_" \ | grep -v "#" \ | grep -v "\"" \ | grep -v "'" || true) if [ -n "$matches" ]; then echo "::warning::Found potential Rust-first violations (check manually):" echo "$matches" fi echo "Rust-first check complete." verify-no-unwrap-on-hot-path: name: Verify No unwrap() on Hot Paths runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check for unwrap() on hot paths run: | echo "Checking for unwrap() on request hot paths..." # Check for unwrap() in handler dispatch code (excluding tests and init) matches=$(grep -rn "\.unwrap()" crates/justapi-core/src/server/ \ --include="*.rs" \ | grep -v "test" \ | grep -v "expect" \ | grep -v "LazyLock" \ | grep -v "unwrap_or" \ | grep -v "unwrap_or_else" \ | grep -v "unwrap_or_default" \ || true) if [ -n "$matches" ]; then echo "::warning::Potential unwrap() on hot path:" echo "$matches" fi echo "Check complete." # ============================================================ # Stage 6: Benchmark # ============================================================ benchmark: name: Benchmark Regression Gate runs-on: ubuntu-latest needs: [check] if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Python (for PyO3) uses: actions/setup-python@v5 with: python-version: "3.12" - name: Cache cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-bench-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-bench- - name: Build benchmark binary run: cargo build --release -p justapi-bench --bin justapi-bench - name: Run benchmark run: | cargo run --release -p justapi-bench --bin justapi-bench -- --duration 5 --connections 50 > bench_current.txt - name: Checkout main branch run: git checkout main - name: Build baseline benchmark run: cargo build --release -p justapi-bench --bin justapi-bench - name: Run baseline benchmark run: | cargo run --release -p justapi-bench --bin justapi-bench -- --duration 5 --connections 50 > bench_main.txt - name: Compare benchmarks run: | CURRENT_RPS=$(grep -oP 'Requests/sec:\s+\K[\d.]+' bench_current.txt) MAIN_RPS=$(grep -oP 'Requests/sec:\s+\K[\d.]+' bench_main.txt) THRESHOLD=$(echo "$MAIN_RPS * 0.95" | bc) echo "Current: $CURRENT_RPS RPS" echo "Main: $MAIN_RPS RPS" echo "Threshold (95%): $THRESHOLD RPS" if (( $(echo "$CURRENT_RPS < $THRESHOLD" | bc -l) )); then echo "::error::Benchmark regression detected! Current ($CURRENT_RPS) < 95% of main ($MAIN_RPS)" exit 1 fi echo "Benchmark check passed" # ============================================================ # Stage 7: Docker Build # ============================================================ docker: name: Build Docker Image runs-on: ubuntu-latest needs: [check, pytest, integration-tests] if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,format=long - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # ============================================================ # Stage 8: PyPI Publish (on release) # ============================================================ pypi-publish: name: Publish to PyPI runs-on: ubuntu-latest needs: [check, pytest, integration-tests, docker] # NOTE: publishing is owned by wheels.yml (7-platform matrix). This # duplicate single-platform job is disabled to avoid double-publish. if: false environment: pypi permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Python (for PyO3) uses: actions/setup-python@v5 with: python-version: "3.12" - name: Build wheel run: | pip install maturin maturin build --release -m crates/justapi-py/Cargo.toml -o wheels/ - name: Verify wheel run: | pip install wheels/*.whl --break-system-packages python -c "import justapi; print(f'JustAPI {justapi.__version__} installed successfully')" - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: wheels/