From dd8c7595bacf0e52205bf33bf8885ab4e1f964ef Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 2 Dec 2025 23:38:20 -0800 Subject: [PATCH] fix: store version in gitignored .local_version to prevent notification spam (bd-tok) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: metadata.json is tracked in git and contains last_bd_version. When git operations (pull, checkout, merge) reset metadata.json to the committed version, the upgrade notification would fire repeatedly. Fix: Store the last used bd version in .beads/.local_version which is gitignored, so git operations don't affect version tracking. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .beads/.gitignore | 4 ++ cmd/bd/version_tracking.go | 61 ++++++++++++++++++++++--------- cmd/bd/version_tracking_test.go | 50 ++++++++++++------------- internal/configfile/configfile.go | 11 ++++-- 4 files changed, 79 insertions(+), 47 deletions(-) diff --git a/.beads/.gitignore b/.beads/.gitignore index f438450f..3f0f4627 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -11,6 +11,10 @@ daemon.log daemon.pid bd.sock +# Local version tracking (prevents upgrade notification spam after git operations) +# bd-tok: Store version locally instead of in tracked metadata.json +.local_version + # Legacy database files db.sqlite bd.db diff --git a/cmd/bd/version_tracking.go b/cmd/bd/version_tracking.go index 05f4c297..2ef3c91e 100644 --- a/cmd/bd/version_tracking.go +++ b/cmd/bd/version_tracking.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" "github.com/steveyegge/beads/internal/beads" @@ -12,11 +13,19 @@ import ( "github.com/steveyegge/beads/internal/storage/sqlite" ) -// trackBdVersion checks if bd version has changed since last run and updates metadata.json. +// localVersionFile is the gitignored file that stores the last bd version used locally. +// This prevents the upgrade notification from firing repeatedly when git operations +// reset the tracked metadata.json file. +// +// bd-tok: Fix upgrade notification persisting after git operations +const localVersionFile = ".local_version" + +// trackBdVersion checks if bd version has changed since last run and updates the local version file. // This function is best-effort - failures are silent to avoid disrupting commands. // Sets global variables versionUpgradeDetected and previousVersion if upgrade detected. // // bd-loka: Built-in version tracking for upgrade awareness +// bd-tok: Use gitignored .local_version file instead of metadata.json func trackBdVersion() { // Find the beads directory beadsDir := beads.FindBeadsDir() @@ -25,16 +34,32 @@ func trackBdVersion() { return } - // Load current config + // Read last version from local (gitignored) file + localVersionPath := filepath.Join(beadsDir, localVersionFile) + lastVersion := readLocalVersion(localVersionPath) + + // Check if version changed + if lastVersion != "" && lastVersion != Version { + // Version upgrade detected! + versionUpgradeDetected = true + previousVersion = lastVersion + } + + // Update local version file (best effort) + // Only write if version actually changed to minimize I/O + if lastVersion != Version { + _ = writeLocalVersion(localVersionPath, Version) + } + + // Also ensure metadata.json exists with proper defaults (for JSONL export name) + // but don't use it for version tracking anymore cfg, err := configfile.Load(beadsDir) if err != nil { - // Silent failure - config might not exist yet return } if cfg == nil { - // No config file yet - create one with current version + // No config file yet - create one cfg = configfile.DefaultConfig() - cfg.LastBdVersion = Version // bd-afd: Auto-detect actual JSONL file instead of using hardcoded default // This prevents mismatches when metadata.json gets deleted (git clean, merge conflict, etc.) @@ -43,23 +68,23 @@ func trackBdVersion() { } _ = cfg.Save(beadsDir) // Best effort - return } +} - // Check if version changed - if cfg.LastBdVersion != "" && cfg.LastBdVersion != Version { - // Version upgrade detected! - versionUpgradeDetected = true - previousVersion = cfg.LastBdVersion +// readLocalVersion reads the last bd version from the local version file. +// Returns empty string if file doesn't exist or can't be read. +func readLocalVersion(path string) string { + // #nosec G304 - path is constructed from beadsDir + constant + data, err := os.ReadFile(path) + if err != nil { + return "" } + return strings.TrimSpace(string(data)) +} - // Update metadata.json with current version (best effort) - // Only write if version actually changed to minimize I/O - // Also update on first run (when LastBdVersion is empty) to initialize tracking - if cfg.LastBdVersion != Version { - cfg.LastBdVersion = Version - _ = cfg.Save(beadsDir) // Silent failure is fine - } +// writeLocalVersion writes the current version to the local version file. +func writeLocalVersion(path, version string) error { + return os.WriteFile(path, []byte(version+"\n"), 0600) } // getVersionsSince returns all version changes since the given version. diff --git a/cmd/bd/version_tracking_test.go b/cmd/bd/version_tracking_test.go index 15f92da5..9a2f21dd 100644 --- a/cmd/bd/version_tracking_test.go +++ b/cmd/bd/version_tracking_test.go @@ -6,7 +6,6 @@ import ( "path/filepath" "testing" - "github.com/steveyegge/beads/internal/configfile" "github.com/steveyegge/beads/internal/storage/sqlite" ) @@ -156,7 +155,7 @@ func TestTrackBdVersion_FirstRun(t *testing.T) { versionUpgradeDetected = false previousVersion = "" - // trackBdVersion should create metadata.json + // trackBdVersion should create .local_version trackBdVersion() // Should not detect upgrade on first run @@ -164,13 +163,11 @@ func TestTrackBdVersion_FirstRun(t *testing.T) { t.Error("Expected no upgrade detection on first run") } - // Should have created metadata.json with current version - cfg, err := configfile.Load(beadsDir) - if err != nil { - t.Fatalf("Failed to load config after tracking: %v", err) - } - if cfg.LastBdVersion != Version { - t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version) + // Should have created .local_version with current version + localVersionPath := filepath.Join(beadsDir, localVersionFile) + localVersion := readLocalVersion(localVersionPath) + if localVersion != Version { + t.Errorf(".local_version = %q, want %q", localVersion, Version) } } @@ -189,11 +186,16 @@ func TestTrackBdVersion_UpgradeDetection(t *testing.T) { t.Fatalf("Failed to change to temp dir: %v", err) } - // Create metadata.json with old version - cfg := configfile.DefaultConfig() - cfg.LastBdVersion = "0.22.0" - if err := cfg.Save(beadsDir); err != nil { - t.Fatalf("Failed to save config: %v", err) + // Create minimal metadata.json so FindBeadsDir can find the directory (bd-420) + metadataPath := filepath.Join(beadsDir, "metadata.json") + if err := os.WriteFile(metadataPath, []byte(`{"database":"beads.db"}`), 0600); err != nil { + t.Fatalf("Failed to create metadata.json: %v", err) + } + + // Create .local_version with old version (simulating previous bd run) + localVersionPath := filepath.Join(beadsDir, localVersionFile) + if err := writeLocalVersion(localVersionPath, "0.22.0"); err != nil { + t.Fatalf("Failed to write local version: %v", err) } // Save original state @@ -220,13 +222,10 @@ func TestTrackBdVersion_UpgradeDetection(t *testing.T) { t.Errorf("previousVersion = %q, want %q", previousVersion, "0.22.0") } - // Should have updated metadata.json to current version - cfg, err := configfile.Load(beadsDir) - if err != nil { - t.Fatalf("Failed to load config after tracking: %v", err) - } - if cfg.LastBdVersion != Version { - t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version) + // Should have updated .local_version to current version + localVersion := readLocalVersion(localVersionPath) + if localVersion != Version { + t.Errorf(".local_version = %q, want %q", localVersion, Version) } } @@ -245,11 +244,10 @@ func TestTrackBdVersion_SameVersion(t *testing.T) { t.Fatalf("Failed to change to temp dir: %v", err) } - // Create metadata.json with current version - cfg := configfile.DefaultConfig() - cfg.LastBdVersion = Version - if err := cfg.Save(beadsDir); err != nil { - t.Fatalf("Failed to save config: %v", err) + // Create .local_version with current version + localVersionPath := filepath.Join(beadsDir, localVersionFile) + if err := writeLocalVersion(localVersionPath, Version); err != nil { + t.Fatalf("Failed to write local version: %v", err) } // Save original state diff --git a/internal/configfile/configfile.go b/internal/configfile/configfile.go index a6906afc..9a9854b8 100644 --- a/internal/configfile/configfile.go +++ b/internal/configfile/configfile.go @@ -10,12 +10,17 @@ import ( const ConfigFileName = "metadata.json" type Config struct { - Database string `json:"database"` - JSONLExport string `json:"jsonl_export,omitempty"` - LastBdVersion string `json:"last_bd_version,omitempty"` + Database string `json:"database"` + JSONLExport string `json:"jsonl_export,omitempty"` // Deletions configuration DeletionsRetentionDays int `json:"deletions_retention_days,omitempty"` // 0 means use default (7 days) + + // Deprecated: LastBdVersion is no longer used for version tracking. + // Version is now stored in .local_version (gitignored) to prevent + // upgrade notifications firing after git operations reset metadata.json. + // bd-tok: This field is kept for backwards compatibility when reading old configs. + LastBdVersion string `json:"last_bd_version,omitempty"` } func DefaultConfig() *Config {