Skip to content

Commit a651600

Browse files
ci: introduce towncrier to generate changelog from fragments (#4573)
* introduce towncrier to generate changelog from fragments Replace manual CHANGELOG.md editing with towncrier fragment-based changelog management. Each PR adds a small text file in .changelog/ instead of editing the changelog directly, eliminating merge conflicts. Root .changelog/ is used for coordinated releases (most packages). Per-package .changelog/ dirs are used for independently released packages (opamp-client, propagator-aws-xray, resource-detector-azure, sdk-extension-aws). Changes: - Add towncrier config to root pyproject.toml and 4 independent package pyproject.toml files - Add shared Jinja2 template at scripts/changelog_template.j2 - Replace sed-based changelog generation with towncrier build in all release workflows (coordinated + per-package) - Rewrite changelog CI to use towncrier check for fragment validation - Add do-not-edit comments and towncrier markers to all CHANGELOG.md files - Update CONTRIBUTING.md with changelog fragment guidance - Update RELEASING.md to reflect towncrier workflow - Add towncrier to dev dependencies Assisted-by: Claude Opus 4.6 * address review: drop towncrier explanation, add changelog backport - Remove towncrier explanation sentence from RELEASING.md (per @xrmx) - Add changelog backport step to prepare-patch-release.yml to sync changelog from release branch to main (per @emdneto, matching core) Assisted-by: Claude Opus 4.6 --------- Co-authored-by: Aaron Abbott <aaronabbott@google.com>
1 parent 287a65e commit a651600

24 files changed

Lines changed: 2404 additions & 1837 deletions

File tree

.changelog/.gitignore

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

.github/workflows/changelog.yml

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# This action requires that any PR targeting the main branch should touch at
2-
# least one CHANGELOG file. If a CHANGELOG entry is not required, add the "Skip
3-
# Changelog" label to disable this action.
1+
# This action requires that any PR targeting the main branch should add a
2+
# changelog fragment file in a .changelog/ directory. If a changelog entry
3+
# is not required, add the "Skip Changelog" label to disable this action.
44

55
name: changelog
66

@@ -22,18 +22,46 @@ jobs:
2222
2323
steps:
2424
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
2527

26-
- name: Check for CHANGELOG changes
28+
- name: Fetch base branch
29+
run: git fetch origin ${{ github.base_ref }} --depth=1
30+
31+
- name: Ensure no direct changes to CHANGELOG.md
2732
run: |
28-
# Only the latest commit of the feature branch is available
29-
# automatically. To diff with the base branch, we need to
30-
# fetch that too (and we only need its latest commit).
31-
git fetch origin ${{ github.base_ref }} --depth=1
32-
if [[ $(git diff --name-only FETCH_HEAD | grep CHANGELOG) ]]
33+
if [[ $(git diff --name-only FETCH_HEAD -- '**/CHANGELOG.md') ]]
3334
then
34-
echo "A CHANGELOG was modified. Looks good!"
35-
else
36-
echo "No CHANGELOG was modified."
37-
echo "Please add a CHANGELOG entry, or add the \"Skip Changelog\" label if not required."
35+
echo "CHANGELOG.md files should not be directly modified."
36+
echo "Please add a changelog fragment file to the appropriate .changelog/ directory instead."
37+
echo "See CONTRIBUTING.md for details."
38+
echo ""
39+
echo "Or add the \"Skip Changelog\" label if this job should be skipped."
3840
false
3941
fi
42+
43+
- name: Check for changelog fragment
44+
run: |
45+
# Check for any new fragment files in any .changelog/ directory
46+
fragments=$(git diff --diff-filter=A --name-only FETCH_HEAD -- '**/.changelog/*' '.changelog/*' | grep -v '.gitignore' || true)
47+
if [[ -z "$fragments" ]]; then
48+
echo "No changelog fragment found for this PR."
49+
echo ""
50+
echo "Add a file named .changelog/${{ github.event.pull_request.number }}.<type>"
51+
echo "where <type> is one of: added, changed, deprecated, removed, fixed"
52+
echo ""
53+
echo "For coordinated packages, add to the root .changelog/ directory."
54+
echo "For independently released packages, add to <package>/.changelog/"
55+
echo "See CONTRIBUTING.md for details."
56+
echo ""
57+
echo "Or add the \"Skip Changelog\" label if this job should be skipped."
58+
exit 1
59+
fi
60+
echo "Found changelog fragment(s):"
61+
echo "$fragments"
62+
63+
- name: Install towncrier
64+
run: pip install towncrier==25.8.0
65+
66+
- name: Preview changelog
67+
run: towncrier build --draft --version Unreleased

.github/workflows/package-prepare-patch-release.yml

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@ jobs:
4646
exit 1
4747
fi
4848
49-
if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then
50-
echo the $changelog is missing an \"Unreleased\" section
51-
exit 1
52-
fi
53-
5449
version=$(./scripts/eachdist.py version --package ${{ inputs.package }})
5550
5651
version_file=$(find $path -type f -path "**/version.py")
@@ -106,11 +101,11 @@ jobs:
106101
- name: run tox
107102
run: tox -e generate
108103

109-
- name: Update the change log with the approximate release date
110-
run: |
111-
# the actual release date on main will be updated at the end of the release workflow
112-
date=$(date "+%Y-%m-%d")
113-
sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${NEXT_VERSION} ($date)/" ${CHANGELOG}
104+
- name: Install towncrier
105+
run: pip install towncrier==25.8.0
106+
107+
- name: Generate changelog
108+
run: towncrier build --yes --version "$NEXT_VERSION" --dir "$(dirname $CHANGELOG)"
114109

115110
- name: Use CLA approved github bot
116111
run: .github/scripts/use-cla-approved-github-bot.sh

.github/workflows/package-prepare-release.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ jobs:
5353
exit 1
5454
fi
5555
56-
if ! grep --quiet "^## Unreleased$" $changelog; then
57-
echo the $changelog is missing an \"Unreleased\" section
58-
exit 1
59-
fi
60-
6156
version_dev=$(./scripts/eachdist.py version --package ${{ inputs.package }})
6257
6358
if [[ ! $version_dev =~ ^([0-9]+)\.([0-9]+)[\.|b]{1}([0-9]+).*.dev$ ]]; then
@@ -134,10 +129,11 @@ jobs:
134129
- name: run tox
135130
run: tox -e generate
136131

137-
- name: Update the change log with the approximate release date
138-
run: |
139-
date=$(date "+%Y-%m-%d")
140-
sed -Ei "s/^## Unreleased$/## Version ${VERSION} ($date)/" ${CHANGELOG}
132+
- name: Install towncrier
133+
run: pip install towncrier==25.8.0
134+
135+
- name: Generate changelog
136+
run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)"
141137

