buildinfo

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 2 Imported by: 0

README

buildinfo

github.com/strongo/buildinfo is one shared way for Go CLIs in this fleet to report their own version, commit and build date - instead of every CLI hand-rolling its own main.version plumbing, disagreeing with whatever its CLI framework does for --version, and eventually shipping something like:

ingitdb --version   →  ingitdb version unknown (built from source)
ingitdb version     →  ingitdb 0.65.11 (none) @ unknown

Two different mechanisms, two different (wrong) answers. This module fixes that by giving every CLI one identical -ldflags -X snippet that targets this module's package path, plus one Wire call - cobracmd.WireCobra for plain cobra, or fangcmd.Wire for the fleet's cobra + fang stack - that feeds both the --version/-v flag and the version subcommand from the exact same resolved value. Only fangcmd imports fang; cobracmd does not, so a binary that never touches fang doesn't link it in just because it wanted --version wired up.

Install

go get github.com/strongo/buildinfo

Core API

package buildinfo

type Info struct {
    Name       string // program name, e.g. "ingitdb"
    Version    string // bare semver, no leading "v"
    Commit     string // full git SHA (with a "+dirty" suffix if the tree was
                       // dirty at build time), or "" when genuinely unknown
    Date       string // RFC 3339 / ISO 8601, or "" when genuinely unknown
    DateSource string // DateSourceBuild, DateSourceCommit, or "" - see below
}

const (
    DateSourceBuild  = "build"  // Date was stamped at link time (-ldflags -X)
    DateSourceCommit = "commit" // Date came from debug.BuildInfo's vcs.time
)

// Get resolves name's build identity: link-time -X values first, falling
// back to runtime/debug.ReadBuildInfo(), falling back to clearly-marked
// placeholders. Never panics; safe to call before any linker stamping
// (e.g. in tests or `go run`).
func Get(name string) Info

// Short is exactly the bare version - for `--version`.
func (i Info) Short() string

// Long is "<name> <version> (<commit>) <date>" - for a `version` subcommand.
func (i Info) Long() string

// JSON returns i as the fleet-wide `version --json` contract value - see
// below.
func (i Info) JSON() VersionJSON

type VersionJSON struct {
    Name       string `json:"name"`
    Version    string `json:"version"`
    Commit     string `json:"commit"`
    Date       string `json:"date"`
    DateSource string `json:"date_source"`
}

Call buildinfo.Get(name) once at startup and use the returned Info everywhere - never read the package's unexported version/commit/date variables directly.

The canonical ldflags snippet

Every CLI in the fleet should use this exact GoReleaser ldflags block, verified against GoReleaser's current template variable names ({{.Version}}, {{.FullCommit}}, {{.Date}} - see GoReleaser's template docs):

builds:
  - ldflags:
      - -s -w
      - -X github.com/strongo/buildinfo.version={{.Version}}
      - -X github.com/strongo/buildinfo.commit={{.FullCommit}}
      - -X github.com/strongo/buildinfo.date={{.Date}}

{{.Version}} is already stripped of its leading v by GoReleaser itself; Info.Version strips it defensively too, so the Short()/Long() contract holds even when Get's runtime/debug.ReadBuildInfo() fallback path is hit instead (module versions there keep the v prefix).

-ldflags -X can target a variable in any imported package, not just main - that is the entire point of this module: point every CLI's GoReleaser config at github.com/strongo/buildinfo.{version,commit,date} and every binary reports through the identical, tested code path instead of each CLI reinventing its own.

Wiring

Two sibling subpackages wire Info into a --version/-v flag and a version subcommand. Pick the one matching your binary's stack - both are driven from the same cobracmd.VersionCommand and the same cobracmd.VersionTemplate, so the two wiring paths cannot disagree with each other, and neither can disagree with itself between its flag and its subcommand.

imports fang? binary cost
cobracmd.WireCobra no plain cobra only
fangcmd.Wire yes cobra + fang's terminal-UI stack (measured ≈ +2 MB, see below)

Use cobracmd.WireCobra unless your binary already depends on fang for something else. Before this package existed, every consumer had to hand-roll this exact pattern (root.Version = info.Short() + root.SetVersionTemplate(...)) because the only shipped helper was fang-shaped; WireCobra is that hand-rolled pattern, written once and tested.

