diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b81d5a..9dad875 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,9 +36,18 @@ jobs: run: ./scripts/lock_requirements.ps1 -Check pytest: + name: ${{ matrix.check_name }} runs-on: ubuntu-24.04 permissions: contents: read + strategy: + fail-fast: false + matrix: + include: + - python_version: '3.14' + check_name: pytest + - python_version: '3.11' + check_name: pytest (Python 3.11 minimum) env: TEST_REDIS_URL: redis://localhost:6379/15 services: @@ -59,8 +68,8 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - # Mirror the runtime version shipped in the Docker image. - python-version: '3.11' + # Test both the production runtime and documented minimum version. + python-version: ${{ matrix.python_version }} cache: pip cache-dependency-path: | requirements.txt @@ -132,7 +141,7 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: # Mirror the runtime version shipped in the Docker image. - python-version: '3.11' + python-version: '3.14' cache: pip cache-dependency-path: | requirements.txt @@ -157,7 +166,8 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.11' + # Mirror the runtime version shipped in the Docker image. + python-version: '3.14' cache: pip cache-dependency-path: | requirements.txt diff --git a/Dockerfile b/Dockerfile index 2ae88dc..a6e1e5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim@sha256:db3ff2e1800a8581e2c48a27c3995339d47bdf046da21c7627accd3d51053a93 +FROM python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6 ARG VCS_REF=unknown diff --git a/tests/test_dependency_policy.py b/tests/test_dependency_policy.py index 3a32b95..656c303 100644 --- a/tests/test_dependency_policy.py +++ b/tests/test_dependency_policy.py @@ -1,4 +1,5 @@ import json +import re from pathlib import Path import pytest @@ -11,6 +12,8 @@ "python -m pip install --require-hashes -r requirements-test.txt" ) ALLOWED_PIP_LINES = {"cache: pip", APPROVED_PIP_INSTALL_COMMAND} +MINIMUM_PYTHON = "3.11" +PRODUCTION_PYTHON = "3.14" def pip_policy_allows(workflow): @@ -25,6 +28,15 @@ def pip_policy_allows(workflow): ) +def workflow_job(workflow, job_name): + marker = f"\n {job_name}:\n" + _, separator, remainder = workflow.partition(marker) + assert separator, f"missing workflow job: {job_name}" + + next_job = re.search(r"(?m)^ [a-zA-Z0-9_-]+:\s*$", remainder) + return remainder[:next_job.start()] if next_job else remainder + + def normalized_names(path): return { canonicalize_name(line.split("=", 1)[0].split("<", 1)[0].split(">", 1)[0]) @@ -147,6 +159,35 @@ def test_lock_generator_compiles_universal_locks(): assert "--universal" in script +def test_python_runtime_contract_stays_synchronized(): + dockerfile = Path("Dockerfile").read_text(encoding="utf-8") + workflow = Path(".github/workflows/tests.yml").read_text(encoding="utf-8") + readme = Path("README.md").read_text(encoding="utf-8") + + pytest_job = workflow_job(workflow, "pytest") + matrix_entries = re.findall( + r"- python_version: '([^']+)'\s+check_name: ([^\n]+)", + pytest_job, + ) + + assert dockerfile.startswith( + f"FROM python:{PRODUCTION_PYTHON}-slim@sha256:" + ) + assert matrix_entries == [ + (PRODUCTION_PYTHON, "pytest"), + (MINIMUM_PYTHON, f"pytest (Python {MINIMUM_PYTHON} minimum)"), + ] + assert "python-version: ${{ matrix.python_version }}" in pytest_job + + for job_name in ("ssh-integration", "browser-e2e"): + assert ( + f"python-version: '{PRODUCTION_PYTHON}'" + in workflow_job(workflow, job_name) + ) + + assert f"python-{MINIMUM_PYTHON}+" in readme + + def test_ci_installs_only_hash_checked_python_dependencies(): workflow = Path(".github/workflows/tests.yml").read_text(encoding="utf-8")