142138
- name: Use CLA approved github bot
143139
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -194,11 +190,11 @@ jobs:
194190
- name: run tox
195191
run: tox -e generate
196192

197-
- name: Update the change log on main
198-
run: |
199-
# the actual release date on main will be updated at the end of the release workflow
200-
date=$(date "+%Y-%m-%d")
201-
sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${VERSION} ($date)/" ${CHANGELOG}
193+
- name: Install towncrier
194+
run: pip install towncrier==25.8.0
195+
196+
- name: Generate changelog
197+
run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)"
202198

203199
- name: Use CLA approved github bot
204200
run: .github/scripts/use-cla-approved-github-bot.sh

.github/workflows/prepare-patch-release.yml

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ jobs:
2020
exit 1
2121
fi
2222
23-
if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then
24-
echo the change log is missing an \"Unreleased\" section
25-
exit 1
26-
fi
27-
2823
- name: Set environment variables
2924
run: |
3025
stable_version=$(./scripts/eachdist.py version --mode stable)
@@ -68,10 +63,11 @@ jobs:
6863
- name: run tox
6964
run: tox -e generate
7065

71-
- name: Update the change log with the approximate release date
72-
run: |
73-
date=$(date "+%Y-%m-%d")
74-
sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
66+
- name: Install towncrier
67+
run: pip install towncrier==25.8.0
68+
69+
- name: Generate changelog
70+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
7571

7672
- name: Use CLA approved github bot
7773
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -105,3 +101,31 @@ jobs:
105101
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
106102
run: |
107103
gh pr edit ${{ steps.create_pr.outputs.pr_url }} --add-label "prepare-release"
104+
105+
- uses: actions/checkout@v4
106+
with:
107+
ref: main
108+
109+
- name: Use CLA approved github bot
110+
run: .github/scripts/use-cla-approved-github-bot.sh
111+
112+
- name: Backport patch release changelog to main
113+
env:
114+
GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
115+
run: |
116+
release_branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}"
117+
message="Backport ${STABLE_VERSION}/${UNSTABLE_VERSION} changelog"
118+
body="Backport \`${STABLE_VERSION}/${UNSTABLE_VERSION}\` changelog"
119+
branch="otelbot/backport-changelog-from-${STABLE_VERSION}-${UNSTABLE_VERSION}"
120+
121+
git fetch origin $release_branch
122+
123+
# Copy the updated CHANGELOG.md from the release branch
124+
git checkout $release_branch -- CHANGELOG.md
125+
git commit -m "$message"
126+
127+
git push origin HEAD:$branch
128+
gh pr create --title "$message" \
129+
--body "$body" \
130+
--head $branch \
131+
--base main

