Skip to content

Commit 646a22a

Browse files
committed
fix: resolve the reported SDK version from build info at runtime
The default X-Spoo-Client tag was built from a hand-bumped Version constant that has said dev since v0.1.0, so every consumer reported sdk-go/dev. The tag version now comes from the consuming binary's runtime/debug build info, cached with sync.OnceValue and stripped of the v prefix to match sdk-ts. Version stays as a manual override, and in-repo or vendored builds without module info still fall back to dev.
1 parent b821c7e commit 646a22a

2 files changed

Lines changed: 162 additions & 4 deletions

File tree

version.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,86 @@
11
package spoo
22

3-
import "regexp"
3+
import (
4+
"regexp"
5+
"runtime/debug"
6+
"strings"
7+
"sync"
8+
)
49

5-
// Version is the SDK release, updated on each tag.
10+
// modulePath is the SDK's own module path, looked up in the consuming
11+
// binary's build info to discover the release version at runtime.
12+
const modulePath = "github.com/spoo-me/spoo-go"
13+
14+
// Version overrides the version reported in the default X-Spoo-Client
15+
// tag. It is normally left at its zero state ("dev"): a Go library has
16+
// no build step, so instead of hand-bumping a constant on every tag,
17+
// the SDK resolves its release version at runtime from the consuming
18+
// binary's build info (see resolveVersion). Set Version to any other
19+
// value to force what the default tag reports.
620
var Version = "dev"
721

822
var versionRe = regexp.MustCompile(`^[A-Za-z0-9._-]{1,16}$`)
923

24+
// buildInfoVersion caches the one-time build info scan. The module
25+
// version cannot change during a process lifetime, so the scan runs
26+
// once and the result is reused for every client.
27+
var buildInfoVersion = sync.OnceValue(func() string {
28+
info, ok := debug.ReadBuildInfo()
29+
if !ok {
30+
return ""
31+
}
32+
return versionFromBuildInfo(info)
33+
})
34+
35+
// versionFromBuildInfo scans a binary's module dependency list for the
36+
// SDK and returns its bare semver ("0.5.3"), or "" when the SDK is not
37+
// present as a dependency (the SDK's own tests, vendored builds
38+
// stripped of module info).
39+
func versionFromBuildInfo(info *debug.BuildInfo) string {
40+
for _, dep := range info.Deps {
41+
if dep.Path != modulePath {
42+
continue
43+
}
44+
v := dep.Version
45+
if dep.Replace != nil {
46+
v = dep.Replace.Version
47+
}
48+
return normalizeModuleVersion(v)
49+
}
50+
return ""
51+
}
52+
53+
// normalizeModuleVersion converts a Go module version ("v0.5.3") to
54+
// the bare form the X-Spoo-Client tag carries ("0.5.3", matching what
55+
// sdk-ts sends). Placeholder versions from local replacements report
56+
// as absent.
57+
func normalizeModuleVersion(v string) string {
58+
if v == "" || v == "(devel)" {
59+
return ""
60+
}
61+
return strings.TrimPrefix(v, "v")
62+
}
63+
64+
// resolveVersion picks the version for the default client tag: an
65+
// explicit Version override wins, then the release version recorded in
66+
// the consuming binary's build info, then "dev".
67+
func resolveVersion() string {
68+
if Version != "dev" {
69+
return Version
70+
}
71+
if v := buildInfoVersion(); v != "" {
72+
return v
73+
}
74+
return "dev"
75+
}
76+
1077
// defaultClientTag identifies the SDK (and its version, when
1178
// well-formed) to the backend so API traffic can be attributed per
1279
// client. Apps building on the SDK set their own tag with
1380
// option.WithClientTag.
1481
func defaultClientTag() string {
15-
if versionRe.MatchString(Version) {
16-
return "sdk-go/" + Version
82+
if v := resolveVersion(); versionRe.MatchString(v) {
83+
return "sdk-go/" + v
1784
}
1885
return "sdk-go"
1986
}

version_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package spoo
2+
3+
import (
4+
"runtime/debug"
5+
"testing"
6+
)
7+
8+
func TestVersionFromBuildInfo(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
info *debug.BuildInfo
12+
want string
13+
}{
14+
{
15+
name: "module absent",
16+
info: &debug.BuildInfo{Deps: []*debug.Module{
17+
{Path: "github.com/other/mod", Version: "v1.0.0"},
18+
}},
19+
want: "",
20+
},
21+
{
22+
name: "no deps",
23+
info: &debug.BuildInfo{},
24+
want: "",
25+
},
26+
{
27+
name: "module present",
28+
info: &debug.BuildInfo{Deps: []*debug.Module{
29+
{Path: "github.com/other/mod", Version: "v1.0.0"},
30+
{Path: modulePath, Version: "v0.5.3"},
31+
}},
32+
want: "0.5.3",
33+
},
34+
{
35+
name: "replaced module uses replacement version",
36+
info: &debug.BuildInfo{Deps: []*debug.Module{
37+
{Path: modulePath, Version: "v0.5.3", Replace: &debug.Module{
38+
Path: "github.com/fork/spoo-go", Version: "v0.5.4",
39+
}},
40+
}},
41+
want: "0.5.4",
42+
},
43+
{
44+
name: "local replacement reports absent",
45+
info: &debug.BuildInfo{Deps: []*debug.Module{
46+
{Path: modulePath, Version: "v0.5.3", Replace: &debug.Module{
47+
Path: "../spoo-go", Version: "(devel)",
48+
}},
49+
}},
50+
want: "",
51+
},
52+
}
53+
for _, tt := range tests {
54+
if got := versionFromBuildInfo(tt.info); got != tt.want {
55+
t.Errorf("%s: versionFromBuildInfo() = %q, want %q", tt.name, got, tt.want)
56+
}
57+
}
58+
}
59+
60+
func TestNormalizeModuleVersion(t *testing.T) {
61+
for in, want := range map[string]string{
62+
"v0.5.3": "0.5.3",
63+
"0.5.3": "0.5.3",
64+
"(devel)": "",
65+
"": "",
66+
} {
67+
if got := normalizeModuleVersion(in); got != want {
68+
t.Errorf("normalizeModuleVersion(%q) = %q, want %q", in, got, want)
69+
}
70+
}
71+
}
72+
73+
// TestResolveVersionFallsBackToDev pins the in-repo behavior: the test
74+
// binary's build info has no spoo-go dependency entry, so resolution
75+
// falls through to "dev". The real-consumption path (a binary that
76+
// depends on the SDK reporting the tagged semver) is exercised by
77+
// downstream consumers such as spoo-cli.
78+
func TestResolveVersionFallsBackToDev(t *testing.T) {
79+
if v := resolveVersion(); v != "dev" {
80+
t.Fatalf("resolveVersion() = %q, want dev", v)
81+
}
82+
}
83+
84+
func TestResolveVersionHonorsOverride(t *testing.T) {
85+
orig := Version
86+
defer func() { Version = orig }()
87+
Version = "9.9.9"
88+
if v := resolveVersion(); v != "9.9.9" {
89+
t.Fatalf("resolveVersion() with override = %q, want 9.9.9", v)
90+
}
91+
}

0 commit comments

Comments
 (0)