Skip to content

Commit fd28b2c

Browse files
Merge pull request #2234 from VWS-Python/codex/towncrier-changelog
[codex] Drive changelog releases from towncrier
2 parents 94eb68f + ccca672 commit fd28b2c

9 files changed

Lines changed: 109 additions & 30 deletions

File tree

.github/workflows/release.yml

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,36 +52,21 @@ jobs:
5252
env:
5353
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5454

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

77-
- name: Check Update changelog was modified
64+
# Assemble the same fragments into CHANGELOG.rst under a new
65+
# ``$RELEASE`` section and delete the consumed fragment files.
66+
- name: Update the changelog
7867
env:
79-
MODIFIED_FILES: ${{ steps.update_changelog.outputs.modifiedFiles }}
80-
run: |
81-
if [ "$MODIFIED_FILES" = "0" ]; then
82-
echo "Error: No files were modified when updating changelog"
83-
exit 1
84-
fi
68+
RELEASE: ${{ steps.calver.outputs.release }}
69+
run: uv run --extra=release towncrier build --yes --version "$RELEASE"
8570

8671
- name: Update VERSION file for Nix flake
8772
env:
@@ -93,7 +78,7 @@ jobs:
9378
id: commit
9479
with:
9580
commit_message: Bump CHANGELOG and VERSION
96-
file_pattern: CHANGELOG.rst VERSION
81+
file_pattern: CHANGELOG.rst newsfragments VERSION
9782
# Error if there are no changes.
9883
skip_dirty_check: true
9984

@@ -203,7 +188,7 @@ jobs:
203188
tag: ${{ steps.tag_version.outputs.new_tag }}
204189
makeLatest: true
205190
name: Release ${{ steps.tag_version.outputs.new_tag }}
206-
body: ${{ steps.tag_version.outputs.changelog }}
191+
bodyFile: release-notes.md
207192

208193
build-linux:
209194
name: Build Linux binary (${{ matrix.binary.name }})

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.02.22
87
----------

docs/source/conf.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@
2222
extensions = [
2323
"sphinx_copybutton",
2424
"sphinxcontrib.spelling",
25+
"sphinxcontrib.towncrier.ext",
2526
"sphinx_click.ext",
2627
"sphinx_inline_tabs",
2728
"sphinx_substitution_extensions",
2829
]
2930

31+
# Render the unreleased ``newsfragments/`` entries into
32+
# ``docs/source/unreleased.rst`` so the Sphinx spelling, doc-build and
33+
# link-checking gates cover the prose before it is assembled into
34+
# CHANGELOG.rst at release time.
35+
towncrier_draft_autoversion_mode = "draft"
36+
towncrier_draft_include_empty = True
37+
towncrier_draft_working_directory = f"{_pyproject_file.parent}"
38+
3039
templates_path = ["_templates"]
3140
source_suffix = ".rst"
3241
master_doc = "index"

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ Reference
2020
commands
2121
contributing
2222
release-process
23+
unreleased
2324
changelog

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
@@ -75,12 +75,16 @@ optional-dependencies.dev = [
7575
"sphinx-pyproject==0.3.0",
7676
"sphinx-substitution-extensions==2026.1.12",
7777
"sphinxcontrib-spelling==8.0.2",
78+
# ``sphinxcontrib-towncrier`` renders unreleased news fragments
79+
# into docs/source/unreleased.rst during Sphinx builds.
80+
"sphinxcontrib-towncrier==0.5.0a0",
7881
# Listed explicitly (despite being transitive via vws-python-mock) so that
7982
# [tool.uv.sources] can redirect to the CPU-only PyTorch index.
8083
# See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
8184
"strict-kwargs==2026.5.19.post3",
8285
"torch>=2.5.1",
8386
"torchvision>=0.20.1",
87+
"towncrier==25.8.0",
8488
"ty==0.0.38",
8589
"types-pyyaml==6.0.12.20260518",
8690
"vulture==2.16",
@@ -277,6 +281,8 @@ ignore = [
277281
"*.enc",
278282
".pre-commit-config.yaml",
279283
"CHANGELOG.rst",
284+
"newsfragments",
285+
"newsfragments/**",
280286
"CODE_OF_CONDUCT.rst",
281287
"CONTRIBUTING.rst",
282288
"LICENSE",
@@ -350,6 +356,30 @@ report.exclude_also = [
350356
]
351357
report.show_missing = true
352358

359+
[tool.towncrier]
360+
# The changelog and the per-release GitHub release notes are both built
361+
# from news fragments under ``newsfragments/``. The release workflow
362+
# runs ``towncrier build`` to assemble them; contributors add one
363+
# fragment file per user-facing change.
364+
directory = "newsfragments"
365+
filename = "CHANGELOG.rst"
366+
# Custom template so an assembled version reproduces the historical
367+
# style exactly: a bare ``<version>`` heading (no project name, no
368+
# date) followed by a flat bullet list with no per-type sub-headings.
369+
template = "docs/towncrier_template.rst.jinja"
370+
title_format = "{version}"
371+
# ``title_format`` underline first, then any nested headings. A bare
372+
# version such as ``2026.05.18`` underlined with ``-`` matches every
373+
# pre-towncrier entry in CHANGELOG.rst.
374+
underlines = [ "-", "~", "^" ]
375+
issue_format = "#{issue}"
376+
type = [
377+
# A single, unnamed fragment type keeps the assembled output as one
378+
# flat bullet list, matching the historical changelog (which never
379+
# grouped entries under "Features"/"Bugfixes"/... sub-headings).
380+
{ directory = "change", name = "", showcontent = true },
381+
]
382+
353383
[tool.pydocstringformatter]
354384
write = true
355385
split-summary-body = false
@@ -410,6 +440,9 @@ ignore_names = [
410440
"spelling_word_list_filename",
411441
"templates_path",
412442
"warning_is_error",
443+
"towncrier_draft_autoversion_mode",
444+
"towncrier_draft_include_empty",
445+
"towncrier_draft_working_directory",
413446
]
414447
exclude = [
415448
# Duplicate some of .gitignore

uv.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)