Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
24 changes: 21 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ API_KEY=your-secret-api-key-here
# Format: postgres://username:password@host:port/database?sslmode=disable
DATABASE_URL=postgres://postgres:postgres@localhost:5432/test_db?sslmode=disable

# Database connection pool (optional). For multi-instance: instances * DATABASE_MAX_CONNS must be below Postgres max_connections.
# DATABASE_MAX_CONNS=25
# DATABASE_MIN_CONNS=0
# DATABASE_MAX_CONN_LIFETIME_SECONDS=3600
# DATABASE_MAX_CONN_IDLE_TIME_SECONDS=1800
# DATABASE_HEALTH_CHECK_PERIOD_SECONDS=60
# DATABASE_CONNECT_TIMEOUT_SECONDS=10

# HTTP server port (optional)
# Default: 8080
PORT=8080
Expand All @@ -23,9 +31,6 @@ PORT=8080
# Valid values: debug, info, warn, error
LOG_LEVEL=info

# Max request body size in bytes (optional). Default: 10485760 (10 MiB). Requests exceeding this return 413.
# MAX_REQUEST_BODY_BYTES=10485760

# Message publisher: event channel buffer size (optional). Default: 1024
MESSAGE_PUBLISHER_QUEUE_MAX_SIZE=16384

Expand All @@ -43,6 +48,19 @@ WEBHOOK_MAX_FAN_OUT_PER_EVENT=500
# Max total webhooks allowed; creation returns 403 Forbidden when limit reached. Default: 500
WEBHOOK_MAX_COUNT=500

# Webhook HTTP timeout (optional). Timeout for each delivery POST; job timeout = this + 5s. Default: 15
# WEBHOOK_HTTP_TIMEOUT_SECONDS=15

# Webhook URL blacklist (optional). Comma-separated hosts/IPs that cannot be used as webhook endpoints (SSRF mitigation).
# Default: localhost,127.0.0.1,::1,169.254.169.254 (includes AWS metadata endpoint)
# WEBHOOK_BLACKLIST=localhost,127.0.0.1,::1,169.254.169.254

# Webhook enqueue retries (optional). When River InsertMany fails, retry with exponential backoff + jitter.
# Defaults: 3 retries, 100ms initial backoff, 2s max backoff.
# WEBHOOK_ENQUEUE_MAX_RETRIES=3
# WEBHOOK_ENQUEUE_INITIAL_BACKOFF_MS=100
# WEBHOOK_ENQUEUE_MAX_BACKOFF_MS=2000

# Embeddings are optional. To enable, set both EMBEDDING_PROVIDER and EMBEDDING_MODEL; if either is unset, embeddings are disabled and no embedding jobs run.
# EMBEDDING_PROVIDER_API_KEY is required for openai and google.
# Vector size is fixed (768) in code.
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/api-contract-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.1'

- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
Expand All @@ -69,7 +69,7 @@ jobs:
run: go mod download

- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
run: go install github.com/pressly/goose/v3/cmd/goose@v3.27.0

- name: Validate migrations
run: make migrate-validate
Expand All @@ -79,7 +79,7 @@ jobs:

- name: Run River migrations
run: |
go install github.com/riverqueue/river/cmd/river@latest
go install github.com/riverqueue/river/cmd/river@v0.31.0
make river-migrate

- name: Build application
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ jobs:
run: npx --yes @stoplight/spectral-cli@6 lint openapi.yaml

- name: Set up Go
id: setup-go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.0'

- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
Expand All @@ -59,21 +60,21 @@ jobs:
go mod tidy
git diff --exit-code go.mod go.sum || (echo "Error: go.mod or go.sum are out of sync. Run 'go mod tidy' and commit the changes." && exit 1)

# Pin tool versions for reproducible builds
# Update these versions periodically and bump the cache key
# Pin tool versions for reproducible builds (must match Makefile)
# Cache key includes Go version so tools are rebuilt when Go upgrades
- name: Cache development tools
id: cache-tools
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/go/bin/golangci-lint
~/go/bin/govulncheck
key: ${{ runner.os }}-dev-tools-lint2.8.0-vuln1.1.4
key: ${{ runner.os }}-go${{ steps.setup-go.outputs.go-version }}-lint2.10.1-vuln1.1.4

