Vectrum
Sign In

codey: let a release stamp its version, and go install report its tag

version is now a var that -ldflags "-X main.version=..." can set, which
is how the release archives will say which tag they were built from. A
go install at a tag falls back to the module version in the build info;
a build from a checkout still says what defaultVersion says, because the
pseudo-version Go would stamp there reads worse than the source's own.
The --version line keeps its shape, "codey v0.6.0".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Jjonicommitted 1 parent 5843884commit9f8d022

Showing 3 changed files with 121 additions and 4 deletions.

+5−4cmd/codey/main.goView File
@@ -35,10 +35,11 @@ import (
3535 "github.com/johannSo/codey/internal/websearch"
3636 )
3737
38- // version is what the TUI's welcome block reports. It is a constant rather
39- // than build-stamped: Codey is built from this tree, and a stamp that
40- // can disagree with the source is worse than no stamp.
41- const version = "v0.6.0"
38+ // version is what the TUI's welcome block and --version report. A release
39+ // build stamps it at link time with -ldflags "-X main.version=..."; left
40+ // empty, it is filled in from the module version or the default in
41+ // version.go, which is also where to bump it.
42+ var version string
4243
4344 // effectiveWebMode is the web-search mode this process actually ended up with.
4445 // Asking for one and getting it are different things: -no-web says no, and a
+68−0cmd/codey/version.goaddedView File
@@ -0,0 +1,68 @@
1+ package main
2+
3+ import (
4+ "regexp"
5+ "runtime/debug"
6+ "strings"
7+ )
8+
9+ // defaultVersion is the version of this source tree, and what a build that
10+ // carries no better answer reports: a go build or go run from a checkout. It
11+ // is bumped with each release, in the same commit the tag is cut from, so a
12+ // build from that commit says the same thing however it was made.
13+ const defaultVersion = "v0.6.0"
14+
15+ func init() {
16+ version = resolveVersion(version, debug.ReadBuildInfo)
17+ }
18+
19+ // resolveVersion picks the version to report, trying the sources from the
20+ // most to the least deliberate:
21+ //
22+ // 1. stamped, which is whatever -ldflags "-X main.version=..." put there.
23+ // goreleaser does this for every release archive, from the tag being
24+ // built, so it is the one answer that cannot be stale.
25+ // 2. The main module's version in the build info. go install
26+ // github.com/johannSo/codey/cmd/codey@v0.7.0 records v0.7.0 there, and
27+ // no flag is involved, so without this such an install would report
28+ // whatever defaultVersion said at that commit — right only if someone
29+ // remembered to bump it.
30+ // 3. defaultVersion.
31+ //
32+ // The build info is passed in rather than read here so the order can be
33+ // tested; a test binary's own build info says nothing useful.
34+ func resolveVersion(stamped string, buildInfo func() (*debug.BuildInfo, bool)) string {
35+ if v := strings.TrimSpace(stamped); v != "" {
36+ // goreleaser's {{.Version}} is the tag without its v. The v goes
37+ // back on so a release reads the way every other build does:
38+ // "codey v0.7.0", not sometimes "codey 0.7.0".
39+ if v[0] >= '0' && v[0] <= '9' {
40+ v = "v" + v
41+ }
42+ return v
43+ }
44+ if info, ok := buildInfo(); ok && info != nil && isRelease(info.Main.Version) {
45+ return info.Main.Version
46+ }
47+ return defaultVersion
48+ }
49+
50+ // pseudoVersion matches the tail Go gives a module version that is not a tag:
51+ // a fourteen-digit UTC timestamp and a twelve-character commit hash, as in
52+ // v0.0.0-20261002101500-219352e3b1c4.
53+ var pseudoVersion = regexp.MustCompile(`[-.]\d{14}-[0-9a-f]{12}$`)
54+
55+ // isRelease reports whether a build-info module version names a tag. Since Go
56+ // 1.24 a plain go build in a git checkout stamps one too, derived from the
57+ // nearest tag and the commit, and with no tag in reach that is
58+ // v0.0.0-<time>-<hash>: true, but a worse answer than defaultVersion for
59+ // someone building the tree they have checked out. So only a tag counts.
60+ // "(devel)" is what a build with no version control information gets, and a
61+ // "+dirty" suffix means the tree had uncommitted changes, which is not the
62+ // tagged source either.
63+ func isRelease(v string) bool {
64+ if v == "" || v == "(devel)" || strings.Contains(v, "+") {
65+ return false
66+ }
67+ return !pseudoVersion.MatchString(v)
68+ }
+48−0cmd/codey/version_test.goaddedView File
@@ -0,0 +1,48 @@
1+ package main
2+
3+ import (
4+ "runtime/debug"
5+ "testing"
6+ )
7+
8+ func TestResolveVersionTriesStampThenModuleThenDefault(t *testing.T) {
9+ info := func(v string) func() (*debug.BuildInfo, bool) {
10+ return func() (*debug.BuildInfo, bool) {
11+ return &debug.BuildInfo{Main: debug.Module{Path: "github.com/johannSo/codey", Version: v}}, true
12+ }
13+ }
14+ none := func() (*debug.BuildInfo, bool) { return nil, false }
15+
16+ cases := []struct {
17+ name string
18+ stamped string
19+ info func() (*debug.BuildInfo, bool)
20+ want string
21+ }{
22+ {"goreleaser stamp, which has no v", "0.7.0", info("v0.7.0"), "v0.7.0"},
23+ {"a stamp wins over the module", "v0.8.0", info("v0.7.0"), "v0.8.0"},
24+ {"snapshot stamp", "0.7.1-SNAPSHOT-abc1234", none, "v0.7.1-SNAPSHOT-abc1234"},
25+ {"go install at a tag", "", info("v0.7.0"), "v0.7.0"},
26+ {"go install at a pre-release tag", "", info("v0.7.0-rc.1"), "v0.7.0-rc.1"},
27+ {"go build with no vcs info", "", info("(devel)"), defaultVersion},
28+ {"go build with no tag in reach", "", info("v0.0.0-20261002101500-219352e3b1c4"), defaultVersion},
29+ {"go build past a tag", "", info("v0.7.1-0.20261002101500-219352e3b1c4"), defaultVersion},
30+ {"go build with local changes", "", info("v0.7.0+dirty"), defaultVersion},
31+ {"no build info at all", "", none, defaultVersion},
32+ }
33+ for _, c := range cases {
34+ if got := resolveVersion(c.stamped, c.info); got != c.want {
35+ t.Errorf("%s: resolveVersion(%q) = %q, want %q", c.name, c.stamped, got, c.want)
36+ }
37+ }
38+ }
39+
40+ // Every answer resolveVersion can give still has to satisfy the client's
41+ // pattern, or a release would read to T3 Code as a broken install.
42+ func TestEveryVersionShapeMatchesWhatTheClientLooksFor(t *testing.T) {
43+ for _, v := range []string{defaultVersion, "v0.7.0", "v0.7.0-rc.1", "v0.7.1-SNAPSHOT-abc1234"} {
44+ if !t3VersionPattern.MatchString("codey " + v) {
45+ t.Errorf("%q does not match %s", v, t3VersionPattern)
46+ }
47+ }
48+ }