Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14.2
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- The Python version is pinned in one place, `.python-version` (`3.14.2`), the
single source of truth: the engine images' `Dockerfile`s pin the same exact
patch (`python:3.14.2-slim`), and `requires-python`, mypy `python_version`,
the trove classifiers and `setup-python` inputs track its major.minor. A new
consistency guard (`tests/test_python_version_consistency.py`) scans every
`pyproject.toml`, `Dockerfile*` and workflow and fails on any drift.
- **Breaking for newly copied templates.** The `template/` release workflow
calls continuo's public release API instead of an unauthenticated webhook:
- `RELEASE_ENDPOINT` keeps its name but is now the origin of continuo's
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.duckdb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.14.2-slim
# One dual-role image per engine: the python-node runtime harness (default
# command `run`) and the blue/green validation runner (`validation-op`). It
# installs the PUBLISHED, versioned libraries, never the repo source:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.postgres
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.14.2-slim
# One dual-role image per engine — the python-node runtime harness (default
# command `run`) and the blue/green validation runner (`validation-op`). It
# installs the PUBLISHED, versioned libraries, never the repo source:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.trino
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.14.2-slim
# One dual-role image per engine — the python-node runtime harness (default
# command `run`) and the blue/green validation runner (`validation-op`). It
# installs the PUBLISHED, versioned libraries, never the repo source:
Expand Down
2 changes: 1 addition & 1 deletion template/Dockerfile.pip
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# requirements file (regenerate with the command in template/README.md).
# Shape 1 (FROM the engine image, see template/Dockerfile) is simpler; use this
# only when you must control the base image.
FROM python:3.14-slim
FROM python:3.14.2-slim
COPY requirements.lock /tmp/requirements.lock
RUN pip install --no-cache-dir --require-hashes -r /tmp/requirements.lock \
&& rm -f /tmp/requirements.lock
Expand Down
2 changes: 1 addition & 1 deletion template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ any extra dependency your script needs). Pin by tag or digest
specific reason not to.

**Shape 2 — `Dockerfile.pip`, your own base (hash-locked).** Builds
`FROM python:3.14-slim` (or another base you control) and installs
`FROM python:3.14.2-slim` (or another base you control) and installs
`continuo-python-runtime` plus one `continuo-<engine>-adapter` from PyPI via
`pip install --require-hashes -r requirements.lock`. Use this when you must
control the base image yourself — e.g. your org mandates a specific base,
Expand Down
85 changes: 85 additions & 0 deletions tests/test_python_version_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""`.python-version` is the single source of truth for the Python version.

Every other file that hardcodes the version must agree with it, so a bump is a
one-line edit to `.python-version` and CI catches any drift. These checks scan
by directory (``rglob``/``glob``), never by a hardcoded file list: a new
``pyproject.toml``, ``Dockerfile*`` or workflow is covered automatically. A new
*kind* of file that hardcodes the version (a shape these patterns do not match)
is the one thing that still needs a pattern added here.

Two tiers, because the version means different things in different files:
- full patch (``.python-version`` and the engine ``Dockerfile``s ``FROM
python:X-slim``) — the exact interpreter dev, CI and the shipped image run;
- major.minor (``requires-python`` floor, mypy ``python_version``, trove
classifiers, ``setup-python`` inputs) — a policy/compatibility statement, and
mypy/``requires-python`` reject a patch component anyway.
"""

import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CANONICAL = (ROOT / ".python-version").read_text().strip() # e.g. "3.14.2"
MAJOR_MINOR = ".".join(CANONICAL.split(".")[:2]) # "3.14"

# Directories whose files are not ours to keep in sync (installed deps, VCS,
# build scratch, the SDD ledger).
_SKIP = {
".venv",
".git",
".superpowers",
"__pycache__",
"node_modules",
"wheelhouse",
"dist",
"build",
".pytest_cache",
".ruff_cache",
".mypy_cache",
}


def _ours(paths):
for path in paths:
if not any(part in _SKIP for part in path.parts):
yield path


def test_python_version_pins_an_exact_patch():
"""The canonical source is a full X.Y.Z, so the interpreter is reproducible."""
assert re.fullmatch(r"\d+\.\d+\.\d+", CANONICAL), f".python-version is {CANONICAL!r}, not X.Y.Z"


def test_every_dockerfile_pins_the_canonical_patch():
"""`FROM python:<tag>-slim` in every Dockerfile equals the canonical full patch."""
checked = []
for dockerfile in _ours(ROOT.rglob("Dockerfile*")):
for tag in re.findall(r"FROM python:(\S+)-slim", dockerfile.read_text()):
checked.append(dockerfile)
assert tag == CANONICAL, f"{dockerfile}: python:{tag}-slim != python:{CANONICAL}-slim"
assert checked, "no `FROM python:<tag>-slim` found — add the pattern if the base moved"


def test_every_pyproject_agrees_on_major_minor():
"""requires-python floor, mypy python_version and trove classifiers match X.Y."""
checked = []
for pyproject in _ours(ROOT.rglob("pyproject.toml")):
text = pyproject.read_text()
for floor in re.findall(r'requires-python\s*=\s*">=\s*([\d.]+)\s*"', text):
checked.append(pyproject)
assert floor == MAJOR_MINOR, f"{pyproject}: requires-python >= {floor} != {MAJOR_MINOR}"
for version in re.findall(r'python_version\s*=\s*"([\d.]+)"', text):
assert version == MAJOR_MINOR, f"{pyproject}: mypy python_version {version} != {MAJOR_MINOR}"
# Only major.minor classifiers (`:: 3.14`); a bare `:: 3` is ignored.
for classifier in re.findall(r"Programming Language :: Python :: (\d+\.\d+)", text):
assert classifier == MAJOR_MINOR, f"{pyproject}: classifier {classifier} != {MAJOR_MINOR}"
assert checked, "no pyproject.toml with requires-python found"


def test_every_workflow_setup_python_agrees_on_major_minor():
"""`actions/setup-python` inputs across all workflows match X.Y (patch allowed)."""
workflows = ROOT / ".github" / "workflows"
for workflow in [*workflows.glob("*.yml"), *workflows.glob("*.yaml")]:
for version in re.findall(r'python-version:\s*"([\d.]+)"', workflow.read_text()):
got = ".".join(version.split(".")[:2])
assert got == MAJOR_MINOR, f"{workflow}: setup-python {version} != {MAJOR_MINOR}"
Loading