name: Optimized CI/CD Pipeline
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main, develop]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
PYTHON_VERSION: "3.12"
NODE_VERSION: "20"
SIMPLENOTE_OFFLINE_MODE: "true" # Disable rate limiting in CI
jobs:
# Quick validation checks
validate:
name: Validate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate files
run: |
# Check for large files
find . -type f -size +1M | grep -v "^./.git" | while read file; do
echo "Warning: Large file detected: $file ($(du -h "$file" | cut -f1))"
done
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Cache dependencies
uses: actions/cache@v4
id: cache
with:
path: |
~/.cache/pip
~/.local
.venv
key: deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
restore-keys: |
deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
- name: Run linters
run: |
ruff check .
ruff format --check .
mypy simplenote_mcp
# Parallel test execution
test:
name: Test (Shard ${{ matrix.shard }}/${{ strategy.job-total }})
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Restore dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.local
.venv
key: deps-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
- name: Run tests (shard ${{ matrix.shard }}/${{ strategy.job-total }})
env:
SIMPLENOTE_EMAIL: test@example.com
SIMPLENOTE_PASSWORD: test-password
SIMPLENOTE_OFFLINE_MODE: "true"
run: |
# Calculate which tests to run for this shard
TOTAL_SHARDS=${{ strategy.job-total }}
SHARD=${{ matrix.shard }}
# Get all test files
TEST_FILES=$(find tests -name "test_*.py" | sort)
TOTAL_FILES=$(echo "$TEST_FILES" | wc -l)
# Calculate files per shard
FILES_PER_SHARD=$(( (TOTAL_FILES + TOTAL_SHARDS - 1) / TOTAL_SHARDS ))
# Calculate start and end for this shard
START=$(( (SHARD - 1) * FILES_PER_SHARD + 1 ))
END=$(( SHARD * FILES_PER_SHARD ))
# Get files for this shard
SHARD_FILES=$(echo "$TEST_FILES" | sed -n "${START},${END}p" | tr '\n' ' ')
echo "Running tests for shard $SHARD: $SHARD_FILES"
# Run tests with coverage
pytest $SHARD_FILES --cov=simplenote_mcp --cov-report=xml --cov-report=term
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests-shard-${{ matrix.shard }}
fail_ci_if_error: false
# Security scanning
security:
name: Security
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install security tools
run: |
pip install bandit safety
- name: Run security scans
run: |
bandit -r simplenote_mcp -ll
pip install -e .
safety check --json || true
# MCP Evaluations (in Docker)
evaluations:
name: MCP Evaluations
needs: [test, security]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install npm dependencies
run: npm ci
- name: Validate evaluation files
run: npm run validate:evals
- name: Build Docker evaluation image
run: docker build -f Dockerfile.eval -t simplenote-mcp-eval:latest .
- name: Run smoke tests in Docker
env:
SIMPLENOTE_EMAIL: test@example.com
SIMPLENOTE_PASSWORD: test-password
run: |
docker run --rm \
-e SIMPLENOTE_EMAIL="${SIMPLENOTE_EMAIL}" \
-e SIMPLENOTE_PASSWORD="${SIMPLENOTE_PASSWORD}" \
-e SIMPLENOTE_OFFLINE_MODE=true \
simplenote-mcp-eval:latest npm run eval:smoke
# Build verification
build:
name: Build
needs: [test, security]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build package
run: |
pip install build
python -m build
- name: Check package
run: |
pip install twine
twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# Docker build
docker:
name: Docker Build
needs: [test, security]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: simplenote-mcp-server:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Status check
status:
name: CI Status
runs-on: ubuntu-latest
needs: [validate, test, security, evaluations, build, docker]
if: always()
steps:
- name: Check status
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "❌ CI pipeline failed"
exit 1
elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "⚠️ CI pipeline was cancelled"
exit 1
else
echo "✅ CI pipeline passed successfully!"
fi
- name: Generate summary
if: always()
run: |
echo "## CI Pipeline Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Validate | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Test | ${{ needs.test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Evaluations | ${{ needs.evaluations.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker | ${{ needs.docker.result }} |" >> $GITHUB_STEP_SUMMARY