diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5365571..f6796af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.13'] env: TERM: xterm-256color @@ -32,9 +32,19 @@ jobs: make dev-deps - name: Build Packages + if: matrix.python != '3.9' run: | make build + - name: Build Packages (Python 3.9) + if: matrix.python == '3.9' + # hatch build fails on Python 3.9: the installed virtualenv dropped the + # propose_interpreters API that hatch depends on. python -m build uses + # stdlib venv instead and is unaffected. All other versions use hatch build. + run: | + make check + python -m build + - name: Upload Packages uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index f3c1a2d..4f20026 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -14,7 +14,7 @@ jobs: TERM: xterm-256color strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.13'] steps: - name: Checkout Code diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 2e166c0..08f4712 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python '3,11' uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.13' - name: Install Dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e29cb9..46b4037 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: TERM: xterm-256color strategy: matrix: - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.13'] steps: - name: Checkout Code diff --git a/Dockerfile.testing b/Dockerfile.testing new file mode 100644 index 0000000..48a768e --- /dev/null +++ b/Dockerfile.testing @@ -0,0 +1,131 @@ +# syntax=docker/dockerfile:1 +# Tagging convention: boilerplate-dev:python-v +# python-ver — Python minor version used in the image (e.g. 3.13) +# testing-ver — Independent semver for the testing environment itself. +# Unlike library repos (where the testing version tracks the +# library release), this boilerplate has no release version of +# its own. Start at v0.0.1 and increment whenever the +# Dockerfile or its pinned dependencies change materially: +# patch — dependency bumps, minor tool config tweaks +# minor — new tools added, Python minor version bump +# major — breaking changes to the dev workflow +# +# Build a specific target: +# docker build \ +# --build-arg UID=$(id -u) --build-arg GID=$(id -g) \ +# --target testing-3.13 \ +# -f Dockerfile.testing -t boilerplate-dev:python3.13-v0.0.1 . +# +# Available targets: testing-3.9 testing-3.10 testing-3.11 testing-3.13 +# Default target : testing (alias for testing-3.13, matching Debian Trixie) +# +# Base image mapping (matches the Debian release that shipped each Python): +# 3.9 → python:3.9-slim-bullseye (Debian 11 — note: NOT buster; buster=3.7) +# 3.10 → python:3.10-slim-bookworm (Debian 12) +# 3.11 → python:3.11-slim-bookworm (Debian 12) +# 3.13 → python:3.13-slim-trixie (Debian 13) +# +# Note: testing-3.9 uses plain pip (not uv) because the shared lockfile +# was compiled for Python 3.11 and anyio>=4.0 requires Python>=3.10. +# +# Run: +# docker run --rm -it \ +# -v "$(pwd)":/app \ +# boilerplate-dev:python3.13-v0.0.1 \ +# make check + +# ── Global build args ───────────────────────────────────────────────────────── +# Defaults live here; each stage redeclares with a bare ARG to inherit them. +ARG UID=1000 +ARG GID=1000 + +# ── Shared uv binary ────────────────────────────────────────────────────────── +# Pin uv version once here; all testing stages except 3.9 copy from this stage. +FROM ghcr.io/astral-sh/uv:0.11.17 AS uv-base + +# ── Python 3.9 (Debian Bullseye) — plain pip, no lockfile ──────────────────── +FROM python:3.9-slim-bullseye AS testing-3.9 +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + make \ + dos2unix \ + shellcheck \ + git \ + && rm -rf /var/lib/apt/lists/* +ARG UID +ARG GID +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser +WORKDIR /app +COPY requirements-dev.txt ./ +# hadolint ignore=DL3013 +RUN pip install --no-cache-dir -r requirements-dev.txt +COPY --chown=appuser:appuser . . +USER appuser +RUN git config --global --add safe.directory /app + +# ── Python 3.10 (Debian Bookworm) ───────────────────────────────────────────── +FROM python:3.10-slim-bookworm AS testing-3.10 +COPY --from=uv-base /uv /uvx /usr/local/bin/ +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + make \ + dos2unix \ + shellcheck \ + git \ + && rm -rf /var/lib/apt/lists/* +ARG UID +ARG GID +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser +WORKDIR /app +COPY requirements-dev.lock ./ +RUN uv pip install --system --no-cache -r requirements-dev.lock +COPY --chown=appuser:appuser . . +USER appuser +RUN git config --global --add safe.directory /app + +# ── Python 3.11 (Debian Bookworm) ───────────────────────────────────────────── +FROM python:3.11-slim-bookworm AS testing-3.11 +COPY --from=uv-base /uv /uvx /usr/local/bin/ +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + make \ + dos2unix \ + shellcheck \ + git \ + && rm -rf /var/lib/apt/lists/* +ARG UID +ARG GID +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser +WORKDIR /app +COPY requirements-dev.lock ./ +RUN uv pip install --system --no-cache -r requirements-dev.lock +COPY --chown=appuser:appuser . . +USER appuser +RUN git config --global --add safe.directory /app + +# ── Python 3.13 (Debian Trixie) ────────────────────────────────────────────── +FROM python:3.13-slim-trixie AS testing-3.13 +COPY --from=uv-base /uv /uvx /usr/local/bin/ +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + make \ + dos2unix \ + shellcheck \ + git \ + && rm -rf /var/lib/apt/lists/* +ARG UID +ARG GID +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser +WORKDIR /app +COPY requirements-dev.lock ./ +RUN uv pip install --system --no-cache -r requirements-dev.lock +COPY --chown=appuser:appuser . . +USER appuser +RUN git config --global --add safe.directory /app + +# ── Default alias (Trixie = 3.13) ───────────────────────────────────────────── +FROM testing-3.13 AS testing diff --git a/LOCAL_QA.md b/LOCAL_QA.md new file mode 100644 index 0000000..095eb1b --- /dev/null +++ b/LOCAL_QA.md @@ -0,0 +1,85 @@ +# Local QA with Docker + +A Docker-based testing image is provided so checks can be run on any machine without +installing tooling locally. Four Python versions are supported, each on the Debian release +that shipped it: + +| Target | Python | Debian base | +|---|---|---| +| `testing-3.9` | 3.9 | Bullseye (11) | +| `testing-3.10` | 3.10 | Bookworm (12) | +| `testing-3.11` | 3.11 | Bookworm (12) | +| `testing-3.13` | 3.13 | Trixie (13) — default | + +## Build the image + +Pass your host UID and GID so that files written inside the container are owned by your +user, not root. Use `--target` to select a Python version; omit it to get the default +(Python 3.13 / Trixie): + +```bash +# Default (Python 3.13 / Trixie) +docker build -f Dockerfile.testing \ + --build-arg UID=$(id -u) \ + --build-arg GID=$(id -g) \ + -t boilerplate-dev:python3.13-v0.0.1 . + +# Specific version +docker build -f Dockerfile.testing \ + --build-arg UID=$(id -u) \ + --build-arg GID=$(id -g) \ + --target testing-3.11 \ + -t boilerplate-dev:python3.11-v0.0.1 . +``` + +> **Image tag convention:** `boilerplate-dev:python-v` +> The testing version is independent of any library release. Start at `v0.0.1` and +> increment: patch for dependency bumps/minor tweaks, minor for new tools or a Python +> version bump, major for breaking changes to the dev workflow. + +## Run checks + +All commands below mount the repository into the container so changes are picked up +without a rebuild. Run them from the repository root. Substitute the tag for whichever +Python version you built. + +**Integrity checks** (trailing whitespace, DOS line-endings, CHANGELOG entry, git tag): + +```bash +docker run --rm -v "$(pwd)":/app boilerplate-dev:python3.13-v0.0.1 make check +``` + +**Shell script linting:** + +```bash +docker run --rm -v "$(pwd)":/app boilerplate-dev:python3.13-v0.0.1 make shellcheck +``` + +**QA** (ruff, isort, codespell, check-manifest, build, twine check): + +```bash +docker run --rm -v "$(pwd)":/app boilerplate-dev:python3.13-v0.0.1 make qa +``` + +**Tests:** + +```bash +docker run --rm -v "$(pwd)":/app boilerplate-dev:python3.13-v0.0.1 make pytest +``` + +## Dependency lock file + +The `testing-3.10`, `testing-3.11`, and `testing-3.13` targets install from +`requirements-dev.lock`. Regenerate it when `requirements-dev.txt` changes, using the +same uv version as the Dockerfile pin: + +```bash +uv self update 0.11.17 # align host uv with Dockerfile pin +uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +``` + +Then rebuild the affected images. + +> **Note on Python 3.9:** the `testing-3.9` target uses plain `pip` directly from +> `requirements-dev.txt` (no lockfile) because the shared lockfile was compiled for +> Python 3.11 and several transitive dependencies require Python ≥ 3.10. diff --git a/check.sh b/check.sh index 3dfe6f1..34d490f 100755 --- a/check.sh +++ b/check.sh @@ -7,6 +7,7 @@ LIBRARY_NAME=$(hatch project metadata name) LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') TERM=${TERM:="xterm-256color"} +export TERM success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..83e7ff0 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,201 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +anyio==4.13.0 + # via httpx +backports-tarfile==1.2.0 + # via jaraco-context +backports-zstd==1.5.0 + # via hatch +build==1.5.0 + # via check-manifest +cachetools==7.1.4 + # via tox +certifi==2026.5.20 + # via + # httpcore + # httpx + # requests +cffi==2.0.0 + # via cryptography +charset-normalizer==3.4.7 + # via requests +check-manifest==0.51 + # via -r requirements-dev.txt +click==8.4.1 + # via + # hatch + # userpath +codespell==2.4.2 + # via -r requirements-dev.txt +colorama==0.4.6 + # via tox +cryptography==48.0.0 + # via secretstorage +distlib==0.4.0 + # via virtualenv +docutils==0.23 + # via readme-renderer +filelock==3.29.0 + # via + # python-discovery + # tox + # virtualenv +h11==0.16.0 + # via httpcore +hatch==1.16.5 + # via -r requirements-dev.txt +hatch-fancy-pypi-readme==25.1.0 + # via -r requirements-dev.txt +hatch-requirements-txt==0.4.1 + # via -r requirements-dev.txt +hatchling==1.29.0 + # via + # hatch + # hatch-fancy-pypi-readme + # hatch-requirements-txt +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via hatch +hyperlink==21.0.0 + # via hatch +id==1.6.1 + # via twine +idna==3.17 + # via + # anyio + # httpx + # hyperlink + # requests +importlib-metadata==9.0.0 + # via keyring +isort==8.0.1 + # via -r requirements-dev.txt +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.5.0 + # via keyring +jeepney==0.9.0 + # via + # keyring + # secretstorage +jinja2==3.1.6 + # via pdoc +keyring==25.7.0 + # via + # hatch + # twine +markdown-it-py==4.2.0 + # via rich +markdown2==2.5.5 + # via pdoc +markupsafe==3.0.3 + # via + # jinja2 + # pdoc +mdurl==0.1.2 + # via markdown-it-py +more-itertools==11.1.0 + # via + # jaraco-classes + # jaraco-functools +nh3==0.3.5 + # via readme-renderer +packaging==26.2 + # via + # build + # hatch + # hatch-requirements-txt + # hatchling + # pyproject-api + # tox + # twine +pathspec==1.1.1 + # via hatchling +pdoc==16.0.0 + # via -r requirements-dev.txt +pexpect==4.9.0 + # via hatch +platformdirs==4.10.0 + # via + # hatch + # python-discovery + # tox + # virtualenv +pluggy==1.6.0 + # via + # hatchling + # tox +ptyprocess==0.7.0 + # via pexpect +pycparser==3.0 + # via cffi +pygments==2.20.0 + # via + # pdoc + # readme-renderer + # rich +pyproject-api==1.10.1 + # via tox +pyproject-hooks==1.2.0 + # via + # build + # hatch +python-discovery==1.4.0 + # via + # hatch + # tox + # virtualenv +readme-renderer==44.0 + # via twine +requests==2.34.2 + # via + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==15.0.0 + # via + # hatch + # twine +ruff==0.15.15 + # via -r requirements-dev.txt +secretstorage==3.5.0 + # via keyring +setuptools==82.0.1 + # via check-manifest +shellingham==1.5.4 + # via hatch +tomli-w==1.2.0 + # via + # hatch + # tox +tomlkit==0.15.0 + # via hatch +tox==4.55.0 + # via -r requirements-dev.txt +trove-classifiers==2026.5.22.10 + # via hatchling +twine==6.2.0 + # via -r requirements-dev.txt +typing-extensions==4.15.0 + # via anyio +urllib3==2.7.0 + # via + # id + # requests + # twine +userpath==1.9.2 + # via hatch +uv==0.11.17 + # via hatch +virtualenv==21.4.1 + # via + # hatch + # tox +zipp==4.1.0 + # via importlib-metadata diff --git a/tox.ini b/tox.ini index 2b6d87b..a0f3ca0 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = build [testenv:qa] +skip_install = true commands = check-manifest python -m build --no-isolation