// migrate — standalone CLI that applies tenant-registry's SQL migrations. // // Usage: // // migrate up apply all pending migrations // migrate down roll back the most recent migration // migrate down --all roll back every migration (DESTRUCTIVE) // migrate version print the current schema version // migrate force mark a specific version applied (recovery) // // Reads DATABASE_URL from the environment. Migrations are embedded so this // binary is self-contained — ship it as an Orca init container in prod. package main import ( "context" "errors" "flag" "fmt" "log/slog" "os" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" "gitea.meghsakha.com/platform/tenant-registry/migrations" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) slog.SetDefault(logger) allDown := flag.Bool("all", false, "with 'down', roll back every migration") flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s >\n", os.Args[0]) flag.PrintDefaults() } flag.Parse() args := flag.Args() if len(args) < 1 { flag.Usage() os.Exit(2) } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { slog.Error("DATABASE_URL not set") os.Exit(1) } if err := run(context.Background(), args, dbURL, *allDown); err != nil { slog.Error("migrate failed", "err", err) os.Exit(1) } } func run(ctx context.Context, args []string, dbURL string, allDown bool) error { src, err := iofs.New(migrations.FS, ".") if err != nil { return fmt.Errorf("load embedded migrations: %w", err) } m, err := migrate.NewWithSourceInstance("iofs", src, dbURL) if err != nil { return fmt.Errorf("open migrate: %w", err) } defer func() { if srcErr, dbErr := m.Close(); srcErr != nil || dbErr != nil { slog.Warn("close error", "src_err", srcErr, "db_err", dbErr) } }() cmd := args[0] switch cmd { case "up": err = m.Up() if errors.Is(err, migrate.ErrNoChange) { slog.Info("no pending migrations") return nil } if err != nil { return fmt.Errorf("up: %w", err) } v, dirty, _ := m.Version() slog.Info("migrate up complete", "version", v, "dirty", dirty) return nil case "down": if allDown { if err := m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return fmt.Errorf("down all: %w", err) } slog.Info("migrate down --all complete (schema empty)") return nil } if err := m.Steps(-1); err != nil { if errors.Is(err, migrate.ErrNoChange) { slog.Info("no migrations to roll back") return nil } return fmt.Errorf("down 1: %w", err) } v, dirty, _ := m.Version() slog.Info("migrate down 1 complete", "version", v, "dirty", dirty) return nil case "version": v, dirty, err := m.Version() if errors.Is(err, migrate.ErrNilVersion) { slog.Info("no migrations applied") return nil } if err != nil { return fmt.Errorf("version: %w", err) } slog.Info("schema version", "version", v, "dirty", dirty) return nil case "force": if len(args) != 2 { return errors.New("usage: migrate force ") } var n int if _, err := fmt.Sscanf(args[1], "%d", &n); err != nil { return fmt.Errorf("invalid version %q", args[1]) } if err := m.Force(n); err != nil { return fmt.Errorf("force: %w", err) } slog.Info("forced version", "version", n) return nil default: return fmt.Errorf("unknown command %q", cmd) } }