package service import ( "strings" "time" "golang.org/x/mod/semver" ) // IsSemanticVersion checks if a version string follows semantic versioning format // Uses the official golang.org/x/mod/semver package for validation // Requires exactly three parts: major.minor.patch (optionally with prerelease/build) func IsSemanticVersion(version string) bool { // The semver package requires a "v" prefix, so add it for validation versionWithV := ensureVPrefix(version) if !semver.IsValid(versionWithV) { return false } // Additional validation: require exactly three parts (major.minor.patch) // Strip the v prefix and any prerelease/build metadata for counting parts // This ensures semver compliance, because the default go module accepts invalid semvers :/ // (See https://pkg.go.dev/golang.org/x/mod/semver) versionCore := strings.TrimPrefix(versionWithV, "v") if idx := strings.Index(versionCore, "-"); idx != -1 { versionCore = versionCore[:idx] } if idx := strings.Index(versionCore, "+"); idx != -1 { versionCore = versionCore[:idx] } parts := strings.Split(versionCore, ".") return len(parts) == 3 } // ensureVPrefix adds a "v" prefix if not present func ensureVPrefix(version string) string { if !strings.HasPrefix(version, "v") { return "v" + version } return version } // compareSemanticVersions compares two semantic version strings // Uses the official golang.org/x/mod/semver package for comparison // Returns: // // -1 if version1 < version2 // 0 if version1 == version2 // +1 if version1 > version2 func compareSemanticVersions(version1 string, version2 string) int { // The semver package requires a "v" prefix, so add it for comparison v1 := ensureVPrefix(version1) v2 := ensureVPrefix(version2) return semver.Compare(v1, v2) } // CompareVersions implements the versioning strategy agreed upon in the discussion: // 1. If both versions are valid semver, use semantic version comparison // 2. If neither are valid semver, use publication timestamp (return 0 to indicate equal for sorting) // 3. If one is semver and one is not, the semver version is always considered higher func CompareVersions(version1 string, version2 string, timestamp1 time.Time, timestamp2 time.Time) int { isSemver1 := IsSemanticVersion(version1) isSemver2 := IsSemanticVersion(version2) if isSemver1 && isSemver2 { // Both are semver - use semantic comparison return compareSemanticVersions(version1, version2) } if !isSemver1 && !isSemver2 { // Neither are semver - use timestamp comparison if timestamp1.Before(timestamp2) { return -1 } else if timestamp1.After(timestamp2) { return 1 } return 0 } // One is semver, one is not - semver is always higher if isSemver1 && !isSemver2 { return 1 } return -1 }