Fix update check: read branch from flake.lock, query Gitea API
This commit is contained in:
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -45,7 +47,10 @@ SYSTEM_AUTOSTART_FILE = "/etc/xdg/autostart/sovran-hub.desktop"
|
|||||||
|
|
||||||
DOWNLOADS_DIR = os.path.join(os.path.expanduser("~"), "Downloads")
|
DOWNLOADS_DIR = os.path.join(os.path.expanduser("~"), "Downloads")
|
||||||
|
|
||||||
FLAKE_DIR = "/etc/nixos"
|
FLAKE_LOCK_PATH = "/etc/nixos/flake.lock"
|
||||||
|
FLAKE_INPUT_NAME = "Sovran_Systems"
|
||||||
|
|
||||||
|
GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits"
|
||||||
|
|
||||||
UPDATE_COMMAND = [
|
UPDATE_COMMAND = [
|
||||||
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes",
|
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes",
|
||||||
@@ -59,7 +64,6 @@ REBOOT_COMMAND = [
|
|||||||
"reboot",
|
"reboot",
|
||||||
]
|
]
|
||||||
|
|
||||||
# How often to check for updates (seconds) — every 30 minutes
|
|
||||||
UPDATE_CHECK_INTERVAL = 1800
|
UPDATE_CHECK_INTERVAL = 1800
|
||||||
|
|
||||||
|
|
||||||
@@ -93,55 +97,47 @@ def set_autostart_enabled(enabled: bool):
|
|||||||
f.write("Hidden=true\n")
|
f.write("Hidden=true\n")
|
||||||
|
|
||||||
|
|
||||||
def _get_local_rev():
|
def _get_locked_info():
|
||||||
"""Get the local HEAD commit of the flake repo."""
|
"""Read the locked revision and branch of Sovran_Systems from flake.lock."""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
with open(FLAKE_LOCK_PATH, "r") as f:
|
||||||
["git", "-C", FLAKE_DIR, "rev-parse", "HEAD"],
|
lock = json.load(f)
|
||||||
capture_output=True, text=True, timeout=10,
|
nodes = lock.get("nodes", {})
|
||||||
)
|
node = nodes.get(FLAKE_INPUT_NAME, {})
|
||||||
if result.returncode == 0:
|
locked = node.get("locked", {})
|
||||||
return result.stdout.strip()
|
rev = locked.get("rev")
|
||||||
|
branch = locked.get("ref")
|
||||||
|
if not branch:
|
||||||
|
branch = node.get("original", {}).get("ref")
|
||||||
|
return rev, branch
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def _get_remote_rev():
|
def _get_remote_rev(branch=None):
|
||||||
"""Fetch and get the remote HEAD commit without pulling."""
|
"""Query Gitea API for the latest commit SHA on the given branch."""
|
||||||
try:
|
try:
|
||||||
# Fetch latest refs from origin
|
url = GITEA_API_BASE + "?limit=1"
|
||||||
subprocess.run(
|
if branch:
|
||||||
["git", "-C", FLAKE_DIR, "fetch", "--quiet", "origin"],
|
url += f"&sha={branch}"
|
||||||
capture_output=True, text=True, timeout=30,
|
req = urllib.request.Request(url, method="GET")
|
||||||
)
|
req.add_header("Accept", "application/json")
|
||||||
# Get the remote branch HEAD
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
result = subprocess.run(
|
data = json.loads(resp.read().decode())
|
||||||
["git", "-C", FLAKE_DIR, "rev-parse", "origin/HEAD"],
|
if isinstance(data, list) and len(data) > 0:
|
||||||
capture_output=True, text=True, timeout=10,
|
return data[0].get("sha")
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
# Fallback: try origin/main or origin/master
|
|
||||||
for branch in ["origin/main", "origin/master"]:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "-C", FLAKE_DIR, "rev-parse", branch],
|
|
||||||
capture_output=True, text=True, timeout=10,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return result.stdout.strip()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates() -> bool:
|
def check_for_updates() -> bool:
|
||||||
"""Return True if remote has new commits ahead of local."""
|
"""Return True if remote has new commits ahead of locked flake."""
|
||||||
local = _get_local_rev()
|
locked_rev, branch = _get_locked_info()
|
||||||
remote = _get_remote_rev()
|
remote_rev = _get_remote_rev(branch)
|
||||||
if local and remote and local != remote:
|
if locked_rev and remote_rev:
|
||||||
return True
|
return locked_rev != remote_rev
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -347,7 +343,6 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
header = Adw.HeaderBar()
|
header = Adw.HeaderBar()
|
||||||
header.set_title_widget(self._build_title_box())
|
header.set_title_widget(self._build_title_box())
|
||||||
|
|
||||||
# ── Update button (left side) ────────────────────────────
|
|
||||||
self._update_btn = Gtk.Button(
|
self._update_btn = Gtk.Button(
|
||||||
label="Update System",
|
label="Update System",
|
||||||
css_classes=["suggested-action"],
|
css_classes=["suggested-action"],
|
||||||
@@ -356,7 +351,6 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
self._update_btn.connect("clicked", self._on_update_clicked)
|
self._update_btn.connect("clicked", self._on_update_clicked)
|
||||||
header.pack_start(self._update_btn)
|
header.pack_start(self._update_btn)
|
||||||
|
|
||||||
# ── Update badge (dot indicator, hidden by default) ──────
|
|
||||||
self._badge = Gtk.Label(
|
self._badge = Gtk.Label(
|
||||||
label=" ●",
|
label=" ●",
|
||||||
css_classes=["update-badge"],
|
css_classes=["update-badge"],
|
||||||
@@ -364,7 +358,6 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
)
|
)
|
||||||
header.pack_start(self._badge)
|
header.pack_start(self._badge)
|
||||||
|
|
||||||
# ── Refresh button ───────────────────────────────────────
|
|
||||||
refresh_btn = Gtk.Button(
|
refresh_btn = Gtk.Button(
|
||||||
icon_name="view-refresh-symbolic",
|
icon_name="view-refresh-symbolic",
|
||||||
tooltip_text="Refresh now",
|
tooltip_text="Refresh now",
|
||||||
@@ -372,7 +365,6 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
refresh_btn.connect("clicked", lambda _b: self._refresh_all())
|
refresh_btn.connect("clicked", lambda _b: self._refresh_all())
|
||||||
header.pack_end(refresh_btn)
|
header.pack_end(refresh_btn)
|
||||||
|
|
||||||
# ── Settings menu ────────────────────────────────────────
|
|
||||||
menu_btn = Gtk.MenuButton(
|
menu_btn = Gtk.MenuButton(
|
||||||
icon_name="open-menu-symbolic",
|
icon_name="open-menu-symbolic",
|
||||||
tooltip_text="Settings",
|
tooltip_text="Settings",
|
||||||
@@ -432,7 +424,6 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
if interval and interval > 0:
|
if interval and interval > 0:
|
||||||
GLib.timeout_add_seconds(interval, self._auto_refresh)
|
GLib.timeout_add_seconds(interval, self._auto_refresh)
|
||||||
|
|
||||||
# ── Kick off first update check, then repeat ─────────────
|
|
||||||
GLib.timeout_add_seconds(5, self._check_for_updates_once)
|
GLib.timeout_add_seconds(5, self._check_for_updates_once)
|
||||||
GLib.timeout_add_seconds(UPDATE_CHECK_INTERVAL, self._periodic_update_check)
|
GLib.timeout_add_seconds(UPDATE_CHECK_INTERVAL, self._periodic_update_check)
|
||||||
|
|
||||||
@@ -516,17 +507,15 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
GLib.idle_add(self._refresh_all)
|
GLib.idle_add(self._refresh_all)
|
||||||
|
|
||||||
# ── Update availability check ────────────────────────────────
|
|
||||||
|
|
||||||
def _check_for_updates_once(self):
|
def _check_for_updates_once(self):
|
||||||
thread = threading.Thread(target=self._do_update_check, daemon=True)
|
thread = threading.Thread(target=self._do_update_check, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
return False # run once
|
return False
|
||||||
|
|
||||||
def _periodic_update_check(self):
|
def _periodic_update_check(self):
|
||||||
thread = threading.Thread(target=self._do_update_check, daemon=True)
|
thread = threading.Thread(target=self._do_update_check, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
return True # keep repeating
|
return True
|
||||||
|
|
||||||
def _do_update_check(self):
|
def _do_update_check(self):
|
||||||
available = check_for_updates()
|
available = check_for_updates()
|
||||||
@@ -545,15 +534,12 @@ class SovranHubWindow(Adw.ApplicationWindow):
|
|||||||
self._update_btn.set_tooltip_text("System is up to date")
|
self._update_btn.set_tooltip_text("System is up to date")
|
||||||
self._badge.set_visible(False)
|
self._badge.set_visible(False)
|
||||||
|
|
||||||
# ── Callbacks ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _on_update_clicked(self, _btn):
|
def _on_update_clicked(self, _btn):
|
||||||
dialog = UpdateDialog(self)
|
dialog = UpdateDialog(self)
|
||||||
dialog.connect("close-request", lambda _w: self._after_update())
|
dialog.connect("close-request", lambda _w: self._after_update())
|
||||||
dialog.present()
|
dialog.present()
|
||||||
|
|
||||||
def _after_update(self):
|
def _after_update(self):
|
||||||
# Re-check after dialog closes
|
|
||||||
GLib.timeout_add_seconds(3, self._check_for_updates_once)
|
GLib.timeout_add_seconds(3, self._check_for_updates_once)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user