Pulse/internal/updates/version.go
2025-08-11 16:05:09 +00:00

221 lines
No EOL
5.2 KiB
Go

package updates
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// Version represents a semantic version
type Version struct {
Major int
Minor int
Patch int
Prerelease string
Build string
}
// VersionInfo contains detailed version information
type VersionInfo struct {
Version string `json:"version"`
Build string `json:"build"`
Runtime string `json:"runtime"`
Channel string `json:"channel,omitempty"`
IsDocker bool `json:"isDocker"`
IsDevelopment bool `json:"isDevelopment"`
}
// ParseVersion parses a version string into a Version struct
func ParseVersion(versionStr string) (*Version, error) {
// Remove 'v' prefix if present
versionStr = strings.TrimPrefix(versionStr, "v")
// Regular expression for semantic versioning
re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return nil, fmt.Errorf("invalid version format: %s", versionStr)
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
patch, _ := strconv.Atoi(matches[3])
return &Version{
Major: major,
Minor: minor,
Patch: patch,
Prerelease: matches[4],
Build: matches[5],
}, nil
}
// String returns the string representation of the version
func (v *Version) String() string {
version := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.Prerelease != "" {
version += "-" + v.Prerelease
}
if v.Build != "" {
version += "+" + v.Build
}
return version
}
// Compare compares two versions
// Returns:
// -1 if v < other
// 0 if v == other
// 1 if v > other
func (v *Version) Compare(other *Version) int {
if v.Major != other.Major {
return compareInts(v.Major, other.Major)
}
if v.Minor != other.Minor {
return compareInts(v.Minor, other.Minor)
}
if v.Patch != other.Patch {
return compareInts(v.Patch, other.Patch)
}
// Handle prerelease versions
if v.Prerelease == "" && other.Prerelease != "" {
return 1 // v is release, other is prerelease
}
if v.Prerelease != "" && other.Prerelease == "" {
return -1 // v is prerelease, other is release
}
if v.Prerelease != other.Prerelease {
// Try to parse RC numbers for comparison
vRC := extractRCNumber(v.Prerelease)
otherRC := extractRCNumber(other.Prerelease)
if vRC >= 0 && otherRC >= 0 {
return compareInts(vRC, otherRC)
}
return strings.Compare(v.Prerelease, other.Prerelease)
}
return 0
}
// IsNewerThan returns true if v is newer than other
func (v *Version) IsNewerThan(other *Version) bool {
return v.Compare(other) > 0
}
// IsPrerelease returns true if this is a prerelease version
func (v *Version) IsPrerelease() bool {
return v.Prerelease != ""
}
// GetCurrentVersion gets the current running version
func GetCurrentVersion() (*VersionInfo, error) {
// Try to get version from git first (development)
gitVersion, err := getGitVersion()
if err == nil && gitVersion != "" {
return &VersionInfo{
Version: gitVersion,
Build: "development",
Runtime: "go",
IsDevelopment: true,
IsDocker: isDockerEnvironment(),
}, nil
}
// Try to read from VERSION file in multiple locations
versionPaths := []string{
"VERSION",
"/opt/pulse/VERSION",
filepath.Join(filepath.Dir(os.Args[0]), "VERSION"),
}
for _, path := range versionPaths {
versionBytes, err := os.ReadFile(path)
if err == nil {
return &VersionInfo{
Version: strings.TrimSpace(string(versionBytes)),
Build: "release",
Runtime: "go",
IsDevelopment: false,
IsDocker: isDockerEnvironment(),
}, nil
}
}
// Final fallback
return &VersionInfo{
Version: "4.2.0",
Build: "release",
Runtime: "go",
IsDevelopment: false,
IsDocker: isDockerEnvironment(),
}, nil
}
// getGitVersion gets version information from git
func getGitVersion() (string, error) {
// Get the latest tag
cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
output, err := cmd.Output()
if err != nil {
return "", err
}
version := strings.TrimSpace(string(output))
if version == "" {
return "", fmt.Errorf("no version found")
}
return version, nil
}
// isDockerEnvironment checks if running in Docker
func isDockerEnvironment() bool {
// Check for Docker-specific files
if fileExists("/.dockerenv") {
return true
}
// Check cgroup for Docker
data, err := exec.Command("cat", "/proc/1/cgroup").Output()
if err == nil && strings.Contains(string(data), "docker") {
return true
}
return false
}
// fileExists checks if a file exists
func fileExists(path string) bool {
cmd := exec.Command("test", "-f", path)
return cmd.Run() == nil
}
// compareInts compares two integers
func compareInts(a, b int) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
// extractRCNumber extracts the RC number from a prerelease string like "rc.9" or "rc9"
func extractRCNumber(prerelease string) int {
re := regexp.MustCompile(`rc\.?(\d+)`)
matches := re.FindStringSubmatch(strings.ToLower(prerelease))
if len(matches) > 1 {
num, err := strconv.Atoi(matches[1])
if err == nil {
return num
}
}
return -1
}