Skip to content

Commit 0c1d86e

Browse files
committed
ci(core): use shared postgres services in actions
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 651563a commit 0c1d86e

File tree

3 files changed

+92
-12
lines changed

3 files changed

+92
-12
lines changed

.github/workflows/test.yml

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,22 @@ jobs:
153153
- python-version: "3.13"
154154
- python-version: "3.14"
155155
runs-on: ubuntu-latest
156-
157-
# Note: No services section needed - testcontainers handles Postgres in Docker
156+
services:
157+
postgres:
158+
image: pgvector/pgvector:pg16
159+
env:
160+
POSTGRES_USER: basic_memory_user
161+
POSTGRES_PASSWORD: dev_password
162+
POSTGRES_DB: basic_memory_test
163+
ports:
164+
- 5432:5432
165+
options: >-
166+
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
167+
--health-interval 10s
168+
--health-timeout 5s
169+
--health-retries 5
170+
env:
171+
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
158172

159173
steps:
160174
- uses: actions/checkout@v4
@@ -197,8 +211,22 @@ jobs:
197211
- python-version: "3.13"
198212
- python-version: "3.14"
199213
runs-on: ubuntu-latest
200-
201-
# Note: No services section needed - testcontainers handles Postgres in Docker
214+
services:
215+
postgres:
216+
image: pgvector/pgvector:pg16
217+
env:
218+
POSTGRES_USER: basic_memory_user
219+
POSTGRES_PASSWORD: dev_password
220+
POSTGRES_DB: basic_memory_test
221+
ports:
222+
- 5432:5432
223+
options: >-
224+
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
225+
--health-interval 10s
226+
--health-timeout 5s
227+
--health-retries 5
228+
env:
229+
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
202230

203231
steps:
204232
- uses: actions/checkout@v4

