fix(upgrade): pin version + verify post-install to prevent silent no-ops#17
Merged
Merged
Conversation
## Summary
A user reported that `dailybot upgrade` claimed success but the on-disk
version stayed at v1.6.1. Reproducible scenario:
$ dailybot upgrade
Latest version: 1.7.0 (update available)
Install method: pip
Running: /usr/local/bin/python3 -m pip install --upgrade dailybot-cli
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: dailybot-cli (1.6.1)
...
OK Upgrade complete.
$ dailybot --version
dailybot 1.6.1 ← unchanged
Two cooperating bugs in pip's resolver caused the no-op:
1. After a fresh release, `_fetch_latest_pypi_version` (PyPI's JSON
API) saw 1.7.0 immediately, but pip's package index cache still
resolved `dailybot-cli` (no version pin) to 1.6.1 as "latest
available".
2. With user-fallback active because system site-packages is
read-only, pip's resolver decided the existing 1.6.1 install
"satisfied" the unpinned requirement and exited 0 without doing
anything.
The user had to run `install.sh` manually to actually upgrade.
## Fix (two layers, defense-in-depth)
1. **Pin the version in pip's argv.** When `_fetch_latest_pypi_version`
returns a concrete version, build `pip install --upgrade
dailybot-cli==<version>` instead of bare `dailybot-cli`. pip can no
longer claim "already satisfied" because the requested version
differs from what's installed. Falls back to bare `dailybot-cli`
when the JSON API is unreachable (preserves the old behavior in
that fallback path).
2. **Verify the on-disk version after pip exits.** New
`_query_installed_version()` spawns a fresh subprocess to read
`importlib.metadata.version("dailybot-cli")` — the running
interpreter has the old version cached, so we need a fresh
process. If the installed version still equals what we started
with, surface a warning that points the user at the install.sh
fallback instead of falsely reporting success.
Layer (2) catches future edge cases beyond the specific pip scenario
(read-only site-packages with broken PATH, package-manager bugs, etc.).
## Change Log
- `commands/upgrade.py::_build_upgrade_argv` — accepts an optional
`latest` argument; pins the version for `pip` when known.
- `commands/upgrade.py::_query_installed_version` — new helper, reads
the installed version from a fresh Python subprocess.
- `commands/upgrade.py::upgrade` — calls verification after the
upgrade subprocess returns, downgrades the success message to a
warning when the version didn't actually change.
- `tests/commands_test.py` — updated 4 existing tests to mock
`_query_installed_version` so they don't accidentally exercise the
verification subprocess. Added 4 new tests:
* pip pins `==<latest>` when the JSON API returns a version
* pip falls back to bare `dailybot-cli` when JSON API returns None
* pip silent no-op (post-version unchanged) prints a warning
pointing at install.sh and does NOT claim success
* post-version actually changed → reports success normally
## Risks
- pipx and uv-tool argv unchanged (they have their own version
resolution; pinning would be redundant or conflict). Verified by
the existing `test_upgrade_pipx_runs_pipx` and `test_upgrade_uv_tool`.
- The verification subprocess adds ~150–300 ms to a successful
upgrade. Acceptable given the alternative is users following a
bogus "Upgrade complete" message.
- If a user genuinely wants to "reinstall the same version", `--force`
on the same version still works — `_query_installed_version` will
match and we'll report success normally (covered by
`test_upgrade_force_runs_even_when_latest`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
Right after
v1.7.0shipped, a user randailybot upgradeand got this:```
$ dailybot upgrade
Checking PyPI for the latest version...
Current version: 1.6.1
Latest version: 1.7.0 (update available)
Install method: pip (/usr/local/lib/python3.14/site-packages/dailybot_cli)
Running: /usr/local/bin/python3 -m pip install --upgrade dailybot-cli
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: dailybot-cli in /usr/local/lib/python3.14/site-packages (1.6.1)
... [no Collecting / Downloading / Installing] ...
OK Upgrade complete. Run 'dailybot --version' to confirm.
$ dailybot --version
dailybot 1.6.1 (Python 3.14.4) ← unchanged
```
The user ended up running
curl -sSL https://cli.dailybot.com/install.sh | bashto actually get1.7.0.Why it happened
Two cooperating issues in pip's resolver:
_fetch_latest_pypi_versionqueries) sees a new release within seconds. pip's package index cache can lag — so even with--upgrade, pip can resolvedailybot-clito1.6.1for a few minutes after a release.--user. With the old version already in system site-packages and the resolver matching1.6.1as "latest available", pip decides the requirement is already satisfied and exits 0 without doing anything.subprocess.run(..., check=True)was happy because pip returned 0. We falsely reported success.Fix (defense-in-depth, two layers)
1. Pin
==<latest>in pip's argvWhen
_fetch_latest_pypi_version()returns a version, the command becomes:```bash
python -m pip install --upgrade dailybot-cli==1.7.0
```
pip can no longer say "already satisfied" because the requested version (1.7.0) differs from what's installed (1.6.1). The pin also forces pip to fetch fresh metadata, sidestepping the stale-cache issue. Falls back to bare
dailybot-cliwhen the JSON API is unreachable so we don't make things worse.2. Verify the on-disk version after pip exits
New
_query_installed_version()helper spawns a freshpython -c "from importlib.metadata import version; print(version('dailybot-cli'))"subprocess to read what's actually on disk now. (Can't use the running interpreter — it has the old version cached.)If the installed version still equals what we started with, the success message becomes a warning pointing at the
install.shfallback. The user gets honest feedback instead of "Upgrade complete" plus a still-old binary.This second layer catches the specific pip scenario AND any future edge cases (broken PATH, package-manager bugs, etc.).
Change log
commands/upgrade.py::_build_upgrade_argv— accepts optionallatest; pins the version forpipwhen known.commands/upgrade.py::_query_installed_version— new helper.commands/upgrade.py::upgrade— calls the verification after pip exits; downgrades the success message to a clear warning when the version didn't actually change.tests/commands_test.py— updated 4 existing upgrade tests to mock_query_installed_version(so they don't accidentally exercise the verification subprocess); added 4 new tests:==<latest>when JSON API returns a versiondailybot-cliwhen JSON API is unreachableCompat / risk
--forceon same version still works. Verified bytest_upgrade_force_runs_even_when_latest.Test plan
ruff check / ruff format --check / mypy / pytest -xall green_fetch_latest_pypi_versionreturns1.7.0, argv becomespip install --upgrade dailybot-cli==1.7.0, pip can no longer no-op, verification confirms 1.7.0 is installed, success.~/.cache/pip/http*between releases (forces stale-index conditions)🤖 Generated with Claude Code