Skip to content

Commit 7b83d7f

Browse files
committed
Use local Postgres service in GHA for integration tests
Replace the Neon-specific DATABASE_URL secret dependency with a Postgres 16 service container in the integration test job. This makes the CI self-contained — no external database or secret configuration is required. - Add `postgres` npm package as a standard PostgreSQL driver - Update lib/db/index.ts to detect Neon URLs and fall back to the postgres driver for standard PostgreSQL connections - Update tests/utils/db-reset.ts with the same dual-driver support - Replace the secrets-gated integration job with a Postgres service container, health checks, and a hardcoded local DATABASE_URL https://claude.ai/code/session_01FhzJavweKuMbx2d4U3ocDx
1 parent c529646 commit 7b83d7f

5 files changed

Lines changed: 122 additions & 35 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,47 +77,40 @@ jobs:
7777
test-integration:
7878
name: Integration Tests
7979
runs-on: ubuntu-latest
80-
# Requires a Neon-compatible DATABASE_URL secret.
81-
# Add it under GitHub → Settings → Secrets and variables → Actions.
82-
# The app uses @neondatabase/serverless (HTTP-based), so a standard local
83-
# Postgres container is not sufficient — use a real Neon test database.
80+
services:
81+
postgres:
82+
image: postgres:16
83+
env:
84+
POSTGRES_USER: postgres
85+
POSTGRES_PASSWORD: postgres
86+
POSTGRES_DB: clabot_test
87+
ports:
88+
- 5432:5432
89+
options: >-
90+
--health-cmd pg_isready
91+
--health-interval 10s
92+
--health-timeout 5s
93+
--health-retries 5
8494
env:
85-
DATABASE_URL: ${{ secrets.DATABASE_URL }}
86-
# SESSION_SECRET can be any string for test purposes
87-
SESSION_SECRET: ${{ secrets.SESSION_SECRET || 'ci-integration-test-secret' }}
95+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/clabot_test
96+
SESSION_SECRET: ci-integration-test-secret
8897
steps:
89-
- name: Check for required secrets
90-
id: check-secrets
91-
run: |
92-
if [ -z "$DATABASE_URL" ]; then
93-
echo "skip=true" >> "$GITHUB_OUTPUT"
94-
echo "::notice::Skipping integration tests — DATABASE_URL secret is not configured"
95-
else
96-
echo "skip=false" >> "$GITHUB_OUTPUT"
97-
fi
98-
9998
- uses: actions/checkout@v6
100-
if: steps.check-secrets.outputs.skip != 'true'
10199

102100
- uses: pnpm/action-setup@v4
103-
if: steps.check-secrets.outputs.skip != 'true'
104101
with:
105102
version: latest
106103

107104
- uses: actions/setup-node@v6
108-
if: steps.check-secrets.outputs.skip != 'true'
109105
with:
110106
node-version: "20"
111107
cache: "pnpm"
112108

113109
- name: Install dependencies
114-
if: steps.check-secrets.outputs.skip != 'true'
115110
run: pnpm install --frozen-lockfile
116111

117112
- name: Apply database migrations
118-
if: steps.check-secrets.outputs.skip != 'true'
119113
run: pnpm db:migrate
120114

121115
- name: Run integration tests
122-
if: steps.check-secrets.outputs.skip != 'true'
123116
run: pnpm test:integration

lib/db/index.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import "server-only"
22
import { neon } from "@neondatabase/serverless"
3-
import { drizzle } from "drizzle-orm/neon-http"
3+
import { drizzle as drizzleNeonHttp } from "drizzle-orm/neon-http"
4+
import postgres from "postgres"
5+
import { drizzle as drizzlePostgresJs } from "drizzle-orm/postgres-js"
46
import * as schema from "./schema"
57

68
// ── Types ──────────────────────────────────────────────────────────
79

8-
export type Database = ReturnType<typeof createNeonDb>
10+
// Tagged-template SQL function compatible with both neon() and postgres()
11+
type RawSqlFn = (
12+
strings: TemplateStringsArray,
13+
...values: unknown[]
14+
) => Promise<Record<string, unknown>[]>
915

10-
// ── Neon client ────────────────────────────────────────────────────
16+
export type Database = ReturnType<typeof createDb>
17+
18+
// ── Helpers ────────────────────────────────────────────────────────
19+
20+
/** Returns true when the URL points at a Neon serverless endpoint. */
21+
function isNeonUrl(url: string): boolean {
22+
return /neon\.tech|neondb\.net/.test(url)
23+
}
1124

1225
function getDatabaseUrl() {
1326
const databaseUrl = process.env.DATABASE_URL
@@ -17,9 +30,30 @@ function getDatabaseUrl() {
1730
return databaseUrl
1831
}
1932

20-
function createNeonDb() {
21-
const sql = neon(getDatabaseUrl())
22-
return drizzle({ client: sql, schema })
33+
// ── Raw SQL (for migration checks & reset) ─────────────────────────
34+
35+
let _pgClient: ReturnType<typeof postgres> | null = null
36+
37+
function createRawSql(url: string): RawSqlFn {
38+
if (isNeonUrl(url)) {
39+
return neon(url) as unknown as RawSqlFn
40+
}
41+
if (!_pgClient) {
42+
_pgClient = postgres(url)
43+
}
44+
return _pgClient as unknown as RawSqlFn
45+
}
46+
47+
// ── Drizzle instance ───────────────────────────────────────────────
48+
49+
function createDb() {
50+
const url = getDatabaseUrl()
51+
if (isNeonUrl(url)) {
52+
const sql = neon(url)
53+
return drizzleNeonHttp({ client: sql, schema })
54+
}
55+
_pgClient = postgres(url)
56+
return drizzlePostgresJs({ client: _pgClient, schema })
2357
}
2458

2559
// ── Singleton ──────────────────────────────────────────────────────
@@ -33,7 +67,7 @@ const globalForDb = globalThis as unknown as {
3367

3468
function getDb(): Database {
3569
if (!globalForDb.__db) {
36-
globalForDb.__db = createNeonDb()
70+
globalForDb.__db = createDb()
3771
// Kick off migration verification + optional seed immediately.
3872
globalForDb.__dbReady = initDb(globalForDb.__db).catch((err) => {
3973
console.error("[db] Init failed, will retry on next request:", err)
@@ -58,7 +92,7 @@ async function initDb(db: Database) {
5892
}
5993

6094
async function assertMigrationsApplied() {
61-
const sql = neon(getDatabaseUrl())
95+
const sql = createRawSql(getDatabaseUrl())
6296
const migrationsTable = process.env.DRIZZLE_MIGRATIONS_TABLE ?? "__drizzle_migrations"
6397
const migrationsSchema = process.env.DRIZZLE_MIGRATIONS_SCHEMA
6498
try {
@@ -127,7 +161,7 @@ export async function ensureDbReady(): Promise<Database> {
127161
* Full reset -- truncate all data and re-seed.
128162
*/
129163
export async function resetDb(): Promise<void> {
130-
const sql = neon(getDatabaseUrl())
164+
const sql = createRawSql(getDatabaseUrl())
131165
await sql`TRUNCATE audit_events, webhook_deliveries, org_cla_bypass_accounts, cla_signatures, cla_archives, organizations, users CASCADE`
132166
const { seedDatabase } = await import("./seed")
133167
await seedDatabase(db)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"lucide-react": "^0.544.0",
6464
"next": "16.1.6",
6565
"next-themes": "^0.4.6",
66+
"postgres": "^3.4.8",
6667
"react": "^19",
6768
"react-day-picker": "8.10.1",
6869
"react-dom": "^19",

0 commit comments

Comments
 (0)