1
0

version_manager.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """
  2. Version management for Dune Weaver
  3. Handles current version reading and GitHub API integration for latest version checking
  4. Testing overrides (environment variables):
  5. FORCE_UPDATE_AVAILABLE=1 - Force update to appear available
  6. FAKE_LATEST_VERSION=5.0.0 - Override the "latest" version for testing
  7. """
  8. import asyncio
  9. import aiohttp
  10. import os
  11. import time
  12. from pathlib import Path
  13. from typing import Dict
  14. import logging
  15. logger = logging.getLogger(__name__)
  16. # Testing overrides via environment variables
  17. FORCE_UPDATE_AVAILABLE = os.environ.get("FORCE_UPDATE_AVAILABLE", "").lower() in ("1", "true", "yes")
  18. FAKE_LATEST_VERSION = os.environ.get("FAKE_LATEST_VERSION", "")
  19. if FORCE_UPDATE_AVAILABLE or FAKE_LATEST_VERSION:
  20. logger.warning(f"Version override active: FORCE_UPDATE_AVAILABLE={FORCE_UPDATE_AVAILABLE}, FAKE_LATEST_VERSION={FAKE_LATEST_VERSION}")
  21. class VersionManager:
  22. def __init__(self):
  23. self.repo_owner = "tuanchris"
  24. self.repo_name = "dune-weaver"
  25. self.github_api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
  26. self._current_version = None
  27. # Caching for GitHub API to avoid rate limits and slow requests
  28. self._latest_release_cache = None
  29. self._cache_timestamp = None
  30. self._cache_duration = 3600 # Cache for 1 hour (in seconds)
  31. async def get_current_version(self) -> str:
  32. """Read current version from VERSION file (async)"""
  33. if self._current_version is None:
  34. try:
  35. version_file = Path(__file__).parent.parent.parent / "VERSION"
  36. if version_file.exists():
  37. self._current_version = await asyncio.to_thread(version_file.read_text)
  38. self._current_version = self._current_version.strip()
  39. else:
  40. logger.warning("VERSION file not found, using default version")
  41. self._current_version = "1.0.0"
  42. except Exception as e:
  43. logger.error(f"Error reading VERSION file: {e}")
  44. self._current_version = "1.0.0"
  45. return self._current_version
  46. async def get_latest_release(self, force_refresh: bool = False) -> Dict[str, any]:
  47. """Get latest release info from GitHub API with caching"""
  48. # Check if we have a valid cache
  49. current_time = time.time()
  50. if not force_refresh and self._latest_release_cache is not None and self._cache_timestamp is not None:
  51. cache_age = current_time - self._cache_timestamp
  52. if cache_age < self._cache_duration:
  53. logger.debug(f"Returning cached version info (age: {cache_age:.0f}s)")
  54. return self._latest_release_cache
  55. # Cache miss or expired - fetch from GitHub
  56. logger.info("Fetching latest release from GitHub API")
  57. try:
  58. async with aiohttp.ClientSession() as session:
  59. async with session.get(
  60. f"{self.github_api_url}/releases/latest",
  61. timeout=aiohttp.ClientTimeout(total=10)
  62. ) as response:
  63. if response.status == 200:
  64. data = await response.json()
  65. release_data = {
  66. "version": data.get("tag_name", "").lstrip("v"),
  67. "name": data.get("name", ""),
  68. "published_at": data.get("published_at", ""),
  69. "html_url": data.get("html_url", ""),
  70. "body": data.get("body", ""),
  71. "prerelease": data.get("prerelease", False)
  72. }
  73. # Update cache
  74. self._latest_release_cache = release_data
  75. self._cache_timestamp = current_time
  76. logger.info(f"Cached new release info: {release_data.get('version')}")
  77. return release_data
  78. elif response.status == 404:
  79. # No releases found
  80. logger.info("No releases found on GitHub")
  81. return None
  82. else:
  83. logger.warning(f"GitHub API returned status {response.status}")
  84. # Return cached data if available, even if stale
  85. return self._latest_release_cache
  86. except asyncio.TimeoutError:
  87. logger.warning("Timeout while fetching latest release from GitHub")
  88. # Return cached data if available
  89. return self._latest_release_cache
  90. except Exception as e:
  91. logger.error(f"Error fetching latest release: {e}")
  92. # Return cached data if available
  93. return self._latest_release_cache
  94. def compare_versions(self, version1: str, version2: str) -> int:
  95. """Compare two semantic versions. Returns -1, 0, or 1"""
  96. try:
  97. # Parse semantic versions (e.g., "1.2.3")
  98. v1_parts = [int(x) for x in version1.split('.')]
  99. v2_parts = [int(x) for x in version2.split('.')]
  100. # Pad shorter version with zeros
  101. max_len = max(len(v1_parts), len(v2_parts))
  102. v1_parts.extend([0] * (max_len - len(v1_parts)))
  103. v2_parts.extend([0] * (max_len - len(v2_parts)))
  104. if v1_parts < v2_parts:
  105. return -1
  106. elif v1_parts > v2_parts:
  107. return 1
  108. else:
  109. return 0
  110. except (ValueError, AttributeError):
  111. logger.warning(f"Invalid version format: {version1} vs {version2}")
  112. return 0
  113. async def get_version_info(self, force_refresh: bool = False) -> Dict[str, any]:
  114. """Get complete version information
  115. Args:
  116. force_refresh: If True, bypass cache and fetch from GitHub API
  117. """
  118. current = await self.get_current_version()
  119. latest_release = await self.get_latest_release(force_refresh=force_refresh)
  120. if latest_release:
  121. latest = latest_release["version"]
  122. comparison = self.compare_versions(current, latest)
  123. update_available = comparison < 0
  124. else:
  125. latest = current # Fallback if no releases found
  126. update_available = False
  127. return {
  128. "current": current,
  129. "latest": latest,
  130. "update_available": update_available,
  131. "latest_release": latest_release
  132. }
  133. def clear_cache(self):
  134. """Clear the cached version data"""
  135. self._latest_release_cache = None
  136. self._cache_timestamp = None
  137. logger.info("Version cache cleared")
  138. # Global instance
  139. version_manager = VersionManager()