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"