fix(updates): fall through when release tag is not ff-reachable
When agent checkouts track main past an older tag but the newest published tag is on a divergent side branch, stop advertising tag-based updates and route check/apply through the upstream branch instead.
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Agent self-update no longer advertises or applies unreachable release tags when the checkout tracks `main` past an older tag but the newest published tag lives on a divergent side branch (for example `v2026.5.29` → `v2026.5.29.2`). The update checker and apply path now fall through to the configured upstream branch when `git pull --ff-only <latest-tag>` cannot fast-forward, matching the existing #2653/#3140 release-vs-branch routing.
|
||||
|
||||
## [v0.51.185] — 2026-05-31 — Release FE (stage-batchE — clarify-card bug-fix batch: identical-prompt dedup + autofill guard + GBK startup crash)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -466,6 +466,14 @@ def _head_contains_ref(path, ref):
|
||||
return bool(ok)
|
||||
|
||||
|
||||
def _can_fast_forward_to(path, ref):
|
||||
"""Return True when ``ref`` is a descendant of HEAD (``git pull --ff-only`` can reach it)."""
|
||||
if not ref:
|
||||
return False
|
||||
_, ok = _run_git(['merge-base', '--is-ancestor', 'HEAD', ref], path)
|
||||
return bool(ok)
|
||||
|
||||
|
||||
def _select_apply_compare_ref(path):
|
||||
"""Return the same remote ref family that the update check reports.
|
||||
|
||||
@@ -497,6 +505,8 @@ def _select_apply_compare_ref(path):
|
||||
behind == 0 and _head_is_past_latest_tag(path, current_tag)
|
||||
) or (
|
||||
behind > 0 and _head_contains_ref(path, latest_tag)
|
||||
) or (
|
||||
behind > 0 and not _can_fast_forward_to(path, latest_tag)
|
||||
):
|
||||
pass
|
||||
else:
|
||||
@@ -537,6 +547,12 @@ def _check_repo_release(path, name):
|
||||
if behind > 0 and _head_contains_ref(path, latest_tag):
|
||||
return None
|
||||
|
||||
# Patch releases can land on a side branch while day-to-day installs track
|
||||
# main past an older tag. A positive tag-name gap then advertises an update
|
||||
# that `git pull --ff-only <latest-tag>` cannot reach.
|
||||
if behind > 0 and not _can_fast_forward_to(path, latest_tag):
|
||||
return None
|
||||
|
||||
remote_url, _ = _run_git(['remote', 'get-url', 'origin'], path)
|
||||
remote_url = _normalize_remote_url(remote_url)
|
||||
|
||||
|
||||
@@ -224,6 +224,8 @@ class TestUpdateChecker:
|
||||
return 'v0.51.35\nv0.51.34\nv0.51.33', True
|
||||
if args[:3] == ['describe', '--tags', '--abbrev=0']:
|
||||
return 'v0.51.34', True
|
||||
if args == ['merge-base', '--is-ancestor', 'HEAD', 'v0.51.35']:
|
||||
return '', True
|
||||
if args[:2] == ['remote', 'get-url']:
|
||||
return 'https://github.com/nesquena/hermes-webui.git', True
|
||||
return '', False
|
||||
|
||||
@@ -13,6 +13,8 @@ def _fake_git_for_release_fetch_failure(args, cwd, timeout=10):
|
||||
return 'v0.51.103', True
|
||||
if args == ['merge-base', '--is-ancestor', 'v0.51.106', 'HEAD']:
|
||||
return '', False
|
||||
if args == ['merge-base', '--is-ancestor', 'HEAD', 'v0.51.106']:
|
||||
return '', True
|
||||
if args == ['remote', 'get-url', 'origin']:
|
||||
return 'https://github.com/nesquena/hermes-webui.git', True
|
||||
raise AssertionError(f'unexpected git args: {args!r}')
|
||||
@@ -675,3 +677,57 @@ def test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_ex
|
||||
'origin/<branch>. Regression for Opus-flagged drift in #2855.'
|
||||
)
|
||||
|
||||
|
||||
def test_check_repo_release_falls_through_when_latest_tag_is_not_ff_reachable(tmp_path):
|
||||
"""Main-tracking HEAD past an older tag cannot ff to a patch release tag.
|
||||
|
||||
Repro: installer puts agent on main at v2026.5.29+N-g..., maintainers cut
|
||||
v2026.5.29.2 from a side branch. Tag gap is positive, but
|
||||
``git pull --ff-only v2026.5.29.2`` fails with diverging branches.
|
||||
The release check must fall through to the upstream branch comparison.
|
||||
"""
|
||||
(tmp_path / '.git').mkdir()
|
||||
|
||||
def fake_git(args, cwd, timeout=10):
|
||||
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
|
||||
return 'v2026.5.29.2\nv2026.5.29', True
|
||||
if args == ['describe', '--tags', '--abbrev=0']:
|
||||
return 'v2026.5.29', True
|
||||
if args == ['describe', '--tags', '--always']:
|
||||
return 'v2026.5.29-265-g5921d6678', True
|
||||
if args == ['merge-base', '--is-ancestor', 'v2026.5.29.2', 'HEAD']:
|
||||
return '', False
|
||||
if args == ['merge-base', '--is-ancestor', 'HEAD', 'v2026.5.29.2']:
|
||||
return '', False
|
||||
raise AssertionError(f'unexpected git args: {args!r}')
|
||||
|
||||
with patch.object(updates, '_run_git', side_effect=fake_git):
|
||||
result = updates._check_repo_release(tmp_path, 'agent')
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_select_apply_compare_ref_falls_through_when_latest_tag_is_not_ff_reachable(tmp_path):
|
||||
"""Apply path mirrors the ff-unreachable release-tag fall-through."""
|
||||
(tmp_path / '.git').mkdir()
|
||||
|
||||
def fake_git(args, cwd, timeout=10):
|
||||
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
|
||||
return 'v2026.5.29.2\nv2026.5.29', True
|
||||
if args == ['describe', '--tags', '--abbrev=0']:
|
||||
return 'v2026.5.29', True
|
||||
if args == ['describe', '--tags', '--always']:
|
||||
return 'v2026.5.29-265-g5921d6678', True
|
||||
if args == ['merge-base', '--is-ancestor', 'v2026.5.29.2', 'HEAD']:
|
||||
return '', False
|
||||
if args == ['merge-base', '--is-ancestor', 'HEAD', 'v2026.5.29.2']:
|
||||
return '', False
|
||||
if args == ['rev-parse', '--abbrev-ref', '@{upstream}']:
|
||||
return 'origin/main', True
|
||||
raise AssertionError(f'unexpected git args: {args!r}')
|
||||
|
||||
with patch.object(updates, '_run_git', side_effect=fake_git):
|
||||
ref = updates._select_apply_compare_ref(tmp_path)
|
||||
|
||||
assert ref == 'origin/main'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user