Skip to content

Commit 5ccb54f

Browse files
ci: introduce towncrier to generate changelog from fragments (open-telemetry#5187)
1 parent b556aa1 commit 5ccb54f

10 files changed

Lines changed: 192 additions & 49 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: 34 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 the .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,39 @@ 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 should not be directly modified."
36+
echo "Please add a changelog fragment file to the .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: Install towncrier
44+
run: pip install towncrier==25.8.0
45+
46+
- name: Check for changelog fragment
47+
run: |
48+
if ! towncrier check --compare-with origin/${{ github.base_ref }}; then
49+
echo ""
50+
echo "No changelog fragment found for this PR."
51+
echo "Add a file named .changelog/${{ github.event.pull_request.number }}.<type>"
52+
echo "where <type> is one of: added, changed, deprecated, removed, fixed"
53+
echo "See CONTRIBUTING.md for details."
54+
echo ""
55+
echo "Or add the \"Skip Changelog\" label if this job should be skipped."
56+
false
57+
fi
58+
59+
- name: Preview changelog
60+
run: towncrier build --draft --version Unreleased

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,15 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17-
- name: Install toml
18-
run: pip install toml
17+
- name: Install dependencies
18+
run: pip install toml towncrier
1919

2020
- run: |
2121
if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then
2222
echo this workflow should only be run against long-term release branches
2323
exit 1
2424
fi
2525
26-
if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then
27-
echo the change log is missing an \"Unreleased\" section
28-
exit 1
29-
fi
30-
3126
- name: Set environment variables
3227
run: |
3328
stable_version=$(./scripts/eachdist.py version --mode stable)
@@ -62,10 +57,8 @@ jobs:
6257
- name: Update version
6358
run: .github/scripts/update-version-patch.sh $STABLE_VERSION $UNSTABLE_VERSION $STABLE_VERSION_PREV $UNSTABLE_VERSION_PREV
6459

65-
- name: Update the change log with the approximate release date
66-
run: |
67-
date=$(date "+%Y-%m-%d")
68-
sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
60+
- name: Generate changelog
61+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
6962

7063
- name: Use CLA approved github bot
7164
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -99,3 +92,31 @@ jobs:
9992
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10093
run: |
10194
gh pr edit ${{ steps.create_pr.outputs.pr_url }} --add-label "prepare-release"
95+
96+
- uses: actions/checkout@v4
97+
with:
98+
ref: main
99+
100+
- name: Use CLA approved github bot
101+
run: .github/scripts/use-cla-approved-github-bot.sh
102+
103+
- name: Backport patch release changelog to main
104+
env:
105+
GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
106+
run: |
107+
release_branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}"
108+
message="Backport ${STABLE_VERSION}/${UNSTABLE_VERSION} changelog"
109+
body="Backport \`${STABLE_VERSION}/${UNSTABLE_VERSION}\` changelog"
110+
branch="otelbot/backport-changelog-from-${STABLE_VERSION}-${UNSTABLE_VERSION}"
111+
112+
git fetch origin $release_branch
113+
114+
# Copy the updated CHANGELOG.md from the release branch
115+
git checkout $release_branch -- CHANGELOG.md
116+
git commit -m "$message"
117+
118+
git push origin HEAD:$branch
119+
gh pr create --title "$message" \
120+
--body "$body" \
121+
--head $branch \
122+
--base main

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ jobs:
2727
exit 1
2828
fi
2929
30-
if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then
31-
echo the change log is missing an \"Unreleased\" section
32-
exit 1
33-
fi
34-
3530
if [[ ! -z $PRERELEASE_VERSION ]]; then
3631
stable_version=$(./scripts/eachdist.py version --mode stable)
3732
stable_version=${stable_version//.dev/}
@@ -50,8 +45,8 @@ jobs:
5045
steps:
5146
- uses: actions/checkout@v4
5247

53-
- name: Install toml
54-
run: pip install toml
48+
- name: Install dependencies
49+
run: pip install toml towncrier
5550

5651
- name: Create release branch
5752
env:
@@ -88,10 +83,8 @@ jobs:
8883
- name: Update version
8984
run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION
9085

91-
- name: Update the change log with the approximate release date
92-
run: |
93-
date=$(date "+%Y-%m-%d")
94-
sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
86+
- name: Generate changelog
87+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
9588

9689
- name: Use CLA approved github bot
9790
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -135,8 +128,8 @@ jobs:
135128
steps:
136129
- uses: actions/checkout@v4
137130

138-
- name: Install toml
139-
run: pip install toml
131+
- name: Install dependencies
132+
run: pip install toml towncrier
140133

141134
- name: Set environment variables
142135
env:
@@ -184,11 +177,8 @@ jobs:
184177
- name: Update version
185178
run: .github/scripts/update-version.sh $STABLE_NEXT_VERSION $UNSTABLE_NEXT_VERSION
186179

187-
- name: Update the change log on main
188-
run: |
189-
# the actual release date on main will be updated at the end of the release workflow
190-
date=$(date "+%Y-%m-%d")
191-
sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md
180+
- name: Generate changelog
181+
run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION"
192182

193183
- name: Use CLA approved github bot
194184
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,6 +2,14 @@
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/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

@@ -10,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1018
> [!IMPORTANT]
1119
> We are working on stabilizing the Log signal that would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for sdk developers.
1220

21+
<!-- changelog start -->
22+
1323
## Unreleased
1424

1525
- `opentelemetry-sdk`: fix multi-processor `force_flush` skipping remaining processors when one returns `None`

CONTRIBUTING.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ You can run `tox` with the following arguments:
7373
- `tox -e tracecontext` to run integration tests for tracecontext.
7474
- `tox -e precommit` to run all `pre-commit` actions
7575

76+
### Changelog
77+
78+
This project uses [towncrier](https://towncrier.readthedocs.io/) to manage the changelog. Instead of editing `CHANGELOG.md` directly, each PR should include a changelog fragment file in the `.changelog/` directory.
79+
80+
**Creating a changelog fragment:**
81+
82+
Create a file named `.changelog/<PR_NUMBER>.<TYPE>` where `TYPE` is one of: `added`, `changed`, `deprecated`, `removed`, `fixed`.
83+
84+
The file should contain a one-line description of the change. For example, `.changelog/1234.added`:
85+
86+
```
87+
`opentelemetry-sdk`: add support for new feature
88+
```
89+
90+
**Writing a good changelog entry:**
91+
92+
- Write in imperative tone, as if completing the phrase "This change will..."
93+
- Keep entries concise — ideally under 80 characters
94+
- Prefix with the affected package name when applicable (e.g. `` `opentelemetry-sdk`: ... ``)
95+
- Don't include the PR number — towncrier adds it automatically
96+
97+
**Preview the changelog:**
98+
99+
```console
100+
towncrier build --draft --version Unreleased
101+
```
102+
103+
The CI will verify that a changelog fragment exists and that `CHANGELOG.md` is not directly modified.
104+
105+
If your change does not need a changelog entry, add the "Skip Changelog" label to the PR.
106+
76107
`ruff check` and `ruff format` are executed when `tox -e ruff` is run. We strongly recommend you to configure [pre-commit](https://pre-commit.com/) locally to run `ruff` and `rstcheck` automatically before each commit by installing it as git hooks. You just need to [install pre-commit](https://pre-commit.com/#install) in your environment:
77108

78109
```console

RELEASING.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@
1919

2020
### Backporting
2121

22-
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
23-
no changes on a repo the patch release preparation workflow requires an empty `## Unreleased` header in `CHANGELOG.md`.
24-
25-
Backport of pull request(s) can be automated by a workflow only if there where no changes that will create conflicts in the release
26-
branch, unfortunately every `CHANGELOG.md` change will create one.
22+
Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI.
2723

2824
To use the workflow to backport pull request(s) to the release branch:
2925
* Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/backport.yml).
@@ -33,8 +29,6 @@ To use the workflow to backport pull request(s) to the release branch:
3329
* Add the label `backport` to the generated pull request.
3430
* 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.
3531
* Review and merge the backport pull request that it generates.
36-
* Merge a pull request to the release branch updating the `CHANGELOG.md`.
37-
* The heading for the unreleased entries should be `## Unreleased`.
3832

3933
### Preparing a patch release
4034

pyproject.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,46 @@ target-python-version = "3.10"
165165
custom-template-dir = "opentelemetry-sdk/codegen"
166166
additional-imports = "typing.ClassVar,opentelemetry.sdk._configuration._common._additional_properties"
167167

168+
[tool.towncrier]
169+
directory = ".changelog"
170+
filename = "CHANGELOG.md"
171+
start_string = "<!-- changelog start -->\n"
172+
template = "scripts/changelog_template.j2"
173+
issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python/pull/{issue})"
174+
wrap = true # wrap fragments to 79 char line length
175+
issue_pattern = "^(\\d+)" # only PR numbers as fragment prefix (e.g. 1234.fixed)
176+
177+
[[tool.towncrier.type]]
178+
directory = "added"
179+
name = "Added"
180+
showcontent = true
181+
182+
[[tool.towncrier.type]]
183+
directory = "changed"
184+
name = "Changed"
185+
showcontent = true
186+
187+
[[tool.towncrier.type]]
188+
directory = "deprecated"
189+
name = "Deprecated"
190+
showcontent = true
191+
192+
[[tool.towncrier.type]]
193+
directory = "removed"
194+
name = "Removed"
195+
showcontent = true
196+
197+
[[tool.towncrier.type]]
198+
directory = "fixed"
199+
name = "Fixed"
200+
showcontent = true
201+
168202
[dependency-groups]
169203
dev = [
170204
"tox",
171205
"tox-uv>=1",
172206
"pre-commit",
173207
"datamodel-code-generator[http]",
174208
"datamodel-code-generator[ruff]",
209+
"towncrier",
175210
]

scripts/changelog_template.j2

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Version {{ versiondata.version }} ({{ versiondata.date }})
2+
3+
{% for section, _ in sections.items() %}
4+
{%- if section %}{{ section }}{% endif -%}
5+
6+
{% if sections[section] %}
7+
{% for category, val in definitions.items() if category in sections[section] %}
8+
### {{ definitions[category]['name'] }}
9+
10+
{% for text, values in sections[section][category].items() %}
11+
{% if "\n - " in text or '\n * ' in text %}
12+
{%- set main_text, sub_items = text.split('\n', 1) %}
13+
- {{ main_text }} ({{ values|join(', ') }})
14+
{% if sub_items %}
15+
{{- sub_items }}
16+
{% endif %}
17+
{% else %}
18+
- {{ text }} ({{ values|join(', ') }})
19+
{% endif %}
20+
{% endfor %}
21+
22+
{% endfor %}
23+
{% endif %}
24+
{% endfor %}

uv.lock

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