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.
Gate Policy
| Job | main (prod) | test / dev-* |
|---|---|---|
| dep-check | Hard gate | Hard gate |
| lint | Soft (visible failure, non-blocking) | Soft |
| unit-test | Hard gate | Hard gate |
| integration-test | Hard gate | Soft (continue-on-error) |
| docker-build | Hard gate | Hard gate |
Branch-to-Environment Mapping
| Branch | Environment | Railway Services |
|---|---|---|
main | prod | syntropy-portal-production, syntropy-api-production |
test | test | syntropy-portal-test, syntropy-api-test |
dev-* | test | syntropy-portal-test, syntropy-api-test |
| Manual dispatch | chosen | per 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:
uv lock --check— ensuresuv.lockmatchespyproject.tomluv sync --frozen— installs exactly what the lockfile specifies- Import validation — imports critical modules (
app.config,app.hydra_config,app.api, etc.)
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
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
Running Tests Locally
Test Markers
| Marker | Usage | Meaning |
|---|---|---|
@pytest.mark.integration | Integration tests | Requires database |
@pytest.mark.slow | Long-running tests | Excluded from CI by default |
@pytest.mark.vcr | HTTP recording | Uses VCRpy cassettes |
@pytest.mark.langsmith | LangSmith integration | Requires API key |
AST-Based Contract Tests
Some tests useimport ast to validate class interfaces without importing the full module chain (which triggers SQLAlchemy metadata collisions in test isolation):
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
| Secret | Scope | Purpose |
|---|---|---|
RAILWAY_TOKEN | Per environment (prod/test) | Railway deployment |
OPENAI_API_KEY | Repository | LLM + embeddings |
CLERK_SECRET_KEY | Repository | Authentication |
STRIPE_SECRET_KEY | Repository | Payments |
REFLEX_DB_URL | Repository | Production database URL |
TEST_DB_URL | Repository | Supabase test database URL |
Secret Sources
Secrets are sourced fromenvs/ files which are gitignored:
envs/base— shared API keys (NEVER committed)envs/dev— local development valuesenvs/test— test environment valuesenvs/prod— production values
envs/template, envs/template.base) are committed with empty values.
Docker Security
CLERK_SECRET_KEYis not baked into Docker images — injected at runtime.envfiles 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
envs/base— shared defaults, loaded first (no override)envs/<APP_ENV>— environment-specific overrides.env— optional local developer overrides
External Services
| Service | Dev | Test | Prod |
|---|---|---|---|
| PostgreSQL | Local Docker | Supabase (pooled) | Supabase (pooled) |
| Vector DB | pgvector (local) | Zilliz Cloud | Zilliz Cloud |
| Auth | Clerk test instance | Clerk test instance | Clerk live instance |
| LLM | OpenRouter (free tier) | OpenRouter | OpenRouter |
| Analytics | PostHog (optional) | PostHog | PostHog |
| Knowledge Graph | Graphiti (optional) | Graphiti | Graphiti |
Dockerfile Architecture
Multi-stage build:- Builder (python:3.11-slim): installs deps, copies libs/, runs
reflex init - Runtime (python:3.11-slim): minimal runtime, startup script runs migrations + seeds +
reflex run
gitincluded in builder for fallback dependency resolutionlibs/copied beforeuv sync --frozenfor local path sources- Node.js 20 required for Reflex frontend
REFLEX_BUILD_PHASE=1skips Hydra config during build
Workflow Maintenance
Adding a New Service Dependency
- Add the connection env var to
envs/template - Add the actual value to
envs/testandenvs/prod - If needed in CI integration tests, add as a GitHub secret
- Update the import validation list in
dep-checkif new modules are added
Upgrading Python Version
Update in all three locations:.github/workflows/deploy.yml—PYTHON_VERSIONenv varDockerfile—FROM python:X.Y-slimpyproject.toml—requires-python
Hardening Lint Gate
Once pre-existing lint errors are cleaned up:- Remove
continue-on-error: truefrom thelintjob - Add
lintto theneedslist ofdocker-build