diff --git a/cmd/root.go b/cmd/root.go index a34fca3a9f..6dd6c2e739 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,10 +20,12 @@ import ( "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" + "knative.dev/func/pkg/version" ) -// DefaultVersion when building source directly (bypassing the Makefile) -const DefaultVersion = "v0.0.0+source" +// DefaultVersion when building source directly (bypassing the Makefile). +// Delegates to version.DefaultVers so the fallback is defined in one place. +const DefaultVersion = version.DefaultVers // DefaultNamespace is the global static default namespace, and is equivalent // to the Kubernetes default namespace. diff --git a/go.mod b/go.mod index 574bdd01e2..01a387d95b 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/BurntSushi/toml v1.6.0 github.com/Masterminds/semver v1.5.0 + github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b @@ -90,7 +91,6 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.2 // indirect github.com/Azure/go-autorest/tracing v0.6.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect diff --git a/pkg/app/app.go b/pkg/app/app.go index eae55e63a8..9ea836d40f 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -32,7 +32,7 @@ func Main() { cfg := cmd.RootCommandConfig{ Name: "func", Version: cmd.Version{ - Vers: version.Vers, + Vers: version.Get().Original(), Kver: version.Kver, Hash: version.Hash, }} diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index b43715ee34..491997a084 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -6,12 +6,12 @@ import ( "strings" "github.com/modelcontextprotocol/go-sdk/mcp" + "knative.dev/func/pkg/version" ) const ( - name = "func" - title = "func" - version = "0.1.0" + name = "func" + title = "func" ) // NOTE: Invoking prompts in some interfaces (such as Claude Code) when all @@ -78,7 +78,7 @@ func New(options ...Option) *Server { &mcp.Implementation{ Name: name, Title: title, - Version: version}, + Version: version.Get().String()}, &mcp.ServerOptions{ Instructions: instructions(s.readonly), HasPrompts: true, diff --git a/pkg/version/version.go b/pkg/version/version.go index 114c9658bc..d4e4894965 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,3 +1,28 @@ package version +import "github.com/Masterminds/semver/v3" + var Vers, Kver, Hash string + +// DefaultVers is the fallback version used when no build-time version was +// injected (e.g. source builds that bypass the Makefile). +const DefaultVers = "v0.0.0+source" + +// Get returns the parsed semver for this binary. When no build-time version +// was injected via ldflags, DefaultVers is used. If the injected string is +// unparseable, DefaultVers is used as a safe fallback. +// String() returns a clean semver without the leading "v" (e.g. "0.0.0+source"), +// suitable for machine-readable consumers such as the MCP server. +// Original() round-trips the injected string verbatim, preserving the leading +// "v" preferred by human-readable output. +func Get() *semver.Version { + s := Vers + if s == "" { + s = DefaultVers + } + v, err := semver.NewVersion(s) // permissive: accepts leading 'v' + if err != nil { + v, _ = semver.NewVersion(DefaultVers) + } + return v +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 0000000000..477ab211c6 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,63 @@ +package version_test + +import ( + "testing" + + "knative.dev/func/pkg/version" +) + +// TestGet_Empty verifies that Get returns the DefaultVers fallback when no +// build-time version has been injected (Vers == ""). +func TestGet_Empty(t *testing.T) { + orig := version.Vers + version.Vers = "" + defer func() { version.Vers = orig }() + + v := version.Get() + if v == nil { + t.Fatal("expected non-nil *semver.Version") + } + // String() must be clean semver without a leading 'v' + if got := v.String(); got != "0.0.0+source" { + t.Errorf("String() = %q; want %q", got, "0.0.0+source") + } + // Original() must round-trip the full default string including 'v' + if got := v.Original(); got != "v0.0.0+source" { + t.Errorf("Original() = %q; want %q", got, "v0.0.0+source") + } +} + +// TestGet_InjectedVersion verifies that a build-time version is parsed and +// exposed correctly. +func TestGet_InjectedVersion(t *testing.T) { + orig := version.Vers + version.Vers = "v1.2.3" + defer func() { version.Vers = orig }() + + v := version.Get() + if v == nil { + t.Fatal("expected non-nil *semver.Version") + } + if got := v.String(); got != "1.2.3" { + t.Errorf("String() = %q; want %q", got, "1.2.3") + } + if got := v.Original(); got != "v1.2.3" { + t.Errorf("Original() = %q; want %q", got, "v1.2.3") + } +} + +// TestGet_InvalidFallsBack verifies that an unparseable injected version does +// not panic and falls back to DefaultVers. +func TestGet_InvalidFallsBack(t *testing.T) { + orig := version.Vers + version.Vers = "not-a-semver!!!" + defer func() { version.Vers = orig }() + + v := version.Get() + if v == nil { + t.Fatal("expected non-nil *semver.Version even for invalid input") + } + if got := v.String(); got != "0.0.0+source" { + t.Errorf("String() = %q; want %q on invalid input", got, "0.0.0+source") + } +}