test-int/conftest.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def postgres_container(db_backend):
109109
Uses testcontainers to spin up a real Postgres instance.
110110
Only starts if db_backend is "postgres".
111111
"""
112-
if db_backend != "postgres":
112+
if db_backend != "postgres" or _configured_postgres_sync_url():
113113
yield None
114114
return
115115

@@ -125,11 +125,35 @@ def postgres_container(db_backend):
125125
]
126126

127127

128+
def _configured_postgres_sync_url() -> str | None:
129+
"""Prefer an externally managed Postgres server when CI provides one."""
130+
configured_url = os.environ.get("BASIC_MEMORY_TEST_POSTGRES_URL") or os.environ.get(
131+
"POSTGRES_TEST_URL"
132+
)
133+
if not configured_url:
134+
return None
135+
136+
return (
137+
configured_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://", 1)
138+
.replace("postgresql://", "postgresql+psycopg2://", 1)
139+
.replace("postgres://", "postgresql+psycopg2://", 1)
140+
)
141+
142+
128143
def _postgres_reset_tables() -> list[str]:
129144
"""Resolve the current ORM table set at reset time."""
130145
return [table.name for table in Base.metadata.sorted_tables] + ["search_index"]
131146

132147

148+
def _resolve_postgres_sync_url(postgres_container) -> str:
149+
"""Use CI's shared service when configured, otherwise fall back to testcontainers."""
150+
configured_url = _configured_postgres_sync_url()
151+
if configured_url:
152+
return configured_url
153+
assert postgres_container is not None
154+
return postgres_container.get_connection_url()
155+
156+
133157
async def _reset_postgres_integration_schema(engine) -> None:
134158
"""Restore the shared Postgres integration schema to a clean baseline."""
135159
from basic_memory.models.search import (
@@ -175,7 +199,7 @@ async def engine_factory(
175199

176200
if db_backend == "postgres":
177201
# Postgres mode using testcontainers
178-
sync_url = postgres_container.get_connection_url()
202+
sync_url = _resolve_postgres_sync_url(postgres_container)
179203
async_url = sync_url.replace("postgresql+psycopg2", "postgresql+asyncpg")
180204

181205
engine = create_async_engine(
@@ -267,8 +291,10 @@ def app_config(
267291
# Configure database backend based on env var
268292
if db_backend == "postgres":
269293
database_backend = DatabaseBackend.POSTGRES
270-
# Get URL from testcontainer and convert to asyncpg driver
271-
sync_url = postgres_container.get_connection_url()
294+
# Trigger: CI jobs can provide a shared Postgres service instead of per-session containers.
295+
# Why: reusing one pgvector-enabled server avoids Docker startup churn on every job.
296+
# Outcome: local runs keep using testcontainers, while CI injects a stable service URL.
297+
sync_url = _resolve_postgres_sync_url(postgres_container)
272298
database_url = sync_url.replace("postgresql+psycopg2", "postgresql+asyncpg")
273299
else:
274300
database_backend = DatabaseBackend.SQLITE

tests/conftest.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def postgres_container(db_backend):
8080
The container is started once per test session and shared across all tests.
8181
Only starts if db_backend is "postgres".
8282
"""
83-
if db_backend != "postgres":
83+
if db_backend != "postgres" or _configured_postgres_sync_url():
8484
yield None
8585
return
8686

@@ -95,6 +95,21 @@ def postgres_container(db_backend):
9595
]
9696

9797

98+
def _configured_postgres_sync_url() -> str | None:
99+
"""Prefer an externally managed Postgres server when CI provides one."""
100+
configured_url = os.environ.get("BASIC_MEMORY_TEST_POSTGRES_URL") or os.environ.get(
101+
"POSTGRES_TEST_URL"
102+
)
103+
if not configured_url:
104+
return None
105+
106+
return (
107+
configured_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://", 1)
108+
.replace("postgresql://", "postgresql+psycopg2://", 1)
109+
.replace("postgres://", "postgresql+psycopg2://", 1)
110+
)
111+
112+
98113
def _postgres_alembic_config(async_url: str) -> Config:
99114
"""Build Alembic config for stamping the shared Postgres test schema."""
100115
alembic_dir = Path(db.__file__).parent / "alembic"
@@ -121,6 +136,15 @@ def _postgres_reset_tables() -> list[str]:
121136
]
122137

123138

139+
def _resolve_postgres_sync_url(postgres_container) -> str:
140+
"""Use CI's shared service when configured, otherwise fall back to testcontainers."""
141+
configured_url = _configured_postgres_sync_url()
142+
if configured_url:
143+
return configured_url
144+
assert postgres_container is not None
145+
return postgres_container.get_connection_url()
146+
147+
124148
async def _reset_postgres_test_schema(engine: AsyncEngine, async_url: str) -> None:
125149
"""Restore the shared Postgres schema to a clean baseline before each test."""
126150
from basic_memory.models.search import (
@@ -198,8 +222,10 @@ def app_config(config_home, db_backend, postgres_container, monkeypatch) -> Basi
198222
# Set backend based on parameterized db_backend fixture
199223
if db_backend == "postgres":
200224
backend = DatabaseBackend.POSTGRES
201-
# Get URL from testcontainer and convert to asyncpg driver
202-
sync_url = postgres_container.get_connection_url()
225+
# Trigger: CI jobs can provide a shared Postgres service instead of per-session containers.
226+
# Why: reusing one pgvector-enabled server avoids Docker startup churn on every job.
227+
# Outcome: local runs keep using testcontainers, while CI injects a stable service URL.
228+
sync_url = _resolve_postgres_sync_url(postgres_container)
203229
database_url = sync_url.replace("postgresql+psycopg2", "postgresql+asyncpg")
204230
else:
205231
backend = DatabaseBackend.SQLITE
@@ -285,7 +311,7 @@ async def engine_factory(
285311
if db_backend == "postgres":
286312
# Postgres mode using testcontainers
287313
# Get async connection URL (asyncpg driver - same as production)
288-
sync_url = postgres_container.get_connection_url()
314+
sync_url = _resolve_postgres_sync_url(postgres_container)
289315
async_url = sync_url.replace("postgresql+psycopg2", "postgresql+asyncpg")
290316

291317
engine = create_async_engine(

0 commit comments

Comments
 (0)