From 8b885eaf46c136d2ad1a62dbc2d7923da606e850 Mon Sep 17 00:00:00 2001 From: emnl51 Date: Fri, 4 Sep 2026 21:17:31 +0200 Subject: [PATCH 1/2] Force recreation and verify deployed version --- scripts/jobtrack_updater.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/jobtrack_updater.py b/scripts/jobtrack_updater.py index e1883c0..965a4b5 100644 --- a/scripts/jobtrack_updater.py +++ b/scripts/jobtrack_updater.py @@ -295,10 +295,10 @@ def _perform_update(self) -> None: build_args.append(self.service) self._compose(*build_args, timeout=1800) self._set_state("restarting", "Restarting the Bert service…", deploy_pending=True) - self._compose("up", "-d", "--no-deps", self.service, timeout=300) + self._compose("up", "-d", "--no-deps", "--force-recreate", self.service, timeout=300) self._set_state("verifying", "Waiting for the health check…", deploy_pending=True) - self._wait_for_health() + self._wait_for_health(expected_version=current.get("remote_version")) self._refresh_repo_state(fetch=False) self._set_state( "succeeded", @@ -319,13 +319,21 @@ def _perform_update(self) -> None: finished_at=utc_now(), ) - def _wait_for_health(self) -> None: + def _wait_for_health(self, expected_version: str | None = None) -> None: last_error = "health endpoint did not respond" for _ in range(45): try: with urllib.request.urlopen(self.health_url, timeout=4) as response: # noqa: S310 - configured admin URL payload = json.loads(response.read().decode("utf-8")) if response.status == 200 and payload.get("status") == "ok": + running_version = str(payload.get("version") or "").removeprefix("v") + if expected_version and running_version != expected_version: + last_error = ( + f"expected Bert {expected_version}, but health reports " + f"{running_version or 'no version'}" + ) + time.sleep(2) + continue self._log(f"Health check passed: {self.health_url}") return last_error = f"unexpected health response: HTTP {response.status}" From 220ab45c0668ef6748d880bc766a2b2197ce043c Mon Sep 17 00:00:00 2001 From: emnl51 Date: Fri, 4 Sep 2026 21:17:33 +0200 Subject: [PATCH 2/2] Cover deployed version verification --- tests/test_update_management.py | 48 ++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_update_management.py b/tests/test_update_management.py index 9dd1e1b..c3deb2e 100644 --- a/tests/test_update_management.py +++ b/tests/test_update_management.py @@ -7,7 +7,7 @@ from app.config import settings from app.update_client import require_same_origin_update, update_status -from scripts.jobtrack_updater import JobTrackUpdater, updater_setting +from scripts.jobtrack_updater import JobTrackUpdater, UpdateFailure, updater_setting def request_with_headers(**headers: str) -> Request: @@ -180,6 +180,8 @@ def test_update_feature_is_wired_without_docker_socket_mount(): assert '"--ff-only"' in updater assert '"--tags"' in updater assert "f\"APP_VERSION={current['remote_version']}\"" in updater + assert '"--force-recreate"' in updater + assert '_wait_for_health(expected_version=current.get("remote_version"))' in updater assert "source.backup(backup)" in updater assert "_wait_for_health" in updater assert "EnvironmentFile=/etc/bert-updater.env" in systemd @@ -203,6 +205,50 @@ def test_updater_reads_dynamic_application_version_fallback(): assert JobTrackUpdater._version_from_text(text) == "17.2.2" +def test_health_verification_requires_expected_deployed_version(monkeypatch): + class Response: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def read(self): + return b'{"status":"ok","version":"19.1.3"}' + + updater = object.__new__(JobTrackUpdater) + updater.health_url = "http://bert.test/health" + updater._log = lambda _: None + monkeypatch.setattr("scripts.jobtrack_updater.urllib.request.urlopen", lambda *_, **__: Response()) + monkeypatch.setattr("scripts.jobtrack_updater.time.sleep", lambda _: None) + + with pytest.raises(UpdateFailure, match="expected Bert 20.1.0, but health reports 19.1.3"): + updater._wait_for_health(expected_version="20.1.0") + + +def test_health_verification_accepts_expected_deployed_version(monkeypatch): + class Response: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def read(self): + return b'{"status":"ok","version":"v20.1.0"}' + + updater = object.__new__(JobTrackUpdater) + updater.health_url = "http://bert.test/health" + updater._log = lambda _: None + monkeypatch.setattr("scripts.jobtrack_updater.urllib.request.urlopen", lambda *_, **__: Response()) + + updater._wait_for_health(expected_version="20.1.0") + + def test_legacy_application_profile_index_is_created_after_column_migration(): database = Path("app/db.py").read_text(encoding="utf-8") migration = "ALTER TABLE applications ADD COLUMN profile_id INTEGER"