From aa216433c7ccf8250c342fed5bada2d7c490da7d Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:25:18 +0400 Subject: [PATCH 1/2] test(parser-emoji): cover rendered changelog labels Cover opt-in descriptive emoji headings, tag-free summaries, and parsed scopes. Implements: #1409 Signed-off-by: Floze <88098863+floze-the-genius@users.noreply.github.com> --- .../commit_parser/test_emoji.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/unit/semantic_release/commit_parser/test_emoji.py b/tests/unit/semantic_release/commit_parser/test_emoji.py index 4e2dcd597..dee1ceee6 100644 --- a/tests/unit/semantic_release/commit_parser/test_emoji.py +++ b/tests/unit/semantic_release/commit_parser/test_emoji.py @@ -97,6 +97,33 @@ def test_default_emoji_parser( assert breaking_descriptions == result.breaking_descriptions +@pytest.mark.parametrize( + "message, category, scope, description", + [ + (":bug: correct some text", "🐛 Bug Fixes", "", "correct some text"), + ( + ":sparkles:(cli): add a new option", + "✨ Features", + "cli", + "add a new option", + ), + ], +) +def test_emoji_parser_render_emoji_for_changelog( + message: str, + category: str, + scope: str, + description: str, +): + parser = EmojiCommitParser(EmojiParserOptions(render_emoji=True)) + + result = parser.parse_message(message) + + assert category == result.category + assert scope == result.scope + assert (description,) == result.descriptions + + @pytest.mark.parametrize( "message, subject, merge_request_number", [ From 6c97882e20afef261ce60e249e631b5a166b3c6c Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:30:09 +0400 Subject: [PATCH 2/2] feat(parser-emoji): render descriptive changelog labels Map built-in shortcode tags to descriptive Unicode changelog section labels. Enable this mapping only through the opt-in parser setting. Remove the classified tag from commit summaries in that mode. Keep custom tags and default output unchanged. Implements: #1409 Signed-off-by: Floze <88098863+floze-the-genius@users.noreply.github.com> --- docs/concepts/commit_parsing.rst | 4 ++ src/semantic_release/commit_parser/emoji.py | 53 +++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/concepts/commit_parsing.rst b/docs/concepts/commit_parsing.rst index 296169c52..b20befea8 100644 --- a/docs/concepts/commit_parsing.rst +++ b/docs/concepts/commit_parsing.rst @@ -337,6 +337,10 @@ how PSR's core features: :ref:`changelog.exclude_commit_patterns ` to ignore those commit styles. + Set ``commit_parser_options.render_emoji`` to ``true`` to render the default shortcode + tags as descriptive Unicode headings, such as ``🐛 Bug Fixes``, and remove the leading + tag from each commit description. + - **Pull/Merge Request Identifier Detection**: This parser implements PSR's :ref:`commit_parser-builtin-linked_merge_request_detection` to identify and extract pull/merge request numbers. The parser will return a string value if a pull/merge diff --git a/src/semantic_release/commit_parser/emoji.py b/src/semantic_release/commit_parser/emoji.py index e2fb5ae30..27a868c30 100644 --- a/src/semantic_release/commit_parser/emoji.py +++ b/src/semantic_release/commit_parser/emoji.py @@ -29,6 +29,34 @@ from semantic_release.globals import logger from semantic_release.helpers import sort_numerically, text_reducer +DEFAULT_EMOJI_CHANGELOG_SECTIONS = { + ":boom:": "💥 Breaking Changes", + ":sparkles:": "✨ Features", + ":children_crossing:": "🚸 User Experience", + ":lipstick:": "💄 UI and Style", + ":iphone:": "📱 Responsive Design", + ":egg:": "🥚 Easter Eggs", + ":chart_with_upwards_trend:": "📈 Analytics", + ":ambulance:": "🚑 Critical Hotfixes", + ":lock:": "🔒 Security", + ":bug:": "🐛 Bug Fixes", + ":zap:": "⚡ Performance", + ":goal_net:": "🥅 Error Handling", + ":alien:": "👽 External API Changes", + ":wheelchair:": "♿ Accessibility", + ":speech_balloon:": "💬 Text and Literals", + ":mag:": "🔍 SEO", + ":apple:": "🍎 macOS", + ":penguin:": "🐧 Linux", + ":checkered_flag:": "🏁 Windows", + ":robot:": "🤖 Android", + ":green_apple:": "🍏 iOS", + ":checkmark:": "✅ Tests", + ":construction_worker:": "👷 Continuous Integration", + ":memo:": "📝 Documentation", + ":recycle:": "♻️ Refactoring", +} + @dataclass class EmojiParserOptions(ParserOptions): @@ -103,6 +131,12 @@ class EmojiParserOptions(ParserOptions): ignore_merge_commits: bool = True """Toggle flag for whether or not to ignore merge commits""" + render_emoji: bool = False + """ + Render default emoji tags as descriptive Unicode changelog section headings + and remove the leading tag from commit descriptions. + """ + @property def tag_to_level(self) -> dict[str, LevelBump]: """A mapping of commit tags to the level bump they should result in.""" @@ -134,8 +168,9 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]): If the message does not contain any known emojis, then the level to bump will be 0 and the type of change "Other". This parser never raises UnknownCommitMessageStyleError. - Emojis are not removed from the description, and will appear alongside - the commit subject in the changelog. + By default, emojis are not removed from the description and will appear + alongside the commit subject in the changelog. Set ``render_emoji`` to + render descriptive Unicode headings and omit the leading tag. """ # TODO: Deprecate in lieu of get_default_options() @@ -289,7 +324,17 @@ def parse_message(self, message: str) -> ParsedMessageResult: primary_emoji, self.options.default_bump_level ) - # All emojis will remain part of the returned description + category = primary_emoji + if ( + self.options.render_emoji + and match + and ( + rendered_category := DEFAULT_EMOJI_CHANGELOG_SECTIONS.get(primary_emoji) + ) + ): + category = rendered_category + subject = subject[match.end() :].lstrip() + body_components: dict[str, list[str]] = reduce( self.commit_body_components_separator, [ @@ -308,7 +353,7 @@ def parse_message(self, message: str) -> ParsedMessageResult: return ParsedMessageResult( bump=level_bump, type=primary_emoji, - category=primary_emoji, + category=category, scope=parsed_scope, descriptions=( descriptions[:1] if level_bump is LevelBump.MAJOR else descriptions