Skip to content

fix(rate-limiter): shared state, eviction, thread safety, config validation #8629

fix(rate-limiter): shared state, eviction, thread safety, config validation

fix(rate-limiter): shared state, eviction, thread safety, config validation #8629

Workflow file for this run

# ===============================================================
# 🧪 PyTest & Coverage - Quality Gate
# ===============================================================
#
# - runs the full test-suite across three Python versions
# - measures branch + line coverage (fails < 90% main, < 35% doctest)
# - uploads the XML/HTML coverage reports as build artifacts
# - (optionally) generates / commits an SVG badge - kept disabled
# - posts a concise per-file coverage table to the job summary
# - executes on every push / PR to *main* ➕ a weekly cron
# ---------------------------------------------------------------
name: Tests & Coverage
on:
push:
branches: ["main"]
paths:
- "mcpgateway/**"
- "tests/**"
- "pyproject.toml"
- ".github/workflows/pytest.yml"
pull_request:
types: [opened, synchronize, ready_for_review]
branches: ["main"]
paths:
- "mcpgateway/**"
- "tests/**"
- "pyproject.toml"
- ".github/workflows/pytest.yml"
# schedule:
# - cron: '42 3 * * 1' # Monday 03:42 UTC
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write # needed *only* if the badge-commit step is enabled
checks: write
actions: read
jobs:
test:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
name: pytest (py${{ matrix.python }})
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
python: ["3.12"]
env:
PYTHONUNBUFFERED: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
steps:
# -----------------------------------------------------------
# 0️⃣ Checkout
# -----------------------------------------------------------
- name: ⬇️ Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0 # diff-cover needs full history to compare branches
# -----------------------------------------------------------
# 1️⃣ Set-up Python
# -----------------------------------------------------------
- name: 🐍 Setup Python ${{ matrix.python }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
cache: pip
# -----------------------------------------------------------
# 2 Install uv
# -----------------------------------------------------------
- name: ⚡ Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.9.2"
python-version: ${{ matrix.python }}
# -----------------------------------------------------------
# 2.5 Setup Rust toolchain for Rust plugins
# -----------------------------------------------------------
- name: 🦀 Install Rust stable
run: rustup default stable
- name: 📦 Cache Cargo dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
plugins_rust/*/target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: 🔨 Build Rust plugins (clean, install, verify stubs)
run: make rust-clean-stubs && make rust-install && make rust-verify-stubs
# -----------------------------------------------------------
# 3️⃣ Run the tests with coverage (fail under 95% coverage)
# -----------------------------------------------------------
- name: 🧪 Run pytest
run: |
uv run pytest -n auto \
--durations=5 \
--ignore=tests/fuzz \
--ignore=tests/e2e/test_entra_id_integration.py \
--cov=mcpgateway \
--cov-report=xml \
--cov-report=html \
--cov-report=term \
--cov-branch \
--cov-fail-under=95
env:
REQUIRE_RUST: "1"
# -----------------------------------------------------------
# 3.5 Diff-cover: enforce 95% coverage on changed lines (PRs only)
# -----------------------------------------------------------
- name: 📈 Diff-cover (changed lines)
if: github.event_name == 'pull_request'
run: |
uv run diff-cover coverage.xml \
--compare-branch=origin/${{ github.base_ref }} \
--fail-under=95
# -----------------------------------------------------------
# 4️⃣ Run doctests (fail under 30% coverage)
# -----------------------------------------------------------
- name: 📊 Doctest coverage with threshold
run: |
# Run doctests with coverage measurement
uv run pytest -n auto \
--doctest-modules mcpgateway/ \
--cov=mcpgateway \
--cov-report=term \
--cov-report=json:doctest-coverage.json \
--cov-fail-under=30 \
--tb=short
# -----------------------------------------------------------
# 5️⃣ Doctest coverage check
# -----------------------------------------------------------
- name: 📊 Doctest coverage validation
run: |
uv run python3 -c "
import subprocess, sys
result = subprocess.run(['uv', 'run', 'python3', '-m', 'pytest', '--doctest-modules', 'mcpgateway/', '--tb=no', '-q'], capture_output=True)
if result.returncode == 0:
print('✅ All doctests passing')
else:
print('❌ Doctest failures detected')
print(result.stdout.decode())
print(result.stderr.decode())
sys.exit(1)
"
# -----------------------------------------------------------
# 4️⃣ Upload coverage artifacts (XML + HTML)
# --- keep disabled unless you need them ---
# -----------------------------------------------------------
# - name: 📤 Upload coverage.xml
# uses: actions/upload-artifact@v4.6.2
# with:
# name: coverage-xml-${{ matrix.python }}
# path: coverage.xml
#
# - name: 📤 Upload HTML coverage
# uses: actions/upload-artifact@v4.6.2
# with:
# name: htmlcov-${{ matrix.python }}
# path: htmlcov/
# -----------------------------------------------------------
# 5️⃣ Publish coverage table to the job summary
# -----------------------------------------------------------
# - name: 📝 Coverage summary
# if: always()
# run: |
# echo "### Coverage - Python ${{ matrix.python }}" >> "$GITHUB_STEP_SUMMARY"
# echo "| File | Stmts | Miss | Branch | BrMiss | Cover |" >> "$GITHUB_STEP_SUMMARY"
# echo "|------|------:|-----:|-------:|-------:|------:|" >> "$GITHUB_STEP_SUMMARY"
# coverage json -q -o cov.json
# python3 - <<'PY'
# import json, pathlib, sys, os
# data = json.load(open("cov.json"))
# root = pathlib.Path().resolve()
# for f in data["files"].values():
# rel = pathlib.Path(f["filename"]).resolve().relative_to(root)
# s = f["summary"]
# print(f"| {rel} | {s['num_statements']} | {s['missing_lines']} | "
# f"{s['num_branches']} | {s['missing_branches']} | "
# f"{s['percent_covered']:.1f}% |")
# PY >> "$GITHUB_STEP_SUMMARY"