feat: Introduce a new upgrade command and enhance built-in tool settings with provider and model configuration.

This commit is contained in:
viettranx committed 2026-02-27 11:38:04 +07:00
1 parent d65d792646
commit 86d58e1021
23 files changed
+1195 -170

No files matched your search

+132 -16
View File
@@ -1,15 +1,19 @@
package cmd
import (
"context"
"database/sql"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/spf13/cobra"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/upgrade"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
@@ -25,7 +29,7 @@ func doctorCmd() *cobra.Command {
func runDoctor() {
fmt.Println("goclaw doctor")
fmt.Printf(" Version: 0.2.0 (protocol %d)\n", protocol.ProtocolVersion)
fmt.Printf(" Version: %s (protocol %d)\n", Version, protocol.ProtocolVersion)
fmt.Printf(" OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Printf(" Go: %s\n", runtime.Version())
fmt.Println()
@@ -45,26 +49,78 @@ func runDoctor() {
return
}
// Providers
// Database (managed mode only) — open early so we can show DB providers.
var db *sql.DB
isManaged := cfg.Database.Mode == "managed" && cfg.Database.PostgresDSN != ""
if isManaged {
fmt.Println()
fmt.Println(" Database:")
fmt.Printf(" %-12s managed\n", "Mode:")
var dbErr error
db, dbErr = sql.Open("pgx", cfg.Database.PostgresDSN)
if dbErr != nil {
fmt.Printf(" %-12s CONNECT FAILED (%s)\n", "Status:", dbErr)
db = nil
} else if pingErr := db.Ping(); pingErr != nil {
fmt.Printf(" %-12s CONNECT FAILED (%s)\n", "Status:", pingErr)
db.Close()
db = nil
} else {
defer db.Close()
s, schemaErr := upgrade.CheckSchema(db)
if schemaErr != nil {
fmt.Printf(" %-12s CHECK FAILED (%s)\n", "Schema:", schemaErr)
} else if s.Dirty {
fmt.Printf(" %-12s v%d (DIRTY — run: goclaw migrate force %d)\n", "Schema:", s.CurrentVersion, s.CurrentVersion-1)
} else if s.Compatible {
fmt.Printf(" %-12s v%d (up to date)\n", "Schema:", s.CurrentVersion)
} else if s.CurrentVersion > s.RequiredVersion {
fmt.Printf(" %-12s v%d (binary too old, requires v%d)\n", "Schema:", s.CurrentVersion, s.RequiredVersion)
} else {
fmt.Printf(" %-12s v%d (upgrade needed — run: goclaw upgrade)\n", "Schema:", s.CurrentVersion)
}
pending, hookErr := upgrade.PendingHooks(context.Background(), db)
if hookErr == nil && len(pending) > 0 {
fmt.Printf(" %-12s %d pending\n", "Data hooks:", len(pending))
} else if hookErr == nil {
fmt.Printf(" %-12s all applied\n", "Data hooks:")
}
}
}
// Providers — show DB providers in managed mode, config providers otherwise.
fmt.Println()
fmt.Println(" Providers:")
checkProvider("Anthropic", cfg.Providers.Anthropic.APIKey)
checkProvider("OpenAI", cfg.Providers.OpenAI.APIKey)
checkProvider("OpenRouter", cfg.Providers.OpenRouter.APIKey)
checkProvider("Gemini", cfg.Providers.Gemini.APIKey)
checkProvider("Groq", cfg.Providers.Groq.APIKey)
checkProvider("DeepSeek", cfg.Providers.DeepSeek.APIKey)
checkProvider("Mistral", cfg.Providers.Mistral.APIKey)
checkProvider("XAI", cfg.Providers.XAI.APIKey)
if isManaged && db != nil {
checkDBProviders(db)
// Also show config-only providers (env vars) not in DB.
checkProvider("Anthropic (env)", cfg.Providers.Anthropic.APIKey)
checkProvider("OpenAI (env)", cfg.Providers.OpenAI.APIKey)
checkProvider("OpenRouter (env)", cfg.Providers.OpenRouter.APIKey)
} else {
checkProvider("Anthropic", cfg.Providers.Anthropic.APIKey)
checkProvider("OpenAI", cfg.Providers.OpenAI.APIKey)
checkProvider("OpenRouter", cfg.Providers.OpenRouter.APIKey)
checkProvider("Gemini", cfg.Providers.Gemini.APIKey)
checkProvider("Groq", cfg.Providers.Groq.APIKey)
checkProvider("DeepSeek", cfg.Providers.DeepSeek.APIKey)
checkProvider("Mistral", cfg.Providers.Mistral.APIKey)
checkProvider("XAI", cfg.Providers.XAI.APIKey)
}
// Channels
// Channels — show DB channels in managed mode, config channels otherwise.
fmt.Println()
fmt.Println(" Channels:")
checkChannel("Telegram", cfg.Channels.Telegram.Enabled, cfg.Channels.Telegram.Token != "")
checkChannel("Discord", cfg.Channels.Discord.Enabled, cfg.Channels.Discord.Token != "")
checkChannel("Zalo", cfg.Channels.Zalo.Enabled, cfg.Channels.Zalo.Token != "")
checkChannel("Feishu", cfg.Channels.Feishu.Enabled, cfg.Channels.Feishu.AppID != "")
checkChannel("WhatsApp", cfg.Channels.WhatsApp.Enabled, cfg.Channels.WhatsApp.BridgeURL != "")
if isManaged && db != nil {
checkDBChannels(db)
} else {
checkChannel("Telegram", cfg.Channels.Telegram.Enabled, cfg.Channels.Telegram.Token != "")
checkChannel("Discord", cfg.Channels.Discord.Enabled, cfg.Channels.Discord.Token != "")
checkChannel("Zalo", cfg.Channels.Zalo.Enabled, cfg.Channels.Zalo.Token != "")
checkChannel("Feishu", cfg.Channels.Feishu.Enabled, cfg.Channels.Feishu.AppID != "")
checkChannel("WhatsApp", cfg.Channels.WhatsApp.Enabled, cfg.Channels.WhatsApp.BridgeURL != "")
}
// External tools
fmt.Println()
@@ -106,6 +162,66 @@ func checkChannel(name string, enabled, hasCredentials bool) {
fmt.Printf(" %-12s %s\n", name+":", status)
}
func checkDBChannels(db *sql.DB) {
rows, err := db.QueryContext(context.Background(),
"SELECT name, channel_type, enabled FROM channel_instances ORDER BY channel_type, name")
if err != nil {
fmt.Printf(" (could not query channels: %s)\n", err)
return
}
defer rows.Close()
found := false
for rows.Next() {
var name, channelType string
var enabled bool
if err := rows.Scan(&name, &channelType, &enabled); err != nil {
continue
}
found = true
status := "enabled"
if !enabled {
status = "disabled"
}
label := fmt.Sprintf("%s/%s", channelType, name)
fmt.Printf(" %-24s %s\n", label+":", status)
}
if !found {
fmt.Println(" (none configured in database)")
}
}
func checkDBProviders(db *sql.DB) {
rows, err := db.QueryContext(context.Background(),
"SELECT name, COALESCE(display_name, name), enabled, (api_key IS NOT NULL AND api_key != '') AS has_key FROM llm_providers ORDER BY name")
if err != nil {
fmt.Printf(" (could not query providers: %s)\n", err)
return
}
defer rows.Close()
found := false
for rows.Next() {
var name, displayName string
var enabled, hasKey bool
if err := rows.Scan(&name, &displayName, &enabled, &hasKey); err != nil {
continue
}
found = true
status := "enabled"
if !enabled {
status = "disabled"
}
if !hasKey {
status += " (no API key)"
}
fmt.Printf(" %-16s %s\n", displayName+":", status)
}
if !found {
fmt.Println(" (none configured in database)")
}
}
func checkBinary(name string) {
path, err := exec.LookPath(name)
if err != nil {
+7 -1
View File
@@ -306,6 +306,12 @@ func runGateway() {
var traceCollector *tracing.Collector
if cfg.Database.Mode == "managed" && cfg.Database.PostgresDSN != "" {
// Schema compatibility check: ensure DB schema matches this binary.
if err := checkSchemaOrAutoUpgrade(cfg.Database.PostgresDSN); err != nil {
slog.Error("schema compatibility check failed", "error", err)
os.Exit(1)
}
storeCfg := store.StoreConfig{
PostgresDSN: cfg.Database.PostgresDSN,
Mode: cfg.Database.Mode,
@@ -872,7 +878,7 @@ func runGateway() {
gatewayMode = "managed"
}
slog.Info("goclaw gateway starting",
"version", "0.2.0",
"version", Version,
"protocol", protocol.ProtocolVersion,
"mode", gatewayMode,
"agents", agentRouter.List(),
+56 -17
View File
@@ -2,6 +2,7 @@ package cmd
import (
"context"
"encoding/json"
"log/slog"
"github.com/nextlevelbuilder/goclaw/internal/store"
@@ -19,23 +20,43 @@ func builtinToolSeedData() []store.BuiltinToolDef {
{Name: "edit", DisplayName: "Edit File", Description: "Apply targeted edits to files (search and replace)", Category: "filesystem", Enabled: true},
// runtime
{Name: "exec", DisplayName: "Execute Command", Description: "Execute shell commands in the workspace", Category: "runtime", Enabled: true},
{Name: "exec", DisplayName: "Execute Command", Description: "Execute shell commands in the workspace", Category: "runtime", Enabled: true,
Metadata: json.RawMessage(`{"config_hint":"Config → Tools → Exec Approval"}`),
},
// web
{Name: "web_search", DisplayName: "Web Search", Description: "Search the web using Brave or DuckDuckGo", Category: "web", Enabled: true},
{Name: "web_search", DisplayName: "Web Search", Description: "Search the web using Brave or DuckDuckGo", Category: "web", Enabled: true,
Metadata: json.RawMessage(`{"config_hint":"Config → Tools → Web Search"}`),
},
{Name: "web_fetch", DisplayName: "Web Fetch", Description: "Fetch and extract content from web URLs", Category: "web", Enabled: true},
// memory
{Name: "memory_search", DisplayName: "Memory Search", Description: "Search through stored memory entries", Category: "memory", Enabled: true},
{Name: "memory_get", DisplayName: "Memory Get", Description: "Retrieve a specific memory entry by key", Category: "memory", Enabled: true},
{Name: "memory_search", DisplayName: "Memory Search", Description: "Search through stored memory entries", Category: "memory", Enabled: true,
Requires: []string{"memory"},
},
{Name: "memory_get", DisplayName: "Memory Get", Description: "Retrieve a specific memory entry by key", Category: "memory", Enabled: true,
Requires: []string{"memory"},
},
// media
{Name: "read_image", DisplayName: "Read Image", Description: "Analyze images using a vision-capable LLM provider", Category: "media", Enabled: true},
{Name: "create_image", DisplayName: "Create Image", Description: "Generate images from text prompts using an image generation provider", Category: "media", Enabled: true},
{Name: "tts", DisplayName: "Text to Speech", Description: "Convert text to speech audio", Category: "media", Enabled: true},
{Name: "read_image", DisplayName: "Read Image", Description: "Analyze images using a vision-capable LLM provider", Category: "media", Enabled: true,
Settings: json.RawMessage(`{"provider":"openrouter","model":"google/gemini-2.5-flash-image"}`),
Requires: []string{"vision_provider"},
},
{Name: "create_image", DisplayName: "Create Image", Description: "Generate images from text prompts using an image generation provider", Category: "media", Enabled: true,
Settings: json.RawMessage(`{"provider":"openrouter","model":"google/gemini-2.5-flash-image"}`),
Requires: []string{"image_gen_provider"},
},
{Name: "tts", DisplayName: "Text to Speech", Description: "Convert text to speech audio", Category: "media", Enabled: true,
Requires: []string{"tts_provider"},
Metadata: json.RawMessage(`{"config_hint":"Config → TTS"}`),
},
// browser
{Name: "browser", DisplayName: "Browser", Description: "Automate browser interactions (navigate, click, screenshot)", Category: "browser", Enabled: true},
{Name: "browser", DisplayName: "Browser", Description: "Automate browser interactions (navigate, click, screenshot)", Category: "browser", Enabled: true,
Requires: []string{"browser"},
Metadata: json.RawMessage(`{"config_hint":"Config → Tools → Browser"}`),
},
// sessions
{Name: "sessions_list", DisplayName: "List Sessions", Description: "List active chat sessions", Category: "sessions", Enabled: true},
@@ -47,24 +68,42 @@ func builtinToolSeedData() []store.BuiltinToolDef {
{Name: "message", DisplayName: "Message", Description: "Send messages to connected channels (Telegram, Discord, etc.)", Category: "messaging", Enabled: true},
// scheduling
{Name: "cron", DisplayName: "Cron Scheduler", Description: "Schedule recurring tasks with cron expressions", Category: "scheduling", Enabled: true},
{Name: "cron", DisplayName: "Cron Scheduler", Description: "Schedule recurring tasks with cron expressions", Category: "scheduling", Enabled: true,
Metadata: json.RawMessage(`{"config_hint":"Config → Cron"}`),
},
// subagents
{Name: "spawn", DisplayName: "Spawn Subagent", Description: "Spawn an asynchronous background subagent", Category: "subagents", Enabled: true},
{Name: "subagent", DisplayName: "Subagent", Description: "Run a synchronous subagent and wait for result", Category: "subagents", Enabled: true},
{Name: "spawn", DisplayName: "Spawn Subagent", Description: "Spawn an asynchronous background subagent", Category: "subagents", Enabled: true,
Metadata: json.RawMessage(`{"config_hint":"Config → Agents Defaults"}`),
},
{Name: "subagent", DisplayName: "Subagent", Description: "Run a synchronous subagent and wait for result", Category: "subagents", Enabled: true,
Metadata: json.RawMessage(`{"config_hint":"Config → Agents Defaults"}`),
},
// skills
{Name: "skill_search", DisplayName: "Skill Search", Description: "Search available skills by keyword or description", Category: "skills", Enabled: true},
// delegation
{Name: "delegate", DisplayName: "Delegate", Description: "Delegate a task to another agent", Category: "delegation", Enabled: true},
{Name: "delegate_search", DisplayName: "Delegate Search", Description: "Search for agents to delegate tasks to", Category: "delegation", Enabled: true},
{Name: "evaluate_loop", DisplayName: "Evaluate Loop", Description: "Run an evaluate-optimize loop with delegated agents", Category: "delegation", Enabled: true},
{Name: "handoff", DisplayName: "Handoff", Description: "Transfer conversation to another agent", Category: "delegation", Enabled: true},
{Name: "delegate", DisplayName: "Delegate", Description: "Delegate a task to another agent", Category: "delegation", Enabled: true,
Requires: []string{"managed_mode", "agent_links"},
},
{Name: "delegate_search", DisplayName: "Delegate Search", Description: "Search for agents to delegate tasks to", Category: "delegation", Enabled: true,
Requires: []string{"managed_mode", "agent_links"},
},
{Name: "evaluate_loop", DisplayName: "Evaluate Loop", Description: "Run an evaluate-optimize loop with delegated agents", Category: "delegation", Enabled: true,
Requires: []string{"managed_mode", "agent_links"},
},
{Name: "handoff", DisplayName: "Handoff", Description: "Transfer conversation to another agent", Category: "delegation", Enabled: true,
Requires: []string{"managed_mode", "agent_links"},
},
// teams
{Name: "team_tasks", DisplayName: "Team Tasks", Description: "Manage tasks within a team of agents", Category: "teams", Enabled: true},
{Name: "team_message", DisplayName: "Team Message", Description: "Send messages between team agents", Category: "teams", Enabled: true},
{Name: "team_tasks", DisplayName: "Team Tasks", Description: "Manage tasks within a team of agents", Category: "teams", Enabled: true,
Requires: []string{"managed_mode", "teams"},
},
{Name: "team_message", DisplayName: "Team Message", Description: "Send messages between team agents", Category: "teams", Enabled: true,
Requires: []string{"managed_mode", "teams"},
},
}
}
+20
View File
@@ -1,6 +1,8 @@
package cmd
import (
"context"
"database/sql"
"fmt"
"log/slog"
"os"
@@ -10,9 +12,11 @@ import (
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/spf13/cobra"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/upgrade"
)
var migrationsDir string
@@ -95,6 +99,22 @@ func migrateUpCmd() *cobra.Command {
v, dirty, _ := m.Version()
slog.Info("migration complete", "version", v, "dirty", dirty)
// Run pending data hooks after SQL migrations.
db, dbErr := sql.Open("pgx", dsn)
if dbErr != nil {
slog.Warn("could not connect for data hooks", "error", dbErr)
return nil
}
defer db.Close()
count, hookErr := upgrade.RunPendingHooks(context.Background(), db)
if hookErr != nil {
slog.Warn("data hooks failed", "error", hookErr)
} else if count > 0 {
slog.Info("data hooks applied", "count", count)
}
return nil
},
}
+1
View File
@@ -42,6 +42,7 @@ func init() {
rootCmd.AddCommand(skillsCmd())
rootCmd.AddCommand(sessionsCmd())
rootCmd.AddCommand(migrateCmd())
rootCmd.AddCommand(upgradeCmd())
}
func versionCmd() *cobra.Command {
+265
View File
@@ -0,0 +1,265 @@
package cmd
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"os"
"github.com/golang-migrate/migrate/v4"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/spf13/cobra"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/upgrade"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
func upgradeCmd() *cobra.Command {
var dryRun bool
var status bool
cmd := &cobra.Command{
Use: "upgrade",
Short: "Upgrade database schema and run data migrations",
Long: "Applies pending SQL migrations and Go-based data hooks. Safe to run multiple times (idempotent).",
RunE: func(cmd *cobra.Command, args []string) error {
if status {
return runUpgradeStatus()
}
return runUpgrade(dryRun)
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be done without applying changes")
cmd.Flags().BoolVar(&status, "status", false, "show current upgrade status")
return cmd
}
func runUpgradeStatus() error {
cfg, err := config.Load(resolveConfigPath())
if err != nil {
return fmt.Errorf("load config: %w", err)
}
fmt.Printf(" App version: %s (protocol %d)\n", Version, protocol.ProtocolVersion)
if cfg.Database.Mode != "managed" || cfg.Database.PostgresDSN == "" {
fmt.Println(" Mode: standalone (no database)")
fmt.Println(" Status: N/A (no schema migrations needed)")
return nil
}
db, err := sql.Open("pgx", cfg.Database.PostgresDSN)
if err != nil {
return fmt.Errorf("connect: %w", err)
}
defer db.Close()
s, err := upgrade.CheckSchema(db)
if err != nil {
return fmt.Errorf("check schema: %w", err)
}
fmt.Printf(" Schema current: %d\n", s.CurrentVersion)
fmt.Printf(" Schema required: %d\n", s.RequiredVersion)
if s.Dirty {
fmt.Println(" Status: DIRTY (failed migration)")
fmt.Println()
fmt.Print(upgrade.FormatError(s))
return nil
}
if s.Compatible {
fmt.Println(" Status: UP TO DATE")
} else if s.CurrentVersion > s.RequiredVersion {
fmt.Println(" Status: BINARY TOO OLD")
} else {
fmt.Printf(" Status: UPGRADE NEEDED (%d -> %d)\n", s.CurrentVersion, s.RequiredVersion)
}
// Check pending data hooks.
pending, err := upgrade.PendingHooks(context.Background(), db)
if err != nil {
slog.Debug("could not check pending data hooks", "error", err)
} else if len(pending) > 0 {
fmt.Printf("\n Pending data hooks: %d\n", len(pending))
for _, name := range pending {
fmt.Printf(" - %s\n", name)
}
}
if s.NeedsMigration {
fmt.Println()
fmt.Println(" Run 'goclaw upgrade' to apply all pending changes.")
}
return nil
}
func runUpgrade(dryRun bool) error {
cfg, err := config.Load(resolveConfigPath())
if err != nil {
return fmt.Errorf("load config: %w", err)
}
if cfg.Database.Mode != "managed" || cfg.Database.PostgresDSN == "" {
fmt.Println("Standalone mode — no database migrations needed.")
return nil
}
dsn := cfg.Database.PostgresDSN
db, err := sql.Open("pgx", dsn)
if err != nil {
return fmt.Errorf("connect: %w", err)
}
defer db.Close()
s, err := upgrade.CheckSchema(db)
if err != nil {
return fmt.Errorf("check schema: %w", err)
}
fmt.Printf(" App version: %s (protocol %d)\n", Version, protocol.ProtocolVersion)
fmt.Printf(" Schema current: %d\n", s.CurrentVersion)
fmt.Printf(" Schema required: %d\n", s.RequiredVersion)
fmt.Println()
if s.Dirty {
fmt.Print(upgrade.FormatError(s))
return ErrUpgradeFailed
}
if s.CurrentVersion > s.RequiredVersion {
fmt.Print(upgrade.FormatError(s))
return ErrUpgradeFailed
}
if dryRun {
if s.NeedsMigration {
fmt.Printf(" Would apply SQL migrations: v%d -> v%d\n", s.CurrentVersion, s.RequiredVersion)
} else {
fmt.Println(" SQL schema is up to date.")
}
pending, err := upgrade.PendingHooks(context.Background(), db)
if err != nil {
slog.Debug("could not check pending data hooks", "error", err)
} else if len(pending) > 0 {
fmt.Printf(" Would run %d data hook(s):\n", len(pending))
for _, name := range pending {
fmt.Printf(" - %s\n", name)
}
} else {
fmt.Println(" No pending data hooks.")
}
return nil
}
// Apply SQL migrations.
if s.NeedsMigration {
fmt.Print(" Applying SQL migrations... ")
m, err := newMigrator(dsn)
if err != nil {
return fmt.Errorf("create migrator: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
fmt.Println("FAILED")
return fmt.Errorf("migrate up: %w", err)
}
v, _, _ := m.Version()
fmt.Printf("OK (v%d -> v%d)\n", s.CurrentVersion, v)
} else {
fmt.Println(" SQL schema is up to date.")
}
// Run data hooks.
fmt.Print(" Running data hooks... ")
count, err := upgrade.RunPendingHooks(context.Background(), db)
if err != nil {
fmt.Println("FAILED")
return fmt.Errorf("data hooks: %w", err)
}
if count > 0 {
fmt.Printf("%d applied\n", count)
} else {
fmt.Println("none pending")
}
fmt.Println()
fmt.Println(" Upgrade complete.")
return nil
}
// ErrUpgradeFailed is returned when upgrade cannot proceed.
var ErrUpgradeFailed = fmt.Errorf("upgrade cannot proceed")
// checkSchemaOrAutoUpgrade is called from gateway startup to gate on schema compatibility.
// If GOCLAW_AUTO_UPGRADE=true and schema is outdated, it runs the upgrade inline.
func checkSchemaOrAutoUpgrade(dsn string) error {
db, err := sql.Open("pgx", dsn)
if err != nil {
return fmt.Errorf("schema check: connect: %w", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
return fmt.Errorf("schema check: ping: %w", err)
}
s, err := upgrade.CheckSchema(db)
if err != nil {
return fmt.Errorf("schema check: %w", err)
}
if s.Compatible {
slog.Info("schema check passed", "current", s.CurrentVersion, "required", s.RequiredVersion)
return nil
}
if s.Dirty {
return errors.New(upgrade.FormatError(s))
}
if s.CurrentVersion > s.RequiredVersion {
return errors.New(upgrade.FormatError(s))
}
// Schema is outdated — check if auto-upgrade is enabled.
if os.Getenv("GOCLAW_AUTO_UPGRADE") == "true" {
slog.Info("auto-upgrade: applying migrations", "from", s.CurrentVersion, "to", s.RequiredVersion)
m, mErr := newMigrator(dsn)
if mErr != nil {
return fmt.Errorf("auto-upgrade: create migrator: %w", mErr)
}
defer m.Close()
if mErr := m.Up(); mErr != nil && mErr != migrate.ErrNoChange {
return fmt.Errorf("auto-upgrade: migrate up: %w", mErr)
}
v, _, _ := m.Version()
slog.Info("auto-upgrade: SQL migrations applied", "version", v)
// Run data hooks.
count, hErr := upgrade.RunPendingHooks(context.Background(), db)
if hErr != nil {
return fmt.Errorf("auto-upgrade: data hooks: %w", hErr)
}
if count > 0 {
slog.Info("auto-upgrade: data hooks applied", "count", count)
}
slog.Info("auto-upgrade complete")
return nil
}
return errors.New(upgrade.FormatError(s))
}