Plain cobra (no fang) - cobracmd.WireCobra
package main

import (
    "os"

    "github.com/spf13/cobra"
    "github.com/strongo/buildinfo"
    "github.com/strongo/buildinfo/cobracmd"
)

func main() {
    info := buildinfo.Get("mycli")

    root := &cobra.Command{
        Use:   "mycli",
        Short: "mycli does things",
    }
    // ... attach the rest of the command tree to root ...

    cobracmd.WireCobra(root, info)
    if err := root.Execute(); err != nil {
        os.Exit(1)
    }
}

cobracmd.WireCobra:

  1. Adds a version subcommand to root that prints info.Long().
  2. Sets root.Version to info.Short() and overrides cobra's default version template, so the --version/-v flag prints exactly info.Short() - no "<name> version " prefix, no decoration.

The cobracmd package imports only cobra and buildinfo - never fang - so a binary that imports cobracmd and nothing else does not link fang.

cobra + fang - fangcmd.Wire

The fleet's cobra + fang stack uses the fangcmd subpackage instead. Left on its own, fang resolves --version/-v from runtime/debug.ReadBuildInfo() and prints "unknown (built from source)" whenever that lookup comes up empty - which is exactly the shipped bug above. fangcmd.Wire closes that gap by feeding fang and the same cobracmd.VersionCommand from the same Info:

package main

import (
    "context"
    "os"

    "charm.land/fang/v2"
    "github.com/spf13/cobra"
    "github.com/strongo/buildinfo"
    "github.com/strongo/buildinfo/fangcmd"
)

func main() {
    info := buildinfo.Get("mycli")

    root := &cobra.Command{
        Use:   "mycli",
        Short: "mycli does things",
    }
    // ... attach the rest of the command tree to root ...

    fangOpts := fangcmd.Wire(root, info)
    if err := fang.Execute(context.Background(), root, fangOpts...); err != nil {
        os.Exit(1)
    }
}

fangcmd.Wire:

  1. Adds a version subcommand to root that prints info.Long().
  2. Overrides cobra's default version template so the --version/-v flag prints exactly info.Short() - no "<name> version " prefix, no fang-appended commit suffix.
  3. Returns the fang.Options that make fang use that same info.Short() instead of its own debug.ReadBuildInfo() guess.

Because both surfaces read from the one Info value passed into Wire, they cannot independently drift the way they did on the shipped binary above - this is covered by a test (fangcmd.TestWire_FlagAndSubcommandReportSameVersion) that drives real fang.Execute calls for both --version and version and asserts they report the same version. cobracmd has the equivalent test (cobracmd.TestWireCobra_FlagAndSubcommandReportSameVersion) for the plain-cobra path.

Measured cost of importing fang

A throwaway binary that imports only cobracmd (no fang, plain cobra --version/version wiring) versus one that imports fangcmd for the identical command tree, both built with the canonical ldflags snippet below and go1.27.0 darwin/arm64:

consumer size fang linked? (go version -m)
buildinfo only (no cobra) 1.6 MB no
cobracmd.WireCobra (fang-free) 4.0 MB no
fangcmd.Wire (fang) 6.2 MB yes - charm.land/fang/v2 v2.0.1

Importing fangcmd costs roughly +2 MB over the identical cobracmd-only binary, entirely from fang's terminal-UI dependency tree. A binary that only calls cobracmd.WireCobra never pays that cost - go version -m on it lists no charm.land/fang dependency at all.

