Extract installed_target_matches_request helper for update skip check and use describe field for exact version matching
Add installed_target_matches_request helper to check if requested update target matches current installation. Replace short_tag comparison with describe field to ensure exact commit match - prevents skipping updates when current version is ahead of requested tag (e.g. v1.11-12-ge9d9c93d vs v1.11). Return false for "latest" selector tags to force update check. Add test coverage
frdel committed
Mar 26, 2026 at 13:16 UTC
0bca80a49ff8e151b77f3a6b98b5531a688c754a
3 files changed
+89
-12
docker/run/fs/exe/self_update_manager.py
+21
-5
@@ -1055,6 +1055,23 @@ def queue_update_request(
1055
return payload
1056
1057
1058
+def installed_target_matches_request(
1059
+ current_info: dict[str, str],
1060
+ *,
1061
+ requested_branch: str,
1062
+ requested_tag: str,
1063
+) -> bool:
1064
+ normalized_tag = requested_tag.strip()
1065
+ if not normalized_tag or is_latest_selector_tag(normalized_tag):
1066
+ return False
1067
+
1068
+ current_branch = current_info.get("branch", "").strip()
1069
+ if requested_branch.strip() and current_branch != requested_branch.strip():
1070
+ return False
1071
+
1072
+ return current_info.get("describe", "").strip() == normalized_tag
1073
+
1074
+
1075
def trigger_update_command(args: list[str]) -> int:
1076
parser = argparse.ArgumentParser(
1077
prog="trigger_self_update.sh",
@@ -1137,11 +1154,10 @@ def docker_run_ui() -> int:
1154
current = get_repo_version_info(REPO_DIR)
1155
requested_branch = str(request_data.get("branch", "")).strip()
1156
requested_tag = str(request_data.get("tag", "")).strip()
1140
- current_branch = current.get("branch", "").strip()
1141
- if (
1142
- requested_tag
1143
- and current["short_tag"] == requested_tag
1144
- and (not requested_branch or current_branch == requested_branch)
1157
+ if installed_target_matches_request(
1158
+ current,
1159
+ requested_branch=requested_branch,
1160
+ requested_tag=requested_tag,
1161
):
1162
logger.log(
1163
"Requested tag already matches the installed version, skipping file replacement."
tests/test_self_update_tag_filter.py
+44
-1
@@ -393,7 +393,9 @@ def test_self_update_frontend_uses_preloaded_select():
393
assert "getLastStatusBadgeClass(status)" in content
394
assert "this.info?.current?.display_version" in content
395
assert "resetRestartState()" in content
396
- assert "restartRequestError" in content
396
+ assert "restartRequestStarted" in content
397
+ assert "restartResponse.status >= 500" in content
398
+ assert "while Agent Zero was shutting down" in content
399
assert "await notificationStore.frontendWarning(" not in content
400
assert "status-pill-error" in content
401
assert "status-pill-success" in content
@@ -658,6 +660,47 @@ def test_self_update_manager_explicit_tag_uses_peeled_commit(monkeypatch):
660
assert resolved["expected_commit"] == "192d6e2cae1a85c0a2e7a6ecf41c153b39f1b4c6"
661
662
663
+def test_self_update_manager_skip_check_requires_exact_describe_match():
664
+ manager = load_self_update_manager()
665
+
666
+ assert (
667
+ manager.installed_target_matches_request(
668
+ {
669
+ "branch": "development",
670
+ "describe": "v1.11",
671
+ "short_tag": "v1.11",
672
+ },
673
+ requested_branch="development",
674
+ requested_tag="v1.11",
675
+ )
676
+ is True
677
+ )
678
+ assert (
679
+ manager.installed_target_matches_request(
680
+ {
681
+ "branch": "development",
682
+ "describe": "v1.11-12-ge9d9c93d",
683
+ "short_tag": "v1.11",
684
+ },
685
+ requested_branch="development",
686
+ requested_tag="v1.11",
687
+ )
688
+ is False
689
+ )
690
+ assert (
691
+ manager.installed_target_matches_request(
692
+ {
693
+ "branch": "development",
694
+ "describe": "v1.11",
695
+ "short_tag": "v1.11",
696
+ },
697
+ requested_branch="development",
698
+ requested_tag="latest",
699
+ )
700
+ is False
701
+ )
702
+
703
+
704
def test_self_update_manager_fetch_release_refs_checks_peeled_tag_commit(monkeypatch):
705
manager = load_self_update_manager()
706
commands = []
webui/components/settings/external/self-update-store.js
+24
-6
@@ -497,9 +497,10 @@ const model = {
497
);
498
this.ensureProgressOverlay();
499
500
- let restartRequestError = null;
500
+ let restartRequestStarted = false;
501
try {
502
const token = await API.getCsrfToken();
503
+ restartRequestStarted = true;
504
const restartResponse = await fetch("/api/restart", {
505
method: "POST",
506
credentials: "same-origin",
@@ -511,19 +512,36 @@ const model = {
512
body: JSON.stringify({}),
513
});
514
if (restartResponse && !restartResponse.ok) {
514
- restartRequestError = new Error(
515
- `Restart request failed with HTTP ${restartResponse.status}.`
515
+ if (restartResponse.status >= 500) {
516
+ console.warn(
517
+ `Restart request returned HTTP ${restartResponse.status} while Agent Zero was shutting down. Continuing to wait for the new runtime.`
518
+ );
519
+ this.setRestartState(
520
+ "Restarting backend",
521
+ "Agent Zero is shutting down and applying the update. Waiting for the new runtime to come back healthy."
522
+ );
523
+ } else {
524
+ throw new Error(
525
+ `Restart request failed with HTTP ${restartResponse.status}.`
526
+ );
527
+ }
528
+ } else {
529
+ this.setRestartState(
530
+ "Restarting backend",
531
+ "Agent Zero accepted the restart request. Waiting for the updater to take over."
532
);
517
- throw restartRequestError;
533
}
534
} catch (error) {
520
- if (restartRequestError && error === restartRequestError) {
535
+ if (!restartRequestStarted) {
536
this.restarting = false;
537
this.resetRestartState();
538
this.removeProgressOverlay();
539
throw error;
540
}
526
- // The restart request often terminates the backend mid-flight.
541
+ console.warn(
542
+ "Restart request connection closed while Agent Zero was restarting:",
543
+ error
544
+ );
545
}
546
547
const maxWaitMs =