package engineversion import ( "fmt" "strings" "golang.org/x/mod/semver" ) // ParseSemver normalises version into the canonical "vMAJOR.MINOR.PATCH" // form expected by `golang.org/x/mod/semver` and reports a wrapped // ErrInvalidSemver when the resulting string is not a valid full semver. // // Whitespace is trimmed; a missing leading "v" is added before the // validity check so callers may pass either "1.2.3" or "v1.2.3". The // stripped base must carry exactly three dot-separated numeric // components — `golang.org/x/mod/semver` accepts shortened forms such // as "v1" or "v1.2", but the engine-version registry requires the full // triple, so this function rejects anything narrower. func ParseSemver(version string) (string, error) { candidate := strings.TrimSpace(version) if candidate == "" { return "", fmt.Errorf("%w: empty", ErrInvalidSemver) } if !strings.HasPrefix(candidate, "v") { candidate = "v" + candidate } if !semver.IsValid(candidate) { return "", fmt.Errorf("%w: %q", ErrInvalidSemver, version) } base := candidate if i := strings.IndexAny(base, "-+"); i >= 0 { base = base[:i] } if strings.Count(base, ".") != 2 { return "", fmt.Errorf( "%w: %q (need vMAJOR.MINOR.PATCH)", ErrInvalidSemver, version, ) } return candidate, nil } // IsPatchUpgrade reports whether next is a same-major.minor upgrade of // current. Both inputs are parsed through ParseSemver so callers may // pass either bare or `v`-prefixed forms. A wrapped ErrInvalidSemver is // returned when either argument fails to parse; the boolean result is // undefined in that case. func IsPatchUpgrade(current, next string) (bool, error) { curr, err := ParseSemver(current) if err != nil { return false, fmt.Errorf("current: %w", err) } nxt, err := ParseSemver(next) if err != nil { return false, fmt.Errorf("next: %w", err) } return semver.MajorMinor(curr) == semver.MajorMinor(nxt), nil }