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.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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user