From c60f3556d79799e33e89e66a5deddf3576f5543f Mon Sep 17 00:00:00 2001 From: Dylan Verheul Date: Sun, 11 Oct 2026 10:38:20 +0200 Subject: [PATCH 1/2] Sync the release preflight from django-marina Propagation of zostera/django-marina#694, where the reasoning lives. just release-check listed only commits touching src/, then closed with "Every commit above must be covered by an entry", which reads as a complete list and is not one. Anything tooling-only was invisible to it. It now prints a second group for commits touching nothing in src/, under a heading that says to read them rather than a rule, because no rule fits: a Dependabot bump of a dev dependency is nothing to a user, while a lint rule or a contributor policy belongs in the notes. just build now fails when uv.lock does not name the version in pyproject.toml, and just relock regenerates the lock without upgrading anything. uv sync --frozen does not check the project's own version, so a stale lock previously passed twine, check-manifest, pyroma and both smoke tests unnoticed. That is not theoretical: it is how a version bump shipped here with the previous version still in the lock. MAINTAINING.md release steps 1 and 2 updated to match. The justfile is byte-identical to django-marina again. Verified: just lint, just build and just test-cov at 100%. just relock was a no-op, so this lock was already current. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 ++ MAINTAINING.md | 17 +++++++++++++---- justfile | 29 +++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac27f087..bccd5b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - Cover every statement and branch in the test suite, and hold it there with `fail_under = 100`. - Store `src/django_bootstrap5/widgets.py` with LF line endings, like every other file and as `.editorconfig` requires. It was the only file in the repository with CRLF. +- Show commits that touch nothing in `src/` in `just release-check` as a second group, instead of hiding them. Tooling, CI and documentation changes belong in the release notes too, and the old output read as a complete list while being a filtered one. +- Fail `just build` when `uv.lock` does not name the version in `pyproject.toml`, and add `just relock` to fix it. `uv sync --frozen` does not check the project's own version, so a stale lock passed every packaging check. ## 26.3 (2026-08-28) - Drop support for Django 4.2 (EOL). diff --git a/MAINTAINING.md b/MAINTAINING.md index d2c934bc..7ae38acd 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -102,10 +102,19 @@ it half-updated. Dependabot reopens it on the next run. ## Release process -1. `just release-check` — lists the commits touching `src/` since the last tag next to the current - `Unreleased` entries. Every one of those commits must be covered by an entry, or be a no-op for - users (a docstring typo, say). A code change that reaches users without an entry ships invisible. -2. On a release branch, update `CHANGELOG.md` and bump `version` in `pyproject.toml`; open a PR and merge it +1. `just release-check` — lists the commits since the last tag next to the current `Unreleased` + entries, in two groups. Every commit touching `src/` must be covered by an entry, or be a + no-op for users (a docstring typo, say). A code change that reaches users without an entry + ships invisible. The second group is everything else, and it needs reading rather than a rule: + a Dependabot bump of a dev dependency is genuinely nothing to a user, while a lint rule, a CI + gate or a contributor policy belongs in the notes under tooling. django-marina 26.3 shipped + with nine such changes unrecorded, and django-bootstrap4 and django-bootstrap5 each found + four or five more of their own, because the recipe only showed the first group. +2. On a release branch, update `CHANGELOG.md`, bump `version` in `pyproject.toml`, and run + `just relock` so `uv.lock` names the new version; open a PR and merge it. `uv sync --frozen` + does not check the project's own version, so a stale lock otherwise survives every packaging + check unnoticed. `just build` now fails on the mismatch, which is what catches it if the + relock is forgotten. 3. Check out and pull `main` 4. `just build` — builds wheel + tarball, runs packaging checks, and smoke-tests both against an isolated env 5. `just release-tag` — creates and pushes the version tag; GitHub Actions publishes to PyPI diff --git a/justfile b/justfile index c465f5db..62ea792d 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,10 @@ VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml` @upgrade: uv sync --all-groups --upgrade +# Regenerate uv.lock without upgrading anything (use after bumping version) +@relock: + uv lock + # Install for development (uses uv.lock) install: uv sync --frozen --all-groups @@ -60,8 +64,20 @@ install: @tests *ARGS: uvx --with tox-uv tox {{ARGS}} +# Fail if uv.lock does not name the version in pyproject.toml. +# `uv sync --frozen` does not check the project's own version, so without this a build +# passes with a stale lock: django-bootstrap4 shipped a bump with the previous version +# still in the lock, and `just build` reported everything green. Fix with `just relock`. +[private] +@_lockfile-matches-version: + LOCKED=`awk -F'"' '/^version = /{v=$2} /editable = "\."/{print v; exit}' uv.lock`; \ + if [ "$LOCKED" != "{{ VERSION }}" ]; then \ + echo "Error - pyproject.toml says {{ VERSION }}, uv.lock says $LOCKED. Run 'just relock'."; \ + exit 1; \ + fi + # Build artefacts + packaging checks (local preflight) -@build: clean-build install +@build: clean-build install _lockfile-matches-version uv build uvx twine check dist/* uvx check-manifest @@ -82,16 +98,21 @@ install: echo "Example not found."; \ fi -# Show src changes and changelog entries since the last tag (release preflight) +# Show changes and changelog entries since the last tag (release preflight) @release-check: TAG=$(git describe --tags --abbrev=0); \ - echo "Commits touching src/ since $TAG:"; \ + echo "Commits touching src/ since $TAG. Each needs an entry, or is a no-op for users:"; \ git log --format=' %h %s' "$TAG"..HEAD -- src/ | grep . || echo " (none)"; \ echo ""; \ + echo "Commits touching nothing in src/. Tooling, CI and docs still ship, so read these too:"; \ + git log --format=' %h %s' "$TAG"..HEAD -- . ':(exclude)src/' | grep . || echo " (none)"; \ + echo ""; \ echo "Entries in the Unreleased section of CHANGELOG.md:"; \ awk '/^## Unreleased/{f=1;next} /^## /{f=0} f' CHANGELOG.md | grep '^- ' || echo " (none)"; \ echo ""; \ - echo "Every commit above must be covered by an entry, or be a no-op for users." + echo "Version: pyproject.toml {{ VERSION }}, uv.lock `awk -F'\"' '/^version = /{v=$2} /editable = \"\\.\"/{print v; exit}' uv.lock`"; \ + echo ""; \ + echo "A commit touching src/ and other paths appears in both lists." # Create and push a release tag (publishing happens in GitHub Actions) @release-tag: porcelain branch build From b6db8117767da99d2ac2fa1b1c122559dffc0d4a Mon Sep 17 00:00:00 2001 From: Dylan Verheul Date: Sun, 11 Oct 2026 11:12:25 +0200 Subject: [PATCH 2/2] Say what release-check reports straight after a cut Propagation of zostera/django-marina#696. In the window between merging the release PR and pushing the tag, just release-check prints "(none)" for the Unreleased entries next to every commit since the previous tag. Stamping the section removes the heading the recipe looks for, and the tag it compares against is still the old one. Correct, and it reads as if the changelog has been lost. Raised by the django-bootstrap4 session, which hit it while cutting 26.3. It does not show up in django-marina, where the tag sits on HEAD so both lists come back empty and the pair looks consistent. Documentation only. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + MAINTAINING.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bccd5b95..2170c75c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Show commits that touch nothing in `src/` in `just release-check` as a second group, instead of hiding them. Tooling, CI and documentation changes belong in the release notes too, and the old output read as a complete list while being a filtered one. - Fail `just build` when `uv.lock` does not name the version in `pyproject.toml`, and add `just relock` to fix it. `uv sync --frozen` does not check the project's own version, so a stale lock passed every packaging check. +- Say in MAINTAINING.md that `just release-check` reporting no `Unreleased` entries straight after a cut means nothing is pending, not that the changelog was lost. ## 26.3 (2026-08-28) - Drop support for Django 4.2 (EOL). diff --git a/MAINTAINING.md b/MAINTAINING.md index 7ae38acd..e866a13c 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -110,6 +110,11 @@ it half-updated. Dependabot reopens it on the next run. gate or a contributor policy belongs in the notes under tooling. django-marina 26.3 shipped with nine such changes unrecorded, and django-bootstrap4 and django-bootstrap5 each found four or five more of their own, because the recipe only showed the first group. + Straight after a cut, in the window between merging the release PR and pushing the tag, this + prints `(none)` for the `Unreleased` entries next to every commit since the previous tag. + That is correct and it means nothing is pending: stamping the section removed the heading the + recipe looks for, and the tag it compares against is still the old one. It does not mean the + changelog was lost. 2. On a release branch, update `CHANGELOG.md`, bump `version` in `pyproject.toml`, and run `just relock` so `uv.lock` names the new version; open a PR and merge it. `uv sync --frozen` does not check the project's own version, so a stale lock otherwise survives every packaging