name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Run tests daily at 2 AM UTC
- cron: '0 2 * * *'
env:
PYTHON_VERSION: '3.10'
CACHE_VERSION: v1
jobs:
test:
name: Test Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11']
exclude:
# Reduce matrix size for faster builds
- os: macos-latest
python-version: '3.8'
- os: macos-latest
python-version: '3.9'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'requirements.txt'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ env.CACHE_VERSION }}-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov pytest-xdist pytest-mock
- name: Verify installation
run: |
python -c "import fastmcp; print('FastMCP version:', fastmcp.__version__)"
python -c "import pydantic; print('Pydantic version:', pydantic.__version__)"
- name: Run tests
run: |
pytest test_server.py -v --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=80
env:
PYTHONPATH: .
- name: Upload coverage to Codecov
if: matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install black flake8 mypy isort bandit safety
pip install -r requirements.txt
- name: Run Black (Code Formatting)
run: |
black --check --diff .
- name: Run isort (Import Sorting)
run: |
isort --check-only --diff .
- name: Run Flake8 (Linting)
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Run MyPy (Type Checking)
run: |
mypy . --ignore-missing-imports --no-strict-optional
- name: Run Bandit (Security)
run: |
bandit -r . -f json -o bandit-report.json || true
bandit -r . --severity-level medium
- name: Run Safety (Dependency Security)
run: |
safety check --json --output safety-report.json || true
safety check
- name: Upload security reports
if: always()
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: [test, lint]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
- name: Run integration tests
run: |
python -m pytest -v -k "integration" --tb=short
env:
PYTHONPATH: .
- name: Test MCP server startup
run: |
timeout 30s python server.py --test || exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "Server started successfully (timeout expected)"
exit 0
elif [ $exit_code -eq 0 ]; then
echo "Server test completed successfully"
exit 0
else
echo "Server failed to start"
exit 1
fi
- name: Test configuration validation
run: |
python -c "from server import validate_config; validate_config()"
performance:
name: Performance Tests
runs-on: ubuntu-latest
needs: [test]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-benchmark
- name: Run performance tests
run: |
python -m pytest -v -k "benchmark" --benchmark-only --benchmark-json=benchmark.json
env:
PYTHONPATH: .
- name: Upload benchmark results
uses: actions/upload-artifact@v3
with:
name: benchmark-results
path: benchmark.json
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install mkdocs mkdocs-material
- name: Check documentation links
run: |
# Check for broken links in markdown files
find . -name "*.md" -exec grep -l "http" {} \; | head -10
- name: Validate README
run: |
python -c "import markdown; markdown.markdown(open('README.md').read())"
- name: Check documentation completeness
run: |
# Ensure all Python files have docstrings
python -c "
import ast
import sys
def check_docstrings(filename):
with open(filename, 'r') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
if not ast.get_docstring(node):
print(f'Missing docstring: {filename}:{node.lineno} {node.name}')
return False
return True
files = ['server.py']
all_good = all(check_docstrings(f) for f in files)
sys.exit(0 if all_good else 1)
"
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
build:
name: Build Distribution
runs-on: ubuntu-latest
needs: [test, lint]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
notify:
name: Notify Results
runs-on: ubuntu-latest
needs: [test, lint, integration, docs, security, build]
if: always()
steps:
- name: Notify success
if: ${{ needs.test.result == 'success' && needs.lint.result == 'success' }}
run: |
echo "✅ All checks passed successfully!"
- name: Notify failure
if: ${{ needs.test.result == 'failure' || needs.lint.result == 'failure' }}
run: |
echo "❌ Some checks failed. Please review the logs."
exit 1