Skip to content
Open
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
12 changes: 11 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions Dockerfile.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# syntax=docker/dockerfile:1
# Tagging convention: boilerplate-dev:python<python-ver>-v<testing-ver>
# 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
Comment thread
dannystaple marked this conversation as resolved.

# ── Default alias (Trixie = 3.13) ─────────────────────────────────────────────
FROM testing-3.13 AS testing
85 changes: 85 additions & 0 deletions LOCAL_QA.md
Original file line number Diff line number Diff line change
@@ -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<python-ver>-v<testing-ver>`
> 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.
1 change: 1 addition & 0 deletions check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
Loading
Loading