From c3bf6d7e0d9936cdd5054d48fea7424e41e9f31b Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 11 Jun 2026 15:30:22 -0700 Subject: [PATCH] feat: add auto db migration in binary --- .build/Dockerfile | 9 +-- .build/docker-entrypoint.sh | 30 ------- cmd/beacon/main.go | 4 + db/migrate.go | 78 +++++++++++++++++++ ...{001_schema.sql => 001_initial_schema.sql} | 0 5 files changed, 85 insertions(+), 36 deletions(-) delete mode 100644 .build/docker-entrypoint.sh create mode 100644 db/migrate.go rename db/migrations/{001_schema.sql => 001_initial_schema.sql} (100%) diff --git a/.build/Dockerfile b/.build/Dockerfile index 291092f..19e41ba 100644 --- a/.build/Dockerfile +++ b/.build/Dockerfile @@ -1,5 +1,5 @@ # Copyright 2026 Beacon Contributors -# SPDX-License-Identifier: agpl +# SPDX-License-Identifier: AGPL-3.0-or-later FROM golang:1.26-alpine AS builder WORKDIR /app @@ -9,10 +9,7 @@ COPY . . RUN go build -o beacon ./cmd/beacon FROM alpine:3.19 -RUN apk add --no-cache ca-certificates tzdata postgresql-client +RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/beacon . -COPY db/migrations/ ./migrations/ -COPY .build/docker-entrypoint.sh . -RUN chmod +x docker-entrypoint.sh -ENTRYPOINT ["./docker-entrypoint.sh"] +CMD ["./beacon"] diff --git a/.build/docker-entrypoint.sh b/.build/docker-entrypoint.sh deleted file mode 100644 index 235ee9e..0000000 --- a/.build/docker-entrypoint.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# Copyright 2026 Beacon Contributors -# SPDX-License-Identifier: agpl - -set -e - -if [ -n "$POSTGRES_DSN" ]; then - echo "Running database migrations..." - - psql "$POSTGRES_DSN" -c "CREATE TABLE IF NOT EXISTS schema_migrations ( - filename TEXT PRIMARY KEY, - applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - );" - - for f in /app/migrations/*.sql; do - name=$(basename "$f") - already=$(psql "$POSTGRES_DSN" -tAc "SELECT 1 FROM schema_migrations WHERE filename = '$name'") - if [ "$already" != "1" ]; then - echo " Applying $name..." - psql "$POSTGRES_DSN" -f "$f" - psql "$POSTGRES_DSN" -c "INSERT INTO schema_migrations (filename) VALUES ('$name');" - else - echo " Skipping $name (already applied)" - fi - done - - echo "Migrations complete." -fi - -exec ./beacon "$@" diff --git a/cmd/beacon/main.go b/cmd/beacon/main.go index b3eada1..783586a 100644 --- a/cmd/beacon/main.go +++ b/cmd/beacon/main.go @@ -114,6 +114,10 @@ func main() { } defer pool.Close() + if err := db.RunMigrations(ctx, pool); err != nil { + log.Fatalf("migrations failed: %v", err) + } + store := db.New(pool) // ── Redis cache layer ──────────────────────────────────────────────────── diff --git a/db/migrate.go b/db/migrate.go new file mode 100644 index 0000000..4852296 --- /dev/null +++ b/db/migrate.go @@ -0,0 +1,78 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "context" + "embed" + "fmt" + "io/fs" + "sort" + "strings" + + "github.com/jackc/pgx/v5/pgxpool" +) + +//go:embed migrations/*.sql +var migrationFiles embed.FS + +func RunMigrations(ctx context.Context, pool *pgxpool.Pool) error { + _, err := pool.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS schema_migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + `) + if err != nil { + return fmt.Errorf("failed to create schema_migrations: %w", err) + } + + entries, err := fs.ReadDir(migrationFiles, "migrations") + if err != nil { + return fmt.Errorf("failed to read migrations: %w", err) + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name() < entries[j].Name() + }) + + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".sql") { + continue + } + var already bool + err := pool.QueryRow( + ctx, + "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE filename = $1)", + entry.Name(), + ).Scan(&already) + if err != nil { + return fmt.Errorf("failed to check migration %s: %w", entry.Name(), err) + } + if already { + continue + } + + sql, err := migrationFiles.ReadFile("migrations/" + entry.Name()) + if err != nil { + return fmt.Errorf("failed to read migration %s: %w", entry.Name(), err) + } + + if _, err := pool.Exec(ctx, string(sql)); err != nil { + return fmt.Errorf("failed to apply migration %s: %w", entry.Name(), err) + } + + if _, err := pool.Exec( + ctx, + "INSERT INTO schema_migrations (filename) VALUES ($1)", + entry.Name(), + ); err != nil { + return fmt.Errorf("failed to record migration %s: %w", entry.Name(), err) + } + + fmt.Printf("applied migration: %s\n", entry.Name()) + } + + return nil +} diff --git a/db/migrations/001_schema.sql b/db/migrations/001_initial_schema.sql similarity index 100% rename from db/migrations/001_schema.sql rename to db/migrations/001_initial_schema.sql