Chasing two warnings in the 0.3.1 release run turned up something bigger, so this ticket covers all of it. Everything here lives in .github/workflows/, so it is one file surface and one branch.
1. The Python matrix does not test more than one Python
ci.yml declares matrix: python-version: ["3.11", "3.12", "3.13"] and passes it to astral-sh/setup-uv@v3:
- uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python-version }}
setup-uv@v3 has no python-version input. It warns and ignores it:
##[warning]Unexpected input(s) 'python-version', valid inputs are ['version', 'checksum', 'github-token', ...]
The next step then runs bare uv venv, which resolves whatever interpreter the runner happens to ship. From the three matrix jobs of run 30725981899:
| Job name |
Interpreter actually used |
test (3.11) |
CPython 3.12.3 at /usr/bin/python3 |
test (3.12) |
CPython 3.12.3 at /usr/bin/python3 |
test (3.13) |
CPython 3.12.3 at /usr/bin/python3 |
All three run the same interpreter. The matrix value only ever reaches the job name, which is what makes this worth fixing rather than tidying: the PR checks page reads as three-version coverage, and a reviewer has no signal that it is one version run three times. Every green check on every PR to date has been that.
pyproject.toml declares requires-python = ">=3.11" and classifiers through 3.14, so the package claims support for four versions and has been verified on one. 3.11 is the floor and therefore the likeliest to break — nothing in the suite has ever exercised it.
Fix: either bump to astral-sh/setup-uv@v5 (which does take python-version) or keep v3 and pass the version explicitly at venv creation:
- run: uv venv --python ${{ matrix.python-version }}
The second is the smaller change and does not depend on the action's input surface at all.
Whatever the mechanism, the workflow should assert the interpreter rather than assume it, so this cannot silently regress again:
- name: Confirm the interpreter matches the matrix
run: |
got=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
[ "$got" = "${{ matrix.python-version }}" ] || {
echo "matrix says ${{ matrix.python-version }}, interpreter is $got"; exit 1; }
Without that step, the fix is unverifiable from the checks page — the same property that let this sit unnoticed.
release.yml:18-20 has the identical mistake with a hardcoded python-version: "3.12". It is harmless there by luck: the runner default currently is 3.12, so the wheel is built with the intended interpreter. It is a pure-Python wheel (py3-none-any), so nothing shipped is wrong. Fix it anyway — the reason it is correct today is coincidence, not configuration.
2. actions/download-artifact@v4 is on deprecated Node 20
From the same run:
Node.js 20 is deprecated. The following actions target Node.js 20 but are being
forced to run on Node.js 24: actions/download-artifact@v4
release.yml:117. Bump to @v5. upload-artifact@v4 at release.yml:99 should be checked in the same pass. Cosmetic today, breaking whenever GitHub finishes the removal.
3. Decision, not a defect: release.yml is not gated on ci.yml
release.yml triggers on push: tags: ["v*"] and runs its own build. It does not require the tagged commit's CI to have passed, so a tag on a red master publishes to PyPI regardless.
This is not hypothetical: 0.3.0 was published from a commit whose master CI was red. Harmlessly — the red was missing checks rather than failing tests — but the guard that would have caught a genuine failure was not there.
Worth deciding rather than assuming. Options, in ascending order of cost:
- Leave it, and treat tagging as the human gate it already is in practice.
- Add a job to
release.yml that queries the tagged SHA's check runs and fails if any concluded failure.
- Move to branch protection with required checks, so a red commit cannot reach master to be tagged.
The third is the real fix and the only one that also closes #62's original concern, which was closed as wontfix precisely because branch protection was not in place.
Checklist
Chasing two warnings in the 0.3.1 release run turned up something bigger, so this ticket covers all of it. Everything here lives in
.github/workflows/, so it is one file surface and one branch.1. The Python matrix does not test more than one Python
ci.ymldeclaresmatrix: python-version: ["3.11", "3.12", "3.13"]and passes it toastral-sh/setup-uv@v3:setup-uv@v3has nopython-versioninput. It warns and ignores it:The next step then runs bare
uv venv, which resolves whatever interpreter the runner happens to ship. From the three matrix jobs of run 30725981899:test (3.11)CPython 3.12.3at/usr/bin/python3test (3.12)CPython 3.12.3at/usr/bin/python3test (3.13)CPython 3.12.3at/usr/bin/python3All three run the same interpreter. The matrix value only ever reaches the job name, which is what makes this worth fixing rather than tidying: the PR checks page reads as three-version coverage, and a reviewer has no signal that it is one version run three times. Every green check on every PR to date has been that.
pyproject.tomldeclaresrequires-python = ">=3.11"and classifiers through 3.14, so the package claims support for four versions and has been verified on one. 3.11 is the floor and therefore the likeliest to break — nothing in the suite has ever exercised it.Fix: either bump to
astral-sh/setup-uv@v5(which does takepython-version) or keep v3 and pass the version explicitly at venv creation:The second is the smaller change and does not depend on the action's input surface at all.
Whatever the mechanism, the workflow should assert the interpreter rather than assume it, so this cannot silently regress again:
Without that step, the fix is unverifiable from the checks page — the same property that let this sit unnoticed.
release.yml:18-20has the identical mistake with a hardcodedpython-version: "3.12". It is harmless there by luck: the runner default currently is 3.12, so the wheel is built with the intended interpreter. It is a pure-Python wheel (py3-none-any), so nothing shipped is wrong. Fix it anyway — the reason it is correct today is coincidence, not configuration.2.
actions/download-artifact@v4is on deprecated Node 20From the same run:
release.yml:117. Bump to@v5.upload-artifact@v4atrelease.yml:99should be checked in the same pass. Cosmetic today, breaking whenever GitHub finishes the removal.3. Decision, not a defect:
release.ymlis not gated onci.ymlrelease.ymltriggers onpush: tags: ["v*"]and runs its own build. It does not require the tagged commit's CI to have passed, so a tag on a red master publishes to PyPI regardless.This is not hypothetical: 0.3.0 was published from a commit whose master CI was red. Harmlessly — the red was missing checks rather than failing tests — but the guard that would have caught a genuine failure was not there.
Worth deciding rather than assuming. Options, in ascending order of cost:
release.ymlthat queries the tagged SHA's check runs and fails if any concludedfailure.The third is the real fix and the only one that also closes #62's original concern, which was closed as
wontfixprecisely because branch protection was not in place.Checklist
release.ymlpins its build interpreter by configuration rather than by runner defaultdownload-artifact(andupload-artifact, if affected) off deprecated Node 20