Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/actions/bot-changelog-generator/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [feature] for new features
- [fix] for bug fixes
- [change] for changes
- [change!] for backward incompatible changes


Usage:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand All @@ -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():
Expand Down
48 changes: 48 additions & 0 deletions .github/actions/bot-changelog-generator/test_generate_changelog.py
Original file line number Diff line number Diff line change
@@ -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__)))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion .github/workflows/bot-changelog-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 6 additions & 4 deletions docs/developer/reusable-github-utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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``)
Expand Down Expand Up @@ -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

Expand Down
Loading