Skip to content

fix(ci): pin the uv interpreter so the matrix tests what it claims - #1429

Merged
phernandez merged 2 commits into
mainfrom
ci-pin-venv-python
Sep 1, 2026
Merged

fix(ci): pin the uv interpreter so the matrix tests what it claims#1429
phernandez merged 2 commits into
mainfrom
ci-pin-venv-python

Conversation

@phernandez

@phernandez phernandez commented Sep 1, 2026

Copy link
Copy Markdown
Member

Problem

Every uv invocation in CI resolved its interpreter from .python-version — which pins 3.14 — rather than the version the job had just set up.

So the matrix has been lying. The rows labeled "Python 3.12" and "Python 3.13" installed those interpreters and then built and tested against 3.14. We have had no 3.12 or 3.13 coverage despite the matrix reporting it, even though requires-python declares >=3.12.

Direct evidence, from the thread dump of a job named "Test SQLite Integration (windows-latest, Python 3.12)":

File "C:\hostedtoolcache\windows\Python\3.14.7\x64\Lib\threading.py", line 1044

Why pinning uv venv is not enough

My first attempt added --python to each uv venv. That fixes the venv and nothing else, because every just recipe shells out to uv run, which re-resolves the interpreter independently.

Reproduced directly, in a throwaway project with .python-version = 3.14:

$ uv venv --python 3.12 && uv pip install -e ".[extra1]"
venv python: Python 3.12.12
six installed: 1.17.0

$ uv run python -c "import sys, six"
Removed virtual environment at: .venv          # <-- deletes the pinned venv
Creating virtual environment at: .venv
uv run python: 3.14.5                          # <-- back to .python-version
ModuleNotFoundError: No module named 'six'     # <-- extras gone with it

uv run deletes a .venv whose version disagrees and rebuilds it from the lockfile, discarding whatever uv pip install -e ".[dev,milvus,pdf]" had put there. That is how the first attempt turned green static-checks into error[unresolved-import]: Cannot resolve imported module 'pymilvus' — the pinned 3.12 venv was destroyed before ty ever ran.

Fix

Set UV_PYTHON at job level. It governs every uv call, including those inside just recipes, so the environment survives and the job runs the version its name claims:

$ UV_PYTHON=3.12 uv run python -c "import sys, six"
uv run python: 3.12.12 | six: 1.17.0     # correct version, extras intact

Applied to all ten jobs that build a uv environment — ${{ matrix.python-version }} where a matrix drives it, literal 3.12 where the job hardcodes one:

  • test.yml — seven jobs
  • release.yml — was building the published artifact on 3.14 while declaring 3.12
  • claude.yml
  • consolidated-packages.yml (hermes) — ran uv python install 3.12, which only downloads that interpreter and never selects it

A note on the force-pushes

An intermediate revision injected a second env: block into the two Postgres jobs, which already had one. That is a duplicate YAML key: GitHub rejects the whole file, while PyYAML's SafeLoader silently keeps the last one — so local validation passed. The only symptom was the run appearing under the name .github/workflows/test.yml instead of Tests, with zero jobs and no error text anywhere in the API.

It would also have silently dropped BASIC_MEMORY_TEST_POSTGRES_URL from both jobs had it parsed. Now merged into the existing blocks, and validation uses a loader that raises on duplicate keys rather than yaml.safe_load. Verified: every workflow parses with no duplicate keys, all ten uv jobs carry UV_PYTHON, the matrix expression appears exactly where a matrix defines that key, and both Postgres jobs retain both variables.

Expect new failures

This makes 3.12 and 3.13 run for the first time, so it may surface real failures that were previously hidden. Any such failure is a pre-existing bug this was masking, not a regression introduced here. Related: #1430 (Windows integration hang), found while triaging the #1421 failure that started this.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T21:00:48.822772Z 7803793 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@phernandez phernandez changed the title fix(ci): pin uv venv to the intended Python version fix(ci): pin the uv interpreter so the matrix tests what it claims Sep 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b440ea051e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/test.yml Outdated
Every uv invocation in CI resolved its interpreter from `.python-version`
(3.14) instead of the version the job set up. The matrix rows labeled
"Python 3.12" and "Python 3.13" therefore built and ran against 3.14 —
we have had no coverage of either version despite reporting it, while
`requires-python` declares `>=3.12`.

Pinning `uv venv` alone is not enough. Every `just` recipe shells out to
`uv run`, which re-resolves the interpreter and DELETES a `.venv` that
disagrees, recreating it from the lockfile. That drops anything installed
by the job's `uv pip install -e ".[dev,milvus,pdf]"` step, which is how a
pinned 3.12 venv ends up failing typecheck on an unresolved `pymilvus`.

Set UV_PYTHON at job level instead: it governs every uv call, including
the ones inside just recipes, so the environment survives and the job
runs the version its name claims.

Covers all ten jobs that build a uv environment. claude.yml and
release.yml had the same defect — release.yml was building the published
artifact on 3.14 while declaring 3.12 — and consolidated-packages.yml's
hermes job ran `uv python install 3.12`, which only downloads that
interpreter without selecting it.

Signed-off-by: phernandez <paul@basicmachines.co>
Pinning UV_PYTHON made the 3.12 and 3.13 matrix rows run their declared
interpreter for the first time, which surfaced seven pre-existing test
bugs. Neither is a regression from that change; both are tests that only
ever ran on 3.14.

CPU-budget auto-tuning (6 tests, 3.12 only)

`os.process_cpu_count` is new in 3.13. `_available_cpu_count` already
handles its absence, but the tests did not: `monkeypatch.setattr` refuses
to set an attribute that does not exist, so every test that pinned a CPU
budget raised AttributeError on 3.12.

Each of those tests pins `process_cpu_count` and `cpu_count` to the same
value, so none of them cares which API reports the budget - they assert on
the resolved number. Collapse both patches into a `pin_cpu_budget` fixture
that passes `raising=False`, which keeps the preferred branch under test on
3.12 as well. monkeypatch deletes an attribute it created during teardown,
so `os` is restored exactly as found on every interpreter (verified).

That left the genuine 3.12 branch - `process_cpu_count` absent, budget from
`os.cpu_count()` - covered by no test on any interpreter, since 3.13+ never
takes it and 3.12 now forces the attribute to exist. Add two tests that
delete the attribute to reproduce the 3.12 runtime anywhere: one asserting
the fallback resolves the same knobs as the preferred API, one asserting an
unreported budget leaves FastEmbed on its own defaults. `_available_cpu_count`
and `_resolve_fastembed_runtime_knobs` are now fully covered on 3.12 and 3.14.

Zero-argument super() under dataclass slots (1 test, 3.12 and 3.13)

`dataclass(slots=True)` cannot add `__slots__` in place, so it rebuilds the
class. Before CPython 3.14 (gh-90562) the rebuilt methods keep a `__class__`
cell pointing at the discarded original, and zero-argument `super()` rejects
`self` with "obj must be an instance or subtype of type". Name the class in
the super() call so it is re-looked-up at call time and resolves to the
rebuilt class on every supported interpreter.

A repo-wide AST scan for slots dataclasses containing zero-argument super()
found this as the only occurrence.

Verified: tests/repository/test_openai_provider.py,
tests/repository/test_fastembed_provider.py and
tests/indexing/test_project_index_maintenance.py all pass (84 tests) on
3.12, 3.13 and 3.14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez merged commit e5b0339 into main Sep 1, 2026
30 checks passed
@phernandez
phernandez deleted the ci-pin-venv-python branch September 1, 2026 23:01
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