fix: bug in go mod version reading (#5652)

This commit is contained in:
phgermanov
2026-02-18 16:17:20 +02:00
committed by GitHub
parent c1457f0c04
commit 867fad55b4
2 changed files with 186 additions and 25 deletions
+43 -24
View File
@@ -1,14 +1,14 @@
package versioning
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"errors"
)
// GoMod utility to interact with Go Modules specific versioning
@@ -37,36 +37,55 @@ func (m *GoMod) init() error {
return nil
}
// GetVersion returns the go.mod descriptor version property
// GetVersion returns the version from a VERSION file, or falls back to go.mod
func (m *GoMod) GetVersion() (string, error) {
buildDescriptorFilePath := m.path
var err error
if strings.Contains(m.path, "go.mod") {
buildDescriptorFilePath, err = searchDescriptor([]string{"version.txt", "VERSION"}, m.fileExists)
if err != nil {
err = m.init()
if err != nil {
return "", fmt.Errorf("failed to read file '%v': %w", m.path, err)
}
parsed, err := modfile.Parse(m.path, []byte(m.buildDescriptorContent), nil)
if err != nil {
return "", fmt.Errorf("failed to parse go.mod file: %w", err)
}
if parsed.Module.Mod.Version != "" {
return parsed.Module.Mod.Version, nil
}
return "", fmt.Errorf("failed to retrieve version: %w", err)
}
// If path is not go.mod, just read it as a version file
if filepath.Base(m.path) != "go.mod" {
return m.readVersionFile(m.path)
}
// For go.mod projects, first try to find a dedicated version file
versionFile, versionFileErr := m.findVersionFile()
if versionFileErr == nil {
return m.readVersionFile(versionFile)
}
// Fall back to extracting version from go.mod itself
version, gomodErr := m.extractVersionFromGoMod()
if gomodErr != nil {
return "", fmt.Errorf("no version file found (%v) and %w", versionFileErr, gomodErr)
}
if version == "" {
return "", fmt.Errorf("no version file found (%v) and go.mod has no version", versionFileErr)
}
return version, nil
}
func (m *GoMod) findVersionFile() (string, error) {
return searchDescriptor([]string{"version.txt", "VERSION"}, m.fileExists)
}
func (m *GoMod) readVersionFile(path string) (string, error) {
artifact := &Versionfile{
path: buildDescriptorFilePath,
path: path,
versioningScheme: m.VersioningScheme(),
}
return artifact.GetVersion()
}
func (m *GoMod) extractVersionFromGoMod() (string, error) {
if err := m.init(); err != nil {
return "", fmt.Errorf("failed to read go.mod: %w", err)
}
parsed, err := modfile.Parse(m.path, []byte(m.buildDescriptorContent), nil)
if err != nil {
return "", fmt.Errorf("failed to parse go.mod: %w", err)
}
return parsed.Module.Mod.Version, nil
}
// SetVersion sets the go.mod descriptor version property
func (m *GoMod) SetVersion(v string) error {
return nil
+143 -1
View File
@@ -9,6 +9,148 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGoModGetVersion(t *testing.T) {
testCases := []struct {
name string
goModContent string
versionFile string // "VERSION", "version.txt", or "" for none
versionContent string
fileExistsFunc func(string) (bool, error)
expectedVer string
expectedErr string
}{
{
name: "reads from VERSION file when present",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "VERSION",
versionContent: "1.2.3",
expectedVer: "1.2.3",
expectedErr: "",
},
{
name: "reads from version.txt file when present",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "version.txt",
versionContent: "2.0.0",
expectedVer: "2.0.0",
expectedErr: "",
},
{
name: "prefers version.txt over VERSION",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "both", // special case: create both files
versionContent: "from-version-txt",
expectedVer: "from-version-txt",
expectedErr: "",
},
{
name: "error when no version file and go.mod has no version",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "",
versionContent: "",
expectedVer: "",
expectedErr: "go.mod has no version",
},
{
name: "error includes version file search failure",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "",
versionContent: "",
expectedVer: "",
expectedErr: "no version file found",
},
{
name: "handles version with trailing newline",
goModContent: "module github.com/test/module\n\ngo 1.21",
versionFile: "VERSION",
versionContent: "1.0.0\n",
expectedVer: "1.0.0",
expectedErr: "",
},
{
name: "fileExists returns error falls back to go.mod",
goModContent: "module github.com/test/module\n\ngo 1.21",
fileExistsFunc: func(f string) (bool, error) {
return false, fmt.Errorf("permission denied")
},
expectedVer: "",
expectedErr: "go.mod has no version",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// prepare temp directory and change to it
tmpFolder := t.TempDir()
originalWd, _ := os.Getwd()
os.Chdir(tmpFolder)
defer os.Chdir(originalWd)
goModFilePath := filepath.Join(tmpFolder, "go.mod")
os.WriteFile(goModFilePath, []byte(tc.goModContent), 0o666)
// create version file(s) if specified
fileExistsFunc := tc.fileExistsFunc
if fileExistsFunc == nil {
switch tc.versionFile {
case "VERSION":
os.WriteFile("VERSION", []byte(tc.versionContent), 0o666)
fileExistsFunc = func(f string) (bool, error) {
return f == "VERSION", nil
}
case "version.txt":
os.WriteFile("version.txt", []byte(tc.versionContent), 0o666)
fileExistsFunc = func(f string) (bool, error) {
return f == "version.txt", nil
}
case "both":
os.WriteFile("version.txt", []byte(tc.versionContent), 0o666)
os.WriteFile("VERSION", []byte("from-VERSION"), 0o666)
fileExistsFunc = func(f string) (bool, error) {
return f == "version.txt" || f == "VERSION", nil
}
default:
fileExistsFunc = func(f string) (bool, error) { return false, nil }
}
}
gomod := &GoMod{
path: goModFilePath,
fileExists: fileExistsFunc,
}
// test
version, err := gomod.GetVersion()
// assert
if tc.expectedErr != "" {
assert.ErrorContains(t, err, tc.expectedErr)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tc.expectedVer, version)
})
}
}
func TestGoModGetVersionNonGoMod(t *testing.T) {
t.Run("reads directly when path is not go.mod", func(t *testing.T) {
tmpFolder := t.TempDir()
versionPath := filepath.Join(tmpFolder, "VERSION")
os.WriteFile(versionPath, []byte("5.0.0"), 0o666)
gomod := &GoMod{
path: versionPath,
fileExists: func(f string) (bool, error) { return false, nil },
}
version, err := gomod.GetVersion()
assert.NoError(t, err)
assert.Equal(t, "5.0.0", version)
})
}
func TestGoModGetCoordinates(t *testing.T) {
testCases := []struct {
name string
@@ -27,7 +169,7 @@ func TestGoModGetCoordinates(t *testing.T) {
// prepare
tmpFolder := t.TempDir()
goModFilePath := filepath.Join(tmpFolder, "go.mod")
os.WriteFile(goModFilePath, []byte(fmt.Sprintf("module %s\n\ngo 1.24.0", tc.moduleName)), 0666)
os.WriteFile(goModFilePath, []byte(fmt.Sprintf("module %s\n\ngo 1.24.0", tc.moduleName)), 0o666)
gomod := &GoMod{
path: goModFilePath,
}