Skip to content

Version

pkg/version provides semantic version handling for GTB-based tools: storing build-time version information, comparing versions for update checks, and detecting development builds.


The Version Interface

[!NOTE] See pkg.go.dev/gitlab.com/phpboyscout/go-tool-base/pkg/version for the full API definition.


Info Struct

Info is the concrete implementation of Version. It is populated at build time via ldflags:

type Info struct {
    Version string `json:"version" yaml:"version"`
    Commit  string `json:"commit"  yaml:"commit"`
    Date    string `json:"date"    yaml:"date"`
}

func NewInfo(version, commit, date string) Info

Usage in a Go binary:

// main.go
var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)

func main() {
    v := version.NewInfo(version, commit, date)
    props := &props.Props{
        Version: v,
        // ...
    }
    root.Execute(props)
}

Setting with GoReleaser / ldflags:

# .goreleaser.yaml
builds:
  - ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.commit={{.Commit}}
      - -X main.date={{.Date}}

Version Comparison

CompareVersions compares two version strings using golang.org/x/mod/semver. Both v-prefixed and bare versions are accepted.

import "gitlab.com/phpboyscout/go-tool-base/pkg/version"

result := version.CompareVersions("1.2.3", "v1.3.0")
// result == -1  (1.2.3 < 1.3.0 → update available)

result = version.CompareVersions("v2.0.0", "1.9.9")
// result == 1   (2.0.0 > 1.9.9 → already ahead)

result = version.CompareVersions("v1.0.0", "1.0.0")
// result == 0   (equal)

Return values follow Go convention: -1 (less than), 0 (equal), 1 (greater than).

The Info.Compare(other string) int method compares the current build version against a remote version string:

if props.Version.Compare(latestRelease) < 0 {
    p.Logger.Warn("update available", "latest", latestRelease)
}

Version Formatting

FormatVersionString normalises version strings by adding or removing the v prefix:

version.FormatVersionString("1.2.3", true)   // "v1.2.3"
version.FormatVersionString("v1.2.3", true)  // "v1.2.3"  (idempotent)
version.FormatVersionString("v1.2.3", false) // "1.2.3"
version.FormatVersionString("", true)        // ""         (empty string preserved)

Development Build Detection

IsDevelopment() returns true when:

  • The version string is not a valid semver (e.g. "dev", "unknown")
  • The version contains -dev or -dirty suffixes
version.NewInfo("dev", "none", "unknown").IsDevelopment()   // true
version.NewInfo("v0.0.0", "abc", "2026-01-01").IsDevelopment() // false (valid semver, no -dev/-dirty)
version.NewInfo("v1.2.3", "abc", "2026-01-01").IsDevelopment() // false
version.NewInfo("v1.2.3-dev", "abc", "2026-01-01").IsDevelopment() // true

The self-updater uses this to require --force when updating from a development build, preventing accidental overwriting of local builds.

How a dev build still gets a commit and date

Even without ldflags, a go build/go install binary is not versionless. internal/version reads Go's embedded build metadata (debug.ReadBuildInfo) at package init and fills in what ldflags would have: the VCS revision (vcs.revision, suffixed -dirty when the working tree was modified), the commit time (vcs.time), and — when no ldflags version was injected — the module version or, failing that, the short commit hash. So IsDevelopment() stays honest (an untagged local build still reports as development) while version, doctor, and the update check all have a real commit to show. This relies on -buildvcs (Go's default) being enabled.


Integration with Props

Props.Version holds the build version:

p := &props.Props{
    Version: version.NewInfo(buildVersion, buildCommit, buildDate),
}

// In a version command
func runVersionCmd(p *props.Props) {
    fmt.Println(p.Version.String())
    // prints: "v1.2.3 (abc1234)"
}

  • Props — dependency injection container
  • Setup — self-updater that uses version comparison
  • Auto-Update Lifecycle — how update checks use version info