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
18 changes: 17 additions & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"groupName": "major updates"
},
{
"description": "A Python minor (3.14 -> 3.15) is a runtime upgrade for every instance that runs Python as its app runtime: its own runtime Dockerfile must move in step, and its tools must support the new version. This one rule covers the devcontainer's PYTHON_VERSION, the Makefile's PYTHON_VERSION (the uv image tag) and pyproject.toml's requires-python, so they move in one PR. Keep it out of the automerged group, as its own PR for manual review.",
"description": "A Python minor (3.14 -> 3.15) is a runtime upgrade for every instance that runs Python as its app runtime: its own runtime Dockerfile must move in step, and its tools must support the new version. This one rule covers the devcontainer's PYTHON_VERSION, the Makefile's PYTHON_VERSION and pyproject.toml's requires-python, so they move in one PR. The UV_IMAGE tag also carries the Python minor, which no manager rewrites: the Makefile guard fails that PR until the tag and digest are moved by hand. Keep it out of the automerged group, as its own PR for manual review.",
"matchDatasources": ["python-version"],
"matchDepNames": ["python"],
"matchUpdateTypes": ["minor"],
Expand All @@ -32,6 +32,11 @@
"matchManagers": ["dockerfile", "docker-compose"],
"pinDigests": true
},
{
"description": "The uv version appears in the Makefile (UV_VERSION and the UV_IMAGE tag) and the devcontainer (UV_VERSION ARG); tests/unit/test_release_contract.py and a Makefile guard fail if they drift, so they move in one PR.",
"matchDepNames": ["astral-sh/uv", "ghcr.io/astral-sh/uv"],
"groupName": "uv"
},
{
"description": "Renovate can't regenerate .devcontainer/devcontainer-lock.json, so features are bumped by hand together with it (see .devcontainer/README.md).",
"matchManagers": ["devcontainer"],
Expand Down Expand Up @@ -69,6 +74,17 @@
"# renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s*\\n\\w+_VERSION := (?<currentValue>.*)\\s"
]
},
{
"description": "The uv and syft images pinned by tag and digest in the Makefile (UV_IMAGE, SYFT_IMAGE); Renovate updates both. The UV_IMAGE tag also carries the Python minor and variant, which the Python minor rule does not rewrite: a Makefile guard fails until it is moved by hand.",
"customType": "regex",
"managerFilePatterns": [
"/^Makefile$/"
],
"matchStrings": [
"(?:UV|SYFT)_IMAGE := (?<depName>ghcr\\.io/astral-sh/uv|anchore/syft):(?<currentValue>[^@\\s]+)@(?<currentDigest>sha256:[a-f0-9]{64})"
],
"datasourceTemplate": "docker"
},
{
"description": "requires-python's lower bound (>=3.14) moves with the Python minor in the Makefile's uv image tag and the devcontainer; the python minor rule groups all three into one PR.",
"customType": "regex",
Expand Down
18 changes: 14 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,30 @@ ARCHES := amd64
NAME := $(shell python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["name"])')
ASSET := dist/$(NAME)-$(RELEASE_VERSION)

# renovate: datasource=docker depName=anchore/syft
SYFT_VERSION := v1.52.0
# Pinned by tag and digest; Renovate updates both together (the
# SYFT_IMAGE/UV_IMAGE manager in .github/renovate.json). It runs as root on
# dist/ between `build` and the upload, so a retagged image must not be able
# to rewrite the wheel and sdist.
SYFT_IMAGE := anchore/syft:v1.52.0@sha256:500e2d872ac019436926e8322b4fc1f39441d94d21f6f4046c6ff29b30e8cb02

# release.yml runs build/sbom/publish-packages on the bare runner, which has docker but
# no uv; release-check runs in the devcontainer, which has uv. So use the
# host's uv when there is one, else the pinned uv image (same uv and Python
# minor as .devcontainer/Dockerfile; tests/unit/test_release_contract.py
# fails if the uv version drifts from it). The OIDC variables are forwarded so
# `uv publish --trusted-publishing always` works inside the container.
# UV_IMAGE is pinned by tag and digest (a retagged image would receive the OIDC
# request token); Renovate updates both. UV_VERSION and PYTHON_VERSION above
# stay the source the devcontainer test compares, and the guard below fails the
# Makefile if the image tag drifts from them.
# renovate: datasource=github-releases depName=astral-sh/uv
UV_VERSION := 0.13.5
# renovate: datasource=python-version depName=python versioning=docker
PYTHON_VERSION := 3.14
UV_IMAGE := ghcr.io/astral-sh/uv:$(UV_VERSION)-python$(PYTHON_VERSION)-trixie-slim
UV_IMAGE := ghcr.io/astral-sh/uv:0.13.5-python3.14-trixie-slim@sha256:d8e732839dfc4225a1ff698803b478b2223c05636c055d3add41ab9dbec0f315
ifeq ($(findstring :$(UV_VERSION)-python$(PYTHON_VERSION)-,$(UV_IMAGE)),)
$(error UV_IMAGE ($(UV_IMAGE)) does not match UV_VERSION $(UV_VERSION) and PYTHON_VERSION $(PYTHON_VERSION); update the three together)
endif
UV := $(shell command -v uv >/dev/null 2>&1 && echo uv || echo \
docker run --rm -u $$(id -u):$$(id -g) -v "$(CURDIR):/src" -w /src \
-e UV_CACHE_DIR=/tmp/uv-cache -e UV_PYTHON_DOWNLOADS=never \
Expand All @@ -56,7 +66,7 @@ build:

sbom:
@set -e; whl=$$(ls dist/*.whl); \
docker run --rm -v "$(CURDIR)/dist:/dist" anchore/syft:$(SYFT_VERSION) \
docker run --rm -v "$(CURDIR)/dist:/dist" $(SYFT_IMAGE) \
"file:/$$whl" -o "spdx-json=/$${whl%.whl}.spdx.json"

release-assets:
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,16 @@ with a warning until you opt in, per repository:
(PyPI: Your account, Publishing, "Add a new pending publisher"; TestPyPI
has its own, separate form on test.pypi.org). Enter the project name from
`pyproject.toml`, the GitHub owner and repository, the workflow name
`release.yml`, and leave the environment empty (or use the one
`release.yml` runs under, if you add one).
`release.yml`, and the environment `release`. Don't leave the environment
empty: without it, any branch workflow with `id-token: write` can
publish by OIDC. The environment name must match what the workflow
sends, so do this only once `release.yml` declares `environment: release`
on the publishing jobs. That arrives with the sync from template-base;
check `release.yml` first, otherwise PyPI rejects the OIDC token. Also
create the `release` environment (deployment branches limited to the
default branch) in Settings, Environments before the first release that
carries it. template-base's `docs/TEMPLATE.md` documents the steps once
that sync has landed.
2. Set the repository variable `PYPI_PUBLISH` (Settings, Secrets and
variables, Actions, Variables):
- unset: `make publish-packages` warns that it skipped and the release only
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dev = [
]

[build-system]
requires = ["hatchling"]
requires = ["hatchling==1.33.0"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/test_release_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ def test_makefile_pins_match_the_devcontainer() -> None:
assert python.group(1).startswith(f"{_makefile_var('PYTHON_VERSION')}.")


@pytest.mark.parametrize("name", ["UV_IMAGE", "SYFT_IMAGE"])
def test_makefile_images_are_pinned_by_digest(name: str) -> None:
"""a retagged image must not be able to replace what holds the OIDC token."""
assert re.fullmatch(r"\S+:\S+@sha256:[0-9a-f]{64}", _makefile_var(name))


def test_verify_published_runs_in_the_pinned_uv_image() -> None:
"""the script gets the pinned image, so the runner needs no Python of its own."""
result = make("-n", "verify-published", publish="pypi")
Expand Down
Loading