Skip to content

Commit c41c9e5

Browse files
docs: add towncrier changelog management (#398)
Introduce towncrier-based changelog management to avoid CHANGELOG.md merge conflicts across the main/beta/httpx branch model. Contributors add one news fragment per user-facing change under changelog.d/; towncrier renders them into CHANGELOG.md at release time. - pyproject.toml: towncrier config + dev dependency - CHANGELOG.md: skeleton with towncrier marker - changelog.d/.gitignore: keep empty fragment dir tracked - create-release.yml: build + commit changelog before release - CONTRIBUTING.md, docs/releasing.md: fragment workflow docs Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 03433be commit c41c9e5

8 files changed

Lines changed: 146 additions & 4 deletions

File tree

.github/workflows/create-release.yml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ on:
1212
types: [completed]
1313
branches: ['main', 'beta', 'httpx']
1414

15-
# The release is created with a GitHub App token; the default token only needs
16-
# read access for checkout and `gh release view`.
15+
# The release is created with a GitHub App token. The "Build changelog" step
16+
# commits the rendered CHANGELOG.md back to the release branch, so the default
17+
# token needs write access to contents.
1718
permissions:
18-
contents: read
19+
contents: write
1920

2021
jobs:
2122
create-release:
@@ -80,6 +81,34 @@ jobs:
8081
exit 1
8182
fi
8283
84+
# Render changelog.d/ fragments into CHANGELOG.md and commit before the
85+
# release is cut. Reuses the version already read from pyproject.toml, so
86+
# there is no separate towncrier version to keep in sync. --generate-notes
87+
# below remains as a fallback for the GitHub release body.
88+
- name: Build changelog
89+
env:
90+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
91+
run: |
92+
VERSION="${{ steps.version.outputs.version }}"
93+
BRANCH="${{ inputs.branch || github.event.workflow_run.head_branch }}"
94+
95+
# Nothing to do if there are no fragments to consume.
96+
if ! ls changelog.d/*.md >/dev/null 2>&1; then
97+
echo "No changelog fragments; skipping towncrier build."
98+
exit 0
99+
fi
100+
101+
pip install --quiet towncrier
102+
towncrier build --yes --version "$VERSION"
103+
104+
git config user.name "github-actions[bot]"
105+
git config user.email "github-actions[bot]@users.noreply.github.com"
106+
git add CHANGELOG.md changelog.d/
107+
if ! git diff --cached --quiet; then
108+
git commit -m "docs: update changelog for $VERSION"
109+
git push origin "HEAD:$BRANCH"
110+
fi
111+
83112
- name: Create release
84113
if: steps.check-release.outputs.exists == 'false'
85114
env:

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ venv/
1919
.DS_Store
2020

2121
# Project
22-
.scratch/
22+
.scratch/
23+
24+
# Git worktrees
25+
.worktrees/

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
3+
All notable changes to this library are documented here.
4+
5+
This file is maintained with [towncrier](https://towncrier.readthedocs.io/);
6+
add a news fragment under `changelog.d/` for every user-facing change. See
7+
[CONTRIBUTING.md](CONTRIBUTING.md) for the fragment format.
8+
9+
<!-- towncrier release notes start -->

CONTRIBUTING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ uv sync
3939
- Generated API scope files (`meraki/api/`, `meraki/aio/api/`) are auto-generated from the OpenAPI spec. Changes here will be overwritten. Fix the generator instead.
4040
- Do not vendor or bundle dependencies.
4141

42+
## Changelog Fragments
43+
44+
Every user-facing change needs a news fragment in `changelog.d/`. Do not edit `CHANGELOG.md` by hand; towncrier builds it at release time.
45+
46+
File name: `changelog.d/{issue}.{type}.md`. With no issue, use a slug prefixed with `+`: `changelog.d/+httpx-migration.changed.md`.
47+
48+
Types: `added`, `changed`, `deprecated`, `removed`, `fixed`, `security`.
49+
50+
```bash
51+
# Issue-linked
52+
echo "Retry on 429 now honors Retry-After." > changelog.d/1234.fixed.md
53+
54+
# Or via towncrier
55+
uv run towncrier create -c "Retry on 429 now honors Retry-After." 1234.fixed.md
56+
57+
# Preview the rendered changelog without consuming fragments
58+
uv run towncrier build --draft --version "$(grep '^version' pyproject.toml | sed 's/version = "\(.*\)"/\1/')"
59+
```
60+
61+
One fragment per change. Multiple fragments per issue are fine (e.g. `1234.added.md` and `1234.fixed.md`). Fragments are not wiped by library regeneration; they survive until the next release build.
62+
4263
## Running the Generator
4364

4465
```bash

changelog.d/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.gitignore

docs/releasing.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ Starting from `main` at `3.1.0`, `beta` at `3.1.0`, `httpx` at `4.0.0`:
4646

4747
When a new prerelease is detected, the `enable-early-access.yml` workflow runs first (waited on synchronously). This enables early access features on the test organizations so the generated beta and dev SDKs can be tested against them.
4848

49+
## Changelog management
50+
51+
`CHANGELOG.md` is generated by [towncrier](https://towncrier.readthedocs.io/) from news fragments in `changelog.d/`. Contributors add one fragment per user-facing change (see [CONTRIBUTING.md](../CONTRIBUTING.md) for the format); nobody edits `CHANGELOG.md` directly.
52+
53+
The `create-release.yml` workflow runs `towncrier build --yes --version <version>` after version validation and before `gh release create`. The version comes from the same `pyproject.toml` read that drives the release tag, so there is no separate towncrier version to keep in sync. The build renders the fragments into `CHANGELOG.md`, deletes the consumed fragments, and commits both changes to the release branch before the GitHub release is cut.
54+
55+
This runs only at release time, not during regeneration. `regenerate-library.yml` rewrites generated code under `meraki/` but never touches `changelog.d/` or `CHANGELOG.md`, so fragments added on `main`, `beta`, or `httpx` survive regeneration PRs and are consumed only by the next release on that branch. Each branch keeps its own fragments and accumulates them independently across the three-branch model.
56+
4957
## Release branches and PRs
5058

5159
| Stage | Source branch | Release branch | PR target |

pyproject.toml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dev = [
4141
"flake8>=7.0,<8",
4242
"pre-commit>=4.6.0",
4343
"ruff>=0.15.12",
44+
"towncrier>=24.8.0",
4445
]
4546
generator = ["jinja2==3.1.6"]
4647

@@ -63,3 +64,46 @@ omit = ["meraki/api/*", "meraki/aio/api/*", "meraki/aio/__init__.py"]
6364
fail_under = 90
6465
show_missing = true
6566
exclude_lines = ["pragma: no cover", "if __name__"]
67+
68+
[tool.towncrier]
69+
directory = "changelog.d"
70+
filename = "CHANGELOG.md"
71+
name = "meraki"
72+
title_format = "## {version} ({project_date})"
73+
issue_format = "[#{issue}](https://github.com/meraki/dashboard-api-python/issues/{issue})"
74+
start_string = "<!-- towncrier release notes start -->\n"
75+
wrap = false
76+
all_bullets = true
77+
78+
# Version is passed via `--version` from a single source of truth (pyproject
79+
# [project].version) at build time, so there is no version pinned here to drift.
80+
81+
[[tool.towncrier.type]]
82+
directory = "added"
83+
name = "Added"
84+
showcontent = true
85+
86+
[[tool.towncrier.type]]
87+
directory = "changed"
88+
name = "Changed"
89+
showcontent = true
90+
91+
[[tool.towncrier.type]]
92+
directory = "deprecated"
93+
name = "Deprecated"
94+
showcontent = true
95+
96+
[[tool.towncrier.type]]
97+
directory = "removed"
98+
name = "Removed"
99+
showcontent = true
100+
101+
[[tool.towncrier.type]]
102+
directory = "fixed"
103+
name = "Fixed"
104+
showcontent = true
105+
106+
[[tool.towncrier.type]]
107+
directory = "security"
108+
name = "Security"
109+
showcontent = true

uv.lock

Lines changed: 27 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)