Skip to main content

CI/CD Pipeline & Testing Strategy

Pipeline Overview

The CI/CD pipeline is defined in .github/workflows/deploy.yml and follows a gated deployment model with environment-aware strictness.
push to main/test/dev-*
         |
    detect-environment
         |
    +-----------+-----------+
    |                       |
dep-check (hard)      lint (soft)
    |                       |
    +-----------+-----------+
    |                       |
unit-test (hard)   integration-test
    |              (hard on prod,
    |               soft on test)
    |                       |
    +-----------+-----------+
         |
   docker-build (hard)
         |
      deploy

Gate Policy

Jobmain (prod)test / dev-*
dep-checkHard gateHard gate
lintSoft (visible failure, non-blocking)Soft
unit-testHard gateHard gate
integration-testHard gateSoft (continue-on-error)
docker-buildHard gateHard gate
Hard gate = failure blocks deployment. Soft gate = failure is visible in GitHub UI but does not block.

Branch-to-Environment Mapping

BranchEnvironmentRailway Services
mainprodsyntropy-portal-production, syntropy-api-production
testtestsyntropy-portal-test, syntropy-api-test
dev-*testsyntropy-portal-test, syntropy-api-test
Manual dispatchchosenper environment

Paths Ignored

Pushes that only modify these paths do not trigger the pipeline:
  • **.md, docs/**, .gitignore, .claude/**

Pipeline Jobs

1. Dependency Check (dep-check)

Validates that the dependency graph is consistent and core modules import cleanly. Steps:
  1. uv lock --check — ensures uv.lock matches pyproject.toml
  2. uv sync --frozen — installs exactly what the lockfile specifies
  3. Import validation — imports critical modules (app.config, app.hydra_config, app.api, etc.)
Why: Catches broken imports, circular dependencies, and lockfile drift before running any tests. This is the cheapest, fastest signal.

2. Lint (lint)

Runs ruff check and ruff format --check on app/ and tests/. Status: Soft gate (continue-on-error: true). The codebase has ~500 pre-existing lint errors. Once cleaned up, this will become a hard gate. Output format: --output-format=github annotates PRs with inline lint warnings.

3. Unit Tests (unit-test)

Runs tests/unit/ with MOCK_LLM=true and APP_ENV=test. No external services needed. Invocation: uv run python -m pytest tests/unit/ (not uv run pytest due to VIRTUAL_ENV mismatch with uv). What’s tested:
  • Chat state interface contracts (AST-based to avoid import chain issues)
  • Chat streaming behavior
  • Chat workflow (36 end-to-end workflow tests)
  • LLM fallback chain logic
  • Theme module constants
  • Tool badges UI components
  • Landing chat interface
  • Checkpointer logic
  • Chat schemas

4. Integration Tests (integration-test)

Runs tests/integration/ against the Supabase test database. Requires: TEST_DB_URL GitHub secret (Supabase test instance connection string). What’s tested:
  • Catalog seeding and CRUD operations
  • Chat chain with real DB persistence
  • Chat state lifecycle (session creation, message storage)
  • LLM integration (via VCRpy recorded cassettes)
  • Timestamped chat history
  • Landing chat flow
Graceful degradation: If TEST_DB_URL is not set, integration tests are skipped (not failed).

5. Docker Build Test (docker-build)

Builds the full Docker image to verify:
  • Dependency resolution works inside the container
  • Local libs/ submodules are accessible
  • Reflex frontend initializes (reflex init)
  • Asset files are present

6. Deploy (deploy)

Deploys to Railway using the Railway CLI. Validates required secrets before deploying.

Testing Strategy

Test Categories

tests/
  unit/                          # No external deps, fast, always run
    test_chat_schemas.py         # Schema validation
    test_chat_state_interface.py # AST-based contract tests
    test_chat_streaming.py       # Streaming behavior
    test_chat_workflow.py        # End-to-end workflow mocks
    test_checkpointer.py         # LangGraph checkpoint logic
    test_landing_chat_interface.py
    test_llm_fallback_chain.py   # Multi-provider fallback
    test_theme_module.py         # Config-driven theme
    test_tool_badges.py          # UI component rendering
  integration/                   # Requires PostgreSQL (Supabase)
    llm/                         # LLM integration with VCRpy cassettes
      test_agent_from_functions.py
      test_llm_integration.py
      test_shrine_agent.py
    test_chat_chain.py           # Full chat chain with DB
    test_chat_state_lifecycle.py # Session lifecycle
    test_landing_chat.py         # Landing page chat flow
    test_seed_catalog.py         # Catalog seeding
    test_timestamped_chat_history.py

Running Tests Locally

# Unit tests (no setup required)
make test-unit

# Integration tests (requires PostgreSQL)
make docker-db            # Start local PostgreSQL
make test-integration     # Run against local DB

# All tests
make test

# With coverage report
make test-coverage

Test Markers

MarkerUsageMeaning
@pytest.mark.integrationIntegration testsRequires database
@pytest.mark.slowLong-running testsExcluded from CI by default
@pytest.mark.vcrHTTP recordingUses VCRpy cassettes
@pytest.mark.langsmithLangSmith integrationRequires API key

AST-Based Contract Tests

Some tests use import ast to validate class interfaces without importing the full module chain (which triggers SQLAlchemy metadata collisions in test isolation):
def test_chat_state_has_send_message():
    """Verify ChatState has send_message method via AST."""
    source = Path("app/syntropy_health/states/chat/chat.py").read_text()
    tree = ast.parse(source)
    methods = [n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]
    assert "send_message" in methods

VCRpy HTTP Recording

LLM integration tests use VCRpy to record and replay HTTP calls:
  • Cassettes stored in tests/integration/llm/cassettes/
  • Sensitive headers (API keys) are automatically filtered
  • First run records real responses; subsequent runs replay

Secrets Management

GitHub Secrets Required

SecretScopePurpose
RAILWAY_TOKENPer environment (prod/test)Railway deployment
OPENAI_API_KEYRepositoryLLM + embeddings
CLERK_SECRET_KEYRepositoryAuthentication
STRIPE_SECRET_KEYRepositoryPayments
REFLEX_DB_URLRepositoryProduction database URL
TEST_DB_URLRepositorySupabase test database URL

Secret Sources

Secrets are sourced from envs/ files which are gitignored:
  • envs/base — shared API keys (NEVER committed)
  • envs/dev — local development values
  • envs/test — test environment values
  • envs/prod — production values
Templates (envs/template, envs/template.base) are committed with empty values.

Docker Security

  • CLERK_SECRET_KEY is not baked into Docker images — injected at runtime
  • .env files are excluded via .dockerignore
  • Only public keys (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) are set at build time
  • Secret validation in CI uses environment variable intermediaries (no shell interpolation)

Environment Configuration

Loading Order

  1. envs/base — shared defaults, loaded first (no override)
  2. envs/<APP_ENV> — environment-specific overrides
  3. .env — optional local developer overrides

External Services

ServiceDevTestProd
PostgreSQLLocal DockerSupabase (pooled)Supabase (pooled)
Vector DBpgvector (local)Zilliz CloudZilliz Cloud
AuthClerk test instanceClerk test instanceClerk live instance
LLMOpenRouter (free tier)OpenRouterOpenRouter
AnalyticsPostHog (optional)PostHogPostHog
Knowledge GraphGraphiti (optional)GraphitiGraphiti

Dockerfile Architecture

Multi-stage build:
  1. Builder (python:3.11-slim): installs deps, copies libs/, runs reflex init
  2. Runtime (python:3.11-slim): minimal runtime, startup script runs migrations + seeds + reflex run
Key decisions:
  • git included in builder for fallback dependency resolution
  • libs/ copied before uv sync --frozen for local path sources
  • Node.js 20 required for Reflex frontend
  • REFLEX_BUILD_PHASE=1 skips Hydra config during build

Workflow Maintenance

Adding a New Service Dependency

  1. Add the connection env var to envs/template
  2. Add the actual value to envs/test and envs/prod
  3. If needed in CI integration tests, add as a GitHub secret
  4. Update the import validation list in dep-check if new modules are added

Upgrading Python Version

Update in all three locations:
  1. .github/workflows/deploy.ymlPYTHON_VERSION env var
  2. DockerfileFROM python:X.Y-slim
  3. pyproject.tomlrequires-python

Hardening Lint Gate

Once pre-existing lint errors are cleaned up:
  1. Remove continue-on-error: true from the lint job
  2. Add lint to the needs list of docker-build