update_manager.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import subprocess
  2. import logging
  3. # Configure logging
  4. logger = logging.getLogger(__name__)
  5. def check_git_updates():
  6. """Check for available Git updates."""
  7. try:
  8. logger.debug("Checking for Git updates")
  9. subprocess.run(["git", "fetch", "--tags", "--force"], check=True)
  10. latest_remote_tag = subprocess.check_output(
  11. ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
  12. ).strip().decode()
  13. latest_local_tag = subprocess.check_output(
  14. ["git", "describe", "--tags", "--abbrev=0"]
  15. ).strip().decode()
  16. tag_behind_count = 0
  17. if latest_local_tag != latest_remote_tag:
  18. tags = subprocess.check_output(
  19. ["git", "tag", "--merged", "origin/main"], text=True
  20. ).splitlines()
  21. found_local = False
  22. for tag in tags:
  23. if tag == latest_local_tag:
  24. found_local = True
  25. elif found_local:
  26. tag_behind_count += 1
  27. if tag == latest_remote_tag:
  28. break
  29. updates_available = latest_remote_tag != latest_local_tag
  30. logger.info(f"Updates available: {updates_available}, {tag_behind_count} versions behind")
  31. return {
  32. "updates_available": updates_available,
  33. "tag_behind_count": tag_behind_count,
  34. "latest_remote_tag": latest_remote_tag,
  35. "latest_local_tag": latest_local_tag,
  36. }
  37. except subprocess.CalledProcessError as e:
  38. logger.error(f"Error checking Git updates: {e}")
  39. return {
  40. "updates_available": False,
  41. "tag_behind_count": 0,
  42. "latest_remote_tag": None,
  43. "latest_local_tag": None,
  44. }
  45. def update_software():
  46. """Update the software to the latest version.
  47. This runs inside the Docker container, so it:
  48. 1. Pulls latest code via git (mounted volume at /app)
  49. 2. Pulls new Docker image for the backend
  50. 3. Restarts the container to apply updates
  51. Note: For a complete update including container recreation,
  52. run 'dw update' from the host machine instead.
  53. """
  54. error_log = []
  55. logger.info("Starting software update process")
  56. def run_command(command, error_message, capture_output=False, cwd=None):
  57. try:
  58. logger.debug(f"Running command: {' '.join(command)}")
  59. result = subprocess.run(command, check=True, capture_output=capture_output, text=True, cwd=cwd)
  60. return result.stdout if capture_output else True
  61. except subprocess.CalledProcessError as e:
  62. logger.error(f"{error_message}: {e}")
  63. error_log.append(error_message)
  64. return None
  65. # Step 1: Pull latest code via git (works because /app is mounted from host)
  66. logger.info("Pulling latest code from git...")
  67. git_result = run_command(
  68. ["git", "pull", "--ff-only"],
  69. "Failed to pull latest code from git",
  70. cwd="/app"
  71. )
  72. if git_result:
  73. logger.info("Git pull completed successfully")
  74. # Step 2: Pull new Docker image for the backend only
  75. # Note: There is no separate frontend image - it's either bundled or built locally
  76. logger.info("Pulling latest Docker image...")
  77. run_command(
  78. ["docker", "pull", "ghcr.io/tuanchris/dune-weaver:main"],
  79. "Failed to pull backend Docker image"
  80. )
  81. # Step 3: Restart the backend container to apply updates
  82. # We can't recreate ourselves from inside the container, so we just restart
  83. # For full container recreation with new images, use 'dw update' from host
  84. logger.info("Restarting backend container...")
  85. # Use docker restart which works from inside the container
  86. restart_result = run_command(
  87. ["docker", "restart", "dune-weaver-backend"],
  88. "Failed to restart backend container"
  89. )
  90. if not restart_result:
  91. # If docker restart fails, try a graceful approach
  92. logger.info("Attempting graceful restart via compose...")
  93. try:
  94. # Just restart, don't try to recreate (which would fail)
  95. subprocess.run(
  96. ["docker", "compose", "restart", "backend"],
  97. check=True,
  98. cwd="/app"
  99. )
  100. logger.info("Container restarted successfully via compose")
  101. except (subprocess.CalledProcessError, FileNotFoundError) as e:
  102. logger.warning(f"Compose restart also failed: {e}")
  103. error_log.append("Container restart failed - please run 'dw update' from host")
  104. if error_log:
  105. logger.error(f"Software update completed with errors: {error_log}")
  106. return False, "Update completed with errors. For best results, run 'dw update' from the host machine.", error_log
  107. logger.info("Software update completed successfully")
  108. return True, None, None