From 9e126c27dac80e0d754680eb6e1a24e9ba1ef38e Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 13 May 2026 14:18:12 -0400 Subject: [PATCH] fix: detect stale server during development with BuildID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version check compared Version strings, but during development both client and server report "devel" — so stale servers were never restarted. BuildID is now derived from the executable's modification time, which changes on every recompilation (including go run). The server exposes it via /v1/version and the client checks both Version and BuildID match. Assisted-by: Crush:AWS Claude Opus 4.6 --- internal/backend/backend.go | 1 + internal/cmd/root.go | 11 ++++++---- internal/proto/version.go | 1 + internal/version/version.go | 40 +++++++++++++++++++++++++++++++------ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/internal/backend/backend.go b/internal/backend/backend.go index f14e5b7229939f5b9af11047cec7bb68a5e59cab..370bdbdcf706f2b25f17bfb53e54c041e473b586 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -174,6 +174,7 @@ func (b *Backend) VersionInfo() proto.VersionInfo { return proto.VersionInfo{ Version: version.Version, Commit: version.Commit, + BuildID: version.BuildID, GoVersion: runtime.Version(), Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b9ba4e8241100383492d20c3e64aefe5c0a5b286..0c02064507caa544100f57953ab8a6dde6f1ff4f 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -614,12 +614,15 @@ func restartIfStale(cmd *cobra.Command, hostURL *url.URL) (restarted bool, err e if err != nil { return false, err } - if vi.Version == version.Version { + if vi.Version == version.Version && vi.BuildID == version.BuildID { return false, nil } - slog.Info("Server version mismatch, restarting", - "server", vi.Version, - "client", version.Version, + slog.Info( + "Server version mismatch, restarting", + "server_version", vi.Version, + "client_version", version.Version, + "server_build_id", vi.BuildID, + "client_build_id", version.BuildID, ) _ = c.ShutdownServer(cmd.Context()) // Give the old process a moment to release the socket. diff --git a/internal/proto/version.go b/internal/proto/version.go index b728a8b966068a7810f86aae74cfcc6f57e03d39..4f837ce2eb561e1e16586e86b891aa0a1dc3dba8 100644 --- a/internal/proto/version.go +++ b/internal/proto/version.go @@ -4,6 +4,7 @@ package proto type VersionInfo struct { Version string `json:"version"` Commit string `json:"commit"` + BuildID string `json:"build_id"` GoVersion string `json:"go_version"` Platform string `json:"platform"` } diff --git a/internal/version/version.go b/internal/version/version.go index 3eb4f74139a752c1567986a8b9344913d55f08b1..99b06243f176cf7e078a14ec3eae867738d48cbc 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,12 +1,21 @@ package version -import "runtime/debug" +import ( + "os" + "runtime/debug" + "strconv" +) // Build-time parameters set via -ldflags. var ( Version = "devel" Commit = "unknown" + // BuildID is a unique identifier for this build. For release builds it + // equals Commit; for development builds (go run / go build without + // ldflags) it is derived from the executable's modification time, which + // changes on every recompilation. + BuildID = "" ) // A user may install crush using `go install github.com/charmbracelet/crush@latest`. @@ -15,11 +24,30 @@ var ( // is only set for `go install` and not for `go build`). func init() { info, ok := debug.ReadBuildInfo() - if !ok { - return + if ok { + mainVersion := info.Main.Version + if mainVersion != "" && mainVersion != "(devel)" { + Version = mainVersion + } + } + + // Derive BuildID when not set via ldflags. + if BuildID == "" { + BuildID = deriveBuildID() + } +} + +// deriveBuildID uses the running executable's modification time as a unique +// build fingerprint. This changes on every recompilation (including `go run`), +// making it reliable for detecting stale servers during development. +func deriveBuildID() string { + exe, err := os.Executable() + if err != nil { + return "unknown" } - mainVersion := info.Main.Version - if mainVersion != "" && mainVersion != "(devel)" { - Version = mainVersion + fi, err := os.Stat(exe) + if err != nil { + return "unknown" } + return strconv.FormatInt(fi.ModTime().UnixNano(), 36) }