Output contract

  • --version / -v flag (via cobracmd.WireCobra + root.Execute(), fangcmd.Wire + fang.Execute(), or any direct use of Info.Short()): exactly the bare semver version, nothing else. No program name, no commit, no date, no parentheses. Consumable as $(mycli --version).

    $ mycli --version
    1.2.3
    
  • version subcommand (via cobracmd.VersionCommand, or any direct use of Info.Long()): "<name> <version> (<commit>) <date>". commit and date degrade to the literal string unknown when genuinely unresolved, so the shape never collapses to empty parens or a trailing gap.

    $ mycli version
    mycli 1.2.3 (a1b2c3d4e5f6...) 2026-08-30T12:00:00Z
    
  • version --json (via cobracmd.VersionCommand, wired automatically by both cobracmd.WireCobra and fangcmd.Wire): the fleet-wide version --json contract - exactly one JSON object on stdout, nothing else, exit 0, no network access, no writes, no telemetry. --json is the fleet's probe flag regardless of a CLI's own output-format flag (--format, -o), which it may also offer alongside it.

    $ mycli version --json
    {"name":"mycli","version":"1.2.3","commit":"a1b2c3d4e5f6...","date":"2026-08-30T12:00:00Z","date_source":"build"}
    
    Key Value
    name the binary name, equal to Info.Name
    version bare semver, no leading v; "dev" when unresolved
    commit full commit SHA, +dirty suffix when the tree was modified; "" when unknown
    date RFC 3339 timestamp; "" when unknown
    date_source "build" when date was stamped at link time (a released build's release build time), "commit" when it came from debug.BuildInfo's vcs.time, "" when date itself is unknown

    Additional keys may be added in the future; readers must ignore keys they don't recognize, and existing keys are never removed or repurposed.

Both plain outputs end with a single trailing newline supplied by the printing caller (Short()/Long() themselves return no newline); version --json's single trailing newline comes from json.Encoder.

Upgrading from v0.1.x

v0.2.0 moves the fang integration out of cobracmd into a new fangcmd package, so cobracmd no longer imports fang. cobracmd.Wire (which returned []fang.Option) is gone.

  • Binaries that use fang (this was every cobracmd.Wire caller before v0.2.0): switch the import from github.com/strongo/buildinfo/cobracmd to github.com/strongo/buildinfo/fangcmd, and change the call from cobracmd.Wire(root, info) to fangcmd.Wire(root, info). Nothing else changes - same signature, same return type, same behaviour.

    -	"github.com/strongo/buildinfo/cobracmd"
    +	"github.com/strongo/buildinfo/fangcmd"
    
    -	fangOpts := cobracmd.Wire(root, info)
    +	fangOpts := fangcmd.Wire(root, info)
    
  • Binaries that don't actually use fang (the reason this release exists): drop the fang.Execute call and the fang import entirely, and call cobracmd.WireCobra(root, info) + root.Execute() instead. This is also the point at which the binary stops linking fang.

    -	"charm.land/fang/v2"
    	"github.com/spf13/cobra"
     	"github.com/strongo/buildinfo"
     	"github.com/strongo/buildinfo/cobracmd"
    
    -	fangOpts := cobracmd.Wire(root, info)
    -	if err := fang.Execute(context.Background(), root, fangOpts...); err != nil {
    +	cobracmd.WireCobra(root, info)
    +	if err := root.Execute(); err != nil {
     		os.Exit(1)
     	}
    

cobracmd.VersionCommand is unchanged and still exported from cobracmd for either path.

Known traps this module already handles

  • runtime/debug.ReadBuildInfo()'s Deps come back empty inside a non-main package's own test binary - Get's fallback only reads Main.Version and Settings (vcs.revision, vcs.time, vcs.modified), never Deps, so this doesn't affect it.
  • go build omits vcs.revision (and the rest of the vcs.* settings) when building from a linked git worktree rather than a normal checkout. Get treats a missing vcs.revision as "genuinely unknown" and degrades to the documented placeholders rather than panicking or fabricating a value.
  • A dirty working tree (vcs.modified == "true") is represented honestly as a +dirty suffix appended to Commit - but Short() only ever returns Version, so a dirty tree can never leak into --version output.

Documentation

Overview

Package buildinfo provides one framework-agnostic way for Go CLIs in this fleet to report their own version, commit and build date.

The variables below are meant to be set at link time with -ldflags -X, targeting this exact package path (github.com/strongo/buildinfo), so every CLI can share the identical ldflags snippet instead of hand-rolling its own main.version plumbing. See the README for the canonical GoReleaser snippet.

Callers should never read version, commit or date directly — call Get instead, which falls back to runtime/debug.ReadBuildInfo() when the linker did not stamp these variables (e.g. `go run`, `go test`, or a binary built without -ldflags).

Index

Constants

View Source
const (
	// DateSourceBuild marks a Date stamped at link time via -ldflags -X
	// (this package's date variable) - a released build's release build
	// time.
	DateSourceBuild = "build"
	// DateSourceCommit marks a Date read from runtime/debug.BuildInfo's
	// vcs.time - the commit's own timestamp, used when no link-time date
	// was stamped (e.g. a plain `go build` or `go install`).
	DateSourceCommit = "commit"
)

Date source labels for Info.DateSource / VersionJSON.DateSource, per the fleet-wide `version --json` contract (cli-install#req:version-json-contract in strongo/cli-helpers): callers must be able to tell a release build's release build time apart from a plain `go build`'s commit timestamp.

Variables

This section is empty.

Functions

This section is empty.

Types

type Info

type Info struct {
	// Name is the program name, e.g. "ingitdb".
	Name string
	// Version is a bare semver string, with any leading "v" stripped.
	// It is "dev" when no version could be determined by any means.
	Version string
	// Commit is the full git SHA, with a "+dirty" suffix appended when the
	// build tree had uncommitted changes. It is "" when genuinely unknown.
	Commit string
	// Date is an RFC 3339 / ISO 8601 timestamp. It is "" when genuinely
	// unknown.
	Date string
	// DateSource records where Date came from: DateSourceBuild when it was
	// stamped at link time, DateSourceCommit when it was read from
	// runtime/debug.BuildInfo's vcs.time, or "" when Date itself is
	// unknown.
	DateSource string
}

Info is a resolved, ready-to-print snapshot of a program's build identity.

func Get

func Get(name string) Info

Get resolves this program's build identity.

Resolution order:

  1. Link-time -X values (version, commit, date package vars), when non-empty.
  2. runtime/debug.ReadBuildInfo(): the main module's Version, plus vcs.revision / vcs.time / vcs.modified from Settings, for whichever of version/commit/date step 1 left unresolved.
  3. Clearly-marked placeholders ("dev" for version, "" for commit/date). Get never panics and never returns a misleading value.

Get is safe to call before any linker stamping (e.g. in tests or `go run`).

func (Info) JSON added in v0.3.0

func (i Info) JSON() VersionJSON

JSON returns i as the fleet-wide version --json contract value. It never performs I/O; callers encode the result themselves (see cobracmd.VersionCommand for the canonical `version --json` writer).

Info and VersionJSON share identical field names, order and types by design (only VersionJSON carries json tags), so this is a plain conversion rather than a field-by-field copy that could drift out of sync as either type grows.

func (Info) Long

func (i Info) Long() string

Long renders the full build identity, suitable for a `version` subcommand:

"<name> <version> (<commit>) <date>"

Commit and date fall back to "unknown" when genuinely unresolved so the shape never collapses to empty parens or a trailing gap.

func (Info) Short

func (i Info) Short() string

Short renders just the bare semver version, suitable for a `--version` flag consumed as `$(mycli --version)`: exactly the version string, no program name, no commit, no date, no decoration, and no trailing newline (the caller's print statement supplies that).

type VersionJSON added in v0.3.0

type VersionJSON struct {
	// Name is the binary name, equal to its catalog id.
	Name string `json:"name"`
	// Version is the bare semver version, no leading "v"; "dev" when the
	// build cannot determine it.
	Version string `json:"version"`
	// Commit is the full commit SHA, with a "+dirty" suffix when built
	// from a modified tree; "" when unknown.
	Commit string `json:"commit"`
	// Date is an RFC 3339 timestamp; "" when unknown.
	Date string `json:"date"`
	// DateSource is DateSourceBuild, DateSourceCommit, or "" when Date
	// itself is unknown.
	DateSource string `json:"date_source"`
}

VersionJSON is the fleet-wide `version --json` contract (cli-install#req:version-json-contract in strongo/cli-helpers): the exact set of string keys every catalog CLI's `version --json` must print. The writer (Info.JSON(), used by cobracmd.VersionCommand and, through it, fangcmd.Wire) and any reader (e.g. cli-helpers' status prober) share this one exported type so the two can never decode a different shape from what was written. New keys may be added in the future; existing keys are never removed or repurposed.

Directories

Path Synopsis
Package cobracmd wires github.com/strongo/buildinfo into a plain cobra command tree - no other CLI framework required.
Package cobracmd wires github.com/strongo/buildinfo into a plain cobra command tree - no other CLI framework required.
Package fangcmd wires github.com/strongo/buildinfo into a cobra command tree fronted by charm.land/fang/v2 - the fleet's standard CLI stack.
Package fangcmd wires github.com/strongo/buildinfo into a cobra command tree fronted by charm.land/fang/v2 - the fleet's standard CLI stack.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL