diff --git a/AGENTS.md b/AGENTS.md index 3a90a6c..27c61a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -410,7 +410,23 @@ myservice_router = Router(myservice_routes) app.mount("/api/env/{env_id}/services/myservice", myservice_router) ``` -**5. Write a seed script** in `backend/utils/seed_myservice_template.py` that: +**5. Register the service with the platform API** in `src/platform/api/models.py`. +Add it to the `Service` enum so `initEnv` (and other platform endpoints that take +a `templateService`) will accept the new value: + +```python +class Service(str, Enum): + slack = "slack" + linear = "linear" + calendar = "calendar" + box = "box" + myservice = "myservice" # ← add this +``` + +If you skip this step, `POST /api/platform/initEnv` returns a Pydantic enum +validation error even though the template rows exist and the router is mounted. + +**6. Write a seed script** in `backend/utils/seed_myservice_template.py` that: - Creates the PostgreSQL schema (e.g. `myservice_default`) - Uses `Base.metadata.create_all()` to create tables - Inserts seed data from a JSON file @@ -419,10 +435,10 @@ app.mount("/api/env/{env_id}/services/myservice", myservice_router) Follow `seed_slack_template.py` as a reference — it shows the full pattern including schema creation, table ordering, and template registration. -**6. Add seed data** in `examples/myservice/seeds/myservice_default.json` and copy to +**7. Add seed data** in `examples/myservice/seeds/myservice_default.json` and copy to `backend/seeds/myservice/` for Docker builds. -**7. Register the seed script** in the Docker startup command in `ops/docker-compose.yml`: +**8. Register the seed script** in the Docker startup command in `ops/docker-compose.yml`: ```yaml command: > diff --git a/backend/src/platform/api/main.py b/backend/src/platform/api/main.py index f5d4df5..aa4b7cf 100644 --- a/backend/src/platform/api/main.py +++ b/backend/src/platform/api/main.py @@ -1,3 +1,5 @@ +from contextlib import asynccontextmanager + from sqlalchemy import create_engine from sqlalchemy.pool import NullPool from src.platform.isolationEngine.session import SessionManager @@ -35,7 +37,13 @@ def create_app(): - app = Starlette() + @asynccontextmanager + async def lifespan(app_): + yield + if getattr(app_.state, "replication_service", None): + app_.state.replication_service.stop() + + app = Starlette(lifespan=lifespan) db_url = environ["DATABASE_URL"] # Use NullPool when using Neon's PgBouncer (-pooler) to avoid double pooling @@ -143,12 +151,6 @@ def create_app(): app.mount("/api/env/{env_id}/services/linear", linear_graphql) - @app.on_event("shutdown") - async def shutdown_event(): - # Stop replication service if running (it's on-demand now) - if app.state.replication_service: - app.state.replication_service.stop() - return app diff --git a/backend/src/platform/api/models.py b/backend/src/platform/api/models.py index 6a6df6a..3bdd9d9 100644 --- a/backend/src/platform/api/models.py +++ b/backend/src/platform/api/models.py @@ -13,6 +13,7 @@ class Service(str, Enum): linear = "linear" calendar = "calendar" box = "box" + github = "github" class Visibility(str, Enum):