Problem
CI runs the Python services on only Python 3.11, but production runs Python 3.10. Every Python-touching job in .github/workflows/tests.yml hardcodes python-version: '3.11' via actions/setup-python@v5 — there is no version matrix:
duckdb-unit (pytest tests/ -q)
image-unit (pytest tests/ -q)
esp-native / esp-firmware (PlatformIO host Python)
e2e-pipeline and the ui-playwright seed step
This gap let a 3.11-only API reach main and crash both Python services on deploy: commit 8938899 (via #180) introduced from datetime import UTC / datetime.now(UTC) in services/log_ring.py (both the duckdb-service and image-service copies). datetime.UTC was added in 3.11; on the 3.10 prod host, log_ring.install() runs at import time in app.py, so the deploy of aac9c78 hit an ImportError before either service could serve traffic. Prod was held on the prior known-good revision until #191 (the UTC → timezone.utc swap) merged.
#191 fixes the symptom but does not add the CI lane that would have caught it at PR time. See the heads-up in #180 (comment).
Goal
Run the Python unit-test lanes across a version matrix so a version-specific API (in either direction — a 3.11-ism that breaks 3.10, or a future deprecation that breaks 3.14) fails at PR time instead of on deploy.
Target range: Python 3.10 → 3.14, where 3.10 is the current prod floor and 3.14 is the forward-compat ceiling:
3.10 → current production host (the floor — must stay green)
3.11 → current CI + Docker image target (python:3.11)
3.12
3.13
3.14 → forward-compat ceiling
Proposed change
Add a matrix to at least duckdb-unit and image-unit (the two pure-pytest lanes — cheapest, highest signal, and the two services that actually broke):
duckdb-unit:
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: duckdb-service/requirements-dev.txt
# ...
fail-fast: false so one version failing doesn't mask the others.
Open questions to settle while implementing
- Dependency availability. Confirm the pinned
requirements-dev.txt deps (notably duckdb) ship wheels for 3.13/3.14 on ubuntu-latest. If a newer interpreter has no wheel yet, either pin the matrix ceiling to what's installable or mark the bleeding-edge entry continue-on-error: true rather than blocking PRs on an upstream packaging gap — and log() / note which versions were dropped so it isn't a silent cap.
- Scope of the matrix.
e2e-pipeline / ui-playwright boot Docker images that already pin their own interpreter, so the matrix likely belongs on the unit lanes only; the host Python there just runs the seed/test driver. Decide whether to matrix those too or leave them single-version.
- PlatformIO lanes.
esp-native / esp-firmware use host Python only to run PlatformIO; a version matrix there is lower value. Probably leave as-is.
Docs to update (per CLAUDE.md)
Acceptance criteria
References
Problem
CI runs the Python services on only Python 3.11, but production runs Python 3.10. Every Python-touching job in
.github/workflows/tests.ymlhardcodespython-version: '3.11'viaactions/setup-python@v5— there is no version matrix:duckdb-unit(pytest tests/ -q)image-unit(pytest tests/ -q)esp-native/esp-firmware(PlatformIO host Python)e2e-pipelineand theui-playwrightseed stepThis gap let a 3.11-only API reach
mainand crash both Python services on deploy: commit8938899(via #180) introducedfrom datetime import UTC/datetime.now(UTC)inservices/log_ring.py(both the duckdb-service and image-service copies).datetime.UTCwas added in 3.11; on the 3.10 prod host,log_ring.install()runs at import time inapp.py, so the deploy ofaac9c78hit anImportErrorbefore either service could serve traffic. Prod was held on the prior known-good revision until #191 (theUTC→timezone.utcswap) merged.#191 fixes the symptom but does not add the CI lane that would have caught it at PR time. See the heads-up in #180 (comment).
Goal
Run the Python unit-test lanes across a version matrix so a version-specific API (in either direction — a 3.11-ism that breaks 3.10, or a future deprecation that breaks 3.14) fails at PR time instead of on deploy.
Target range: Python 3.10 → 3.14, where 3.10 is the current prod floor and 3.14 is the forward-compat ceiling:
Proposed change
Add a matrix to at least
duckdb-unitandimage-unit(the two pure-pytest lanes — cheapest, highest signal, and the two services that actually broke):fail-fast: falseso one version failing doesn't mask the others.Open questions to settle while implementing
requirements-dev.txtdeps (notablyduckdb) ship wheels for 3.13/3.14 onubuntu-latest. If a newer interpreter has no wheel yet, either pin the matrix ceiling to what's installable or mark the bleeding-edge entrycontinue-on-error: truerather than blocking PRs on an upstream packaging gap — andlog()/ note which versions were dropped so it isn't a silent cap.e2e-pipeline/ui-playwrightboot Docker images that already pin their own interpreter, so the matrix likely belongs on the unit lanes only; the host Python there just runs the seed/test driver. Decide whether to matrix those too or leave them single-version.esp-native/esp-firmwareuse host Python only to run PlatformIO; a version matrix there is lower value. Probably leave as-is.Docs to update (per CLAUDE.md)
docs/10-quality-requirements/ci-gates.md.datetime.UTCimport crashed both services on deploy" incident belongs indocs/11-risks-and-technical-debt/README.md→ "Lessons learned" (what happened / why / how to avoid).Acceptance criteria
duckdb-unitandimage-unitrun green on Python 3.10 and 3.11 at minimum, with 3.12–3.14 added (or explicitly, visibly deferred with a logged reason if an upstream wheel is missing).from datetime import UTC) fails the 3.10 lane.ci-gates.mdreflects the new matrix.References
8938899(introduced via feat: live structured server-log console (closes #178) #180), brokemainataac9c78