Report the Go build info version when linker flags set none (closes #422) #444
No files matched your search
@@ -1041,20 +1041,37 @@ is `git describe --tags --always --dirty`, which `script/version` runs for the
|
||||
A `docker build .` of a clone, with no build arguments, runs the same
|
||||
`git describe` (without `--dirty`) on the `.git` in its build context, so it
|
||||
stamps the same value for a clean commit; the build fails if the context carries
|
||||
`.git` and no version, commit or commit date comes out. A binary built without
|
||||
git metadata reports `dev`, or `unknown` when `script/docker` or
|
||||
`script/cibuild` built it outside a git checkout.
|
||||
`.git` and no version, commit or commit date comes out. `make` outside a git
|
||||
checkout and `docker build .` without `.git` both stamp `dev`. `script/docker`
|
||||
and `script/cibuild` stamp `unknown` when they run outside a git checkout.
|
||||
|
||||
A binary built by plain `go install` or `go build` carries no stamped version.
|
||||
It reports the version Go recorded in it instead:
|
||||
|
||||
- a `go install` of the tag `v1.0.0` → `v1.0.0`.
|
||||
- a `go build` of an untagged commit → a Go pseudo-version, such as
|
||||
`v1.0.1-0.20261110231226-0123456789ab`.
|
||||
- a build where Go recorded no version → `dev`.
|
||||
|
||||
`goreleaser` stamps a release binary with the tag minus its leading `v`, so the
|
||||
tag `v1.0.0` produces `vaultik 1.0.0`, matching the archive name
|
||||
`vaultik_1.0.0_linux_amd64.tar.gz`. `goreleaser --snapshot` stamps
|
||||
`dev-<12 chars of the commit sha>` rather than inventing the next patch number.
|
||||
`vaultik version` calls a build a development build when its version is `dev`,
|
||||
`unknown`, `dev-<sha>`, the short commit sha or `<tag>-<N>-g<short sha>`, with
|
||||
or without `-dirty`; only a plain tag, such as `v1.0.0` or `1.0.0`, is a
|
||||
release. If `script/version` cannot be run at all, `make` stops with an error
|
||||
instead of building an unversioned binary, and a binary that somehow carries an
|
||||
empty version string still reports itself as a development build.
|
||||
|
||||
`vaultik version` calls a build a development build when its version is one of
|
||||
these:
|
||||
|
||||
- `dev` or `unknown`.
|
||||
- `dev-<sha>`.
|
||||
- the short commit sha.
|
||||
- `<tag>-<N>-g<short sha>`.
|
||||
- a Go pseudo-version.
|
||||
- any version with a `-dirty` or `+dirty` suffix.
|
||||
|
||||
Only a plain tag, such as `v1.0.0` or `1.0.0`, is a release. If `script/version`
|
||||
cannot be run at all, `make` stops with an error instead of building an
|
||||
unversioned binary. A binary stamped with an empty version reports the version
|
||||
Go recorded in it; if Go recorded none, it is a development build.
|
||||
|
||||
### cutting a release
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/dig v1.19.0
|
||||
go.uber.org/fx v1.24.0
|
||||
golang.org/x/mod v0.29.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/term v0.37.0
|
||||
|
||||
@@ -21,6 +21,8 @@ const shortCommitLen = 12
|
||||
const exitCodeInterrupted = 130
|
||||
|
||||
// Entry is the main entry point for the CLI application.
|
||||
// It first calls globals.FillUnsetFromBuildInfo, which fills in the
|
||||
// version, commit and commit date that the linker flags did not set.
|
||||
// It prints the startup banner to stderr (unless a banner-suppressing
|
||||
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
||||
// root cobra command, and routes any returned error through the
|
||||
@@ -33,6 +35,7 @@ const exitCodeInterrupted = 130
|
||||
// deferred profile writers run before the process ends. See run in
|
||||
// cmd/vaultik/main.go.
|
||||
func Entry() int {
|
||||
globals.FillUnsetFromBuildInfo()
|
||||
emitStartupBanner(os.Args[1:], os.Stderr)
|
||||
|
||||
rootCmd := NewRootCommand()
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
// Package globals holds application-wide metadata (name, version,
|
||||
// commit) that is populated at build time via linker flags.
|
||||
// commit) that is populated at build time via linker flags, or from the
|
||||
// build info Go records in the binary when the linker flags set none.
|
||||
package globals
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
)
|
||||
|
||||
// Appname is the application name, populated from main().
|
||||
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
|
||||
// DevVersion is the version a binary reports when it was built without
|
||||
// git metadata: script/version emits it outside a git checkout, and an
|
||||
// unstamped `go build` keeps it. goreleaser's snapshot template stamps
|
||||
// git metadata: script/version emits it outside a git checkout, and a
|
||||
// binary keeps it when neither its linker flags nor its Go build info
|
||||
// name a version. goreleaser's snapshot template stamps
|
||||
// it followed by "-" and the commit it was built from. It is
|
||||
// deliberately not a number.
|
||||
const DevVersion = "dev"
|
||||
@@ -32,6 +37,54 @@ var Commit = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
// CommitDate is the ISO-8601 date of the commit, populated from main().
|
||||
var CommitDate = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
|
||||
// develVersion is the main module version Go records when it cannot
|
||||
// tell which version it built.
|
||||
const develVersion = "(devel)"
|
||||
|
||||
// FillUnsetFromBuildInfo applies FillUnset to Version, Commit and
|
||||
// CommitDate, with the build info of the running binary.
|
||||
func FillUnsetFromBuildInfo() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
Version, Commit, CommitDate = FillUnset(
|
||||
info, Version, Commit, CommitDate)
|
||||
}
|
||||
|
||||
// FillUnset returns version, commit and commitDate. Each one that
|
||||
// still holds its default ("dev" or empty for the version, "unknown"
|
||||
// for the others) is replaced by the value Go recorded in info. A
|
||||
// `go install` of a tag sets no linker flags, but Go records the tag
|
||||
// as the main module's version. Go records the commit and its time
|
||||
// only when it builds from a git checkout. The time is cut to a date,
|
||||
// as the Makefile stamps it.
|
||||
func FillUnset(
|
||||
info *debug.BuildInfo, version, commit, commitDate string,
|
||||
) (string, string, string) {
|
||||
if (version == "" || version == DevVersion) &&
|
||||
info.Main.Version != "" && info.Main.Version != develVersion {
|
||||
version = info.Main.Version
|
||||
}
|
||||
|
||||
for _, s := range info.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
if commit == Unknown {
|
||||
commit = s.Value
|
||||
}
|
||||
case "vcs.time":
|
||||
t, err := time.Parse(time.RFC3339, s.Value)
|
||||
if commitDate == Unknown && err == nil {
|
||||
commitDate = t.UTC().Format(time.DateOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return version, commit, commitDate
|
||||
}
|
||||
|
||||
// Author identifies the upstream author of vaultik.
|
||||
const Author = "Jeffrey Paul <sneak@sneak.berlin>"
|
||||
|
||||
@@ -68,8 +121,10 @@ func New() (*Globals, error) {
|
||||
// a release. "dev" and goreleaser's snapshot "dev-<sha>" count, and so
|
||||
// does what `git describe --tags --always --dirty` gives a make or
|
||||
// docker build of an untagged commit: the bare short commit, or
|
||||
// tag-N-gHASH on a commit after a tag. Any version ending in "-dirty"
|
||||
// counts, a modified checkout of a tag ("v1.0.0-dirty") included.
|
||||
// tag-N-gHASH on a commit after a tag. A Go pseudo-version counts: Go
|
||||
// records one for a build of an untagged commit without linker flags.
|
||||
// Any version ending in "-dirty" or "+dirty" counts, a modified
|
||||
// checkout of a tag ("v1.0.0-dirty", "v1.0.0+dirty") included.
|
||||
// A plain tag such as "v1.0.0" or "1.0.0" is a release.
|
||||
//
|
||||
// The empty string counts too. Nothing that knows its version reports
|
||||
@@ -82,7 +137,8 @@ func New() (*Globals, error) {
|
||||
func IsDevVersion(v string) bool {
|
||||
if v == "" || v == Unknown || v == DevVersion ||
|
||||
strings.HasPrefix(v, DevVersion+"-") ||
|
||||
strings.HasSuffix(v, "-dirty") {
|
||||
strings.HasSuffix(v, "-dirty") || strings.HasSuffix(v, "+dirty") ||
|
||||
module.IsPseudoVersion(v) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package globals_test
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
@@ -59,12 +60,20 @@ func TestIsDevVersion(t *testing.T) {
|
||||
{"1.0.0-rc.1-12-g877eb2f", true},
|
||||
// A tagged commit with uncommitted changes is not that tag.
|
||||
{"v1.0.0-dirty", true},
|
||||
// What Go records for a build without linker flags: a
|
||||
// pseudo-version for an untagged commit, "+dirty" for a
|
||||
// modified checkout.
|
||||
{"v1.0.0-rc3.0.20261111001226-abcdefabcdef", true},
|
||||
{"v0.0.0-20261111001226-abcdefabcdef", true},
|
||||
{"v1.0.1-0.20261111001226-abcdefabcdef", true},
|
||||
{"v1.0.0+dirty", true},
|
||||
// What a tagged build produces (goreleaser's .Version strips
|
||||
// the leading "v"; script/version keeps it).
|
||||
{"1.0.0", false},
|
||||
{"0.1.0", false},
|
||||
{"1.0.0-rc.1", false},
|
||||
{"v1.0.0", false},
|
||||
{"v1.0.0-rc3", false},
|
||||
// A release must not be mistaken for a dev build just because
|
||||
// the string happens to contain "dev".
|
||||
{"1.0.0-dev", false},
|
||||
@@ -86,10 +95,9 @@ func TestIsDevVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultVersionIsDev pins the linker-flag contract: an unstamped
|
||||
// binary (no -ldflags at all, which is what `go build ./...` and `go
|
||||
// install` produce) must report itself as a development build rather
|
||||
// than as some default release number.
|
||||
// TestDefaultVersionIsDev pins the default: a binary whose linker flags
|
||||
// and build info name no version must report itself as a development
|
||||
// build rather than as some default release number.
|
||||
func TestDefaultVersionIsDev(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -98,3 +106,104 @@ func TestDefaultVersionIsDev(t *testing.T) {
|
||||
globals.DevVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// buildInfo returns build info with the given main module version and,
|
||||
// when revision is not empty, the commit settings Go records for a
|
||||
// build from a git checkout.
|
||||
func buildInfo(version, revision string) *debug.BuildInfo {
|
||||
info := &debug.BuildInfo{
|
||||
Main: debug.Module{Path: "sneak.berlin/go/vaultik", Version: version},
|
||||
}
|
||||
|
||||
if revision != "" {
|
||||
info.Settings = []debug.BuildSetting{
|
||||
{Key: "vcs.revision", Value: revision},
|
||||
{Key: "vcs.time", Value: "2026-11-10T23:12:26Z"},
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// TestFillUnset covers a binary from `go install` of a tag, which
|
||||
// carries no linker flags: it must report the tag. Values the linker
|
||||
// flags set are kept.
|
||||
func TestFillUnset(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
tag = "v1.0.0-rc3"
|
||||
rev = "0123456789abcdef0123456789abcdef01234567"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
info *debug.BuildInfo
|
||||
version, commit, date string
|
||||
wantVersion, wantCommit string
|
||||
wantDate string
|
||||
}{
|
||||
{
|
||||
name: "go install of a tag",
|
||||
info: buildInfo(tag, ""),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "empty version",
|
||||
info: buildInfo(tag, ""),
|
||||
version: "",
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "go build in a git checkout",
|
||||
info: buildInfo(tag, rev),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: rev,
|
||||
wantDate: "2026-11-10",
|
||||
},
|
||||
{
|
||||
name: "Go could not tell the version",
|
||||
info: buildInfo("(devel)", ""),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: globals.DevVersion,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "linker flags set",
|
||||
info: buildInfo(tag, rev),
|
||||
version: "v1.0.0-rc2-4-gabcdef1",
|
||||
commit: "abcdef1",
|
||||
date: "2026-11-01",
|
||||
wantVersion: "v1.0.0-rc2-4-gabcdef1",
|
||||
wantCommit: "abcdef1",
|
||||
wantDate: "2026-11-01",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
version, commit, date := globals.FillUnset(
|
||||
tc.info, tc.version, tc.commit, tc.date)
|
||||
|
||||
if version != tc.wantVersion || commit != tc.wantCommit ||
|
||||
date != tc.wantDate {
|
||||
t.Errorf("%s: got %q, %q, %q; want %q, %q, %q", tc.name,
|
||||
version, commit, date,
|
||||
tc.wantVersion, tc.wantCommit, tc.wantDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -11,8 +11,8 @@
|
||||
# when no tag is reachable, each with a "-dirty" suffix when tracked
|
||||
# files have uncommitted changes. Untracked files are ignored: a stray
|
||||
# scratch file does not change what was compiled. Outside a git checkout
|
||||
# (release tarball, `go install`), or in one with no commits, it is
|
||||
# "dev".
|
||||
# (such as an unpacked release tarball), or in one with no commits, it
|
||||
# is "dev".
|
||||
#
|
||||
# Nothing here ever invents a version number.
|
||||
set -eu
|
||||
|
||||
Reference in new issue
Block a user