From 59805417a17193f5ce0b81807830048434d1df98 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Tue, 26 May 2026 19:29:26 -0300 Subject: [PATCH] [fix] Triggered changelog bot for backward incompatible changes Accept [change!] entries in the changelog bot trigger and validation so backward incompatible PRs receive generated changelog suggestions. --- .../generate_changelog.py | 11 +++-- .../test_generate_changelog.py | 48 +++++++++++++++++++ .github/workflows/bot-changelog-trigger.yml | 2 +- docs/developer/reusable-github-utils.rst | 10 ++-- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.github/actions/bot-changelog-generator/generate_changelog.py b/.github/actions/bot-changelog-generator/generate_changelog.py index 180a9e9c..43a7804b 100644 --- a/.github/actions/bot-changelog-generator/generate_changelog.py +++ b/.github/actions/bot-changelog-generator/generate_changelog.py @@ -10,6 +10,7 @@ - [feature] for new features - [fix] for bug fixes - [change] for changes +- [change!] for backward incompatible changes Usage: @@ -365,7 +366,8 @@ def build_prompt( "If any rule below is broken, the output is invalid.\n" "RULES YOU MUST FOLLOW:\n" "- Output exactly one plain-text git commit message\n" - "- Start the first line with exactly one tag: [feature], [fix], or [change]\n" + "- Start the first line with exactly one tag: [feature], [fix], " + "[change], or [change!]\n" f"- Keep the first line within {COMMIT_SUBJECT_LIMIT} characters, " "including the tag and spaces\n" "- Capitalize the first word after the tag\n" @@ -399,6 +401,7 @@ def build_prompt( "- [feature] - New functionality\n" "- [fix] - Bug fixes\n" "- [change] - Non-breaking changes, refactors, updates\n" + "- [change!] - Backward incompatible changes\n" "Length: Provide enough body detail to help " "a maintainer reuse the output as a high-quality squash merge commit " "message.\n" @@ -509,7 +512,7 @@ def get_commit_message_validation_errors(text: str) -> list[str]: def get_changelog_bot_validation_errors(text: str) -> list[str]: """Validate bot-specific safety and formatting requirements.""" errors = [] - required_tags = ("[feature]", "[fix]", "[change]") + required_tags = ("[feature]", "[fix]", "[change]", "[change!]") suspicious_patterns = [ r"ignore\s+previous\s+instructions", r"ignore_[a-z_]*instructions", @@ -520,7 +523,9 @@ def get_changelog_bot_validation_errors(text: str) -> list[str]: ] if not any(text.startswith(tag) for tag in required_tags): - errors.append("Commit message must start with [feature], [fix], or [change].") + errors.append( + "Commit message must start with [feature], [fix], [change], or [change!]." + ) if "```" in text: errors.append("Commit message must not contain fenced code blocks.") if CHANGELOG_COMMENT_INTRO.lower() in text.lower(): diff --git a/.github/actions/bot-changelog-generator/test_generate_changelog.py b/.github/actions/bot-changelog-generator/test_generate_changelog.py index 776bbb97..f56573fe 100644 --- a/.github/actions/bot-changelog-generator/test_generate_changelog.py +++ b/.github/actions/bot-changelog-generator/test_generate_changelog.py @@ -1,7 +1,9 @@ """Tests for the changelog generator GitHub action.""" import os +import re import sys +from pathlib import Path # Add the directory to path for importing (must be before local imports) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -342,6 +344,7 @@ def test_builds_basic_prompt(self): self.assertIn("[feature]", system_instruction) self.assertIn("[fix]", system_instruction) self.assertIn("[change]", system_instruction) + self.assertIn("[change!]", system_instruction) self.assertIn( f"within {COMMIT_SUBJECT_LIMIT} characters, including the tag and spaces", system_instruction, @@ -615,6 +618,29 @@ def test_valid_feature_tag_rst(self, mock_get_commitizen): self.assertTrue(result) mock_plugin.validate_commit_message.assert_called_once() + @patch("generate_changelog.get_openwisp_commitizen") + def test_valid_backward_incompatible_change_tag(self, mock_get_commitizen): + mock_plugin = MagicMock() + mock_plugin.schema_pattern.return_value = ".*" + mock_plugin.validate_commit_message.return_value = MagicMock( + is_valid=True, errors=[] + ) + mock_get_commitizen.return_value = mock_plugin + + text = ( + "[change!] Removed legacy setting\n\n" + "Drops the deprecated setting so users must update their configuration." + ) + + errors = get_changelog_validation_errors(text, "rst") + + self.assertEqual( + errors, + [], + "[change!] marks backward incompatible changes and must be accepted " + "by the changelog bot validation.", + ) + def test_invalid_no_tag(self): text = "Added new functionality\n\nAdds useful context.\n\nCloses #123" result = validate_changelog_output(text, "rst") @@ -771,5 +797,27 @@ def test_returns_last_attempt_after_max_retries( self.assertEqual(mock_call_gemini.call_count, MAX_GENERATION_ATTEMPTS) +class TestChangelogTriggerWorkflow(unittest.TestCase): + """Tests for the workflow that decides whether the bot should run.""" + + def test_noteworthy_regex_matches_backward_incompatible_changes(self): + workflow_path = ( + Path(__file__).resolve().parents[2] + / "workflows" + / "bot-changelog-trigger.yml" + ) + workflow = workflow_path.read_text(encoding="utf-8") + match = re.search(r"grep -qiE '([^']+)'", workflow) + self.assertIsNotNone(match, "Could not find the noteworthy PR regex.") + noteworthy_regex = match.group(1) + + self.assertRegex( + "[change!] Removed legacy behavior", + noteworthy_regex, + "PRs titled with [change!] mark backward incompatible changes and " + "must trigger the changelog bot.", + ) + + if __name__ == "__main__": unittest.main() diff --git a/.github/workflows/bot-changelog-trigger.yml b/.github/workflows/bot-changelog-trigger.yml index c2ed5f07..4f231ed5 100644 --- a/.github/workflows/bot-changelog-trigger.yml +++ b/.github/workflows/bot-changelog-trigger.yml @@ -18,7 +18,7 @@ jobs: env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - if echo "$PR_TITLE" | grep -qiE '^\[(feature|fix|change)\]'; then + if echo "$PR_TITLE" | grep -qiE '^\[(feature|fix|change!?)\]'; then echo "has_noteworthy=true" >> $GITHUB_OUTPUT fi diff --git a/docs/developer/reusable-github-utils.rst b/docs/developer/reusable-github-utils.rst index 00e6ded4..da41bdcb 100644 --- a/docs/developer/reusable-github-utils.rst +++ b/docs/developer/reusable-github-utils.rst @@ -530,8 +530,9 @@ Changelog Bot This workflow automatically generates changelog entry suggestions for Pull Requests using Google Gemini. It gets triggered when a PR with a title -prefixed with ``[feature]``, ``[fix]``, or ``[change]`` is approved by a -maintainer. It analyzes the PR's title, description, code changes, and +prefixed with ``[feature]``, ``[fix]``, ``[change]``, or ``[change!]`` is +approved by a maintainer. ``[change!]`` marks backward incompatible +changes. The bot analyzes the PR's title, description, code changes, and linked issues, then posts a properly formatted changelog entry as a comment on the PR. @@ -559,7 +560,8 @@ following two workflow files under ``.github/workflows/``. The trigger workflow runs when a PR review is submitted. If the PR is approved by a maintainer and its title starts with ``[feature]``, -``[fix]``, or ``[change]``, it stores the PR number as workflow metadata. +``[fix]``, ``[change]``, or ``[change!]``, it stores the PR number as +workflow metadata. **1. Changelog Bot Trigger** (``.github/workflows/bot-changelog-trigger.yml``) @@ -588,7 +590,7 @@ approved by a maintainer and its title starts with ``[feature]``, env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - if echo "$PR_TITLE" | grep -qiE '^\[(feature|fix|change)\]'; then + if echo "$PR_TITLE" | grep -qiE '^\[(feature|fix|change!?)\]'; then echo "has_noteworthy=true" >> $GITHUB_OUTPUT fi