From 2b57a7d43bd5c914d238e321a2cdb998213b3b91 Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin <3090891+riccardomanfrin@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:56:50 +0200 Subject: [PATCH] [client, management, misc] expose VCS revision in dev build version output (#6263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor to use a common checker for development version * Adds commit sha to development version for cobra command only Leave dashboard unaffected * Adjust for "v0.31.1-dev" test case which must be considered pre-release * Drop synthetic "dev"/"0.50.0-dev" firewall feature-gate fixtures These test cases encoded the loose strings.Contains(v, "dev") semantics inherited from peerSupportedFirewallFeatures, but NetbirdVersion() never produces those values — only the literal "development" (and now "development-[-dirty]") ever flows through the wire. The agent owns the semantics of an ephemeral development build, so the tests should exercise the strings we actually emit. Replaced with development, development- and development--dirty cases that match the HasPrefix("development") predicate introduced upstream. * Remove unexistent tests on wire format The sha / dirty flag are added only when the CLI asks the version. Account versions is unaffacted and can only strictly match "development" * Adds tests for IsDevelopmentVersion --- client/cmd/version.go | 8 ++- client/internal/lazyconn/support.go | 4 +- client/internal/updater/manager.go | 4 +- .../network_map/controller/controller.go | 3 +- management/server/peer.go | 3 +- management/server/types/account.go | 3 +- management/server/types/account_test.go | 36 +---------- version/version.go | 60 ++++++++++++++++++- version/version_test.go | 26 ++++++++ 9 files changed, 102 insertions(+), 45 deletions(-) create mode 100644 version/version_test.go diff --git a/client/cmd/version.go b/client/cmd/version.go index 249854444..5deeae1a0 100644 --- a/client/cmd/version.go +++ b/client/cmd/version.go @@ -12,7 +12,13 @@ var ( Short: "Print the NetBird's client application version", Run: func(cmd *cobra.Command, args []string) { cmd.SetOut(cmd.OutOrStdout()) - cmd.Println(version.NetbirdVersion()) + out := version.NetbirdVersion() + if version.IsDevelopmentVersion(out) { + if commit := version.NetbirdCommit(); commit != "" { + out += "-" + commit + } + } + cmd.Println(out) }, } ) diff --git a/client/internal/lazyconn/support.go b/client/internal/lazyconn/support.go index 5e765c2d6..cc0e95e53 100644 --- a/client/internal/lazyconn/support.go +++ b/client/internal/lazyconn/support.go @@ -4,6 +4,8 @@ import ( "strings" "github.com/hashicorp/go-version" + + nbversion "github.com/netbirdio/netbird/version" ) var ( @@ -11,7 +13,7 @@ var ( ) func IsSupported(agentVersion string) bool { - if agentVersion == "development" { + if nbversion.IsDevelopmentVersion(agentVersion) { return true } diff --git a/client/internal/updater/manager.go b/client/internal/updater/manager.go index dfcb93177..7fc300739 100644 --- a/client/internal/updater/manager.go +++ b/client/internal/updater/manager.go @@ -19,8 +19,6 @@ import ( const ( latestVersion = "latest" - // this version will be ignored - developmentVersion = "development" ) var errNoUpdateState = errors.New("no update state found") @@ -483,7 +481,7 @@ func (m *Manager) loadAndDeleteUpdateState(ctx context.Context) (*UpdateState, e } func (m *Manager) shouldUpdate(updateVersion *v.Version, forceUpdate bool) bool { - if m.currentVersion == developmentVersion { + if version.IsDevelopmentVersion(m.currentVersion) { log.Debugf("skipping auto-update, running development version") return false } diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 4199b2b27..2b81cd6e5 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -32,6 +32,7 @@ import ( "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/util" + "github.com/netbirdio/netbird/version" ) type Controller struct { @@ -514,7 +515,7 @@ func computeForwarderPort(peers []*nbpeer.Peer, requiredVersion string) int64 { for _, peer := range peers { // Development version is always supported - if peer.Meta.WtVersion == "development" { + if version.IsDevelopmentVersion(peer.Meta.WtVersion) { continue } peerVersion := semver.Canonical("v" + peer.Meta.WtVersion) diff --git a/management/server/peer.go b/management/server/peer.go index 4942e44c1..d4e3ebb49 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -30,6 +30,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/shared/management/status" + "github.com/netbirdio/netbird/version" ) const remoteJobsMinVer = "0.64.0" @@ -372,7 +373,7 @@ func (am *DefaultAccountManager) CreatePeerJob(ctx context.Context, accountID, p } meetMinVer, err := posture.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion) - if !strings.Contains(p.Meta.WtVersion, "dev") && (!meetMinVer || err != nil) { + if !version.IsDevelopmentVersion(p.Meta.WtVersion) && (!meetMinVer || err != nil) { return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer) } diff --git a/management/server/types/account.go b/management/server/types/account.go index dc0c5a685..0d0893e28 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -29,6 +29,7 @@ import ( "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/status" + "github.com/netbirdio/netbird/version" ) const ( @@ -1804,7 +1805,7 @@ func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *n // peerSupportedFirewallFeatures checks if the peer version supports port ranges. func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { - if strings.Contains(peerVer, "dev") { + if version.IsDevelopmentVersion(peerVer) { return supportedFeatures{true, true} } diff --git a/management/server/types/account_test.go b/management/server/types/account_test.go index b55b41638..d8e2e1f8c 100644 --- a/management/server/types/account_test.go +++ b/management/server/types/account_test.go @@ -646,41 +646,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) { expectedPorts: []string{"20-25", "10-100", "22022"}, }, { - name: "dev suffix version supports all features", - peer: &nbpeer.Peer{ - ID: "peer1", - SSHEnabled: true, - Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.50.0-dev", - Flags: nbpeer.Flags{ServerSSHAllowed: true}, - }, - }, - rule: &PolicyRule{ - Protocol: PolicyRuleProtocolTCP, - Ports: []string{"22"}, - }, - base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"}, - expectedPorts: []string{"22", "22022"}, - }, - { - name: "dev suffix version supports all features", - peer: &nbpeer.Peer{ - ID: "peer1", - SSHEnabled: true, - Meta: nbpeer.PeerSystemMeta{ - WtVersion: "dev", - Flags: nbpeer.Flags{ServerSSHAllowed: true}, - }, - }, - rule: &PolicyRule{ - Protocol: PolicyRuleProtocolTCP, - Ports: []string{"22"}, - }, - base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"}, - expectedPorts: []string{"22", "22022"}, - }, - { - name: "development suffix version supports all features", + name: "development version supports all features", peer: &nbpeer.Peer{ ID: "peer1", SSHEnabled: true, diff --git a/version/version.go b/version/version.go index d70a5effa..f33ff133c 100644 --- a/version/version.go +++ b/version/version.go @@ -2,19 +2,75 @@ package version import ( "regexp" + "runtime/debug" + "strings" v "github.com/hashicorp/go-version" ) +// DevelopmentVersion is the value of NetbirdVersion() for non-release builds. +// Wire-format consumers (management server, dashboard) match against this +// string, so it must not change without coordinating those consumers. +const DevelopmentVersion = "development" + // will be replaced with the release version when using goreleaser -var version = "development" +var version = DevelopmentVersion var ( VersionRegexp = regexp.MustCompile("^" + v.VersionRegexpRaw + "$") SemverRegexp = regexp.MustCompile("^" + v.SemverRegexpRaw + "$") ) -// NetbirdVersion returns the Netbird version +// NetbirdVersion returns the Netbird version. For non-release builds the +// value is the literal DevelopmentVersion constant; the VCS revision is +// exposed separately via NetbirdCommit so the wire format stays stable. func NetbirdVersion() string { return version } + +// NetbirdCommit returns the VCS revision (truncated to 12 chars) of the +// build, with a "-dirty" suffix when the working tree was modified. +// Returns an empty string when no build info is embedded (e.g. release +// builds compiled by goreleaser without -buildvcs). +func NetbirdCommit() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + + var revision string + var modified bool + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + case "vcs.modified": + modified = s.Value == "true" + } + } + + if revision == "" { + return "" + } + + if len(revision) > 12 { + revision = revision[:12] + } + + if modified { + revision += "-dirty" + } + return revision +} + +// IsDevelopmentVersion reports whether the given version string identifies +// a non-release / development build. It is the single source of truth for +// "is this a dev build" checks across the codebase; use it instead of +// comparing against the "development" literal or ad-hoc substring checks. +// +// Matches the bare DevelopmentVersion constant as well as any future +// extension such as "development-" or "development--dirty", +// while excluding tagged prereleases like "v0.31.1-dev". +func IsDevelopmentVersion(v string) bool { + return strings.HasPrefix(v, DevelopmentVersion) +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 000000000..47b77b50d --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,26 @@ +package version + +import "testing" + +func TestIsDevelopmentVersion(t *testing.T) { + tests := []struct { + version string + want bool + }{ + {"development", true}, + {"development-0823f3ff9ab1", true}, + {"development-0823f3ff9ab1-dirty", true}, + {"0.50.0", false}, + {"v0.31.1-dev", false}, + {"1.0.0-dev", false}, + {"dev", false}, + {"", false}, + } + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + if got := IsDevelopmentVersion(tt.version); got != tt.want { + t.Errorf("IsDevelopmentVersion(%q) = %v, want %v", tt.version, got, tt.want) + } + }) + } +}