403 lines
15 KiB
Markdown
403 lines
15 KiB
Markdown
---
|
|
phase: 03-postgresql-support
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: [03-01]
|
|
files_modified:
|
|
- cmd/diunwebhook/main.go
|
|
- pkg/diunwebhook/diunwebhook.go
|
|
- pkg/diunwebhook/postgres_test.go
|
|
- pkg/diunwebhook/export_test.go
|
|
- compose.yml
|
|
- compose.dev.yml
|
|
autonomous: true
|
|
requirements: [DB-01, DB-02, DB-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Setting DATABASE_URL starts the app using PostgreSQL; omitting it falls back to SQLite with DB_PATH"
|
|
- "Startup log clearly indicates which backend is active"
|
|
- "Docker Compose with --profile postgres activates a PostgreSQL service"
|
|
- "Default docker compose (no profile) remains SQLite-only"
|
|
- "Duplicate tag creation returns 409 on both SQLite and PostgreSQL"
|
|
artifacts:
|
|
- path: "cmd/diunwebhook/main.go"
|
|
provides: "DATABASE_URL branching logic"
|
|
contains: "DATABASE_URL"
|
|
- path: "compose.yml"
|
|
provides: "Production compose with postgres profile"
|
|
contains: "profiles:"
|
|
- path: "compose.dev.yml"
|
|
provides: "Dev compose with postgres profile"
|
|
contains: "profiles:"
|
|
- path: "pkg/diunwebhook/postgres_test.go"
|
|
provides: "Build-tagged PostgreSQL integration test helper"
|
|
contains: "go:build postgres"
|
|
- path: "pkg/diunwebhook/diunwebhook.go"
|
|
provides: "Case-insensitive UNIQUE constraint detection"
|
|
contains: "strings.ToLower"
|
|
key_links:
|
|
- from: "cmd/diunwebhook/main.go"
|
|
to: "pkg/diunwebhook/postgres_store.go"
|
|
via: "diun.NewPostgresStore(db)"
|
|
pattern: "NewPostgresStore"
|
|
- from: "cmd/diunwebhook/main.go"
|
|
to: "pkg/diunwebhook/migrate.go"
|
|
via: "diun.RunPostgresMigrations(db)"
|
|
pattern: "RunPostgresMigrations"
|
|
- from: "cmd/diunwebhook/main.go"
|
|
to: "pgx/v5/stdlib"
|
|
via: "blank import for driver registration"
|
|
pattern: '_ "github.com/jackc/pgx/v5/stdlib"'
|
|
---
|
|
|
|
<objective>
|
|
Wire PostgresStore into the application and deployment infrastructure.
|
|
|
|
Purpose: Connects the PostgresStore (built in Plan 01) to the startup path, adds Docker Compose profiles for PostgreSQL deployments, creates build-tagged integration test helpers, and fixes the UNIQUE constraint detection to work across both database backends.
|
|
Output: Updated main.go with DATABASE_URL branching, compose files with postgres profiles, build-tagged test helper, cross-dialect error handling fix.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/03-postgresql-support/03-CONTEXT.md
|
|
@.planning/phases/03-postgresql-support/03-RESEARCH.md
|
|
@.planning/phases/03-postgresql-support/03-01-SUMMARY.md
|
|
|
|
@cmd/diunwebhook/main.go
|
|
@pkg/diunwebhook/diunwebhook.go
|
|
@pkg/diunwebhook/export_test.go
|
|
@compose.yml
|
|
@compose.dev.yml
|
|
|
|
<interfaces>
|
|
<!-- From Plan 01 outputs -->
|
|
From pkg/diunwebhook/postgres_store.go:
|
|
```go
|
|
func NewPostgresStore(db *sql.DB) *PostgresStore
|
|
```
|
|
|
|
From pkg/diunwebhook/migrate.go:
|
|
```go
|
|
func RunSQLiteMigrations(db *sql.DB) error
|
|
func RunPostgresMigrations(db *sql.DB) error
|
|
```
|
|
|
|
From pkg/diunwebhook/store.go:
|
|
```go
|
|
type Store interface { ... } // 9 methods
|
|
```
|
|
|
|
From pkg/diunwebhook/diunwebhook.go:
|
|
```go
|
|
func NewServer(store Store, webhookSecret string) *Server
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Wire DATABASE_URL branching in main.go and fix cross-dialect UNIQUE detection</name>
|
|
<read_first>
|
|
- cmd/diunwebhook/main.go (current SQLite-only startup to add branching)
|
|
- pkg/diunwebhook/diunwebhook.go (TagsHandler line 172 - UNIQUE detection to fix)
|
|
- pkg/diunwebhook/postgres_store.go (verify NewPostgresStore exists from Plan 01)
|
|
- pkg/diunwebhook/migrate.go (verify RunSQLiteMigrations and RunPostgresMigrations exist from Plan 01)
|
|
</read_first>
|
|
<files>cmd/diunwebhook/main.go, pkg/diunwebhook/diunwebhook.go</files>
|
|
<action>
|
|
**1. Update `cmd/diunwebhook/main.go`** to branch on `DATABASE_URL` per D-07, D-08, D-09.
|
|
|
|
Replace the current database setup block (lines 18-33) with DATABASE_URL branching. The full main function should:
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
diun "awesomeProject/pkg/diunwebhook"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func main() {
|
|
databaseURL := os.Getenv("DATABASE_URL")
|
|
var store diun.Store
|
|
if databaseURL != "" {
|
|
db, err := sql.Open("pgx", databaseURL)
|
|
if err != nil {
|
|
log.Fatalf("sql.Open postgres: %v", err)
|
|
}
|
|
if err := diun.RunPostgresMigrations(db); err != nil {
|
|
log.Fatalf("RunPostgresMigrations: %v", err)
|
|
}
|
|
store = diun.NewPostgresStore(db)
|
|
log.Println("Using PostgreSQL database")
|
|
} else {
|
|
dbPath := os.Getenv("DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "./diun.db"
|
|
}
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
log.Fatalf("sql.Open sqlite: %v", err)
|
|
}
|
|
if err := diun.RunSQLiteMigrations(db); err != nil {
|
|
log.Fatalf("RunSQLiteMigrations: %v", err)
|
|
}
|
|
store = diun.NewSQLiteStore(db)
|
|
log.Printf("Using SQLite database at %s", dbPath)
|
|
}
|
|
|
|
// ... rest of main unchanged (secret, server, mux, httpSrv, graceful shutdown)
|
|
}
|
|
```
|
|
|
|
Key changes:
|
|
- Add blank import `_ "github.com/jackc/pgx/v5/stdlib"` to register "pgx" driver name
|
|
- `DATABASE_URL` present -> `sql.Open("pgx", databaseURL)` -> `RunPostgresMigrations` -> `NewPostgresStore`
|
|
- `DATABASE_URL` absent -> existing SQLite path with `RunSQLiteMigrations` (renamed in Plan 01)
|
|
- Log `"Using PostgreSQL database"` or `"Using SQLite database at %s"` per D-09
|
|
- Keep all existing code after the store setup unchanged (secret, server, mux, httpSrv, shutdown)
|
|
|
|
**2. Fix cross-dialect UNIQUE constraint detection in `pkg/diunwebhook/diunwebhook.go`.**
|
|
|
|
In the `TagsHandler` method, line 172, change:
|
|
```go
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
```
|
|
to:
|
|
```go
|
|
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
|
```
|
|
|
|
Why: SQLite errors contain uppercase "UNIQUE" (e.g., `UNIQUE constraint failed: tags.name`). PostgreSQL/pgx errors contain lowercase "unique" (e.g., `duplicate key value violates unique constraint "tags_name_key"`). Case-insensitive matching ensures 409 Conflict is returned for both backends.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./... && go test -v -count=1 ./pkg/diunwebhook/ 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- cmd/diunwebhook/main.go contains `databaseURL := os.Getenv("DATABASE_URL")`
|
|
- cmd/diunwebhook/main.go contains `sql.Open("pgx", databaseURL)`
|
|
- cmd/diunwebhook/main.go contains `diun.RunPostgresMigrations(db)`
|
|
- cmd/diunwebhook/main.go contains `diun.NewPostgresStore(db)`
|
|
- cmd/diunwebhook/main.go contains `log.Println("Using PostgreSQL database")`
|
|
- cmd/diunwebhook/main.go contains `log.Printf("Using SQLite database at %s", dbPath)`
|
|
- cmd/diunwebhook/main.go contains `_ "github.com/jackc/pgx/v5/stdlib"`
|
|
- cmd/diunwebhook/main.go contains `diun.RunSQLiteMigrations(db)` (not RunMigrations)
|
|
- pkg/diunwebhook/diunwebhook.go contains `strings.Contains(strings.ToLower(err.Error()), "unique")`
|
|
- pkg/diunwebhook/diunwebhook.go does NOT contain `strings.Contains(err.Error(), "UNIQUE")` (old pattern removed)
|
|
- `go build ./...` exits 0
|
|
- `go test -count=1 ./pkg/diunwebhook/` exits 0
|
|
</acceptance_criteria>
|
|
<done>main.go branches on DATABASE_URL to select PostgreSQL or SQLite. pgx/v5/stdlib is blank-imported to register the driver. Startup log identifies the active backend. UNIQUE detection is case-insensitive for cross-dialect compatibility. All existing tests pass.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add Docker Compose postgres profiles and build-tagged test helper</name>
|
|
<read_first>
|
|
- compose.yml (current production compose to add postgres profile)
|
|
- compose.dev.yml (current dev compose to add postgres profile)
|
|
- pkg/diunwebhook/export_test.go (pattern for NewTestPostgresServer)
|
|
- Dockerfile (verify no changes needed -- pgx/v5 is pure Go, CGO_ENABLED=0 is fine)
|
|
</read_first>
|
|
<files>compose.yml, compose.dev.yml, pkg/diunwebhook/postgres_test.go</files>
|
|
<action>
|
|
**1. Update `compose.yml`** (production) to add postgres profile per D-14, D-15, D-16:
|
|
|
|
```yaml
|
|
# Minimum Docker Compose v2.20 required for depends_on.required
|
|
services:
|
|
app:
|
|
image: gitea.jeanlucmakiola.de/makiolaj/diundashboard:latest
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
- WEBHOOK_SECRET=${WEBHOOK_SECRET:-}
|
|
- PORT=${PORT:-8080}
|
|
- DB_PATH=/data/diun.db
|
|
- DATABASE_URL=${DATABASE_URL:-}
|
|
volumes:
|
|
- diun-data:/data
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
required: false
|
|
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
profiles:
|
|
- postgres
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-diun}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-diun}
|
|
POSTGRES_DB: ${POSTGRES_DB:-diundashboard}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-diun}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
diun-data:
|
|
postgres-data:
|
|
```
|
|
|
|
Default `docker compose up` still uses SQLite (DATABASE_URL is empty string, app falls back to DB_PATH).
|
|
`docker compose --profile postgres up` starts the postgres service; user sets `DATABASE_URL=postgres://diun:diun@postgres:5432/diundashboard?sslmode=disable` in .env.
|
|
|
|
**2. Update `compose.dev.yml`** to add postgres profile for local development:
|
|
|
|
```yaml
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
- WEBHOOK_SECRET=${WEBHOOK_SECRET:-}
|
|
- DATABASE_URL=${DATABASE_URL:-}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
required: false
|
|
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
profiles:
|
|
- postgres
|
|
ports:
|
|
- "5432:5432"
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-diun}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-diun}
|
|
POSTGRES_DB: ${POSTGRES_DB:-diundashboard}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-diun}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres-data:
|
|
```
|
|
|
|
Dev compose exposes port 5432 on host for direct psql access during development.
|
|
|
|
**3. Create `pkg/diunwebhook/postgres_test.go`** with build tag per D-17, D-19:
|
|
|
|
```go
|
|
//go:build postgres
|
|
|
|
package diunwebhook
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
)
|
|
|
|
// NewTestPostgresServer constructs a Server backed by a PostgreSQL database.
|
|
// Requires a running PostgreSQL instance. Set TEST_DATABASE_URL to override
|
|
// the default connection string.
|
|
func NewTestPostgresServer() (*Server, error) {
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
|
if databaseURL == "" {
|
|
databaseURL = "postgres://diun:diun@localhost:5432/diundashboard_test?sslmode=disable"
|
|
}
|
|
db, err := sql.Open("pgx", databaseURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := RunPostgresMigrations(db); err != nil {
|
|
return nil, err
|
|
}
|
|
store := NewPostgresStore(db)
|
|
return NewServer(store, ""), nil
|
|
}
|
|
```
|
|
|
|
This file is in the `diunwebhook` package (internal, same as export_test.go pattern). The `//go:build postgres` tag ensures it only compiles when explicitly requested with `go test -tags postgres`. Without the tag, `go test ./pkg/diunwebhook/` skips this file entirely -- no pgx import, no PostgreSQL dependency.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./... && docker compose config --quiet 2>&1; echo "exit: $?"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- compose.yml contains `profiles:` under the postgres service
|
|
- compose.yml contains `- postgres` (profile name)
|
|
- compose.yml contains `postgres:17-alpine`
|
|
- compose.yml contains `pg_isready`
|
|
- compose.yml contains `required: false` (conditional depends_on)
|
|
- compose.yml contains `DATABASE_URL=${DATABASE_URL:-}`
|
|
- compose.yml contains `postgres-data:` in volumes
|
|
- compose.dev.yml contains `profiles:` under the postgres service
|
|
- compose.dev.yml contains `- postgres` (profile name)
|
|
- compose.dev.yml contains `"5432:5432"` (exposed for dev)
|
|
- compose.dev.yml contains `required: false`
|
|
- compose.dev.yml contains `DATABASE_URL=${DATABASE_URL:-}`
|
|
- pkg/diunwebhook/postgres_test.go contains `//go:build postgres`
|
|
- pkg/diunwebhook/postgres_test.go contains `func NewTestPostgresServer()`
|
|
- pkg/diunwebhook/postgres_test.go contains `sql.Open("pgx", databaseURL)`
|
|
- pkg/diunwebhook/postgres_test.go contains `RunPostgresMigrations(db)`
|
|
- pkg/diunwebhook/postgres_test.go contains `NewPostgresStore(db)`
|
|
- pkg/diunwebhook/postgres_test.go contains `TEST_DATABASE_URL`
|
|
- `go build ./...` exits 0 (postgres_test.go is not compiled without build tag)
|
|
- `go test -count=1 ./pkg/diunwebhook/` exits 0 (SQLite tests still pass, postgres_test.go skipped)
|
|
</acceptance_criteria>
|
|
<done>Docker Compose files support optional PostgreSQL via profiles. Default deploy remains SQLite-only. Build-tagged test helper exists for PostgreSQL integration testing. Dockerfile needs no changes (pgx/v5 is pure Go).</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `go build ./...` succeeds
|
|
2. `go test -v -count=1 ./pkg/diunwebhook/` passes (all existing SQLite tests)
|
|
3. `docker compose config` validates without errors
|
|
4. `docker compose --profile postgres config` shows postgres service
|
|
5. `grep -c "DATABASE_URL" cmd/diunwebhook/main.go` returns at least 1
|
|
6. `grep "strings.ToLower" pkg/diunwebhook/diunwebhook.go` shows case-insensitive UNIQUE check
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- DATABASE_URL present: app opens pgx connection, runs PostgreSQL migrations, creates PostgresStore, logs "Using PostgreSQL database"
|
|
- DATABASE_URL absent: app opens sqlite connection, runs SQLite migrations, creates SQLiteStore, logs "Using SQLite database at {path}"
|
|
- `docker compose up` (no profile) works with SQLite only
|
|
- `docker compose --profile postgres up` starts PostgreSQL service with health check
|
|
- Build-tagged test helper available for PostgreSQL integration tests
|
|
- UNIQUE constraint detection works for both SQLite and PostgreSQL error messages
|
|
- All existing SQLite tests continue to pass
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-postgresql-support/03-02-SUMMARY.md`
|
|
</output>
|