Skip to content

Reuse the venv on upgrade instead of recreating it - #17

Merged
duncankmckinnon merged 1 commit into
mainfrom
fix-launcher-upgrade
Aug 13, 2026
Merged

Reuse the venv on upgrade instead of recreating it#17
duncankmckinnon merged 1 commit into
mainfrom
fix-launcher-upgrade

Conversation

@duncankmckinnon

Copy link
Copy Markdown
Owner

brew upgrade left valcore unable to start in any non-interactive context.

The bug

A formula bump changes VALCORE_VERSION, the stamp goes stale, and the launcher ran uv venv against a path that already held a venv. uv refuses that, and what happens next depends entirely on whether a terminal is attached:

  • At a TTY: uv prompts to replace. Answering yes destroys the venv and reinstalls ~115 packages in order to change one of them.
  • Without a TTY: uv errors out. Under set -euo pipefail the launcher died before reaching the install, so every post-upgrade run from a script, cron job, or CI step failed until a human ran valcore by hand and answered the prompt.

Reproduced against the real script:

$ VALCORE_VERSION=0.0.9 bash packaging/valcore.sh version </dev/null
Creating virtual environment at: .../venv
error: Failed to create virtual environment
  Caused by: A virtual environment already exists at: .../venv

The fix

Create the venv only when there is no usable interpreter at $venv/bin/python; otherwise install the pinned version straight into the existing venv and let uv work out what actually changed.

A real 0.0.8 → 0.0.9 upgrade, stdin closed:

Resolved 115 packages in 104ms
Prepared 1 package  →  - valcore==0.0.8
Installed 1 package →  + valcore==0.0.9
0.0.9

One package instead of 115, no prompt, exit 0.

Three related changes come with it:

  • uv venv takes --clear on the paths that do create one, so a half-written venv from an interrupted first run is replaced rather than aborting for the same reason. On a fresh path it is a no-op.
  • If installing into a reused venv fails — most likely its interpreter no longer satisfies a new release's requires-python — rebuild once and retry, rather than leaving the user with a launcher that cannot recover on its own.
  • The hardcoded (first run) message now says upgrading when that is what is happening.

Why CI didn't catch this

The stub uv in test_launcher.py implemented venv as a bare mkdir -p, which succeeds whether or not the target already exists. test_stale_stamp_triggers_reprovision then asserted "venv --python >=3.11" in log, so the test actively enshrined the broken behavior: the upgrade path looked healthy in CI while failing for every real user without a TTY.

The stub now refuses an existing venv the way real uv does, and UV_FAIL_INSTALL_MARKER lets a test simulate an install that keeps failing until the venv is rebuilt.

New coverage: upgrades reuse the venv rather than recreating it; the message distinguishes upgrade from first run; a reused venv that cannot take the install is rebuilt; and a partial venv from an interrupted run is cleared.

Testing

954 Python tests pass, ruff clean. Verified end-to-end against the real uv and real PyPI packages for both a fresh install and an upgrade with stdin closed.

The shellcheck test skips on machines without shellcheck installed, so that assertion ran in CI rather than locally.

🤖 Generated with Claude Code

`brew upgrade` bumps VALCORE_VERSION, the stamp goes stale, and the launcher ran
`uv venv` against a path that already held a venv. uv refuses that: it prompts to
replace when a TTY is attached, and errors out when one is not. Under `set -e` the
launcher then died before reaching the install, so every post-upgrade run from a
script, cron job, or CI step failed until someone ran valcore by hand and answered
the prompt. Answering it destroyed the venv and reinstalled ~115 packages to change
one of them.

Create the venv only when there is no usable interpreter at $venv/bin/python, and
otherwise install the pinned version straight into the existing venv. A real
0.0.8 -> 0.0.9 upgrade now prepares and installs 1 package rather than 115, with no
prompt and nothing interactive.

`uv venv` gains --clear on the paths that do create one, so a half-written venv from
an interrupted first run is replaced rather than aborting the same way. If installing
into a reused venv fails -- most likely its interpreter no longer satisfies the new
release's requires-python -- rebuild once and retry rather than leaving the user
stuck. The "(first run)" message now says "upgrading" when that is what is happening.

The stub uv in the launcher tests grew a `mkdir -p` where the real one refuses an
existing venv, which is why the upgrade path looked healthy in CI while failing for
real users. It now models the refusal, and can simulate an install that fails until
the venv is rebuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@duncankmckinnon
duncankmckinnon merged commit d2e77f3 into main Aug 13, 2026
6 checks passed
@duncankmckinnon
duncankmckinnon deleted the fix-launcher-upgrade branch August 13, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant