Skip to content

Commit a0082f1

Browse files
committed
fix(cli): soften self-upgrade rollback hints
1 parent b0ac2f2 commit a0082f1

4 files changed

Lines changed: 45 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ specify self upgrade --dry-run
104104
# Upgrade in place to the latest stable release (auto-detects uv tool vs pipx install)
105105
specify self upgrade
106106

107-
# Or pin a specific release tag
108-
specify self upgrade --tag vX.Y.Z
107+
# Or pin a specific release tag (replace with your desired release tag)
108+
specify self upgrade --tag v0.8.6
109109
```
110110

111111
Bare `specify self upgrade` executes immediately, matching the `pip install -U` / `uv tool upgrade` / `npm update` convention. `uvx`-ephemeral runs and source checkouts are detected and produce path-specific guidance instead of running an installer. Set `SPECIFY_UPGRADE_TIMEOUT_SECS` to cap how long the installer subprocess may run (default: no timeout — interrupt with `Ctrl+C` if needed).

docs/upgrade.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
| What to Upgrade | Command | When to Use |
1010
|----------------|---------|-------------|
1111
| **CLI Tool (recommended)** | `specify self upgrade` | Latest stable release, in place. Auto-detects whether you installed via `uv tool` or `pipx`. |
12-
| **CLI Tool — pin a version** | `specify self upgrade --tag vX.Y.Z` | Upgrade to a specific release tag instead of the latest stable. |
12+
| **CLI Tool — pin a version** | `specify self upgrade --tag v0.8.6` | Upgrade to a specific release tag instead of the latest stable. Replace `v0.8.6` with the release tag you want. |
1313
| **CLI Tool — manual fallback** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | When `specify self upgrade` isn't available (older installs) or when you want explicit control. |
1414
| **CLI Tool — manual fallback (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Same as above, for pipx installs. |
1515
| **Project Files** | `specify init --here --force --integration <your-agent>` | Update slash commands, templates, and scripts in your project. |
@@ -35,8 +35,8 @@ specify self upgrade --dry-run
3535
# Upgrade in place to the latest stable release (auto-detects uv tool vs pipx install)
3636
specify self upgrade
3737

38-
# Or pin a specific release tag
39-
specify self upgrade --tag vX.Y.Z
38+
# Or pin a specific release tag (replace with the release tag you want)
39+
specify self upgrade --tag v0.8.6
4040
```
4141

4242
Bare `specify self upgrade` executes immediately, matching the `pip install -U` / `uv tool upgrade` / `npm update` convention. The CLI classifies your runtime into one of: `uv tool`, `pipx`, `uvx (ephemeral)`, source checkout, or unsupported. Only `uv tool` and `pipx` are upgraded automatically; the other paths print path-specific guidance and exit 0 without touching anything.

src/specify_cli/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,20 @@ def _canonicalize_version_text(value: str) -> str:
17531753
return normalized
17541754

17551755

1756+
def _stable_release_tag_for_version(version_text: str) -> str | None:
1757+
"""Return `vX.Y.Z` only for exact stable release versions."""
1758+
try:
1759+
parsed = Version(version_text)
1760+
except InvalidVersion:
1761+
return None
1762+
if parsed.pre or parsed.post or parsed.dev or parsed.local:
1763+
return None
1764+
release = parsed.release
1765+
if len(release) != 3:
1766+
return None
1767+
return f"v{release[0]}.{release[1]}.{release[2]}"
1768+
1769+
17561770
def _render_argv(argv: list[str]) -> str:
17571771
"""Render argv for copy/paste on the current platform."""
17581772
return subprocess.list2cmdline(argv) if os.name == "nt" else shlex.join(argv)
@@ -2425,14 +2439,20 @@ def _rollback_hint(plan: _UpgradePlan) -> str:
24252439
"Could not determine the previous version; "
24262440
"reinstall manually from: https://github.com/github/spec-kit/releases"
24272441
)
2442+
rollback_tag = _stable_release_tag_for_version(plan.pre_upgrade_snapshot)
2443+
if rollback_tag is None:
2444+
return (
2445+
"Previous version was not an exact stable release tag; "
2446+
"reinstall manually from: https://github.com/github/spec-kit/releases"
2447+
)
24282448
if plan.method == _InstallMethod.PIPX:
24292449
return (
24302450
f"To pin back to the previous version: pipx install --force "
2431-
f"git+https://github.com/github/spec-kit.git@v{plan.pre_upgrade_snapshot}"
2451+
f"git+https://github.com/github/spec-kit.git@{rollback_tag}"
24322452
)
24332453
return (
24342454
f"To pin back to the previous version: uv tool install specify-cli --force "
2435-
f"--from git+https://github.com/github/spec-kit.git@v{plan.pre_upgrade_snapshot}"
2455+
f"--from git+https://github.com/github/spec-kit.git@{rollback_tag}"
24362456
)
24372457

24382458

tests/test_self_upgrade.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,24 @@ def test_pipx_failure_prints_pipx_rollback_hint(self, pipx_argv0, clean_environ)
10821082
"git+https://github.com/github/spec-kit.git@v0.7.5"
10831083
) in out
10841084

1085+
def test_prerelease_failure_degrades_rollback_hint_to_releases_page(
1086+
self, uv_tool_argv0, clean_environ
1087+
):
1088+
with patch("specify_cli.urllib.request.urlopen") as mock_urlopen, patch(
1089+
"specify_cli.shutil.which", return_value="/usr/bin/uv"
1090+
), patch("specify_cli.subprocess.run") as mock_run, patch(
1091+
"specify_cli._get_installed_version", return_value="1.0.0rc1"
1092+
):
1093+
mock_urlopen.return_value = _mock_urlopen_response({"tag_name": "v1.0.0"})
1094+
mock_run.side_effect = [_completed_process(2)]
1095+
result = runner.invoke(app, ["self", "upgrade"])
1096+
1097+
assert result.exit_code == 2
1098+
out = strip_ansi(result.output)
1099+
assert "Previous version was not an exact stable release tag" in out
1100+
assert "https://github.com/github/spec-kit/releases" in out
1101+
assert "git+https://github.com/github/spec-kit.git@v1.0.0rc1" not in out
1102+
10851103

10861104
class TestVerificationMismatch:
10871105
"""Installer says 0 but the binary is still the old version → exit 2."""

0 commit comments

Comments
 (0)