.github/workflows/prepare-release-branch.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ jobs:
2424
exit 1
2525
fi
2626
27-
if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then
28-
echo the change log is missing an \"Unreleased\" section
29-
exit 1
30-
fi
31-
3227
if [[ ! -z $PRERELEASE_VERSION ]]; then
3328
stable_version=$(./scripts/eachdist.py version --mode stable)
3429
stable_version=${stable_version//.dev/}
@@ -91,10 +86,11 @@ jobs:
9186
- name: run tox
9287
run: tox -e generate
9388

94-
- name: Update the change log with the approximate release date
95-
run: |
96-
date=$(date "+%Y-%m-%d")
97-
sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
89+
- name: Install towncrier
90+
run: pip install towncrier==25.8.0
91+
92+
- name: Generate changelog
93+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
9894

9995
- name: Use CLA approved github bot
10096
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -193,11 +189,11 @@ jobs:
193189
- name: run tox
194190
run: tox -e generate
195191

196-
- name: Update the change log on main
197-
run: |
198-
# the actual release date on main will be updated at the end of the release workflow
199-
date=$(date "+%Y-%m-%d")
200-
sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
192+
- name: Install towncrier
193+
run: pip install towncrier==25.8.0
194+
195+
- name: Generate changelog
196+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
201197

202198
- name: Use CLA approved github bot
203199
run: .github/scripts/use-cla-approved-github-bot.sh

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
<!--
6+
Do *NOT* add changelog entries here!
7+
8+
This changelog is managed by towncrier and is compiled at release time.
9+
10+
See https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md#changelog for details.
11+
-->
12+
513
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
614
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
715

816
> [!NOTE]
917
> The following components are released independently and maintain individual CHANGELOG files.
1018
> Use [this search for a list of all CHANGELOG.md files in this repo](https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python-contrib+path%3A**%2FCHANGELOG.md&type=code).
1119
20+
<!-- changelog start -->
21+
1222
## Unreleased
1323

1424
### Added

CONTRIBUTING.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,46 @@ A PR is considered to be **ready to merge** when:
247247
reasonable time to review.
248248
* Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day.
249249
* Urgent fix can take exception as long as it has been actively communicated.
250-
* A changelog entry is added to the corresponding changelog for the code base, if there is any impact on behavior. e.g. doc entries are not required, but small bug entries are.
250+
* A changelog fragment is added (see [Changelog](#changelog) below), if there is any impact on behavior. e.g. doc entries are not required, but small bug entries are.
251251

252252
Any Approver / Maintainer can merge the PR once it is **ready to merge**.
253253

254+
### Changelog
255+
256+
This project uses [towncrier](https://towncrier.readthedocs.io/) to manage changelogs. Instead of editing `CHANGELOG.md` directly, each PR should include a changelog fragment file.
257+
258+
**Where to add fragments:**
259+
260+
- **Coordinated packages** (most instrumentations, exporters, propagators): add to the root `.changelog/` directory
261+
- **Independently released packages** (`opentelemetry-opamp-client`, `opentelemetry-propagator-aws-xray`, `opentelemetry-resource-detector-azure`, `opentelemetry-sdk-extension-aws`): add to `<package>/.changelog/`
262+
263+
**Creating a changelog fragment:**
264+
265+
Create a file named `.changelog/<PR_NUMBER>.<TYPE>` where `TYPE` is one of: `added`, `changed`, `deprecated`, `removed`, `fixed`.
266+
267+
The file should contain a one-line description of the change. For example, `.changelog/1234.fixed`:
268+
269+
```
270+
`opentelemetry-instrumentation-flask`: fix request hook not being called for websocket connections
271+
```
272+
273+
**Writing a good changelog entry:**
274+
275+
- Write in imperative tone, as if completing the phrase "This change will..."
276+
- Keep entries concise — ideally under 80 characters
277+
- Prefix with the affected package name (e.g. `` `opentelemetry-instrumentation-flask`: ... ``)
278+
- Don't include the PR number — towncrier adds it automatically
279+
280+
**Preview the changelog:**
281+
282+
```console
283+
towncrier build --draft --version Unreleased
284+
```
285+
286+
The CI will verify that a changelog fragment exists and that `CHANGELOG.md` files are not directly modified.
287+
288+
If your change does not need a changelog entry, add the "Skip Changelog" label to the PR.
289+
254290
### Stale PRs
255291

256292
PRs with no activity for 14 days will be automatically marked as stale and closed after a further 14 days of inactivity. To prevent a PR from being marked stale, ensure there is regular activity (commits, comments, reviews, etc).

RELEASING.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ To keep the process lightweight, it's OK to approve the PRs you generate and mer
4646

4747
### Backporting
4848

49-
Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI. Even if there where
50-
no changes on a repo the patch release preparation workflow requires an empty `## Unreleased` header in `CHANGELOG.md`.
51-
52-
Backport of pull request(s) can be automated by a workflow only if there where no changes that will create conflicts in the release
53-
branch, unfortunately every `CHANGELOG.md` change will create one.
49+
Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI.
5450

5551
To use the workflow to backport pull request(s) to the release branch:
5652
* Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/backport.yml).
@@ -60,8 +56,6 @@ To use the workflow to backport pull request(s) to the release branch:
6056
* Add the label `backport` to the generated pull request.
6157
* In case label automation doesn't work, just close and reopen the PR so that the workflow will take into account the label automation we have in place.
6258
* Review and merge the backport pull request that it generates.
63-
* Merge a pull request to the release branch updating the `CHANGELOG.md`.
64-
* The heading for the unreleased entries should be `## Unreleased`.
6559

6660
### Preparing a patch release
6761

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

0 commit comments

Comments
 (0)