feat: Auto-migrate database on CLI version bump (bd-jgxi)
When CLI is upgraded (e.g., 0.24.0 → 0.24.1), the database version is now automatically updated to match the CLI version during PersistentPreRun. This fixes the recurring UX issue where bd doctor shows version mismatch after every CLI upgrade. Implementation: - Added autoMigrateOnVersionBump() function in version_tracking.go - Calls after trackBdVersion() in PersistentPreRun - Best-effort and silent failures to avoid disrupting commands - Only updates bd_version metadata field - Includes comprehensive test coverage Changes: - cmd/bd/main.go: Call autoMigrateOnVersionBump() in PersistentPreRun - cmd/bd/version_tracking.go: Implement auto-migration logic - cmd/bd/version_tracking_test.go: Add tests for auto-migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -302,6 +302,10 @@ var rootCmd = &cobra.Command{
|
||||
// Best-effort tracking - failures are silent
|
||||
trackBdVersion()
|
||||
|
||||
// Auto-migrate database on version bump (bd-jgxi)
|
||||
// Best-effort migration - failures are silent to avoid disrupting commands
|
||||
autoMigrateOnVersionBump()
|
||||
|
||||
// Initialize daemon status
|
||||
socketPath := getSocketPath()
|
||||
daemonStatus = DaemonStatus{
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
)
|
||||
|
||||
// trackBdVersion checks if bd version has changed since last run and updates metadata.json.
|
||||
@@ -112,3 +116,82 @@ func maybeShowUpgradeNotification() {
|
||||
fmt.Println("💡 Run 'bd upgrade review' to see what changed")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// autoMigrateOnVersionBump automatically migrates the database when CLI version changes.
|
||||
// This function is best-effort - failures are silent to avoid disrupting commands.
|
||||
// Called from PersistentPreRun after trackBdVersion().
|
||||
//
|
||||
// bd-jgxi: Auto-migrate database on CLI version bump
|
||||
func autoMigrateOnVersionBump() {
|
||||
// Only migrate if version upgrade was detected
|
||||
if !versionUpgradeDetected {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the beads directory
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
// No .beads directory - nothing to migrate
|
||||
return
|
||||
}
|
||||
|
||||
// Load config to get database path
|
||||
cfg, err := configfile.Load(beadsDir)
|
||||
if err != nil || cfg == nil {
|
||||
// Config load failed or doesn't exist - skip migration
|
||||
debug.Logf("auto-migrate: skipping migration, config load failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get database path
|
||||
dbPath := cfg.DatabasePath(beadsDir)
|
||||
|
||||
// Check if database exists
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// No database file - nothing to migrate
|
||||
debug.Logf("auto-migrate: skipping migration, database does not exist: %s", dbPath)
|
||||
return
|
||||
}
|
||||
|
||||
// Open database to check current version
|
||||
ctx := context.Background()
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
// Failed to open database - skip migration
|
||||
debug.Logf("auto-migrate: failed to open database: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current database version
|
||||
dbVersion, err := store.GetMetadata(ctx, "bd_version")
|
||||
if err != nil {
|
||||
// Failed to read version - skip migration
|
||||
debug.Logf("auto-migrate: failed to read database version: %v", err)
|
||||
_ = store.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if migration is needed
|
||||
if dbVersion == Version {
|
||||
// Database is already at current version
|
||||
debug.Logf("auto-migrate: database already at version %s", Version)
|
||||
_ = store.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Perform migration: update database version
|
||||
debug.Logf("auto-migrate: migrating database from %s to %s", dbVersion, Version)
|
||||
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
|
||||
// Migration failed - log and continue
|
||||
debug.Logf("auto-migrate: failed to update database version: %v", err)
|
||||
_ = store.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Close database
|
||||
if err := store.Close(); err != nil {
|
||||
debug.Logf("auto-migrate: warning: failed to close database: %v", err)
|
||||
}
|
||||
|
||||
debug.Logf("auto-migrate: successfully migrated database to version %s", Version)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
)
|
||||
|
||||
func TestGetVersionsSince(t *testing.T) {
|
||||
@@ -308,3 +311,187 @@ func TestMaybeShowUpgradeNotification(t *testing.T) {
|
||||
t.Error("Should not change acknowledged state on subsequent calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoMigrateOnVersionBump_NoUpgrade(t *testing.T) {
|
||||
// Save original state
|
||||
origUpgradeDetected := versionUpgradeDetected
|
||||
defer func() {
|
||||
versionUpgradeDetected = origUpgradeDetected
|
||||
}()
|
||||
|
||||
// Reset state - no upgrade detected
|
||||
versionUpgradeDetected = false
|
||||
|
||||
// Should return early without doing anything
|
||||
autoMigrateOnVersionBump()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
func TestAutoMigrateOnVersionBump_NoDatabase(t *testing.T) {
|
||||
// Create temp .beads directory without a database
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory
|
||||
origWd, _ := os.Getwd()
|
||||
defer os.Chdir(origWd)
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change to temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json
|
||||
cfg := configfile.DefaultConfig()
|
||||
cfg.LastBdVersion = "0.22.0"
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Save original state
|
||||
origUpgradeDetected := versionUpgradeDetected
|
||||
defer func() {
|
||||
versionUpgradeDetected = origUpgradeDetected
|
||||
}()
|
||||
|
||||
// Simulate version upgrade
|
||||
versionUpgradeDetected = true
|
||||
|
||||
// Should handle gracefully when database doesn't exist
|
||||
autoMigrateOnVersionBump()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
func TestAutoMigrateOnVersionBump_MigratesVersion(t *testing.T) {
|
||||
// Create temp .beads directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory
|
||||
origWd, _ := os.Getwd()
|
||||
defer os.Chdir(origWd)
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change to temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json
|
||||
cfg := configfile.DefaultConfig()
|
||||
cfg.LastBdVersion = "0.22.0"
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Create database with old version
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
ctx := context.Background()
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create database: %v", err)
|
||||
}
|
||||
|
||||
// Set old database version
|
||||
oldVersion := "0.22.0"
|
||||
if err := store.SetMetadata(ctx, "bd_version", oldVersion); err != nil {
|
||||
t.Fatalf("Failed to set old version: %v", err)
|
||||
}
|
||||
_ = store.Close()
|
||||
|
||||
// Save original state
|
||||
origUpgradeDetected := versionUpgradeDetected
|
||||
defer func() {
|
||||
versionUpgradeDetected = origUpgradeDetected
|
||||
}()
|
||||
|
||||
// Simulate version upgrade
|
||||
versionUpgradeDetected = true
|
||||
|
||||
// Call auto-migration
|
||||
autoMigrateOnVersionBump()
|
||||
|
||||
// Verify database version was updated
|
||||
store, err = sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
newVersion, err := store.GetMetadata(ctx, "bd_version")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read database version: %v", err)
|
||||
}
|
||||
|
||||
if newVersion != Version {
|
||||
t.Errorf("Database version not updated: got %q, want %q", newVersion, Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoMigrateOnVersionBump_AlreadyMigrated(t *testing.T) {
|
||||
// Create temp .beads directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory
|
||||
origWd, _ := os.Getwd()
|
||||
defer os.Chdir(origWd)
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change to temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Create metadata.json
|
||||
cfg := configfile.DefaultConfig()
|
||||
cfg.LastBdVersion = Version
|
||||
if err := cfg.Save(beadsDir); err != nil {
|
||||
t.Fatalf("Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Create database with current version
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
ctx := context.Background()
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create database: %v", err)
|
||||
}
|
||||
|
||||
// Set current database version
|
||||
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
|
||||
t.Fatalf("Failed to set version: %v", err)
|
||||
}
|
||||
_ = store.Close()
|
||||
|
||||
// Save original state
|
||||
origUpgradeDetected := versionUpgradeDetected
|
||||
defer func() {
|
||||
versionUpgradeDetected = origUpgradeDetected
|
||||
}()
|
||||
|
||||
// Simulate version upgrade
|
||||
versionUpgradeDetected = true
|
||||
|
||||
// Call auto-migration - should be a no-op
|
||||
autoMigrateOnVersionBump()
|
||||
|
||||
// Verify database version is still current
|
||||
store, err = sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
currentVersion, err := store.GetMetadata(ctx, "bd_version")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read database version: %v", err)
|
||||
}
|
||||
|
||||
if currentVersion != Version {
|
||||
t.Errorf("Database version changed unexpectedly: got %q, want %q", currentVersion, Version)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user