- name: Install development tools
if: steps.cache-tools.outputs.cache-hit != 'true'
run: |
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1
go install golang.org/x/vuln/cmd/govulncheck@v1.1.4

- name: Format check (make fmt)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/migrations-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.1'

- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
run: go install github.com/pressly/goose/v3/cmd/goose@v3.27.0

- name: Validate migrations
run: make migrate-validate
14 changes: 7 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.1'

- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.1'

- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
Expand All @@ -96,7 +96,7 @@ jobs:
run: go mod download

- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
run: go install github.com/pressly/goose/v3/cmd/goose@v3.27.0

- name: Validate migrations
run: make migrate-validate
Expand All @@ -106,7 +106,7 @@ jobs:

- name: Run River migrations
run: |
go install github.com/riverqueue/river/cmd/river@v0.30.2
go install github.com/riverqueue/river/cmd/river@v0.31.0
make river-migrate

- name: Run integration tests
Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version: '1.25.7'
go-version: '1.26.1'

- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
Expand All @@ -159,14 +159,14 @@ jobs:
run: go mod download

- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@v3.26.0
run: go install github.com/pressly/goose/v3/cmd/goose@v3.27.0

- name: Initialize database schema
run: make init-db

- name: Run River migrations
run: |
go install github.com/riverqueue/river/cmd/river@v0.30.2
go install github.com/riverqueue/river/cmd/river@v0.31.0
make river-migrate

- name: Check coverage (excludes cmd/api)
Expand Down
11 changes: 10 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ run:
timeout: 5m
concurrency: 4
modules-download-mode: readonly
go: "1.25"
go: "1.26.1"
# Allow linting test files but suppress specific strict rules via exclusion below
tests: true

Expand Down Expand Up @@ -43,6 +43,15 @@ linters:
linters:
- nilnil # mocks often return (nil, nil) for success
- err113 # mocks and test helpers use dynamic errors
# gosec false positives in tests only: G706 log injection, G704 SSRF (localhost), G101 test DB credential
- path: _test\.go$
linters:
- gosec
text: "G70[46]|G101"
- path: tests/
linters:
- gosec
text: "G70[46]|G101"
settings:
lll:
line-length: 140 # consider 120–140 for new code
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Stage 1: Build
# =============================================================================
# TARGETOS/TARGETARCH are set by Docker Buildx for multi-platform builds (e.g. linux/arm64 on Mac M1).
FROM golang:1.25.7-alpine AS builder
FROM golang:1.26.1-alpine AS builder
ARG TARGETOS=linux
ARG TARGETARCH

Expand All @@ -11,8 +11,8 @@ RUN apk add --no-cache git ca-certificates
WORKDIR /build

# Install goose and river CLI for migrations (for target platform)
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go install github.com/pressly/goose/v3/cmd/goose@v3.26.0 && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go install github.com/riverqueue/river/cmd/river@v0.30.2
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go install github.com/pressly/goose/v3/cmd/goose@v3.27.0 && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go install github.com/riverqueue/river/cmd/river@v0.31.0

# Cache dependencies
COPY go.mod go.sum ./
Expand Down
12 changes: 6 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ lint-openapi:
# Loads .env if present so DATABASE_URL (e.g. port 5433) is used when Postgres runs on a non-default port.
tests:
@echo "Running integration tests..."
@(set -a && [ -f .env ] && . ./.env && set +a; go test ./tests/... -v)
@(set -a && [ -f .env ] && . ./.env && set +a; go test ./tests/... -v -timeout 120s)

# Run unit tests (fast, no database required)
test-unit:
Expand All @@ -70,7 +70,7 @@ COVERAGE_THRESHOLD ?= 15
check-coverage:
@echo "Running tests with coverage (threshold: $(COVERAGE_THRESHOLD)%)..."
@(set -a && [ -f .env ] && . ./.env && set +a; go test ./internal/... ./pkg/... ./tests/... -coverprofile=coverage.out)
@COV=$$(go tool cover -func=coverage.out | tail -1 | awk '{gsub(/%/, ""); print $$3}') && \
@COV=$$(go tool cover -func=coverage.out | \tail -1 | awk '{gsub(/%/, ""); print $$3}') && \
if [ -z "$$COV" ] || ! awk -v c="$$COV" -v t="$(COVERAGE_THRESHOLD)" 'BEGIN { exit (c+0 >= t) ? 0 : 1 }'; then \
echo ""; \
echo "❌ Coverage $$COV% is below threshold $(COVERAGE_THRESHOLD)%"; \
Expand Down Expand Up @@ -155,7 +155,7 @@ migrate-validate:

