Skip to content

Commit d36fcd3

Browse files
sergeykadSergeyclaude
authored
docs: Redesign changelog for end-user readability (#434)
* docs: Redesign changelog for end-user readability - Add custom Jinja2 template with two-tier structure: - User-facing section: Added, Fixed, Changed (Keep a Changelog format) - Collapsible "Internal Changes" section for CI, deps, refactoring - Add (internal) scope convention to AGENTS.md for filtering - Exclude changelog sync commits from output - Strip duplicate PR references from commit descriptions - Consistent category naming between sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Address PR review feedback - Move exclude_commit_patterns to [tool.semantic_release.changelog] - Fix case-sensitivity in sync changelog pattern - Change PR reference stripping from +3 to +2 (use trim for spacing) - Remove refactor from internal_types to align with AGENTS.md - Add refactor to get_category mapping (maps to Changed) - Add catch-all loops for non-standard categories in both sections - Add chores/testing to internal_types for plural forms Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Sergey <sergey@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b2d0b31 commit d36fcd3

3 files changed

Lines changed: 186 additions & 11 deletions

File tree

AGENTS.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -531,12 +531,20 @@ await mcp.call_tool("ha_config_get_script", {"script_id": "nonexistent"})
531531

532532
Uses [semantic-release](https://python-semantic-release.readthedocs.io/) with conventional commits.
533533

534-
| Prefix | Bump |
535-
|--------|------|
536-
| `fix:`, `perf:`, `refactor:` | Patch |
537-
| `feat:` | Minor |
538-
| `feat!:` or `BREAKING CHANGE:` | Major |
539-
| `chore:`, `docs:`, `test:` | No release |
534+
| Prefix | Bump | Changelog |
535+
|--------|------|-----------|
536+
| `fix:`, `perf:`, `refactor:` | Patch | User-facing |
537+
| `feat:` | Minor | User-facing |
538+
| `feat!:` or `BREAKING CHANGE:` | Major | User-facing |
539+
| `chore:`, `ci:`, `test:` | No release | Internal |
540+
| `docs:` | No release | User-facing |
541+
| `*:(internal)` | Same as type | Internal |
542+
543+
**Use `(internal)` scope** for changes that aren't user-facing:
544+
```bash
545+
feat(internal): Log package version on startup # Internal, not in user changelog
546+
feat: Add dark mode # User-facing
547+
```
540548

541549
| Channel | When Updated |
542550
|---------|--------------|

pyproject.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,16 @@ remove_dist = false
195195
commit_parser = "angular"
196196
commit_parser_options = { major_tags = ["BREAKING", "!"], minor_tags = ["feat"], patch_tags = ["fix", "perf", "refactor"] }
197197

198-
# Exclude commits that only touch the static site
198+
# Changelog configuration
199+
[tool.semantic_release.changelog]
200+
template_dir = "templates"
199201
exclude_commit_patterns = [
200-
"^.*\\(site\\).*$", # Exclude commits with (site) scope
202+
"^.*\\(site\\).*$", # Commits with (site) scope
203+
'''chore\(addon\): [Ss]ync changelog.*''', # Changelog sync commits
201204
]
202205

203-
# Changelog configuration
204-
changelog_sections = "feature,fix,breaking,documentation,performance"
205-
changelog_components = "feat,fix,docs,style,refactor,perf,test,chore"
206+
[tool.semantic_release.changelog.default_templates]
207+
changelog_file = "CHANGELOG.md"
206208

207209
# Version scheme
208210
major_on_zero = true

templates/CHANGELOG.md.j2

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{#
2+
Changelog template with two-tier structure:
3+
- User-facing changes (Added, Fixed, Changed, etc.) at top
4+
- Internal changes in collapsible section
5+
#}
6+
{%- set insertion_flag = ctx.changelog_insertion_flag -%}
7+
{%- set releases = ctx.history.released.values() | list -%}
8+
9+
{#- Capitalize first letter only -#}
10+
{%- macro cap_first(text) -%}
11+
{{ text[0] | upper }}{{ text[1:] }}
12+
{%- endmacro -%}
13+
14+
{#- Macro to format commit with links -#}
15+
{%- macro format_commit(commit) -%}
16+
{%- if commit.error is undefined -%}
17+
{#- Strip trailing PR reference like "(#123)" since we add our own link -#}
18+
{%- set raw_summary = commit.descriptions[0] -%}
19+
{%- if commit.linked_merge_request and raw_summary.endswith('(' ~ commit.linked_merge_request ~ ')') -%}
20+
{%- set raw_summary = raw_summary[:-( commit.linked_merge_request | length + 2)] | trim -%}
21+
{%- endif -%}
22+
{%- set summary = cap_first(raw_summary) -%}
23+
{%- if commit.scope and commit.scope != 'internal' -%}
24+
**{{ commit.scope }}**: {{ summary }}
25+
{%- else -%}
26+
{{ summary }}
27+
{%- endif -%}
28+
{%- if commit.linked_merge_request %}
29+
([{{ commit.linked_merge_request }}](https://github.com/{{ ctx.repo_owner }}/{{ ctx.repo_name }}/pull/{{ commit.linked_merge_request | replace('#', '') }}))
30+
{%- else %}
31+
([`{{ commit.short_hash }}`](https://github.com/{{ ctx.repo_owner }}/{{ ctx.repo_name }}/commit/{{ commit.hexsha }}))
32+
{%- endif -%}
33+
{%- endif -%}
34+
{%- endmacro -%}
35+
36+
{#- Check if commit is internal -#}
37+
{%- macro is_internal(commit, type_) -%}
38+
{%- set internal_scopes = ['internal', 'debug', 'addon-dev', 'ci', 'deps', 'deps-dev'] -%}
39+
{%- set internal_types = ['chore', 'chores', 'ci', 'test', 'testing', 'build', 'style', 'continuous integration'] -%}
40+
{%- if commit.scope and commit.scope | lower in internal_scopes -%}
41+
true
42+
{%- elif commit.scope and 'internal' in commit.scope | lower -%}
43+
true
44+
{%- elif type_ | lower in internal_types -%}
45+
true
46+
{%- else -%}
47+
false
48+
{%- endif -%}
49+
{%- endmacro -%}
50+
51+
{#- Map type to Keep a Changelog category -#}
52+
{%- macro get_category(type_) -%}
53+
{%- set t = type_ | lower -%}
54+
{%- if t in ['feature', 'feat', 'features'] -%}
55+
Added
56+
{%- elif t in ['fix', 'bug fixes'] -%}
57+
Fixed
58+
{%- elif t in ['perf', 'performance', 'refactor'] -%}
59+
Changed
60+
{%- elif t in ['docs', 'documentation'] -%}
61+
Changed
62+
{%- elif t == 'breaking' -%}
63+
Breaking Changes
64+
{%- elif t == 'security' -%}
65+
Security
66+
{%- elif t in ['deprecate', 'deprecated'] -%}
67+
Deprecated
68+
{%- elif t in ['remove', 'removed'] -%}
69+
Removed
70+
{%- else -%}
71+
{{ type_ | title }}
72+
{%- endif -%}
73+
{%- endmacro -%}
74+
75+
{#- Render a single release -#}
76+
{%- macro render_release(release) -%}
77+
## {{ release.version.as_semver_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }})
78+
79+
{%- set user_facing = {} -%}
80+
{%- set internal = {} -%}
81+
82+
{#- Separate user-facing and internal commits -#}
83+
{%- for type_, commits in release.elements.items() -%}
84+
{%- for commit in commits -%}
85+
{%- if commit.error is undefined -%}
86+
{%- if is_internal(commit, type_) | trim == 'true' -%}
87+
{%- set _ = internal.setdefault(type_, []).append(commit) -%}
88+
{%- else -%}
89+
{%- set cat = get_category(type_) | trim -%}
90+
{%- set _ = user_facing.setdefault(cat, []).append(commit) -%}
91+
{%- endif -%}
92+
{%- endif -%}
93+
{%- endfor -%}
94+
{%- endfor -%}
95+
96+
{#- Standard Keep a Changelog categories -#}
97+
{%- set standard_categories = ['Breaking Changes', 'Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'] -%}
98+
99+
{#- Render user-facing changes in Keep a Changelog order -#}
100+
{%- for category in standard_categories -%}
101+
{%- if category in user_facing and user_facing[category] | length > 0 %}
102+
103+
### {{ category }}
104+
{% for commit in user_facing[category] %}
105+
- {{ format_commit(commit) }}
106+
{%- endfor %}
107+
{%- endif -%}
108+
{%- endfor %}
109+
{#- Render any remaining categories not in standard list -#}
110+
{%- for category in user_facing.keys() | sort -%}
111+
{%- if category not in standard_categories and user_facing[category] | length > 0 %}
112+
113+
### {{ category }}
114+
{% for commit in user_facing[category] %}
115+
- {{ format_commit(commit) }}
116+
{%- endfor %}
117+
{%- endif -%}
118+
{%- endfor %}
119+
120+
{#- Render internal changes in collapsible section -#}
121+
{%- set has_internal = internal.keys() | list | length > 0 -%}
122+
{%- if has_internal %}
123+
{#- Regroup internal by category for consistent naming -#}
124+
{%- set internal_by_cat = {} -%}
125+
{%- for type_, commits in internal.items() -%}
126+
{%- set cat = get_category(type_) | trim -%}
127+
{%- for commit in commits -%}
128+
{%- set _ = internal_by_cat.setdefault(cat, []).append(commit) -%}
129+
{%- endfor -%}
130+
{%- endfor %}
131+
132+
---
133+
<details>
134+
<summary>Internal Changes</summary>
135+
{% for category in standard_categories -%}
136+
{%- if category in internal_by_cat and internal_by_cat[category] | length > 0 %}
137+
138+
### {{ category }}
139+
{% for commit in internal_by_cat[category] %}
140+
- {{ format_commit(commit) }}
141+
{%- endfor %}
142+
{%- endif -%}
143+
{%- endfor %}
144+
{#- Render any remaining internal categories not in standard list -#}
145+
{%- for category in internal_by_cat.keys() | sort -%}
146+
{%- if category not in standard_categories and internal_by_cat[category] | length > 0 %}
147+
148+
### {{ category }}
149+
{% for commit in internal_by_cat[category] %}
150+
- {{ format_commit(commit) }}
151+
{%- endfor %}
152+
{%- endif -%}
153+
{%- endfor %}
154+
</details>
155+
{%- endif %}
156+
{%- endmacro -%}
157+
158+
{#- Main template -#}
159+
# CHANGELOG
160+
161+
{{ insertion_flag }}
162+
{% for release in releases %}
163+
164+
{{ render_release(release) }}
165+
{% endfor %}

0 commit comments

Comments
 (0)