From 6694ec687326e3634fe48e33466e06d8c09edbfe Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 23 Aug 2026 23:35:02 -0700 Subject: [PATCH] fix(main): only trust build version when major matches module path debug.ReadBuildInfo() can report a version whose major version does not match the module path's major version suffix (e.g. a v2 tag on a module without a /v2 path), which is unreliable per Go's module versioning rules. Extract the decision into resolveVersion() and only accept the build version when its major matches the module path's implied major version, otherwise fall back to the dev version. Fixes #231. Co-Authored-By: Codet (GPT-5-Codex) --- main.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++-- main_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 main_test.go diff --git a/main.go b/main.go index f6b862d..11974f1 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,10 @@ package main import ( + "path" "runtime/debug" + "strconv" + "strings" "gitea.com/gitea/gitea-mcp/cmd" "gitea.com/gitea/gitea-mcp/pkg/flag" @@ -11,13 +14,65 @@ var Version = "dev" func init() { if Version == "dev" { - if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" { - Version = info.Main.Version + if info, ok := debug.ReadBuildInfo(); ok { + Version = resolveVersion(Version, info) } } flag.Version = Version } +// resolveVersion returns the version reported by debug.ReadBuildInfo when its +// major version matches the major version encoded in the module path (e.g. +// "/v2" suffix). Otherwise it falls back to devVersion, since Go's module +// versioning rules make a mismatched major version untrustworthy (see #231). +func resolveVersion(devVersion string, info *debug.BuildInfo) string { + if info == nil { + return devVersion + } + buildVersion := info.Main.Version + if buildVersion == "" || buildVersion == "(devel)" { + return devVersion + } + + buildMajor := majorVersionOf(buildVersion) + pathMajor := majorVersionFromModulePath(info.Main.Path) + if buildMajor != pathMajor { + return devVersion + } + + return buildVersion +} + +// majorVersionOf extracts the numeric major version from a semver-like +// string such as "v1.2.3", returning 0 if it cannot be parsed. +func majorVersionOf(version string) int { + version = strings.TrimPrefix(version, "v") + dot := strings.IndexByte(version, '.') + if dot >= 0 { + version = version[:dot] + } + major, err := strconv.Atoi(version) + if err != nil { + return 0 + } + return major +} + +// majorVersionFromModulePath returns the major version encoded in a module +// path's "/vN" suffix, or 1 if the module path has no such suffix (as is the +// case for v0 and v1 modules). +func majorVersionFromModulePath(modulePath string) int { + suffix := path.Base(modulePath) + if len(suffix) < 2 || suffix[0] != 'v' { + return 1 + } + major, err := strconv.Atoi(suffix[1:]) + if err != nil { + return 1 + } + return major +} + func main() { cmd.Execute() } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..b5bc2a9 --- /dev/null +++ b/main_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "runtime/debug" + "testing" +) + +func TestResolveVersion(t *testing.T) { + cases := []struct { + name string + dev string + info *debug.BuildInfo + want string + }{ + { + name: "nil build info falls back to dev version", + dev: "dev", + info: nil, + want: "dev", + }, + { + name: "devel version falls back to dev version", + dev: "dev", + info: &debug.BuildInfo{Main: debug.Module{Path: "gitea.com/gitea/gitea-mcp", Version: "(devel)"}}, + want: "dev", + }, + { + name: "empty version falls back to dev version", + dev: "dev", + info: &debug.BuildInfo{Main: debug.Module{Path: "gitea.com/gitea/gitea-mcp", Version: ""}}, + want: "dev", + }, + { + name: "v1 version accepted for module path without major suffix", + dev: "dev", + info: &debug.BuildInfo{Main: debug.Module{Path: "gitea.com/gitea/gitea-mcp", Version: "v1.2.3"}}, + want: "v1.2.3", + }, + { + name: "v2 version rejected when module path has no /v2 suffix", + dev: "dev", + info: &debug.BuildInfo{Main: debug.Module{Path: "gitea.com/gitea/gitea-mcp", Version: "v2.0.0"}}, + want: "dev", + }, + { + name: "v2 version accepted when module path has /v2 suffix", + dev: "dev", + info: &debug.BuildInfo{Main: debug.Module{Path: "gitea.com/gitea/gitea-mcp/v2", Version: "v2.0.0"}}, + want: "v2.0.0", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := resolveVersion(tc.dev, tc.info) + if got != tc.want { + t.Errorf("resolveVersion(%q, %+v) = %q, want %q", tc.dev, tc.info, got, tc.want) + } + }) + } +}