Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 79 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,10 @@ jobs:
"$PG_BIN/pg_ctl" -D "$RUNNER_TEMP/pgdata" -m fast stop || true
fi

e2e:
e2e-stubbed:
runs-on: ubuntu-latest
needs: [check]
timeout-minutes: 10

steps:
- name: Checkout
Expand All @@ -338,29 +339,28 @@ jobs:
- name: Cache Next.js build cache
uses: actions/cache@v4
with:
path: |
frontend/.next/cache
path: frontend/.next/cache
key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-


- name: Start frontend (dev server)
env:
NEXT_PUBLIC_API_URL: "http://localhost:8000"
NEXT_PUBLIC_AUTH_MODE: "local"
NEXT_TELEMETRY_DISABLED: "1"
run: |
cd frontend
npm run dev -- --hostname 0.0.0.0 --port 3000 &
npm run dev -- --hostname 0.0.0.0 --port 3000 > /tmp/frontend.log 2>&1 &
for i in {1..60}; do
if curl -sf http://localhost:3000/ > /dev/null; then exit 0; fi
sleep 2
done
echo "Frontend did not start"
echo "Frontend did not start. Logs:"
cat /tmp/frontend.log
exit 1

- name: Run Cypress E2E
- name: Run Cypress stubbed E2E
env:
NEXT_PUBLIC_API_URL: "http://localhost:8000"
NEXT_PUBLIC_AUTH_MODE: "local"
Expand All @@ -373,8 +373,79 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
name: cypress-stubbed-artifacts
if-no-files-found: ignore
path: |
frontend/cypress/screenshots/**
frontend/cypress/videos/**
/tmp/frontend.log

e2e-integration:
runs-on: ubuntu-latest
needs: [check]
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
id: setup-node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Start E2E Docker stack
run: make e2e-up

- name: Seed E2E database
run: make e2e-seed

- name: Install frontend dependencies
run: make frontend-sync

- name: Start frontend (dev server pointing to E2E backend)
env:
NEXT_PUBLIC_API_URL: "http://localhost:8001"
NEXT_PUBLIC_AUTH_MODE: "local"
NEXT_TELEMETRY_DISABLED: "1"
run: |
cd frontend
npm run dev -- --hostname 0.0.0.0 --port 3000 > /tmp/frontend-e2e.log 2>&1 &
for i in {1..60}; do
if curl -sf http://localhost:3000/ > /dev/null; then exit 0; fi
sleep 2
done
echo "Frontend did not start. Logs:"
cat /tmp/frontend-e2e.log
exit 1

- name: Run integration E2E tests
run: make e2e-integration

- name: Dump Docker logs on failure
if: failure()
run: |
docker compose --profile e2e logs backend-e2e --tail 200 || true
docker compose --profile e2e logs db-test --tail 50 || true
docker compose --profile e2e logs redis --tail 50 || true

- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-integration-artifacts
if-no-files-found: ignore
path: |
frontend/cypress/screenshots/**
frontend/cypress/videos/**
/tmp/frontend-e2e.log

- name: Tear down E2E stack
if: always()
run: make e2e-down
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,27 @@ docs-link-check: ## Check for broken relative links in markdown docs

.PHONY: docs-check
docs-check: docs-lint docs-link-check ## Run all docs quality gates

# ── E2E Integration Targets ─────────────────────────────────────
.PHONY: e2e-up
e2e-up: ## Start E2E Docker stack (test DB + backend + redis)
docker compose --profile e2e up -d --wait db-test backend-e2e redis

.PHONY: e2e-seed
e2e-seed: ## Seed E2E test database with prerequisite data
docker compose --profile e2e run --rm e2e-seed

.PHONY: e2e-down
e2e-down: ## Tear down E2E Docker stack and volumes
docker compose --profile e2e down -v

.PHONY: e2e-integration
e2e-integration: ## Run integration E2E tests (requires e2e-up + e2e-seed first)
cd $(FRONTEND_DIR) && npx cypress run --config-file cypress.integration.config.ts --browser chrome

.PHONY: e2e-full
e2e-full: ## Full E2E cycle: start stack → seed → run tests → tear down
$(MAKE) e2e-up
$(MAKE) e2e-seed || ($(MAKE) e2e-down && exit 1)
$(MAKE) e2e-integration || ($(MAKE) e2e-down && exit 1)
$(MAKE) e2e-down
17 changes: 13 additions & 4 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any

from fastapi import APIRouter, FastAPI, status
from fastapi import APIRouter, Depends, FastAPI, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi_pagination import add_pagination
Expand Down Expand Up @@ -37,8 +37,9 @@
from app.core.rate_limit import validate_rate_limit_redis
from app.core.rate_limit_backend import RateLimitBackend
from app.core.security_headers import SecurityHeadersMiddleware
from app.db.session import init_db
from app.db.session import get_session, init_db
from app.schemas.health import HealthStatusResponse
from sqlmodel.ext.asyncio.session import AsyncSession

if TYPE_CHECKING:
from collections.abc import AsyncIterator
Expand Down Expand Up @@ -531,8 +532,16 @@ def healthz() -> HealthStatusResponse:
}
},
)
def readyz() -> HealthStatusResponse:
"""Readiness probe endpoint for service orchestration checks."""
async def readyz(
session: AsyncSession = Depends(get_session),
) -> HealthStatusResponse:
"""Readiness probe endpoint for service orchestration checks.

Verifies the database connection is live by executing a simple query.
"""
from sqlalchemy import text

await session.execute(text("SELECT 1"))
return HealthStatusResponse(ok=True)


Expand Down
57 changes: 57 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,62 @@ services:
RQ_DISPATCH_MAX_RETRIES: ${RQ_DISPATCH_MAX_RETRIES:-3}
restart: unless-stopped

# ── E2E Integration Test Services ──────────────────────────────
db-test:
profiles: ["e2e"]
image: postgres:16-alpine
environment:
POSTGRES_DB: ocmc_test
POSTGRES_USER: ocmc
POSTGRES_PASSWORD: test
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ocmc -d ocmc_test"]
interval: 5s
timeout: 3s
retries: 20
tmpfs:
- /var/lib/postgresql/data

backend-e2e:
profiles: ["e2e"]
build:
context: .
dockerfile: backend/Dockerfile
depends_on:
db-test:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: "postgresql+psycopg://ocmc:test@db-test:5432/ocmc_test"
AUTH_MODE: "local"
LOCAL_AUTH_TOKEN: "e2e-local-auth-token-0123456789-0123456789-0123456789x"
DB_AUTO_MIGRATE: "true"
CORS_ORIGINS: "http://localhost:3000"
RQ_REDIS_URL: "redis://redis:6379/0"
BASE_URL: "http://localhost:8000"
ports:
- "8001:8000"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8000/readyz || exit 1"]
interval: 5s
timeout: 10s
retries: 12

e2e-seed:
profiles: ["e2e"]
build:
context: .
dockerfile: backend/Dockerfile
depends_on:
db-test:
condition: service_healthy
environment:
DATABASE_URL: "postgresql+psycopg://ocmc:test@db-test:5432/ocmc_test"
command: ["python", "-m", "scripts.e2e_seed"]
restart: "no"
Comment on lines +157 to +168
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e2e-seed only depends on db-test being healthy, but the seed script assumes the schema/migrations are already applied (handled by backend-e2e via DB_AUTO_MIGRATE). If someone runs make e2e-seed (or docker compose run e2e-seed) without having started/awaited backend-e2e, this can fail with missing tables. Consider adding a depends_on: backend-e2e: { condition: service_healthy } (or running migrations in the seed container) to make seeding robust when invoked independently.

Copilot uses AI. Check for mistakes.

volumes:
postgres_data:
Loading
Loading