Skip to content

Commit cb2775c

Browse files
Merge pull request #3017 from VWS-Python/codex/towncrier-changelog
[codex] Drive changelog releases from towncrier
2 parents 94ef760 + a573f07 commit cb2775c

8 files changed

Lines changed: 80 additions & 26 deletions

File tree

.github/workflows/release.yml

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,37 +47,27 @@ jobs:
4747
env:
4848
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4949

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

68-
- name: Check Update changelog was modified
59+
# Assemble the same fragments into CHANGELOG.rst under a new
60+
# ``$RELEASE`` section and delete the consumed fragment files.
61+
- name: Update the changelog
6962
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
63+
RELEASE: ${{ steps.calver.outputs.release }}
64+
run: uv run --extra=release towncrier build --yes --version "$RELEASE"
65+
7666
- uses: stefanzweifel/git-auto-commit-action@v7
7767
id: commit
7868
with:
7969
commit_message: Bump CHANGELOG
80-
file_pattern: CHANGELOG.rst
70+
file_pattern: CHANGELOG.rst newsfragments
8171
# Error if there are no changes.
8272
skip_dirty_check: true
8373

@@ -96,7 +86,7 @@ jobs:
9686
tag: ${{ steps.tag_version.outputs.new_tag }}
9787
makeLatest: true
9888
name: Release ${{ steps.tag_version.outputs.new_tag }}
99-
body: ${{ steps.tag_version.outputs.changelog }}
89+
bodyFile: release-notes.md
10090

10191
- name: Build a binary wheel and a source tarball
10292
env:

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

docs/source/conf.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@
2323
"sphinx.ext.napoleon",
2424
"sphinx_substitution_extensions",
2525
"sphinxcontrib.spelling",
26+
"sphinxcontrib.towncrier.ext",
2627
]
2728

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

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,5 @@ Reference
124124
exceptions
125125
contributing
126126
release-process
127+
unreleased
127128
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
@@ -76,13 +76,17 @@ optional-dependencies.dev = [
7676
"sphinx-pyproject==0.3.0",
7777
"sphinx-substitution-extensions==2026.1.12",
7878
"sphinxcontrib-spelling==8.0.2",
79+
# ``sphinxcontrib-towncrier`` renders unreleased news fragments
80+
# into docs/source/unreleased.rst during Sphinx builds.
81+
"sphinxcontrib-towncrier==0.5.0a0",
7982
"strict-kwargs==2026.5.19.post3",
8083
"sybil==9.3.0",
8184
# Listed explicitly (despite being transitive via vws-python-mock) so that
8285
# [tool.uv.sources] can redirect to the CPU-only PyTorch index.
8386
# See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
8487
"torch>=2.5.1",
8588
"torchvision>=0.20.1",
89+
"towncrier==25.8.0",
8690
"ty==0.0.37",
8791
"types-requests==2.33.0.20260518",
8892
"vulture==2.16",
@@ -284,6 +288,8 @@ ignore = [
284288
"*.enc",
285289
".pre-commit-config.yaml",
286290
"CHANGELOG.rst",
291+
"newsfragments",
292+
"newsfragments/**",
287293
"CODE_OF_CONDUCT.rst",
288294
"CONTRIBUTING.rst",
289295
"LICENSE",
@@ -349,6 +355,30 @@ report.exclude_also = [
349355
]
350356
report.show_missing = true
351357

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

0 commit comments

Comments
 (0)