// Package version provides build metadata and version information management. // // This package manages application version information including semantic version, // Git revision, build time, and provides integration with CLI frameworks (Cobra, Kong) // and Prometheus metrics for operational visibility. // // Version information can be injected at build time using ldflags: // // go build -ldflags "-X go.ntppool.org/common/version.VERSION=v1.0.0 \ // -X go.ntppool.org/common/version.buildTime=2023-01-01T00:00:00Z \ // -X go.ntppool.org/common/version.gitVersion=abc123" // // The package also automatically extracts build information from Go's debug.BuildInfo // when available, providing fallback values for VCS time and revision. package version import ( "fmt" "log/slog" "runtime" "runtime/debug" "strings" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "golang.org/x/mod/semver" ) // VERSION contains the current software version (typically set during the build process via ldflags). // If not set, defaults to "dev-snapshot". The version should follow semantic versioning. var ( VERSION string // Semantic version (e.g., "1.0.0" or "v1.0.0") buildTime string // Build timestamp (RFC3339 format) gitVersion string // Git commit hash gitModified bool // Whether the working tree was modified during build ) // info holds the consolidated version information extracted from build variables and debug.BuildInfo. var info Info // Info represents structured version and build information. // This struct is used for JSON serialization and programmatic access to build metadata. type Info struct { Version string `json:",omitempty"` // Semantic version with "v" prefix GitRev string `json:",omitempty"` // Full Git commit hash GitRevShort string `json:",omitempty"` // Shortened Git commit hash (7 characters) BuildTime string `json:",omitempty"` // Build timestamp } func init() { info.BuildTime = buildTime info.GitRev = gitVersion if len(VERSION) == 0 { VERSION = "dev-snapshot" } else { if !strings.HasPrefix(VERSION, "v") { VERSION = "v" + VERSION } if !semver.IsValid(VERSION) { slog.Default().Warn("invalid version number", "version", VERSION) } } if bi, ok := debug.ReadBuildInfo(); ok { for _, h := range bi.Settings { // logger.Setup().Info("build info", "k", h.Key, "v", h.Value) switch h.Key { case "vcs.time": if len(buildTime) == 0 { buildTime = h.Value } info.BuildTime = h.Value case "vcs.revision": // https://blog.carlmjohnson.net/post/2023/golang-git-hash-how-to/ // todo: use BuildInfo.Main.Version if revision is empty info.GitRev = h.Value // if gitVersion != h.Value { // logger.Setup().Warn("gitVersion and info.GitRev differs", "gitVersion", gitVersion, "gitRev", info.GitRev) // } gitVersion = h.Value case "vcs.modified": if h.Value == "true" { gitModified = true } } } } info.GitRevShort = info.GitRev if len(info.GitRevShort) > 7 { info.GitRevShort = info.GitRevShort[:7] } info.Version = VERSION Version() } // VersionCmd creates a Cobra command for displaying version information. // The name parameter is used as a prefix in the output (e.g., "myapp v1.0.0"). // Returns a configured cobra.Command that can be added to any CLI application. func VersionCmd(name string) *cobra.Command { versionCmd := &cobra.Command{ Use: "version", Short: "Print version and build information", Long: `Print detailed version information including semantic version, Git revision, build time, and Go version. Build information is automatically extracted from Go's debug.BuildInfo when available.`, Run: func(cmd *cobra.Command, args []string) { ver := Version() fmt.Printf("%s %s\n", name, ver) }, } return versionCmd } // KongVersionCmd provides a Kong CLI framework compatible version command. // The Name field should be set to the application name for proper output formatting. type KongVersionCmd struct { Name string `kong:"-"` // Application name, excluded from Kong parsing } // Run executes the version command for Kong CLI framework. // Prints the application name and version information to stdout. func (cmd *KongVersionCmd) Run() error { fmt.Printf("%s %s\n", cmd.Name, Version()) return nil } // RegisterMetric registers a Prometheus gauge metric with build information. // If name is provided, it creates a metric named "{name}_build_info", otherwise "build_info". // The metric includes labels for version, build time, Git time, and Git revision. // This is useful for exposing build information in monitoring systems. func RegisterMetric(name string, registry prometheus.Registerer) { if len(name) > 0 { name = strings.ReplaceAll(name, "-", "_") name = name + "_build_info" } else { name = "build_info" } buildInfo := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: name, Help: "Build information including version, build time, and git revision", }, []string{ "version", // Combined version/git format (e.g., "v1.0.0/abc123") "buildtime", // Build timestamp from ldflags "gittime", // Git commit timestamp from VCS info "git", // Full Git commit hash }, ) registry.MustRegister(buildInfo) info := VersionInfo() buildInfo.WithLabelValues( fmt.Sprintf("%s/%s", info.Version, info.GitRevShort, ), buildTime, info.BuildTime, info.GitRev, ).Set(1) } // v caches the formatted version string to avoid repeated computation. var v string // VersionInfo returns the structured version information. // This provides programmatic access to version details for JSON serialization // or other structured uses. func VersionInfo() Info { return info } // Version returns a human-readable version string suitable for display. // The format includes semantic version, Git revision, build time, and Go version. // Example: "v1.0.0/abc123f-M (2023-01-01T00:00:00Z, go1.21.0)" // The "-M" suffix indicates the working tree was modified during build. func Version() string { if len(v) > 0 { return v } extra := []string{} if len(buildTime) > 0 { extra = append(extra, buildTime) } extra = append(extra, runtime.Version()) v := VERSION if len(gitVersion) > 0 { g := gitVersion if len(g) > 7 { g = g[0:7] } v += "/" + g if gitModified { v += "-M" } } v = fmt.Sprintf("%s (%s)", v, strings.Join(extra, ", ")) return v } // CheckVersion compares a version against a minimum required version. // Returns true if the version meets or exceeds the minimum requirement. // // Special handling: // - "dev-snapshot" is always considered valid (returns true) // - Git hash suffixes (e.g., "v1.0.0/abc123") are stripped before comparison // - Uses semantic version comparison rules // // Both version and minimumVersion should follow semantic versioning with "v" prefix. func CheckVersion(version, minimumVersion string) bool { if version == "dev-snapshot" { return true } // Strip Git hash suffix if present (e.g., "v1.0.0/abc123" -> "v1.0.0") if idx := strings.Index(version, "/"); idx >= 0 { version = version[0:idx] } if semver.Compare(version, minimumVersion) < 0 { // log.Debug("version too old", "v", cl.Version.Version) return false } return true }