Merge branch 'pr-395' into dev

This commit is contained in:
Rightup committed 2026-08-18 08:40:11 +01:00
commit aa59c313e4
3 files changed
+145 -16

No files matched your search

+12 -6
View File
@@ -730,8 +730,11 @@ if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
exit 1
fi
# If caller supplied a version string, tell setuptools_scm to use it (sudo
# strips env vars so it is passed as a positional argument instead).
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION="$PRETEND_VERSION"
# strips env vars so it is passed as a positional argument instead). Scoped to
# this distribution: the unscoped variable applies to every setuptools_scm
# project built in the same pip run, so a dependency built from an sdist
# instead of a wheel would be stamped with our version too.
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$PRETEND_VERSION"
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
if [ ! -x "$VENV_PYTHON" ]; then
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
@@ -841,7 +844,7 @@ UPGRADEEOF
# Only inspect git metadata when this directory is a checkout of the expected repo.
if is_expected_repo_checkout "$SCRIPT_DIR"; then
if GIT_VERSION="$(python3 -m setuptools_scm 2>/dev/null)"; then
export SETUPTOOLS_SCM_PRETEND_VERSION="$GIT_VERSION"
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$GIT_VERSION"
echo "Installing version: $GIT_VERSION"
fi
fi
@@ -1082,7 +1085,7 @@ upgrade_repeater() {
if is_expected_repo_checkout "$SCRIPT_DIR"; then
if GIT_VERSION="$(python3 -m setuptools_scm 2>/dev/null)"; then
export SETUPTOOLS_SCM_PRETEND_VERSION="$GIT_VERSION"
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$GIT_VERSION"
echo "Upgrading to version: $GIT_VERSION"
fi
fi
@@ -1227,8 +1230,11 @@ if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
exit 1
fi
# If caller supplied a version string, tell setuptools_scm to use it (sudo
# strips env vars so it is passed as a positional argument instead).
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION="$PRETEND_VERSION"
# strips env vars so it is passed as a positional argument instead). Scoped to
# this distribution: the unscoped variable applies to every setuptools_scm
# project built in the same pip run, so a dependency built from an sdist
# instead of a wheel would be stamped with our version too.
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$PRETEND_VERSION"
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
if [ ! -x "$VENV_PYTHON" ]; then
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
+29 -10
View File
@@ -671,6 +671,13 @@ def _has_update(installed: str, latest: str) -> bool:
def _fetch_latest_version(channel: str) -> str:
"""Return the version currently published on ``channel``.
Raises when the version cannot be determined. Returning an approximation
(such as the newest tag) would be actively harmful: the caller stores this
value as the channel's latest version, so a wrong value both misreports
the update state and gets baked into the install as the built version.
"""
base_tag = _get_latest_tag() # always needed for dynamic branches
@@ -680,22 +687,28 @@ def _fetch_latest_version(channel: str) -> str:
body = _fetch_url(compare_url, timeout=10)
data = json.loads(body)
ahead_by = int(data.get("ahead_by", 0))
return _next_dev_version(base_tag, ahead_by)
except _RateLimitError:
raise
except Exception as exc:
logger.debug(f"[Update] Dynamic version compare failed for {channel!r}: {exc}")
return base_tag # fallback: show the tag
raise RuntimeError(
f"Could not compare {base_tag}...{channel} on GitHub: {exc}"
) from exc
return _next_dev_version(base_tag, ahead_by)
# Static version channel — read the pinned version from pyproject.toml on
# that branch directly, so tags created on other branches don't affect it.
try:
toml_url = f"{GITHUB_RAW_BASE}/{channel}/pyproject.toml"
toml_text = _fetch_url(toml_url, timeout=8)
m = re.search(r'^version\s*=\s*["\']([0-9][^"\']*)["\']', toml_text, re.MULTILINE)
if m:
return m.group(1)
except _RateLimitError:
raise
except Exception as exc:
logger.debug(f"[Update] Static version lookup failed for {channel!r}: {exc}")
return base_tag # last-resort fallback
raise RuntimeError(f"Could not read pyproject.toml from {channel}: {exc}") from exc
m = re.search(r'^version\s*=\s*["\']([0-9][^"\']*)["\']', toml_text, re.MULTILINE)
if not m:
raise RuntimeError(f"No pinned version found in pyproject.toml on {channel}")
return m.group(1)
def _fetch_changelog(channel: str, installed: str, max_commits: int = 50) -> List[dict]:
@@ -932,8 +945,13 @@ def _do_install() -> None:
import os as _os
# No SETUPTOOLS_SCM_PRETEND_VERSION here: the version reported by the last
# check is a prediction, and pip derives the real one from the checkout it
# clones. Forcing the predicted value stamped the build with a version that
# did not match its code — and when that value was stale (a failed check, or
# a channel switch) pip saw the requirement as already satisfied and skipped
# the install, pinning the node to a version it could no longer leave.
env = _os.environ.copy()
env["SETUPTOOLS_SCM_PRETEND_VERSION"] = _state.latest_version or "1.0.0"
_VENV_DIR = "/opt/openhop_repeater/venv"
_VENV_PIP = os.path.join(_VENV_DIR, "bin", "pip")
@@ -991,7 +1009,8 @@ def _do_install() -> None:
elif _os.path.isfile(_UPGRADE_WRAPPER):
_state.append_line(f"[pyMC updater] Using sudo wrapper: {_UPGRADE_WRAPPER}")
# The wrapper handles venv creation/migration internally
cmd = [_SUDO_BIN, _UPGRADE_WRAPPER, channel, _state.latest_version or ""]
# Second argument (pretend-version) deliberately omitted — see above.
cmd = [_SUDO_BIN, _UPGRADE_WRAPPER, channel]
else:
msg = (
f"Upgrade wrapper not found at {_UPGRADE_WRAPPER}. "
+104
View File
@@ -470,3 +470,107 @@ def test_do_install_wrapper_success_container_path(isolated_state, monkeypatch):
assert st.state == "complete"
assert st.error_message is None
assert any("container will restart" in line for line in st.progress_lines)
def test_fetch_latest_version_refuses_to_guess_on_failure(monkeypatch):
# The value returned here is stored as the channel's latest version and is
# what the UI compares against, so a guess is worse than an error.
monkeypatch.setattr(ue, "_get_latest_tag", lambda: "1.1.1")
monkeypatch.setattr(ue, "_branch_is_dynamic", lambda _ch: True)
monkeypatch.setattr(
ue,
"_fetch_url",
lambda _url, timeout=10: (_ for _ in ()).throw(RuntimeError("compare unavailable")),
)
with pytest.raises(RuntimeError, match="Could not compare"):
ue._fetch_latest_version("dev")
def test_fetch_latest_version_propagates_rate_limit(monkeypatch):
# A rate-limited check must reach _do_check as a _RateLimitError so the
# existing backoff path runs instead of being reported as a version.
monkeypatch.setattr(ue, "_get_latest_tag", lambda: "1.1.1")
monkeypatch.setattr(ue, "_branch_is_dynamic", lambda _ch: True)
monkeypatch.setattr(
ue,
"_fetch_url",
lambda _url, timeout=10: (_ for _ in ()).throw(ue._RateLimitError("limited")),
)
with pytest.raises(ue._RateLimitError):
ue._fetch_latest_version("dev")
def test_fetch_latest_version_static_channel_without_pin_raises(monkeypatch):
monkeypatch.setattr(ue, "_get_latest_tag", lambda: "1.1.1")
monkeypatch.setattr(ue, "_branch_is_dynamic", lambda _ch: False)
monkeypatch.setattr(ue, "_fetch_url", lambda _url, timeout=8: 'name = "x"\n')
with pytest.raises(RuntimeError, match="No pinned version"):
ue._fetch_latest_version("main")
def _capturing_popen(monkeypatch, calls):
class _Proc:
def __init__(self, cmd, env):
calls.append({"cmd": cmd, "env": env or {}})
self.stdout = ["ok\n"]
self.returncode = 0
def wait(self):
return None
monkeypatch.setattr(ue.subprocess, "Popen", lambda cmd, **kwargs: _Proc(cmd, kwargs.get("env")))
def test_do_install_root_does_not_force_a_predicted_version(isolated_state, monkeypatch):
st = isolated_state
st.channel = "dev"
# A stale prediction, e.g. left by a check that fell back to a tag.
st.latest_version = "1.1.1"
monkeypatch.setattr(ue.os, "geteuid", lambda: 0)
monkeypatch.setattr(ue, "is_buildroot", lambda: False)
monkeypatch.setattr(ue, "is_container", lambda: False)
monkeypatch.setattr(ue, "_migrate_service_unit", lambda: None)
monkeypatch.setattr(ue, "_disable_legacy_services", lambda: None)
monkeypatch.setattr(ue, "_cleanup_stale_dist_info", lambda *args, **kwargs: None)
monkeypatch.setattr(ue.os.path, "isfile", lambda p: True)
monkeypatch.setattr(ue.os.path, "isdir", lambda p: False)
monkeypatch.setattr(ue.time, "sleep", lambda _s: None)
monkeypatch.setattr("repeater.service_utils.restart_service", lambda: (True, "ok"))
calls = []
_capturing_popen(monkeypatch, calls)
ue._do_install()
assert calls, "expected the installer to run at least one command"
for call in calls:
assert "SETUPTOOLS_SCM_PRETEND_VERSION" not in call["env"]
install = [c for c in calls if any("git+https://github.com" in str(x) for x in c["cmd"])]
assert install, "expected a pip install from git"
assert any("@dev" in str(x) for x in install[-1]["cmd"])
def test_do_install_wrapper_call_omits_pretend_version(isolated_state, monkeypatch):
st = isolated_state
st.channel = "dev"
st.latest_version = "1.1.1"
monkeypatch.setattr(ue.os, "geteuid", lambda: 1000)
monkeypatch.setattr(ue, "is_buildroot", lambda: False)
monkeypatch.setattr(ue, "is_container", lambda: False)
monkeypatch.setattr(ue, "_cleanup_stale_dist_info", lambda *args, **kwargs: None)
monkeypatch.setattr(ue.time, "sleep", lambda _s: None)
monkeypatch.setattr(ue.os.path, "isfile", lambda p: p == "/usr/local/bin/pymc-do-upgrade")
monkeypatch.setattr("repeater.service_utils.restart_service", lambda: (True, "ok"))
calls = []
_capturing_popen(monkeypatch, calls)
ue._do_install()
assert calls[-1]["cmd"] == [ue._SUDO_BIN, "/usr/local/bin/pymc-do-upgrade", "dev"]
assert "SETUPTOOLS_SCM_PRETEND_VERSION" not in calls[-1]["env"]