# Run River job queue migrations (required for webhook delivery)
river-migrate:
@command -v river >/dev/null 2>&1 || { echo "Error: river CLI not found. Install with: go install github.com/riverqueue/river/cmd/river@latest"; exit 1; }
@command -v river >/dev/null 2>&1 || { echo "Error: river CLI not found. Install with: make install-tools or go install github.com/riverqueue/river/cmd/river@$(RIVER_VERSION)"; exit 1; }
@if [ -f .env ]; then \
export $$(grep -v '^#' .env | xargs) && \
if [ -z "$$DATABASE_URL" ]; then echo "Error: DATABASE_URL not found in .env"; exit 1; fi && \
Expand Down Expand Up @@ -200,10 +200,10 @@ deps:

# Install development tools
# Tool versions - update these periodically
GOLANGCI_LINT_VERSION := v2.8.0
GOLANGCI_LINT_VERSION := v2.10.1
GOVULNCHECK_VERSION := v1.1.4
GOOSE_VERSION := v3.26.0
RIVER_VERSION := v0.30.2
GOOSE_VERSION := v3.27.0
RIVER_VERSION := v0.31.0
# Use pinned path so lint uses the version from make install-tools, not PATH
GOLANGCI_LINT ?= $(HOME)/go/bin/golangci-lint

Expand Down
4 changes: 2 additions & 2 deletions cmd/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func NewApp(cfg *config.Config, db *pgxpool.Pool) (*App, error) {
messageManager := service.NewMessagePublisherManager(cfg.MessagePublisherBufferSize, cfg.MessagePublisherPerEventTimeout, eventMetrics)

webhooksRepo := repository.NewWebhooksRepository(db)
webhookSender := service.NewWebhookSenderImpl(webhooksRepo, webhookMetrics)
webhookSender := service.NewWebhookSenderImpl(webhooksRepo, webhookMetrics, cfg.WebhookURLBlacklist, nil)
webhookWorker := workers.NewWebhookDispatchWorker(webhooksRepo, webhookSender, webhookMetrics)
riverWorkers := river.NewWorkers()
river.AddWorker(riverWorkers, webhookWorker)
Expand Down Expand Up @@ -303,7 +303,7 @@ func NewApp(cfg *config.Config, db *pgxpool.Pool) (*App, error) {
messageManager.RegisterProvider(embeddingProv)
}

webhooksService := service.NewWebhooksService(webhooksRepo, messageManager, cfg.WebhookMaxCount)
webhooksService := service.NewWebhooksService(webhooksRepo, messageManager, cfg.WebhookMaxCount, cfg.WebhookURLBlacklist)
webhooksHandler := handlers.NewWebhooksHandler(webhooksService)

feedbackRecordsHandler := handlers.NewFeedbackRecordsHandler(feedbackRecordsService)
Expand Down
12 changes: 11 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@ func run() int {

ctx := context.Background()

db, err := database.NewPostgresPool(ctx, cfg.DatabaseURL, database.WithAfterConnect(pgxvec.RegisterTypes))
db, err := database.NewPostgresPool(ctx, cfg.DatabaseURL,
database.WithPoolConfig(database.PoolConfig{
MaxConns: cfg.DatabaseMaxConns,
MinConns: cfg.DatabaseMinConns,
MaxConnLifetime: cfg.DatabaseMaxConnLifetime,
MaxConnIdleTime: cfg.DatabaseMaxConnIdleTime,
HealthCheckPeriod: cfg.DatabaseHealthCheckPeriod,
ConnectTimeout: cfg.DatabaseConnectTimeout,
}),
database.WithAfterConnect(pgxvec.RegisterTypes),
)
if err != nil {
slog.Error("Failed to connect to database", "error", err)

Expand Down
2 changes: 1 addition & 1 deletion cmd/backfill-embeddings/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func run() int {
return exitFailure
}

slog.Info("Backfill complete", "enqueued", enqueued)
slog.Info("Backfill complete", "enqueued", enqueued) // #nosec G706 -- enqueued is an int, not user input

fmt.Printf("Enqueued %d embedding job(s).\n", enqueued)

Expand Down
Loading
Loading