ci: test the interpreter we actually ship (3.12 + 3.14 matrix) - #46
Merged
Conversation
docker/Dockerfile ships python:3.14-slim, but the CI test job ran only on Python 3.12. The image was exercised on 3.14 by container-ci and conformance; the test suite never was. A 3.14-only stdlib or typing regression could ship uncaught. The quality job now runs a ["3.12", "3.14"] matrix. Both ends matter: 3.12 is the declared requires-python floor and must stay tested or the floor is a false claim, and 3.14 is what reaches production. The coverage gate still runs once (on 3.14, push to main), so the cost is one extra fast test run per event. The matrix needs UV_PYTHON to be real. uv resolves its interpreter from .python-version -- pinned to 3.12 for local dev -- before it consults PATH, so actions/setup-python alone does not move it: both legs would build a 3.12 environment and the "3.14" leg would test the floor twice, green and meaningless. The job-level env var also covers every `uv run` inside `make ci-local`, not just the explicit `uv sync`. tests/unit/test_ci_python_matrix.py pins the matrix against pyproject's floor and the Dockerfile's FROM python:<x.y>-slim, plus the UV_PYTHON wiring and the single coverage run, so a base-image bump that is not mirrored into CI fails instead of silently narrowing it. requires-python, ruff target-version and mypy python_version are deliberately unchanged at 3.12. Verified locally on both interpreters: 478 passed, 2 skipped (live conformance probes) on 3.12 and on 3.14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUS8yK2hqkNpfoTxFR4RPR
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.
The gap
docker/Dockerfileshipspython:3.14-slim, but the CI test job ran only onPython 3.12.
container-ciandconformanceexercise the image on 3.14; thetest suite never ran on it. A 3.14-only stdlib or typing regression would have
shipped uncaught.
The fix
The
qualityjob now runs a["3.12", "3.14"]matrix — a matrix, not a swap:requires-pythonfloor. Drop it and the floor is afalse claim.
The coverage gate still runs once (on 3.14, push to
main), so the cost isone extra fast test run per event, not two coverage runs.
UV_PYTHONis load-bearingThe matrix alone would have been a green lie.
uvresolves its interpreter from.python-version— pinned to3.12for local dev — before it consultsPATH, soactions/setup-pythoninstalling 3.14 changes nothing: both legswould build a 3.12 environment and the "3.14" leg would test the floor twice.
The job therefore exports
UV_PYTHON: ${{ matrix.python-version }}, whichoutranks
.python-versionand applies to everyuv runinsidemake ci-local,not only the explicit
uv sync.Kept honest by a guard
New
tests/unit/test_ci_python_matrix.pyasserts the matrix contains therequires-pythonfloor and the Dockerfile'sFROM python:<x.y>-slim, thatUV_PYTHONis wired to the matrix, and that the coverage gate runs once ratherthan per leg. A future base-image bump that is not mirrored into CI now fails
instead of silently re-opening the blind spot.
requires-python, rufftarget-versionand mypypython_versionaredeliberately unchanged at 3.12.
Verification
make ci-locallocally on both interpreters — 478 passed, 2 skipped on each(the 2 skips are the live conformance probes, which need a running server).
🤖 Generated with Claude Code
https://claude.ai/code/session_01EUS8yK2hqkNpfoTxFR4RPR