package config import ( "fmt" "strconv" "strings" ) // CompareVersions compares two semver-ish version strings. // // Accepts "v1.2.3", "1.2.3", "v1.2.3-rc1" (suffix ignored), with missing // components defaulting to 0 ("v1" == "1.0.0"). Returns -1 if a < b, 0 if // equal, +1 if a > b. Returns an error if either argument can't be parsed // at all. // // Used by apply.go to enforce MinCompatibleVersion. Pre-release suffix // handling is deliberately simple — we ignore it, treating "v1.2.3-rc1" // equal to "v1.2.3". Edge case: production releases should never carry // a pre-release suffix, and dev releases are the consumer's responsibility. func CompareVersions(a, b string) (int, error) { pa, err := parseVersion(a) if err != nil { return 0, fmt.Errorf("parse %q: %w", a, err) } pb, err := parseVersion(b) if err != nil { return 0, fmt.Errorf("parse %q: %w", b, err) } for i := 0; i < 3; i++ { if pa[i] < pb[i] { return -1, nil } if pa[i] > pb[i] { return 1, nil } } return 0, nil } func parseVersion(s string) ([3]int, error) { var out [3]int s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "v") // Drop pre-release suffix: "1.2.3-rc1" -> "1.2.3" if i := strings.IndexAny(s, "-+"); i >= 0 { s = s[:i] } parts := strings.SplitN(s, ".", 3) for i, p := range parts { n, err := strconv.Atoi(p) if err != nil { return out, fmt.Errorf("component %q not numeric", p) } if n < 0 { return out, fmt.Errorf("component %d negative", n) } out[i] = n } return out, nil }