Record the py-canon fleet exemption (#76) #426
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }}/rmcp-ci | |
| jobs: | |
| # Fast Python-only validation (no R required) | |
| python-checks: | |
| name: Python Linting & Unit Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: | | |
| uv sync --group dev | |
| uv pip install -e . | |
| - name: Run linting | |
| run: | | |
| uv run ruff check . | |
| uv run ruff format --check . | |
| - name: Run Python-only unit tests | |
| run: | | |
| # Test CLI basic functionality | |
| uv run rmcp --version | |
| uv run rmcp list-capabilities > /dev/null | |
| # Run all unit tests (Python-only, schema validation, etc.) | |
| uv run pytest tests/unit/ -v --tb=short | |
| windows-vfs: | |
| name: Windows VFS Tests | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --group dev | |
| - name: Run VFS tests | |
| run: uv run pytest tests/unit/core/test_contract_validation.py -v --tb=short | |
| # Build Docker images for R testing (both development and production) | |
| docker-build: | |
| name: Build R Testing Environment | |
| runs-on: ubuntu-latest | |
| needs: [python-checks] | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| packages: write | |
| attestations: write | |
| id-token: write | |
| outputs: | |
| image: ${{ steps.image.outputs.image }} | |
| production-image: ${{ steps.prod-image.outputs.image }} | |
| digest: ${{ steps.build.outputs.digest }} | |
| production-digest: ${{ steps.prod-build.outputs.digest }} | |
| should_build: ${{ steps.check-changes.outputs.should_build }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 2 # Need previous commit to check changes | |
| - name: Check if Docker build is needed | |
| id: check-changes | |
| run: | | |
| echo "Checking if Docker build is needed..." | |
| # Always build if it's a manual trigger | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "🔄 Building: Manual trigger (forced)" | |
| exit 0 | |
| fi | |
| # Always build on main branch pushes (production deployments) | |
| if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "🔄 Building: Main branch push" | |
| exit 0 | |
| fi | |
| # For local testing with act, always build (git diff doesn't work reliably) | |
| if [ -n "${ACT:-}" ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "🔄 Building: Local testing with act" | |
| exit 0 | |
| fi | |
| # Check if relevant files changed (for PR and other scenarios) | |
| if git rev-parse HEAD~1 >/dev/null 2>&1; then | |
| changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") | |
| echo "Changed files: $changed_files" | |
| # Files that require Docker rebuild. uv.lock belongs here because the | |
| # image now copies it and installs with `uv sync --frozen`, so a | |
| # dependency bump changes what the image contains. | |
| docker_relevant_files=( | |
| "Dockerfile" | |
| "pyproject.toml" | |
| "uv.lock" | |
| "rmcp/" | |
| ".github/workflows/ci.yml" | |
| ) | |
| needs_build=false | |
| for file_pattern in "${docker_relevant_files[@]}"; do | |
| if echo "$changed_files" | grep -q "^$file_pattern"; then | |
| echo "🔄 Building: $file_pattern changed" | |
| needs_build=true | |
| break | |
| fi | |
| done | |
| if [ "$needs_build" = true ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| echo "⏭️ Skipping: No Docker-relevant files changed" | |
| fi | |
| else | |
| # No previous commit available (initial commit or shallow clone) | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "🔄 Building: No previous commit available for comparison" | |
| fi | |
| - name: Set up Docker Buildx | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Container Registry | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata for development image | |
| id: meta | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| # {{branch}} is only populated for branch pushes; on a pull_request | |
| # it is empty and the tag becomes "-<sha>", which docker rejects. | |
| type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'pull_request' }} | |
| type=sha,prefix=pr-,enable=${{ github.event_name == 'pull_request' }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Extract metadata for production image | |
| id: prod-meta | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=ref,event=branch,suffix=-production | |
| type=ref,event=pr,suffix=-production | |
| type=sha,prefix={{branch}}-production-,enable=${{ github.event_name != 'pull_request' }} | |
| type=sha,prefix=pr-production-,enable=${{ github.event_name == 'pull_request' }} | |
| type=raw,value=production-latest,enable={{is_default_branch}} | |
| - name: Build and push development Docker image | |
| id: build | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| target: development | |
| platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: | | |
| type=gha | |
| type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest | |
| type=registry,ref=ghcr.io/finite-sample/rmcp/rmcp-base:latest | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| BUILDKIT_INLINE_CACHE=1 | |
| - name: Build and push production Docker image | |
| id: prod-build | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| target: production | |
| platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} | |
| push: true | |
| tags: ${{ steps.prod-meta.outputs.tags }} | |
| labels: ${{ steps.prod-meta.outputs.labels }} | |
| cache-from: | | |
| type=gha | |
| type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:production-latest | |
| type=registry,ref=ghcr.io/finite-sample/rmcp/rmcp-base:latest | |
| type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| BUILDKIT_INLINE_CACHE=1 | |
| - name: Use existing development image (skip build) | |
| id: skip-build | |
| if: steps.check-changes.outputs.should_build == 'false' | |
| run: | | |
| echo "Using existing development image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" | |
| echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT" | |
| - name: Generate artifact attestation for development image | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| - name: Generate artifact attestation for production image | |
| if: steps.check-changes.outputs.should_build == 'true' | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.prod-build.outputs.digest }} | |
| push-to-registry: true | |
| # `latest` and `production-latest` are only pushed from the default branch, | |
| # so pointing PR jobs at them makes them test main's images instead of the | |
| # PR's -- false signal in both directions. Use the pr-<n> tags this run | |
| # actually pushed. | |
| - name: Output image references | |
| id: image | |
| env: | |
| PR_NUMBER: ${{ github.event.number }} | |
| SHOULD_BUILD: ${{ steps.check-changes.outputs.should_build }} | |
| run: | | |
| # Prefer this PR's own image, but only when the run actually built and | |
| # pushed it -- otherwise the pr-<n> tag does not exist and the pull | |
| # fails. Falling back to main's `latest` is the only sensible image for | |
| # a PR that changed nothing image-relevant. | |
| if [ "${{ github.event_name }}" = "pull_request" ] && [ "$SHOULD_BUILD" = "true" ]; then | |
| tag="pr-${PR_NUMBER}" | |
| else | |
| tag="latest" | |
| fi | |
| echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT" | |
| echo "📌 Development image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${tag}" | |
| - name: Output production image reference | |
| id: prod-image | |
| env: | |
| PR_NUMBER: ${{ github.event.number }} | |
| SHOULD_BUILD: ${{ steps.check-changes.outputs.should_build }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ] && [ "$SHOULD_BUILD" = "true" ]; then | |
| tag="pr-${PR_NUMBER}-production" | |
| else | |
| tag="production-latest" | |
| fi | |
| echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT" | |
| echo "📌 Production image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${tag}" | |
| - name: Compare image sizes | |
| run: | | |
| echo "📊 Comparing image sizes..." | |
| docker images --filter "reference=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" | |
| echo "✅ Image size comparison completed" | |
| # Comprehensive R testing with real execution (replaces all R-specific jobs) | |
| r-testing: | |
| name: R Integration & Workflow Tests | |
| runs-on: ubuntu-latest | |
| needs: [python-checks, docker-build] | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' | |
| container: | |
| image: ${{ needs.docker-build.outputs.image }} | |
| credentials: | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| options: --user root | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Report build optimization status | |
| run: | | |
| if [ "${{ needs.docker-build.outputs.should_build }}" = "true" ]; then | |
| echo "🔄 Using freshly built Docker image with optimizations:" | |
| echo " ⚡ BuildKit cache mounts for pip, R, and apt packages" | |
| echo " 📦 Pre-optimized base image with Python environment" | |
| echo " 🗜️ Optimized .dockerignore reducing build context by ~70%" | |
| echo " 🏗️ Merged RUN commands reducing layers by ~40%" | |
| echo " 📈 Expected build time: 1-3 minutes (was 7+ minutes)" | |
| else | |
| echo "⚡ Using cached Docker image (no relevant files changed)" | |
| echo "📈 Build time saved: ~15-20 minutes" | |
| fi | |
| echo "🐳 Container image: ${{ needs.docker-build.outputs.image }}" | |
| - name: Setup Python environment in container | |
| run: | | |
| # RMCP is already installed in the Docker development image via uv sync | |
| # PATH is already configured in Docker image: /workspace/.venv/bin:$PATH | |
| echo "Using pre-installed RMCP from Docker image" | |
| which python | |
| which rmcp | |
| - name: Verify R and Python integration | |
| run: | | |
| python --version | |
| R --version | head -1 | |
| python -c "from rmcp.r_integration import diagnose_r_installation; import json; print(json.dumps(diagnose_r_installation(), indent=2))" | |
| - name: Run R code style checks | |
| run: | | |
| cd rmcp/r_assets | |
| R -e " | |
| library(styler) | |
| files_to_check <- list.files(c('R', 'scripts'), pattern='[.]R\$', recursive=TRUE, full.names=TRUE) | |
| if (length(files_to_check) > 0) { | |
| cat('Checking style for', length(files_to_check), 'R files...\\n') | |
| tryCatch({ | |
| style_results <- styler::style_file(files_to_check, dry='on', include_roxygen_examples = FALSE) | |
| if (!is.null(style_results) && !any(is.na(style_results\$changed))) { | |
| if (any(style_results\$changed)) { | |
| cat('❌ R code style issues found\\n') | |
| quit(status=1) | |
| } else { | |
| cat('✅ R code style check passed\\n') | |
| } | |
| } else { | |
| cat('❌ R code style check encountered errors\\n') | |
| quit(status=1) | |
| } | |
| }, error = function(e) { | |
| cat('❌ R styling error:', e\$message, '\\n') | |
| quit(status=1) | |
| }) | |
| } else { | |
| cat('✅ No R files found to check\\n') | |
| } | |
| " | |
| - name: Diagnose environment before testing | |
| run: | | |
| echo "=== Environment Diagnostics ===" | |
| echo "Working directory: $(pwd)" | |
| echo "Python version: $(python --version)" | |
| echo "Python path: $(which python)" | |
| echo "Pip packages: $(pip list | grep -E '(pytest|rmcp)')" | |
| echo "R availability: $(which R || echo 'R not found')" | |
| if which R; then | |
| echo "R version: $(R --version | head -1)" | |
| fi | |
| echo "PYTHONPATH: $PYTHONPATH" | |
| echo "PATH: $PATH" | |
| echo "=== Test Discovery Diagnostics ===" | |
| pytest --collect-only tests/smoke/ -q || echo "Test collection failed" | |
| echo "=== Python Import Test ===" | |
| python -c "import rmcp; print('✅ rmcp imported successfully')" || echo "❌ rmcp import failed" | |
| python -c "import rmcp.core.server; print('✅ rmcp.core.server imported')" || echo "❌ rmcp.core.server import failed" | |
| echo "=== File System Check ===" | |
| ls -la tests/ | |
| ls -la tests/smoke/ | |
| - name: Run smoke tests (basic functionality) | |
| run: | | |
| # Ensure R is in PATH for skip condition checks | |
| if which R; then | |
| export R_AVAILABLE=1 | |
| else | |
| export R_AVAILABLE=0 | |
| fi | |
| echo "R available: $R_AVAILABLE" | |
| pytest tests/smoke/ -v --tb=short --cov=rmcp --cov-report=xml | |
| - name: Run protocol tests (MCP protocol validation) | |
| run: | | |
| which R && echo "✅ R available for protocol tests" || echo "⚠️ R not available" | |
| pytest tests/integration/protocol/ -v --tb=short --cov=rmcp --cov-append | |
| - name: Run integration tests - tools (R tool integration) | |
| run: | | |
| which R && echo "✅ R available for tool integration tests" || echo "❌ R required but not available" | |
| pytest tests/integration/tools/ -v --tb=short --cov=rmcp --cov-append | |
| - name: Run integration tests - transport (HTTP transport) | |
| run: | | |
| pytest tests/integration/transport/ -v --tb=short --cov=rmcp --cov-append | |
| - name: Test HTTPS functionality with mkcert | |
| run: | | |
| echo "🔒 Testing HTTPS functionality..." | |
| # Verify mkcert is available (installed in Dockerfile) | |
| mkcert -version | |
| # Generate test certificates | |
| mkdir -p /tmp/test-certs | |
| mkcert -cert-file /tmp/test-certs/test.pem -key-file /tmp/test-certs/test-key.pem localhost 127.0.0.1 | |
| # End-to-end: serve-http over TLS, then hit /health with the mkcert CA | |
| rmcp serve-http --host 127.0.0.1 --port 8443 \ | |
| --ssl-keyfile /tmp/test-certs/test-key.pem \ | |
| --ssl-certfile /tmp/test-certs/test.pem & | |
| SERVER_PID=$! | |
| HEALTHY=0 | |
| for _ in $(seq 1 30); do | |
| if curl -s --cacert "$(mkcert -CAROOT)/rootCA.pem" https://localhost:8443/health | grep -q healthy; then | |
| HEALTHY=1 | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| kill "$SERVER_PID" || true | |
| if [ "$HEALTHY" != "1" ]; then | |
| echo "❌ HTTPS health check failed" | |
| exit 1 | |
| fi | |
| echo "✅ HTTPS health check passed" | |
| echo "✅ HTTPS functionality tests completed" | |
| - name: Run integration tests - core (server & registries) | |
| run: | | |
| which R && echo "✅ R available for core integration tests" || echo "⚠️ R not available" | |
| pytest tests/integration/core/ -v --tb=short --cov=rmcp --cov-append | |
| - name: Run scenario tests (end-to-end user scenarios) | |
| run: | | |
| which R && echo "✅ R available for scenario tests" || echo "❌ R required but not available" | |
| pytest tests/scenarios/ -v --tb=short --cov=rmcp --cov-append | |
| - name: Install gpg for Codecov uploader verification | |
| # Best-effort by design. The only thing that wants gpg is the Codecov | |
| # upload below, and that step already runs with fail_ci_if_error: false | |
| # -- so a required job was hard-failing on a helper for an upload that | |
| # is itself allowed to fail. The criticality was inverted. | |
| # | |
| # Not hypothetical. On 5 August this step failed with | |
| # E: Failed to fetch .../noble-cran40/Packages.gz | |
| # File has unexpected size (62232 != 61419). Mirror sync in progress? | |
| # *after* the tests had already passed. Because "R Integration & Workflow | |
| # Tests" is a required context, that mirror hiccup blocked PR #66 -- a | |
| # security bump closing an open advisory -- for five days. | |
| # | |
| # Retries cover a mirror mid-sync, which clears in minutes. | |
| # continue-on-error covers the case where it does not. | |
| continue-on-error: true | |
| run: | | |
| set -u | |
| for attempt in 1 2 3; do | |
| if apt-get update -qq && apt-get install -y -qq gpg; then | |
| echo "gpg installed on attempt $attempt" | |
| exit 0 | |
| fi | |
| echo "::warning::apt failed on attempt $attempt; retrying" | |
| sleep $((attempt * 10)) | |
| done | |
| echo "::warning::could not install gpg; the Codecov upload proceeds unverified" | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| files: ./coverage.xml | |
| flags: comprehensive | |
| name: comprehensive-coverage | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Test CLI and MCP protocol | |
| run: | | |
| # Verify CLI works with R integration | |
| rmcp --version | |
| rmcp list-capabilities | |
| # Test MCP protocol over stdio with the official client (handshake + tools/list + real R tool call) | |
| python - <<'PYEOF' | |
| import asyncio | |
| from mcp import ClientSession, StdioServerParameters | |
| from mcp.client.stdio import stdio_client | |
| async def main(): | |
| params = StdioServerParameters(command="rmcp", args=["start"]) | |
| async with stdio_client(params) as (read, write): | |
| async with ClientSession(read, write) as session: | |
| await session.initialize() | |
| tools = await session.list_tools() | |
| names = [tool.name for tool in tools.tools] | |
| assert "linear_model" in names, names[:5] | |
| result = await session.call_tool( | |
| "summary_stats", | |
| {"data": {"x": [1, 2, 3, 4, 5]}, "variables": ["x"]}, | |
| ) | |
| assert not result.is_error, result.content | |
| print(f"MCP stdio OK: {len(names)} tools, summary_stats ran") | |
| asyncio.run(main()) | |
| PYEOF | |
| echo "✅ MCP protocol verified over stdio" | |
| # Docker scenario tests run outside production container (need Docker access) | |
| docker-scenarios: | |
| name: Docker Scenario Tests | |
| runs-on: ubuntu-latest | |
| needs: [python-checks, docker-build] | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies for scenario tests | |
| run: | | |
| uv sync --group dev | |
| echo "🐳 Verifying Docker availability..." | |
| docker --version | |
| docker info | |
| # Authenticate to pull production image built in docker-build job | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify Docker authentication and pull production image | |
| env: | |
| PRODUCTION_IMAGE: ${{ needs.docker-build.outputs.production-image }} | |
| run: | | |
| echo "🔍 Verifying Docker authentication..." | |
| docker info | grep -E "(Username|Registry)" | |
| echo "🔍 Production image to test: $PRODUCTION_IMAGE" | |
| echo "🔍 Attempting to pull production image..." | |
| if docker pull "$PRODUCTION_IMAGE"; then | |
| echo "✅ Production image pulled successfully" | |
| export RMCP_PRODUCTION_IMAGE="$PRODUCTION_IMAGE" | |
| else | |
| echo "❌ Failed to pull production image from registry" | |
| echo "🔧 Building production image locally as fallback..." | |
| docker build --target production -t rmcp-prod-fallback . | |
| export RMCP_PRODUCTION_IMAGE="rmcp-prod-fallback" | |
| echo "✅ Local production image built: $RMCP_PRODUCTION_IMAGE" | |
| fi | |
| # Verify the image exists and works | |
| echo "🔍 Testing image availability..." | |
| docker images | grep rmcp || echo "No RMCP images found" | |
| # Export for subsequent steps | |
| echo "RMCP_PRODUCTION_IMAGE=$RMCP_PRODUCTION_IMAGE" >> "$GITHUB_ENV" | |
| - name: Run Docker deployment scenario tests | |
| run: | | |
| echo "🎯 Testing Docker deployment scenarios..." | |
| echo "Using production image: $RMCP_PRODUCTION_IMAGE" | |
| # Test basic Docker functionality | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_basic_functionality -v --tb=short | |
| # Test production image functionality (uses pre-built image) | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerProductionScenarios::test_docker_production_image_functionality -v --tb=short | |
| # Test security configuration | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerProductionScenarios::test_docker_security_configuration -v --tb=short | |
| # Test environment variables and volume mounts | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerProductionScenarios::test_docker_environment_variables -v --tb=short | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerProductionScenarios::test_docker_volume_mounts -v --tb=short | |
| # Test R environment | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_r_environment_validation -v --tb=short | |
| # Test end-to-end workflows | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_mcp_protocol_communication -v --tb=short | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_complete_analysis_workflow -v --tb=short | |
| # Test performance and resource usage | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_performance_benchmarks -v --tb=short | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerWorkflowValidation::test_docker_resource_usage -v --tb=short | |
| # Test platform-specific features (cross-platform compatibility) | |
| uv run pytest tests/scenarios/test_deployment_scenarios.py::TestDockerCrossplatformCompatibility::test_docker_platform_specific_features -v --tb=short | |
| echo "✅ All Docker scenario tests completed" | |
| - name: Run production MCP evaluations | |
| run: | | |
| test -n "$RMCP_PRODUCTION_IMAGE" | |
| RMCP_EVAL_DOCKER_IMAGE="$RMCP_PRODUCTION_IMAGE" \ | |
| uv run pytest tests/evals/test_mcp_server_evals.py -v --tb=short | |
| # Container-based tests for production image validation | |
| production-container-tests: | |
| name: Production Container Tests | |
| runs-on: ubuntu-latest | |
| needs: [python-checks, docker-build] | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Test production image functionality | |
| run: | | |
| echo "🐳 Testing production image functionality..." | |
| PROD_IMAGE="${{ needs.docker-build.outputs.production-image }}" | |
| echo "Target production image: $PROD_IMAGE" | |
| # Check if we can pull the production image, with fallback | |
| if docker pull "$PROD_IMAGE" 2>/dev/null; then | |
| echo "✅ Successfully pulled production image from registry" | |
| elif [ -n "${ACT:-}" ]; then | |
| # Local testing with act - try to build the production image locally | |
| echo "⚠️ Cannot pull image in local environment, building locally..." | |
| if docker build --target production -t "rmcp-prod-local" .; then | |
| PROD_IMAGE="rmcp-prod-local" | |
| echo "✅ Successfully built production image locally: $PROD_IMAGE" | |
| else | |
| echo "❌ Failed to build production image locally" | |
| exit 1 | |
| fi | |
| else | |
| echo "❌ Failed to pull production image: $PROD_IMAGE" | |
| echo "This might indicate the image wasn't built or registry authentication failed" | |
| exit 1 | |
| fi | |
| # Test basic Python imports | |
| echo "🔍 Testing Python imports..." | |
| docker run --rm "$PROD_IMAGE" \ | |
| python -c "import rmcp; print('✅ Production image RMCP import successful')" | |
| # Test HTTP transport dependencies | |
| echo "🔍 Testing HTTP transport dependencies..." | |
| docker run --rm "$PROD_IMAGE" \ | |
| python -c "import mcp, uvicorn; print('✅ Production image HTTP transport ready')" | |
| # Test R availability | |
| echo "🔍 Testing R availability..." | |
| docker run --rm "$PROD_IMAGE" \ | |
| R -e "cat('✅ Production image R version:', R.version.string, '\n')" | |
| echo "✅ Production image functionality verified" | |
| # Cross-platform Python testing (validates Python-only functionality) | |
| cross-platform: | |
| name: Cross-platform Python Tests | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ['3.11', '3.12'] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: | | |
| uv sync --group dev | |
| uv pip install -e . | |
| - name: Run cross-platform smoke tests | |
| run: | | |
| uv run pytest tests/smoke/ -v |