Compare commits

..

1 Commits

Author SHA1 Message Date
Lunny Xiao 6694ec6873 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 <codet@commitgo.dev> (GPT-5-Codex)
2026-08-23 23:35:02 -07:00
8 changed files with 118 additions and 71 deletions
-2
View File
@@ -31,7 +31,5 @@ USER nonroot:nonroot
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.source="https://gitea.com/gitea/gitea-mcp"
LABEL org.opencontainers.image.title="Gitea MCP Server"
LABEL org.opencontainers.image.description="Model Context Protocol server connecting Gitea instances to MCP clients, published as docker.gitea.com/gitea-mcp-server."
CMD ["/app/gitea-mcp"]
-2
View File
@@ -10,8 +10,6 @@
Download a binary from the [releases page](https://gitea.com/gitea/gitea-mcp/releases) and put it in your `PATH`, use the `docker.gitea.com/gitea-mcp-server` image, or build from source into `$GOPATH/bin` with `make` and Go 1.26 or later:
> The published Docker image is `docker.gitea.com/gitea-mcp-server`, which does not match the `gitea-mcp` repository name. This is a known mismatch, tracked in [issue #181](https://gitea.com/gitea/gitea-mcp/issues/181); the image name stays as-is for now to avoid breaking existing installs. See [docs/adr/docker-image-naming.md](docs/adr/docker-image-naming.md) for details.
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
cd gitea-mcp
-2
View File
@@ -10,8 +10,6 @@
可从 [发布页面](https://gitea.com/gitea/gitea-mcp/releases) 下载二进制文件并放入 `PATH`,或使用 `docker.gitea.com/gitea-mcp-server` 镜像,也可用 `make` 和 Go 1.26 及以上从源码构建到 `$GOPATH/bin`
> 已发布的 Docker 镜像名为 `docker.gitea.com/gitea-mcp-server`,与 `gitea-mcp` 仓库名不一致。这是 [issue #181](https://gitea.com/gitea/gitea-mcp/issues/181) 中记录的已知不一致,为避免破坏现有安装,镜像名暂不改动。详见 [docs/adr/docker-image-naming.md](docs/adr/docker-image-naming.md)。
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
cd gitea-mcp
-2
View File
@@ -10,8 +10,6 @@
可從 [發布頁面](https://gitea.com/gitea/gitea-mcp/releases) 下載二進位檔並放入 `PATH`,或使用 `docker.gitea.com/gitea-mcp-server` 映像檔,也可用 `make` 與 Go 1.26 以上從原始碼建置到 `$GOPATH/bin`
> 已發布的 Docker 映像檔名稱為 `docker.gitea.com/gitea-mcp-server`,與 `gitea-mcp` 倉庫名稱不一致。這是 [issue #181](https://gitea.com/gitea/gitea-mcp/issues/181) 中記錄的已知不一致,為避免破壞現有安裝,映像檔名稱暫不變更。詳見 [docs/adr/docker-image-naming.md](docs/adr/docker-image-naming.md)。
```bash
git clone https://gitea.com/gitea/gitea-mcp.git
cd gitea-mcp
-29
View File
@@ -1,29 +0,0 @@
# Docker image naming mismatch (issue #181)
## Current state
The project repository is `gitea.com/gitea/gitea-mcp`, but the Docker image is
published as `docker.gitea.com/gitea-mcp-server`. This mismatch was raised in
[issue #181](https://gitea.com/gitea/gitea-mcp/issues/181).
## Maintainers' direction
Per the issue discussion, the maintainers' preferred long-term name is
`gitea/mcp-server`, matching the naming scheme used by `gitea/act_runner`
(published as a `runner`-style short name) and by comparable projects such as
`github/github-mcp-server`. The name `mcp-server` was preferred over
`gitea-mcp-server` because the repository is primarily run as a local client
process, not a traditional long-running server.
## Decision
No rename has happened yet; the discussion was explicitly left open in the
issue. The current image name, `docker.gitea.com/gitea-mcp-server`, remains in
use so that existing user configurations and documentation keep working.
`org.opencontainers.image.title` and `org.opencontainers.image.description`
labels were added to the `Dockerfile` to make the image self-describing
regardless of the tag it is published under.
If the image is renamed in the future, update the `docker.gitea.com/gitea-mcp-server`
references across `README.md`, `README.zh-cn.md` and `README.zh-tw.md` together
with this document.
+57 -2
View File
@@ -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()
}
+61
View File
@@ -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)
}
})
}
}
-32
View File
@@ -22,38 +22,6 @@ var readmeAccessLabels = map[string]map[string]string{
"../README.zh-tw.md": {"讀取": "read", "寫入": "write"},
}
// publishedDockerImage is the image name published for this project. See
// docs/adr/docker-image-naming.md for why it differs from the repository name.
const publishedDockerImage = "docker.gitea.com/gitea-mcp-server"
// dockerImageReference matches any docker.gitea.com/<name> image reference so
// stray typos or partial renames in the README files can be caught.
var dockerImageReference = regexp.MustCompile(`docker\.gitea\.com/[a-zA-Z0-9._-]+`)
// TestReadmeDockerImageReferencesAreConsistent ensures every README mentions
// the same, currently published Docker image name. A partial rename, where
// one example is updated but another is missed, would otherwise leave users
// copying a command that pulls a nonexistent image.
func TestReadmeDockerImageReferencesAreConsistent(t *testing.T) {
for _, path := range []string{"../README.md", "../README.zh-cn.md", "../README.zh-tw.md"} {
t.Run(filepath.Base(path), func(t *testing.T) {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
references := dockerImageReference.FindAllString(string(content), -1)
if len(references) == 0 {
t.Fatalf("no docker.gitea.com image reference found in %s", path)
}
for _, ref := range references {
if ref != publishedDockerImage {
t.Errorf("found image reference %q in %s, want %q", ref, path, publishedDockerImage)
}
}
})
}
}
// toolInfo is what TestReadmeToolTables tracks per tool, both as registered
// in code and as documented in a README, so the two can be compared.
type toolInfo struct {