Skip to content

Commit

Permalink
feat(app): more robust version reporting
Browse files Browse the repository at this point in the history
This is messy, but unfortunately the Go runtime doesn't provide
sufficient information (e.g. tag), and I want to gracefully handle all
possible variations of the `git describe` output, and also the case when
the binary is not built with -ldflags.
  • Loading branch information
imiric committed Apr 18, 2024
1 parent 3b8373c commit e72df14
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 13 deletions.
12 changes: 9 additions & 3 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

// App is the application.
type App struct {
name string
ctx *actx.Context
cli *cli.CLI
args []string
Expand All @@ -35,13 +36,17 @@ type App struct {
// the directory where application data will be stored, though this can be
// overriden with the DISCO_DATA_DIR environment variable, and --data-dir CLI
// flag.
func New(dataDir string, opts ...Option) (*App, error) {
func New(name, dataDir string, opts ...Option) (*App, error) {
version, err := actx.GetVersion()
if err != nil {
return nil, err
}
defaultCtx := &actx.Context{
Ctx: context.Background(),
Logger: slog.Default(),
Version: version,
}
app := &App{ctx: defaultCtx}
app := &App{name: name, ctx: defaultCtx}

for _, opt := range opts {
opt(app)
Expand All @@ -54,7 +59,8 @@ func New(dataDir string, opts ...Option) (*App, error) {
}
app.ctx.UUIDGen = uuidgen

app.cli, err = cli.New(dataDir, app.ctx.Version)
ver := fmt.Sprintf("%s %s", app.name, app.ctx.Version.String())
app.cli, err = cli.New(dataDir, ver)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions app/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func (c *Init) Run(appCtx *actx.Context) error {
return fmt.Errorf("failed generating the server TLS certificate: %w", err)
}

appCtx.User, err = appCtx.DB.Init(appCtx.Version, tlsCert, tlsPrivKey, rndSAN, appCtx.Logger)
appCtx.User, err = appCtx.DB.Init(appCtx.Version.Semantic, tlsCert, tlsPrivKey, rndSAN, appCtx.Logger)
if err != nil {
return aerrors.NewRuntimeError("failed initializing database", err, "")
}

if err = appCtx.Store.Init(appCtx.Version, appCtx.Logger); err != nil {
if err = appCtx.Store.Init(appCtx.Version.Semantic, appCtx.Logger); err != nil {
return aerrors.NewRuntimeError("failed initializing store", err, "")
}

Expand Down
3 changes: 1 addition & 2 deletions app/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ type Context struct {
User *models.User // current app user

// Metadata
Version string // short semantic app version
VersionFull string // app version including Go runtime information
Version *VersionInfo
VersionInit string // app version the DB was initialized with
}

Expand Down
127 changes: 127 additions & 0 deletions app/context/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package context

import (
"fmt"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"strings"
)

// The semantic version of the application.
// This can be overriden by vcsVersion.
const version = "0.0.0"

var (
vcsVersion string // version from VCS set at build time
// Simplified semver regex. A more complete one can be found on https://semver.org/
versionRx = regexp.MustCompile(`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*).*`)
shaRx = regexp.MustCompile(`^g[0-9a-f]{6,}`)
)

// VersionInfo stores app version information.
type VersionInfo struct {
Commit string
Semantic string
TagDistance int // number of commits since latest tag
Dirty bool
goInfo string
}

// GetVersion returns the app version including VCS and Go runtime information.
// It first reads the VCS version set at build time, and falls back to the VCS
// information provided by the Go runtime.
// The information in the Go runtime is not as extensive as the one extracted
// from Git, and we use the Go runtime information in case the binary is built
// without setting -ldflags (e.g. Homebrew), so we still need both.
// See https://github.com/golang/go/issues/50603
func GetVersion() (*VersionInfo, error) {
vi := &VersionInfo{}
vi.goInfo = fmt.Sprintf("%s, %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)

if vcsVersion != "" {
if err := vi.UnmarshalText([]byte(vcsVersion)); err != nil {
return nil, fmt.Errorf("failed reading VCS version '%s': %w", vcsVersion, err)
}
}

vi.extractVersionFromRuntime()

if vi.Semantic == "" {
vi.Semantic = version
}

return vi, nil
}

// String returns the full version information.
func (vi *VersionInfo) String() string {
var distance string
if vi.TagDistance > 0 {
distance = fmt.Sprintf("-%d", vi.TagDistance)
}

var dirty string
if vi.Dirty {
dirty = "-dirty"
}

return fmt.Sprintf("v%s (commit/%s%s%s, %s)",
vi.Semantic, vi.Commit, distance, dirty, vi.goInfo)
}

// UnmarshalText parses the output of `git describe`.
func (vi *VersionInfo) UnmarshalText(data []byte) error {
parts := strings.Split(string(data), "-")
verParts := []string{}

for _, part := range parts {
if shaRx.Match([]byte(part)) {
vi.Commit = strings.TrimPrefix(part, "g")
continue
}
if part == "dirty" {
vi.Dirty = true
continue
}
if distance, err := strconv.Atoi(part); err == nil {
vi.TagDistance = distance
continue
}
verParts = append(verParts, part)
}

ver := strings.Join(verParts, "-")
if versionRx.Match([]byte(ver)) {
vi.Semantic = strings.TrimPrefix(ver, "v")
} else if vi.Commit == "" {
vi.Commit = ver
}

return nil
}

func (vi *VersionInfo) extractVersionFromRuntime() {
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return
}

for _, s := range buildInfo.Settings {
switch s.Key {
case "vcs.revision":
commitLen := 10
if len(s.Value) < commitLen {
commitLen = len(s.Value)
}
if vi.Commit == "" {
vi.Commit = s.Value[:commitLen]
}
case "vcs.modified":
if s.Value == "true" {
vi.Dirty = true
}
}
}
}
2 changes: 1 addition & 1 deletion app/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func newTestApp(ctx context.Context, options ...Option) (*testApp, error) {
WithUser(localUser),
}
opts = append(opts, options...)
app, err := New("/disco", opts...)
app, err := New("Disco", "/disco", opts...)
if err != nil {
return nil, err
}
Expand Down
4 changes: 0 additions & 4 deletions app/version.go

This file was deleted.

2 changes: 1 addition & 1 deletion cmd/disco/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
func main() {
// NOTE: The order of the passed options is significant, as some options
// depend on the values set by previous ones.
a, err := app.New(filepath.Join(xdg.DataHome, "disco"),
a, err := app.New("Disco", filepath.Join(xdg.DataHome, "disco"),
app.WithFDs(
os.Stdin,
colorable.NewColorable(os.Stdout),
Expand Down

0 comments on commit e72df14

Please sign in to comment.