summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2026-04-13 19:49:30 +0000
committerBrad Fitzpatrick <brad@danga.com>2026-04-13 15:20:56 -0700
commit9dfe7875fdc65841ef0212dbb4f6f4d6d1b79ad6 (patch)
tree075736f2d9200923efb41490c4d0e8fd616c4b4f
parent5a7ef4a533ed5cb3f5c2934e2498b19cd1531d26 (diff)
downloadtailscale-9dfe7875fdc65841ef0212dbb4f6f4d6d1b79ad6.tar.xz
tailscale-9dfe7875fdc65841ef0212dbb4f6f4d6d1b79ad6.zip
version: show tailscale/go toolchain git hash in version output
When built with the Tailscale Go toolchain, include the toolchain's git revision in the version output. The non-JSON output shows the first 10 hex digits: go version: go1.26.2 (tailscale/go dfe2a5fd8e) The JSON output includes the full hash as "tailscaleGoGitHash", or omits the field when not using tsgo. The toolchain rev is read via a separate sync.OnceValue rather than piggybacking on getEmbeddedInfo, because that function discards all data when VCS fields are absent (e.g. in test binaries), while the tailscale.toolchain.rev setting is still present. Also add a CI-only test verifying tailscaleToolchainRev is non-empty when built with the tailscale_go build tag. Fixes #19374 Change-Id: Ied0b16d7aead5471d8c614c30cba8b0dcf80c691 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
-rw-r--r--version/print.go9
-rw-r--r--version/prop.go28
-rw-r--r--version/version.go17
-rw-r--r--version/version_internal_test.go28
4 files changed, 69 insertions, 13 deletions
diff --git a/version/print.go b/version/print.go
index ca62226ee..3b4a256cf 100644
--- a/version/print.go
+++ b/version/print.go
@@ -24,7 +24,14 @@ var stringLazy = sync.OnceValue(func() string {
if extraGitCommitStamp != "" {
fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommitStamp)
}
- fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
+ if tsGoRev := tailscaleToolchainRev(); tsGoRev != "" {
+ if len(tsGoRev) > 10 {
+ tsGoRev = tsGoRev[:10]
+ }
+ fmt.Fprintf(&ret, " go version: %s (tailscale/go %s)\n", runtime.Version(), tsGoRev)
+ } else {
+ fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
+ }
return strings.TrimSpace(ret.String())
})
diff --git a/version/prop.go b/version/prop.go
index 36d769917..59ca74086 100644
--- a/version/prop.go
+++ b/version/prop.go
@@ -312,6 +312,11 @@ type Meta struct {
// GitCommitTime is the commit time of the git commit in GitCommit.
GitCommitTime string `json:"gitCommitTime,omitempty"`
+ // TailscaleGoGitHash is the git commit hash from
+ // https://github.com/tailscale/go used to build this binary, if built
+ // with the Tailscale Go toolchain. Otherwise it is empty.
+ TailscaleGoGitHash string `json:"tailscaleGoGitHash,omitempty"`
+
// Cap is the current Tailscale capability version. It's a monotonically
// incrementing integer that's incremented whenever a new capability is
// added.
@@ -324,17 +329,18 @@ var getMeta lazy.SyncValue[Meta]
func GetMeta() Meta {
return getMeta.Get(func() Meta {
return Meta{
- MajorMinorPatch: majorMinorPatch(),
- Short: Short(),
- Long: Long(),
- GitCommitTime: getEmbeddedInfo().commitTime,
- GitCommit: gitCommit(),
- GitDirty: gitDirty(),
- OSVariant: osVariant(),
- ExtraGitCommit: extraGitCommitStamp,
- IsDev: isDev(),
- UnstableBranch: IsUnstableBuild(),
- Cap: int(tailcfg.CurrentCapabilityVersion),
+ MajorMinorPatch: majorMinorPatch(),
+ Short: Short(),
+ Long: Long(),
+ GitCommitTime: getEmbeddedInfo().commitTime,
+ GitCommit: gitCommit(),
+ GitDirty: gitDirty(),
+ OSVariant: osVariant(),
+ ExtraGitCommit: extraGitCommitStamp,
+ IsDev: isDev(),
+ UnstableBranch: IsUnstableBuild(),
+ TailscaleGoGitHash: tailscaleToolchainRev(),
+ Cap: int(tailcfg.CurrentCapabilityVersion),
}
})
}
diff --git a/version/version.go b/version/version.go
index 7d8efc375..8ffc21832 100644
--- a/version/version.go
+++ b/version/version.go
@@ -146,6 +146,23 @@ var getEmbeddedInfo = sync.OnceValue(func() embeddedInfo {
return ret
})
+// tailscaleToolchainRev returns the git hash of the Tailscale Go toolchain
+// used to build this binary, if any. It is read separately from getEmbeddedInfo
+// because that function discards build info when VCS fields are missing (e.g.
+// in test binaries), but the toolchain rev is still present.
+var tailscaleToolchainRev = sync.OnceValue(func() string {
+ bi, ok := debug.ReadBuildInfo()
+ if !ok {
+ return ""
+ }
+ for _, s := range bi.Settings {
+ if s.Key == "tailscale.toolchain.rev" {
+ return s.Value
+ }
+ }
+ return ""
+})
+
func gitCommit() string {
if gitCommitStamp != "" {
return gitCommitStamp
diff --git a/version/version_internal_test.go b/version/version_internal_test.go
index c78df4ff8..72b2dcd5f 100644
--- a/version/version_internal_test.go
+++ b/version/version_internal_test.go
@@ -3,7 +3,13 @@
package version
-import "testing"
+import (
+ "os/exec"
+ "strings"
+ "testing"
+
+ "tailscale.com/util/cibuild"
+)
func TestIsValidLongWithTwoRepos(t *testing.T) {
tests := []struct {
@@ -26,6 +32,26 @@ func TestIsValidLongWithTwoRepos(t *testing.T) {
}
}
+func TestTailscaleToolchainRev(t *testing.T) {
+ out, err := exec.Command("go", "env", "GOROOT").Output()
+ if err != nil {
+ t.Fatalf("go env GOROOT: %v", err)
+ }
+ goRoot := strings.TrimSpace(string(out))
+ isTsgo := strings.Contains(goRoot, "/.cache/tsgo/")
+ if !cibuild.On() && !isTsgo {
+ t.Skip("skipping; not in CI and not using the Tailscale Go toolchain")
+ }
+ if !isTailscaleGo {
+ t.Skip("skipping; not built with tailscale_go build tag")
+ }
+ rev := tailscaleToolchainRev()
+ if rev == "" {
+ t.Fatal("tailscale.toolchain.rev is empty in build info; expected non-empty when using tsgo")
+ }
+ t.Logf("tailscale.toolchain.rev = %s", rev)
+}
+
func TestPrepExeNameForCmp(t *testing.T) {
cases := []struct {
exe string