chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// Package version exposes the running build's semver as a structured
|
||||
// value, parses SemVer 2.0.0 strings, and owns the rendering of the
|
||||
// canonical form `vMAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]`.
|
||||
//
|
||||
// For Gortex, the build slot always carries the short commit SHA — so
|
||||
// released binaries identify the exact source they were built from
|
||||
// even inside a single tag (rare but possible with retag + rebuild).
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Version is a parsed SemVer 2.0.0 value. Pre-release and build labels
|
||||
// are kept as raw strings because their grammar is identifier-based
|
||||
// rather than numeric-only.
|
||||
type Version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
Prerelease string // e.g. "rc.1", "alpha.2" — no leading '-'
|
||||
Build string // e.g. commit SHA — no leading '+'
|
||||
}
|
||||
|
||||
// String returns the canonical SemVer form with a leading 'v'. Empty
|
||||
// pre-release / build sections are omitted. A zero-valued Version
|
||||
// returns "v0.0.0".
|
||||
func (v Version) String() string {
|
||||
s := fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
if v.Prerelease != "" {
|
||||
s += "-" + v.Prerelease
|
||||
}
|
||||
if v.Build != "" {
|
||||
s += "+" + v.Build
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IsZero reports whether the Version is the zero value. Used by
|
||||
// rendering code to decide whether to print "dev build (no version)"
|
||||
// vs a real semver string.
|
||||
func (v Version) IsZero() bool {
|
||||
return v.Major == 0 && v.Minor == 0 && v.Patch == 0 &&
|
||||
v.Prerelease == "" && v.Build == ""
|
||||
}
|
||||
|
||||
// semverRe matches the SemVer 2.0.0 grammar:
|
||||
//
|
||||
// - optional leading 'v'
|
||||
// - major.minor.patch (all required, numeric, no leading zeros
|
||||
// except literal "0")
|
||||
// - optional "-<prerelease>" (dot-separated identifiers, alphanumeric
|
||||
// or hyphen; numeric identifiers no leading zeros)
|
||||
// - optional "+<build>" (dot-separated identifiers, alphanumeric or
|
||||
// hyphen; leading zeros allowed here)
|
||||
//
|
||||
// Kept as a single regex because the format is small and the cost of
|
||||
// a full recursive-descent parser isn't justified.
|
||||
var semverRe = regexp.MustCompile(
|
||||
`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
|
||||
`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
|
||||
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`,
|
||||
)
|
||||
|
||||
// Parse accepts a SemVer string (with or without leading 'v') and
|
||||
// returns the structured Version. Common non-semver inputs that crop
|
||||
// up during development — "dev", "", "unknown" — are rejected with a
|
||||
// clear error rather than silently returning a zero value, so callers
|
||||
// can decide whether to fall back or fail.
|
||||
func Parse(s string) (Version, error) {
|
||||
m := semverRe.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return Version{}, fmt.Errorf("invalid semver: %q", s)
|
||||
}
|
||||
major, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("parse major: %w", err)
|
||||
}
|
||||
minor, err := strconv.Atoi(m[2])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("parse minor: %w", err)
|
||||
}
|
||||
patch, err := strconv.Atoi(m[3])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("parse patch: %w", err)
|
||||
}
|
||||
return Version{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Patch: patch,
|
||||
Prerelease: m[4],
|
||||
Build: m[5],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MustParse is Parse with panic-on-error. Intended for compile-time
|
||||
// constants (tests, internal defaults); do not use on user input.
|
||||
func MustParse(s string) Version {
|
||||
v, err := Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Compose builds a Version from its components without going through
|
||||
// the string form. Useful at program startup when main() has the
|
||||
// ldflag-injected pieces as separate variables.
|
||||
//
|
||||
// An empty semver string returns a zero Version with no error — that's
|
||||
// the "dev build" signal. A malformed semver returns an error so the
|
||||
// caller can decide whether to panic or fall back.
|
||||
func Compose(semver, build string) (Version, error) {
|
||||
if semver == "" || semver == "dev" || semver == "unknown" {
|
||||
return Version{Build: build}, nil
|
||||
}
|
||||
v, err := Parse(semver)
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
// Explicit build slot overrides whatever was in the semver string.
|
||||
if build != "" {
|
||||
v.Build = build
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParse_Valid(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want Version
|
||||
}{
|
||||
{"1.2.3", Version{Major: 1, Minor: 2, Patch: 3}},
|
||||
{"v1.2.3", Version{Major: 1, Minor: 2, Patch: 3}},
|
||||
{"v0.0.1", Version{Patch: 1}},
|
||||
{"0.0.0", Version{}},
|
||||
{"v1.2.3-rc1", Version{Major: 1, Minor: 2, Patch: 3, Prerelease: "rc1"}},
|
||||
{"v1.2.3-rc.1", Version{Major: 1, Minor: 2, Patch: 3, Prerelease: "rc.1"}},
|
||||
{"v1.2.3+abc1234", Version{Major: 1, Minor: 2, Patch: 3, Build: "abc1234"}},
|
||||
{"v1.2.3-alpha.2+abc1234", Version{Major: 1, Minor: 2, Patch: 3, Prerelease: "alpha.2", Build: "abc1234"}},
|
||||
{"v10.20.30+build.456", Version{Major: 10, Minor: 20, Patch: 30, Build: "build.456"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
got, err := Parse(tt.in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_Invalid(t *testing.T) {
|
||||
// A spec-tight parser should reject these — they're the shapes we
|
||||
// actually see from tooling (dev builds, shell variable not set,
|
||||
// cut-off tags) that must not silently produce 0.0.0.
|
||||
bad := []string{
|
||||
"",
|
||||
"dev",
|
||||
"unknown",
|
||||
"v",
|
||||
"1",
|
||||
"1.2",
|
||||
"1.2.3.4",
|
||||
"v01.2.3", // leading zero
|
||||
"v1.2.3-", // empty prerelease
|
||||
"v1.2.3+", // empty build
|
||||
"v1.2.3-01", // leading-zero numeric prerelease identifier
|
||||
"v1.2.3 extra", // trailing garbage
|
||||
}
|
||||
for _, s := range bad {
|
||||
t.Run(s, func(t *testing.T) {
|
||||
_, err := Parse(s)
|
||||
assert.Error(t, err, "Parse(%q) must fail", s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestString_Canonical(t *testing.T) {
|
||||
// Round-trip: Parse → String → Parse returns an identical value.
|
||||
// Catches regressions where String drops or reorders fields.
|
||||
samples := []string{
|
||||
"v0.0.0",
|
||||
"v1.2.3",
|
||||
"v1.2.3-rc.1",
|
||||
"v1.2.3+abc1234",
|
||||
"v1.2.3-alpha+build.456",
|
||||
}
|
||||
for _, s := range samples {
|
||||
v, err := Parse(s)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, s, v.String(), "round-trip failed for %s", s)
|
||||
|
||||
v2, err := Parse(v.String())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v, v2, "second parse drifted for %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestString_Zero(t *testing.T) {
|
||||
var v Version
|
||||
assert.Equal(t, "v0.0.0", v.String())
|
||||
assert.True(t, v.IsZero())
|
||||
}
|
||||
|
||||
func TestCompose_Semver(t *testing.T) {
|
||||
v, err := Compose("v1.2.3", "abc1234")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{Major: 1, Minor: 2, Patch: 3, Build: "abc1234"}, v)
|
||||
assert.Equal(t, "v1.2.3+abc1234", v.String())
|
||||
}
|
||||
|
||||
func TestCompose_DevBuild(t *testing.T) {
|
||||
// The "dev" / "" / "unknown" sentinels mean "no ldflags injected"
|
||||
// (local go build without goreleaser). Compose must not error —
|
||||
// callers rely on this to render "(dev build)" cleanly.
|
||||
for _, s := range []string{"", "dev", "unknown"} {
|
||||
v, err := Compose(s, "abc1234")
|
||||
require.NoError(t, err, "Compose(%q, ...) must not error", s)
|
||||
assert.True(t, v.IsZero() == (v.Build == ""), "dev-build Version")
|
||||
assert.Equal(t, "abc1234", v.Build,
|
||||
"dev builds must still surface the commit SHA when one was injected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompose_BuildSlotOverridesEmbedded(t *testing.T) {
|
||||
// If the semver already carries a +build slot but an explicit
|
||||
// build arg is provided, the explicit one wins. Lets goreleaser
|
||||
// pass ShortCommit unconditionally without worrying about whether
|
||||
// the tag author also hand-added one.
|
||||
v, err := Compose("v1.2.3+old", "new1234")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new1234", v.Build)
|
||||
}
|
||||
|
||||
func TestMustParse_PanicsOnInvalid(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("MustParse should panic on invalid input")
|
||||
}
|
||||
}()
|
||||
MustParse("nope")
|
||||
}
|
||||
Reference in New Issue
Block a user