Skip to content

Commit 5ae71f0

Browse files
authored
feat: Add buildinfo package to embed version (#840)
This also resolves build time and commit hash using the Go 1.18 debug/buildinfo package. An external URL is outputted on running version as well to easily route the caller to a release or commit.
1 parent d4e26ff commit 5ae71f0

File tree

6 files changed

+137
-3
lines changed

6 files changed

+137
-3
lines changed

.goreleaser.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ before:
1616
builds:
1717
- id: coder-slim
1818
dir: cmd/coder
19-
ldflags: ["-s -w"]
19+
ldflags: ["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
2020
env: [CGO_ENABLED=0]
2121
goos: [darwin, linux, windows]
2222
goarch: [amd64]
@@ -28,7 +28,7 @@ builds:
2828
- id: coder
2929
dir: cmd/coder
3030
flags: [-tags=embed]
31-
ldflags: ["-s -w"]
31+
ldflags: ["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
3232
env: [CGO_ENABLED=0]
3333
goos: [darwin, linux, windows]
3434
goarch: [amd64, arm64]
@@ -58,3 +58,6 @@ nfpms:
5858

5959
release:
6060
ids: [coder, packages]
61+
62+
snapshot:
63+
name_template: '{{ .Version }}-devel+{{ .ShortCommit }}'

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"coderd",
66
"coderdtest",
77
"codersdk",
8+
"devel",
89
"drpc",
910
"drpcconn",
1011
"drpcmux",

cli/buildinfo/buildinfo.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package buildinfo
2+
3+
import (
4+
"path"
5+
"runtime/debug"
6+
"sync"
7+
"time"
8+
9+
"golang.org/x/mod/semver"
10+
)
11+
12+
var (
13+
buildInfo *debug.BuildInfo
14+
buildInfoValid bool
15+
readBuildInfo sync.Once
16+
17+
// Injected with ldflags at build!
18+
tag string
19+
)
20+
21+
// Version returns the semantic version of the build.
22+
// Use golang.org/x/mod/semver to compare versions.
23+
func Version() string {
24+
revision, valid := revision()
25+
if valid {
26+
revision = "+" + revision[:7]
27+
}
28+
if tag == "" {
29+
return "v0.0.0-devel" + revision
30+
}
31+
if semver.Build(tag) == "" {
32+
tag += revision
33+
}
34+
return "v" + tag
35+
}
36+
37+
// ExternalURL returns a URL referencing the current Coder version.
38+
// For production builds, this will link directly to a release.
39+
// For development builds, this will link to a commit.
40+
func ExternalURL() string {
41+
repo := "https://github.com/coder/coder"
42+
revision, valid := revision()
43+
if !valid {
44+
return repo
45+
}
46+
return path.Join(repo, "commit", revision)
47+
}
48+
49+
// Time returns when the Git revision was published.
50+
func Time() (time.Time, bool) {
51+
value, valid := find("vcs.time")
52+
if !valid {
53+
return time.Time{}, false
54+
}
55+
parsed, err := time.Parse(time.RFC3339, value)
56+
if err != nil {
57+
panic("couldn't parse time: " + err.Error())
58+
}
59+
return parsed, true
60+
}
61+
62+
// revision returns the Git hash of the build.
63+
func revision() (string, bool) {
64+
return find("vcs.revision")
65+
}
66+
67+
// find panics if a setting with the specific key was not
68+
// found in the build info.
69+
func find(key string) (string, bool) {
70+
readBuildInfo.Do(func() {
71+
buildInfo, buildInfoValid = debug.ReadBuildInfo()
72+
})
73+
if !buildInfoValid {
74+
panic("couldn't read build info")
75+
}
76+
for _, setting := range buildInfo.Settings {
77+
if setting.Key != key {
78+
continue
79+
}
80+
return setting.Value, true
81+
}
82+
return "", false
83+
}

cli/buildinfo/buildinfo_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package buildinfo_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"golang.org/x/mod/semver"
8+
9+
"github.com/coder/coder/cli/buildinfo"
10+
)
11+
12+
func TestBuildInfo(t *testing.T) {
13+
t.Parallel()
14+
t.Run("Version", func(t *testing.T) {
15+
t.Parallel()
16+
version := buildinfo.Version()
17+
require.True(t, semver.IsValid(version))
18+
prerelease := semver.Prerelease(version)
19+
require.Equal(t, "-devel", prerelease)
20+
require.Equal(t, "v0", semver.Major(version))
21+
})
22+
t.Run("ExternalURL", func(t *testing.T) {
23+
t.Parallel()
24+
require.Equal(t, "https://github.com/coder/coder", buildinfo.ExternalURL())
25+
})
26+
// Tests don't include Go build info.
27+
t.Run("NoTime", func(t *testing.T) {
28+
t.Parallel()
29+
_, valid := buildinfo.Time()
30+
require.False(t, valid)
31+
})
32+
}

cli/root.go

+15
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"net/url"
55
"os"
66
"strings"
7+
"time"
78

89
"github.com/charmbracelet/lipgloss"
910
"github.com/kirsle/configdir"
1011
"github.com/mattn/go-isatty"
1112
"github.com/spf13/cobra"
1213

14+
"github.com/coder/coder/cli/buildinfo"
1315
"github.com/coder/coder/cli/cliui"
1416
"github.com/coder/coder/cli/config"
1517
"github.com/coder/coder/codersdk"
@@ -28,6 +30,7 @@ const (
2830
func Root() *cobra.Command {
2931
cmd := &cobra.Command{
3032
Use: "coder",
33+
Version: buildinfo.Version(),
3134
SilenceUsage: true,
3235
Long: ` ▄█▀ ▀█▄
3336
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
@@ -55,6 +58,7 @@ func Root() *cobra.Command {
5558
`Flags:`, header.Render("Flags:"),
5659
`Additional help topics:`, header.Render("Additional help:"),
5760
).Replace(cmd.UsageTemplate()))
61+
cmd.SetVersionTemplate(versionTemplate())
5862

5963
cmd.AddCommand(
6064
configSSH(),
@@ -142,3 +146,14 @@ func isTTY(cmd *cobra.Command) bool {
142146
}
143147
return isatty.IsTerminal(file.Fd())
144148
}
149+
150+
func versionTemplate() string {
151+
template := `Coder {{printf "%s" .Version}}`
152+
buildTime, valid := buildinfo.Time()
153+
if valid {
154+
template += " " + buildTime.Format(time.UnixDate)
155+
}
156+
template += "\r\n" + buildinfo.ExternalURL()
157+
template += "\r\n"
158+
return template
159+
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ require (
237237
github.com/zclconf/go-cty v1.10.0 // indirect
238238
github.com/zeebo/errs v1.2.2 // indirect
239239
go.opencensus.io v0.23.0 // indirect
240-
golang.org/x/mod v0.5.1 // indirect
240+
golang.org/x/mod v0.5.1
241241
golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
242242
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
243243
golang.org/x/text v0.3.7 // indirect

0 commit comments

Comments
 (0)