Use peeled commit refs (^{commit}) for tag resolution in self-update manager

Replace direct tag refs with peeled commit refs (refs/tags/{tag}^{commit}) in git rev-parse and merge-base operations to ensure resolution to commit objects rather than annotated tag objects. Extract get_tag_commit_ref helper for consistent ref formatting. Update fetch_release_refs to check peeled ref in ancestry validation. Add test coverage for peeled commit resolution in explicit tag selection and fetch operations. Update

frdel committed Mar 26, 2026 at 12:54 UTC 8078693e4120f5c61b9b2eef899e6b5b1da1f88a
3 files changed +55 -5
docker/run/fs/exe/self_update_manager.py
+10 -3
@@ -166,6 +166,10 @@ def is_latest_selector_tag(tag: str) -> bool:
166 return tag.strip().lower() == LATEST_SELECTOR_TAG
167
168
169 +def get_tag_commit_ref(tag: str) -> str:
170 + return f"refs/tags/{tag}^{{commit}}"
171 +
172 +
173 def build_default_backup_name() -> str:
174 timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
175 return f"usr-{timestamp}.zip"
@@ -493,6 +497,7 @@ def clean_repo_worktree(
497
498 def fetch_release_refs(repo_dir: Path, branch: str, tag: str, logger: AttemptLogger) -> None:
499 remote_branch_ref = f"refs/remotes/a0-self-update/{branch}"
500 + tag_commit_ref = get_tag_commit_ref(tag)
501 logger.log(f"Fetching branch {branch} and tag {tag} from {OFFICIAL_REPO_URL}")
502 run_command(
503 [
@@ -516,7 +521,7 @@ def fetch_release_refs(repo_dir: Path, branch: str, tag: str, logger: AttemptLog
521 str(repo_dir),
522 "merge-base",
523 "--is-ancestor",
519 - f"refs/tags/{tag}",
524 + tag_commit_ref,
525 remote_branch_ref,
526 ],
527 cwd=None,
@@ -557,12 +562,13 @@ def resolve_requested_target(
562
563 if not is_latest_selector_tag(normalized_tag):
564 fetch_release_refs(repo_dir, branch, normalized_tag, logger)
565 + tag_commit_ref = get_tag_commit_ref(normalized_tag)
566 return {
567 "requested_tag": normalized_tag,
568 "effective_tag": normalized_tag,
569 "target_ref": f"refs/tags/{normalized_tag}",
570 "expected_short_tag": normalized_tag,
565 - "expected_commit": git_output(repo_dir, "rev-parse", f"refs/tags/{normalized_tag}"),
571 + "expected_commit": git_output(repo_dir, "rev-parse", tag_commit_ref),
572 "target_description": f"tag {normalized_tag}",
573 }
574
@@ -573,13 +579,14 @@ def resolve_requested_target(
579 branch_ref=remote_branch_ref,
580 current_version=current_version,
581 )
582 + tag_commit_ref = get_tag_commit_ref(effective_tag)
583 logger.log(f"Resolved latest on main to tag {effective_tag}")
584 return {
585 "requested_tag": LATEST_SELECTOR_TAG,
586 "effective_tag": effective_tag,
587 "target_ref": f"refs/tags/{effective_tag}",
588 "expected_short_tag": effective_tag,
582 - "expected_commit": git_output(repo_dir, "rev-parse", f"refs/tags/{effective_tag}"),
589 + "expected_commit": git_output(repo_dir, "rev-parse", tag_commit_ref),
590 "target_description": f"latest tag {effective_tag}",
591 }
592
tests/test_self_update_tag_filter.py
+44 -1
@@ -613,7 +613,7 @@ def test_self_update_manager_latest_on_main_uses_current_major_release(monkeypat
613 "git_output",
614 lambda repo_dir, *args: {
615 ("tag", "--merged", "refs/remotes/a0-self-update/main"): "v2.0\nv1.4\nv1.2\n",
616 - ("rev-parse", "refs/tags/v1.4"): "deadbeef1234",
616 + ("rev-parse", "refs/tags/v1.4^{commit}"): "deadbeef1234",
617 }[args],
618 )
619
@@ -627,6 +627,49 @@ def test_self_update_manager_latest_on_main_uses_current_major_release(monkeypat
627
628 assert resolved["effective_tag"] == "v1.4"
629 assert resolved["expected_short_tag"] == "v1.4"
630 + assert resolved["expected_commit"] == "deadbeef1234"
631 +
632 +
633 +def test_self_update_manager_explicit_tag_uses_peeled_commit(monkeypatch):
634 + manager = load_self_update_manager()
635 + monkeypatch.setattr(manager, "fetch_release_refs", lambda repo_dir, branch, tag, logger: None)
636 + monkeypatch.setattr(
637 + manager,
638 + "git_output",
639 + lambda repo_dir, *args: {
640 + ("rev-parse", "refs/tags/v1.10^{commit}"): "192d6e2cae1a85c0a2e7a6ecf41c153b39f1b4c6",
641 + }[args],
642 + )
643 +
644 + resolved = manager.resolve_requested_target(
645 + Path("/tmp/repo"),
646 + "development",
647 + "v1.10",
648 + "v1.11",
649 + manager.NullLogger(),
650 + )
651 +
652 + assert resolved["effective_tag"] == "v1.10"
653 + assert resolved["expected_commit"] == "192d6e2cae1a85c0a2e7a6ecf41c153b39f1b4c6"
654 +
655 +
656 +def test_self_update_manager_fetch_release_refs_checks_peeled_tag_commit(monkeypatch):
657 + manager = load_self_update_manager()
658 + commands = []
659 + monkeypatch.setattr(
660 + manager,
661 + "run_command",
662 + lambda command, **kwargs: commands.append(command),
663 + )
664 +
665 + manager.fetch_release_refs(
666 + Path("/tmp/repo"),
667 + "development",
668 + "v1.10",
669 + manager.NullLogger(),
670 + )
671 +
672 + assert commands[1][-2] == "refs/tags/v1.10^{commit}"
673
674
675 def test_self_update_manager_latest_on_non_main_rejects_cross_major(monkeypatch):
webui/components/settings/external/self-update-modal.html
+1 -1
@@ -347,7 +347,7 @@
347 @click="$store.selfUpdateStore.scheduleUpdate()"
348 :disabled="!$store.selfUpdateStore.canScheduleUpdate"
349 >
350 - Schedule Update And Restart
350 + Restart and Update
351 </button>
352 <button
353 type="button"