diff --git a/main.go b/main.go index a5248577..95581b62 100644 --- a/main.go +++ b/main.go @@ -3717,6 +3717,26 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, folderName strin } } +// isLocalDevBuild reports whether this binary is an unstamped build running +// outside of any package sandbox, e.g. `make build` or `go run .` from a +// clone. It matters for the update checks: detectInstalledVersion falls back +// to asking the system package manager, so a contributor running their own +// build is told the version of the *packaged* Matcha they happen to have +// installed, which almost never matches the latest release tag. +// +// Version alone isn't a reliable signal: snap and flatpak builds are also +// unstamped (snapcraft.yaml and the flatpak manifest build without ldflags), +// but they *are* managed by the update path (trySnapRefresh, tryFlatpakUpdate). +// SNAP and FLATPAK_ID are always injected by their respective runtimes +// (snapenv.basicEnv; flatpak-run.c's "FLATPAK_ID is always set"), so their +// presence rules out a local dev build even when the version is unstamped. +func isLocalDevBuild() bool { + v := strings.TrimSpace(version) + unstamped := v == "" || v == "dev" + sandboxed := os.Getenv("SNAP") != "" || os.Getenv("FLATPAK_ID") != "" + return unstamped && !sandboxed +} + /* detectInstalledVersion returns a best-effort installed version string. Priority: @@ -3808,6 +3828,10 @@ installed version. This runs in the background when the TUI initializes. */ func checkForUpdatesCmd() tea.Cmd { return func() tea.Msg { + // A local dev build is not something `matcha update` can upgrade. + if isLocalDevBuild() { + return nil + } // Non-fatal: if anything goes wrong we just don't show the update message. const api = "https://api.github.com/repos/floatpane/matcha/releases/latest" resp, err := httpClient.Get(api) @@ -3834,6 +3858,10 @@ func checkForUpdatesCmd() tea.Cmd { // a V1RCAvailableMsg when the installed version is still pre-v1. func checkForV1RCCmd() tea.Cmd { return func() tea.Msg { + // Same reasoning as checkForUpdatesCmd. + if isLocalDevBuild() { + return nil + } const api = "https://api.github.com/repos/floatpane/matcha/releases" resp, err := httpClient.Get(api) if err != nil { diff --git a/main_test.go b/main_test.go index e698f416..03e9d0e4 100644 --- a/main_test.go +++ b/main_test.go @@ -171,6 +171,60 @@ func TestUndoDeleteRestoresFolderCounter(t *testing.T) { } } +func TestIsLocalDevBuildDetectsUnstampedVersions(t *testing.T) { + original := version + t.Cleanup(func() { version = original }) + + for _, tc := range []struct { + name string + version string + snap string + flatpak string + want bool + }{ + {name: "plain go build", version: "dev", want: true}, + {name: "empty ldflags value", version: "", want: true}, + {name: "whitespace only", version: " ", want: true}, + {name: "released version", version: "0.44.0", want: false}, + {name: "tagged version", version: "v1.0.0-rc1", want: false}, + {name: "unstamped snap build", version: "dev", snap: "/snap/matcha/42", want: false}, + {name: "unstamped flatpak build", version: "dev", flatpak: "com.floatpane.matcha", want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + version = tc.version + // An empty value stands in for "unset": isLocalDevBuild compares + // against "", so absent and set-but-empty are equivalent. Setting + // both also isolates the test from a runner that is itself inside a + // snap or flatpak. + t.Setenv("SNAP", tc.snap) + t.Setenv("FLATPAK_ID", tc.flatpak) + + if got := isLocalDevBuild(); got != tc.want { + t.Fatalf("isLocalDevBuild() with version %q, SNAP=%q, FLATPAK_ID=%q = %t, want %t", + tc.version, tc.snap, tc.flatpak, got, tc.want) + } + }) + } +} + +func TestUpdateChecksAreSkippedOnLocalDevBuilds(t *testing.T) { + original := version + t.Cleanup(func() { version = original }) + version = "dev" + t.Setenv("SNAP", "") + t.Setenv("FLATPAK_ID", "") + + // Both checks must bail out before reaching the GitHub API, so a dev build + // neither advertises an update nor makes a network call to find one. + if msg := checkForUpdatesCmd()(); msg != nil { + t.Fatalf("checkForUpdatesCmd() on a dev build = %#v, want nil", msg) + } + + if msg := checkForV1RCCmd()(); msg != nil { + t.Fatalf("checkForV1RCCmd() on a dev build = %#v, want nil", msg) + } +} + func TestUnreadBadgeCountDeduplicatesOverlappingStores(t *testing.T) { email := fetcher.Email{UID: 42, AccountID: "acct-a"} got := unreadBadgeCount(