feat(version): add Unix epoch support for buildTime
Support both Unix epoch timestamps and RFC3339 format for build time injection via ldflags. Unix epoch format provides simpler build commands: $(date +%s) vs $(date -u +%Y-%m-%dT%H:%M:%SZ). - Add parseBuildTime() to convert epoch to RFC3339 - Maintain backward compatibility with existing RFC3339 format - Ensure consistent RFC3339 output regardless of input format - Fix build date priority over git commit time
This commit is contained in:
parent
c6230be91e
commit
9dadd9edc3
@ -10,8 +10,18 @@
|
|||||||
// -X go.ntppool.org/common/version.buildTime=2023-01-01T00:00:00Z \
|
// -X go.ntppool.org/common/version.buildTime=2023-01-01T00:00:00Z \
|
||||||
// -X go.ntppool.org/common/version.gitVersion=abc123"
|
// -X go.ntppool.org/common/version.gitVersion=abc123"
|
||||||
//
|
//
|
||||||
// The package also automatically extracts build information from Go's debug.BuildInfo
|
// Build time supports both Unix epoch timestamps and RFC3339 format:
|
||||||
// when available, providing fallback values for VCS time and revision.
|
//
|
||||||
|
// # Unix epoch (simpler, recommended)
|
||||||
|
// go build -ldflags "-X go.ntppool.org/common/version.buildTime=$(date +%s)"
|
||||||
|
//
|
||||||
|
// # RFC3339 format
|
||||||
|
// go build -ldflags "-X go.ntppool.org/common/version.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
//
|
||||||
|
// Both formats are automatically converted to RFC3339 for consistent output. The buildTime
|
||||||
|
// parameter takes priority over Git commit time. If buildTime is not specified, the package
|
||||||
|
// automatically extracts build information from Go's debug.BuildInfo when available,
|
||||||
|
// providing fallback values for VCS time and revision.
|
||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -19,7 +29,9 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -30,7 +42,7 @@ import (
|
|||||||
// If not set, defaults to "dev-snapshot". The version should follow semantic versioning.
|
// If not set, defaults to "dev-snapshot". The version should follow semantic versioning.
|
||||||
var (
|
var (
|
||||||
VERSION string // Semantic version (e.g., "1.0.0" or "v1.0.0")
|
VERSION string // Semantic version (e.g., "1.0.0" or "v1.0.0")
|
||||||
buildTime string // Build timestamp (RFC3339 format)
|
buildTime string // Build timestamp (Unix epoch or RFC3339, normalized to RFC3339)
|
||||||
gitVersion string // Git commit hash
|
gitVersion string // Git commit hash
|
||||||
gitModified bool // Whether the working tree was modified during build
|
gitModified bool // Whether the working tree was modified during build
|
||||||
)
|
)
|
||||||
@ -38,6 +50,28 @@ var (
|
|||||||
// info holds the consolidated version information extracted from build variables and debug.BuildInfo.
|
// info holds the consolidated version information extracted from build variables and debug.BuildInfo.
|
||||||
var info Info
|
var info Info
|
||||||
|
|
||||||
|
// parseBuildTime converts a build time string to RFC3339 format.
|
||||||
|
// Supports both Unix epoch timestamps (numeric strings) and RFC3339 format.
|
||||||
|
// Returns the input unchanged if it cannot be parsed as either format.
|
||||||
|
func parseBuildTime(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as Unix epoch timestamp (numeric string)
|
||||||
|
if epoch, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||||
|
return time.Unix(epoch, 0).UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as RFC3339 to validate format
|
||||||
|
if _, err := time.Parse(time.RFC3339, s); err == nil {
|
||||||
|
return s // Already in RFC3339 format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original string if neither format works (graceful fallback)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// Info represents structured version and build information.
|
// Info represents structured version and build information.
|
||||||
// This struct is used for JSON serialization and programmatic access to build metadata.
|
// This struct is used for JSON serialization and programmatic access to build metadata.
|
||||||
type Info struct {
|
type Info struct {
|
||||||
@ -48,6 +82,7 @@ type Info struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
buildTime = parseBuildTime(buildTime)
|
||||||
info.BuildTime = buildTime
|
info.BuildTime = buildTime
|
||||||
info.GitRev = gitVersion
|
info.GitRev = gitVersion
|
||||||
|
|
||||||
@ -67,9 +102,9 @@ func init() {
|
|||||||
switch h.Key {
|
switch h.Key {
|
||||||
case "vcs.time":
|
case "vcs.time":
|
||||||
if len(buildTime) == 0 {
|
if len(buildTime) == 0 {
|
||||||
buildTime = h.Value
|
buildTime = parseBuildTime(h.Value)
|
||||||
|
info.BuildTime = buildTime
|
||||||
}
|
}
|
||||||
info.BuildTime = h.Value
|
|
||||||
case "vcs.revision":
|
case "vcs.revision":
|
||||||
// https://blog.carlmjohnson.net/post/2023/golang-git-hash-how-to/
|
// https://blog.carlmjohnson.net/post/2023/golang-git-hash-how-to/
|
||||||
// todo: use BuildInfo.Main.Version if revision is empty
|
// todo: use BuildInfo.Main.Version if revision is empty
|
||||||
|
@ -309,3 +309,106 @@ func BenchmarkCheckVersionDevSnapshot(b *testing.B) {
|
|||||||
_ = CheckVersion(version, minimum)
|
_ = CheckVersion(version, minimum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseBuildTime(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Unix epoch timestamp",
|
||||||
|
input: "1672531200", // 2023-01-01T00:00:00Z
|
||||||
|
expected: "2023-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unix epoch zero",
|
||||||
|
input: "0",
|
||||||
|
expected: "1970-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid RFC3339 format",
|
||||||
|
input: "2023-12-25T15:30:45Z",
|
||||||
|
expected: "2023-12-25T15:30:45Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RFC3339 with timezone",
|
||||||
|
input: "2023-12-25T10:30:45-05:00",
|
||||||
|
expected: "2023-12-25T10:30:45-05:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty string",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid format - return unchanged",
|
||||||
|
input: "not-a-date",
|
||||||
|
expected: "not-a-date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid timestamp - return unchanged",
|
||||||
|
input: "invalid-timestamp",
|
||||||
|
expected: "invalid-timestamp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Partial date - return unchanged",
|
||||||
|
input: "2023-01-01",
|
||||||
|
expected: "2023-01-01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Negative epoch - should work",
|
||||||
|
input: "-1",
|
||||||
|
expected: "1969-12-31T23:59:59Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Large epoch timestamp",
|
||||||
|
input: "4102444800", // 2100-01-01T00:00:00Z
|
||||||
|
expected: "2100-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parseBuildTime(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("parseBuildTime(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBuildTimeConsistency(t *testing.T) {
|
||||||
|
// Test that calling parseBuildTime multiple times with the same input returns the same result
|
||||||
|
testInputs := []string{
|
||||||
|
"1672531200",
|
||||||
|
"2023-01-01T00:00:00Z",
|
||||||
|
"invalid-date",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range testInputs {
|
||||||
|
result1 := parseBuildTime(input)
|
||||||
|
result2 := parseBuildTime(input)
|
||||||
|
if result1 != result2 {
|
||||||
|
t.Errorf("parseBuildTime(%q) not consistent: %q != %q", input, result1, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseBuildTime(b *testing.B) {
|
||||||
|
inputs := []string{
|
||||||
|
"1672531200", // Unix epoch
|
||||||
|
"2023-01-01T00:00:00Z", // RFC3339
|
||||||
|
"invalid-timestamp", // Invalid
|
||||||
|
"", // Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range inputs {
|
||||||
|
b.Run(input, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = parseBuildTime(input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user