diff --git a/docker/run/fs/exe/self_update_manager.py b/docker/run/fs/exe/self_update_manager.py index d963541c2..d3abaf266 100644 --- a/docker/run/fs/exe/self_update_manager.py +++ b/docker/run/fs/exe/self_update_manager.py @@ -1055,6 +1055,23 @@ def queue_update_request( return payload +def installed_target_matches_request( + current_info: dict[str, str], + *, + requested_branch: str, + requested_tag: str, +) -> bool: + normalized_tag = requested_tag.strip() + if not normalized_tag or is_latest_selector_tag(normalized_tag): + return False + + current_branch = current_info.get("branch", "").strip() + if requested_branch.strip() and current_branch != requested_branch.strip(): + return False + + return current_info.get("describe", "").strip() == normalized_tag + + def trigger_update_command(args: list[str]) -> int: parser = argparse.ArgumentParser( prog="trigger_self_update.sh", @@ -1137,11 +1154,10 @@ def docker_run_ui() -> int: current = get_repo_version_info(REPO_DIR) requested_branch = str(request_data.get("branch", "")).strip() requested_tag = str(request_data.get("tag", "")).strip() - current_branch = current.get("branch", "").strip() - if ( - requested_tag - and current["short_tag"] == requested_tag - and (not requested_branch or current_branch == requested_branch) + if installed_target_matches_request( + current, + requested_branch=requested_branch, + requested_tag=requested_tag, ): logger.log( "Requested tag already matches the installed version, skipping file replacement." diff --git a/tests/test_self_update_tag_filter.py b/tests/test_self_update_tag_filter.py index d6298e477..dbbb70d48 100644 --- a/tests/test_self_update_tag_filter.py +++ b/tests/test_self_update_tag_filter.py @@ -393,7 +393,9 @@ def test_self_update_frontend_uses_preloaded_select(): assert "getLastStatusBadgeClass(status)" in content assert "this.info?.current?.display_version" in content assert "resetRestartState()" in content - assert "restartRequestError" in content + assert "restartRequestStarted" in content + assert "restartResponse.status >= 500" in content + assert "while Agent Zero was shutting down" in content assert "await notificationStore.frontendWarning(" not in content assert "status-pill-error" in content assert "status-pill-success" in content @@ -658,6 +660,47 @@ def test_self_update_manager_explicit_tag_uses_peeled_commit(monkeypatch): assert resolved["expected_commit"] == "192d6e2cae1a85c0a2e7a6ecf41c153b39f1b4c6" +def test_self_update_manager_skip_check_requires_exact_describe_match(): + manager = load_self_update_manager() + + assert ( + manager.installed_target_matches_request( + { + "branch": "development", + "describe": "v1.11", + "short_tag": "v1.11", + }, + requested_branch="development", + requested_tag="v1.11", + ) + is True + ) + assert ( + manager.installed_target_matches_request( + { + "branch": "development", + "describe": "v1.11-12-ge9d9c93d", + "short_tag": "v1.11", + }, + requested_branch="development", + requested_tag="v1.11", + ) + is False + ) + assert ( + manager.installed_target_matches_request( + { + "branch": "development", + "describe": "v1.11", + "short_tag": "v1.11", + }, + requested_branch="development", + requested_tag="latest", + ) + is False + ) + + def test_self_update_manager_fetch_release_refs_checks_peeled_tag_commit(monkeypatch): manager = load_self_update_manager() commands = [] diff --git a/webui/components/settings/external/self-update-store.js b/webui/components/settings/external/self-update-store.js index 515e4b33b..87cf02b14 100644 --- a/webui/components/settings/external/self-update-store.js +++ b/webui/components/settings/external/self-update-store.js @@ -497,9 +497,10 @@ const model = { ); this.ensureProgressOverlay(); - let restartRequestError = null; + let restartRequestStarted = false; try { const token = await API.getCsrfToken(); + restartRequestStarted = true; const restartResponse = await fetch("/api/restart", { method: "POST", credentials: "same-origin", @@ -511,19 +512,36 @@ const model = { body: JSON.stringify({}), }); if (restartResponse && !restartResponse.ok) { - restartRequestError = new Error( - `Restart request failed with HTTP ${restartResponse.status}.` + if (restartResponse.status >= 500) { + console.warn( + `Restart request returned HTTP ${restartResponse.status} while Agent Zero was shutting down. Continuing to wait for the new runtime.` + ); + this.setRestartState( + "Restarting backend", + "Agent Zero is shutting down and applying the update. Waiting for the new runtime to come back healthy." + ); + } else { + throw new Error( + `Restart request failed with HTTP ${restartResponse.status}.` + ); + } + } else { + this.setRestartState( + "Restarting backend", + "Agent Zero accepted the restart request. Waiting for the updater to take over." ); - throw restartRequestError; } } catch (error) { - if (restartRequestError && error === restartRequestError) { + if (!restartRequestStarted) { this.restarting = false; this.resetRestartState(); this.removeProgressOverlay(); throw error; } - // The restart request often terminates the backend mid-flight. + console.warn( + "Restart request connection closed while Agent Zero was restarting:", + error + ); } const maxWaitMs =