Skip to content

Commit 42e5e2e

Browse files
committed
fix(cli): avoid indeterminate self-upgrade no-ops
1 parent a0082f1 commit 42e5e2e

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

src/specify_cli/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,15 @@ def _stable_release_tag_for_version(version_text: str) -> str | None:
17671767
return f"v{release[0]}.{release[1]}.{release[2]}"
17681768

17691769

1770+
def _is_comparable_version_text(value: str) -> bool:
1771+
"""Return whether version-like text parses under PEP 440 after tag normalization."""
1772+
try:
1773+
Version(_normalize_tag(value))
1774+
return True
1775+
except InvalidVersion:
1776+
return False
1777+
1778+
17701779
def _render_argv(argv: list[str]) -> str:
17711780
"""Render argv for copy/paste on the current platform."""
17721781
return subprocess.list2cmdline(argv) if os.name == "nt" else shlex.join(argv)
@@ -2713,7 +2722,12 @@ def self_upgrade(
27132722
else "unknown"
27142723
)
27152724
if plan.current_version != "unknown":
2716-
if tag is None and not _is_newer(target_canonical, plan.current_version):
2725+
versions_comparable = _is_comparable_version_text(
2726+
plan.current_version
2727+
) and _is_comparable_version_text(target_canonical)
2728+
if tag is None and versions_comparable and not _is_newer(
2729+
target_canonical, plan.current_version
2730+
):
27172731
if current_canonical == target_canonical:
27182732
console.print(f"Already on latest release: {plan.target_tag}")
27192733
else:

tests/test_self_upgrade.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,27 @@ def test_dev_build_ahead_of_release_reports_newer_noop(
360360
assert "Already on latest release or newer: 0.7.7.dev0" in strip_ansi(result.output)
361361
assert mock_run.call_count == 0
362362

363+
def test_unparseable_current_version_does_not_false_noop(
364+
self, uv_tool_argv0, clean_environ
365+
):
366+
with patch("specify_cli.urllib.request.urlopen") as mock_urlopen, patch(
367+
"specify_cli.subprocess.run"
368+
) as mock_run, patch(
369+
"specify_cli.shutil.which", return_value="/usr/bin/uv"
370+
), patch("specify_cli._get_installed_version", return_value="release-main"):
371+
mock_urlopen.return_value = _mock_urlopen_response({"tag_name": "v0.7.6"})
372+
mock_run.side_effect = [
373+
_completed_process(0),
374+
_completed_process(0, stdout="specify 0.7.6\n"),
375+
]
376+
result = runner.invoke(app, ["self", "upgrade"])
377+
378+
assert result.exit_code == 0
379+
out = strip_ansi(result.output)
380+
assert "Already on latest release" not in out
381+
assert "Upgrading specify-cli release-main → v0.7.6 via uv tool:" in out
382+
assert mock_run.call_count == 2
383+
363384
def test_pinned_older_tag_still_runs_installer(
364385
self, uv_tool_argv0, clean_environ
365386
):

0 commit comments

Comments
 (0)