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) + } + }) + } +}