Skip to content

Commit 97e34fa

Browse files
Merge pull request #3188 from VWS-Python/codex/towncrier-changelog
[codex] Drive changelog releases from towncrier
2 parents 33c3272 + a16672b commit 97e34fa

8 files changed

Lines changed: 80 additions & 30 deletions

File tree

.github/workflows/release.yml

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,41 +43,27 @@ jobs:
4343
env:
4444
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4545

46-
- name: Get the changelog underline
47-
id: changelog_underline
46+
# towncrier writes the rendered notes to stdout (informational
47+
# chatter goes to stderr), so this is the curated release body for
48+
# this version, not github-tag-action's commit-derived changelog.
49+
- name: Generate the GitHub release notes
4850
env:
4951
RELEASE: ${{ steps.calver.outputs.release }}
50-
run: |
51-
underline="$(echo "$RELEASE" | tr -c '\n' '-')"
52-
echo "underline=${underline}" >> "$GITHUB_OUTPUT"
53-
54-
- name: Update changelog
55-
id: update_changelog
56-
uses: jacobtomlinson/gha-find-replace@v3
57-
with:
58-
find: "Next\n----"
59-
replace: |
60-
Next
61-
----
62-
63-
${{ steps.calver.outputs.release }}
64-
${{ steps.changelog_underline.outputs.underline }}
65-
include: CHANGELOG.rst
66-
regex: false
52+
run: uv run --extra=release towncrier build --draft --version "$RELEASE" >
53+
release-notes.md
6754

68-
- name: Check Update changelog was modified
55+
# Assemble the same fragments into CHANGELOG.rst under a new
56+
# ``$RELEASE`` section and delete the consumed fragment files.
57+
- name: Update the changelog
6958
env:
70-
MODIFIED_FILES: ${{ steps.update_changelog.outputs.modifiedFiles }}
71-
run: |
72-
if [ "$MODIFIED_FILES" = "0" ]; then
73-
echo "Error: No files were modified when updating changelog"
74-
exit 1
75-
fi
59+
RELEASE: ${{ steps.calver.outputs.release }}
60+
run: uv run --extra=release towncrier build --yes --version "$RELEASE"
61+
7662
- uses: stefanzweifel/git-auto-commit-action@v7
7763
id: commit
7864
with:
7965
commit_message: Bump CHANGELOG
80-
file_pattern: CHANGELOG.rst
66+
file_pattern: CHANGELOG.rst newsfragments
8167
# Error if there are no changes.
8268
skip_dirty_check: true
8369

@@ -96,7 +82,7 @@ jobs:
9682
tag: ${{ steps.tag_version.outputs.new_tag }}
9783
makeLatest: true
9884
name: Release ${{ steps.tag_version.outputs.new_tag }}
99-
body: ${{ steps.tag_version.outputs.changelog }}
85+
bodyFile: release-notes.md
10086

10187
pypi:
10288
name: Publish to PyPI

CHANGELOG.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
Changelog
22
=========
33

4-
Next
5-
----
4+
.. towncrier release notes start
65
76
2026.04.26
87
----------

docs/source/conf.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,19 @@
2424
"sphinx_paramlinks",
2525
"sphinx_substitution_extensions",
2626
"sphinxcontrib.spelling",
27+
"sphinxcontrib.towncrier.ext",
2728
"sphinxcontrib.autohttp.flask",
2829
"sphinx_toolbox.more_autodoc.autoprotocol",
2930
]
3031

32+
# Render the unreleased ``newsfragments/`` entries into
33+
# ``docs/source/unreleased.rst`` so the Sphinx spelling, doc-build and
34+
# link-checking gates cover the prose before it is assembled into
35+
# CHANGELOG.rst at release time.
36+
towncrier_draft_autoversion_mode = "draft"
37+
towncrier_draft_include_empty = True
38+
towncrier_draft_working_directory = f"{_pyproject_file.parent}"
39+
3140
# Required by sphinx-toolbox 4.2.0rc1 for compatibility with Sphinx 9.
3241
# See https://github.com/sphinx-toolbox/sphinx-toolbox/issues/201#issuecomment-4313483053.
3342
autodoc_use_legacy_class_based = True

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Reference
4040
.. toctree::
4141
:hidden:
4242

