From 9dadd9edc30ebbb16a279326db101ae8020bcc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 2 Aug 2025 10:16:41 -0700 Subject: [PATCH] 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 --- version/version.go | 45 ++++++++++++++++-- version/version_test.go | 103 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/version/version.go b/version/version.go index 409fcaa..8c46f10 100644 --- a/version/version.go +++ b/version/version.go @@ -10,8 +10,18 @@ // -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. +// Build time supports both Unix epoch timestamps and RFC3339 format: +// +// # 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 import ( @@ -19,7 +29,9 @@ import ( "log/slog" "runtime" "runtime/debug" + "strconv" "strings" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" @@ -30,7 +42,7 @@ import ( // 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) + buildTime string // Build timestamp (Unix epoch or RFC3339, normalized to RFC3339) gitVersion string // Git commit hash 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. 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. // This struct is used for JSON serialization and programmatic access to build metadata. type Info struct { @@ -48,6 +82,7 @@ type Info struct { } func init() { + buildTime = parseBuildTime(buildTime) info.BuildTime = buildTime info.GitRev = gitVersion @@ -67,9 +102,9 @@ func init() { switch h.Key { case "vcs.time": if len(buildTime) == 0 { - buildTime = h.Value + buildTime = parseBuildTime(h.Value) + info.BuildTime = buildTime } - 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 diff --git a/version/version_test.go b/version/version_test.go index 651168b..898b4d8 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -309,3 +309,106 @@ func BenchmarkCheckVersionDevSnapshot(b *testing.B) { _ = 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) + } + }) + } +}