43+
unreleased
4344
changelog
4445
release-process
4546
ci-setup

docs/source/unreleased.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Unreleased changes
2+
==================
3+
4+
Changes that have landed on the main branch but are not yet part of a
5+
tagged release. These entries are assembled into the
6+
:doc:`changelog` when the next release is published.
7+
8+
.. towncrier-draft-entries::

docs/towncrier_template.rst.jinja

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
{% for section_name, section in sections.items() %}
3+
{% if section %}
4+
{% for category, entries in section.items() %}
5+
{% for text, _ in entries.items() %}
6+
- {{ text }}
7+
8+
{% endfor %}
9+
{% endfor %}
10+
{% else %}
11+
No significant changes.
12+
13+
{% endif %}
14+
{% endfor %}

newsfragments/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,13 @@ optional-dependencies.dev = [
9696
"sphinx-toolbox==4.2.0rc1",
9797
"sphinxcontrib-httpdomain==2.0.0",
9898
"sphinxcontrib-spelling==8.0.2",
99+
# ``sphinxcontrib-towncrier`` renders unreleased news fragments
100+
# into docs/source/unreleased.rst during Sphinx builds.
101+
"sphinxcontrib-towncrier==0.5.0a0",
99102
"strict-kwargs==2026.5.20",
100103
"sybil==10.0.1",
101104
"tenacity==9.1.4",
105+
"towncrier==25.8.0",
102106
"ty==0.0.38",
103107
"types-docker==7.1.0.20260518",
104108
"types-pyyaml==6.0.12.20260518",
@@ -315,6 +319,8 @@ ignore = [
315319
"*.enc",
316320
"admin/**",
317321
"CHANGELOG.rst",
322+
"newsfragments",
323+
"newsfragments/**",
318324
"CODE_OF_CONDUCT.rst",
319325
"CONTRIBUTING.rst",
320326
"LICENSE",
@@ -405,6 +411,30 @@ report.exclude_also = [
405411
report.fail_under = 100
406412
report.show_missing = true
407413

414+
[tool.towncrier]
415+
# The changelog and the per-release GitHub release notes are both built
416+
# from news fragments under ``newsfragments/``. The release workflow
417+
# runs ``towncrier build`` to assemble them; contributors add one
418+
# fragment file per user-facing change.
419+
directory = "newsfragments"
420+
filename = "CHANGELOG.rst"
421+
# Custom template so an assembled version reproduces the historical
422+
# style exactly: a bare ``<version>`` heading (no project name, no
423+
# date) followed by a flat bullet list with no per-type sub-headings.
424+
template = "docs/towncrier_template.rst.jinja"
425+
title_format = "{version}"
426+
# ``title_format`` underline first, then any nested headings. A bare
427+
# version such as ``2026.05.18`` underlined with ``-`` matches every
428+
# pre-towncrier entry in CHANGELOG.rst.
429+
underlines = [ "-", "~", "^" ]
430+
issue_format = "#{issue}"
431+
type = [
432+
# A single, unnamed fragment type keeps the assembled output as one
433+
# flat bullet list, matching the historical changelog (which never
434+
# grouped entries under "Features"/"Bugfixes"/... sub-headings).
435+
{ directory = "change", name = "", showcontent = true },
436+
]
437+
408438
[tool.pydocstringformatter]
409439
write = true
410440
split-summary-body = false
@@ -483,6 +513,9 @@ ignore_names = [
483513
"DatabaseDict",
484514
"VuMarkDatabaseDict",
485515
"VuMarkTargetDict",
516+
"towncrier_draft_autoversion_mode",
517+
"towncrier_draft_include_empty",
518+
"towncrier_draft_working_directory",
486519
]
487520
# Duplicate some of .gitignore
488521
exclude = [ ".venv" ]

0 commit comments

Comments
 (0)