feat: Add @codebot changelog command for Keep a Changelog format generation
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 41s

Implements PR changelog generator that analyzes diffs and generates
Keep a Changelog format entries ready for CHANGELOG.md.

Features:
- Generates structured changelog entries (Added/Changed/Fixed/etc.)
- Automatically detects breaking changes
- Includes technical details (files, LOC, components)
- User-focused language filtering out noise
- Ready to copy-paste into CHANGELOG.md

Implementation:
- Added changelog.md prompt template with Keep a Changelog format
- Implemented _handle_changelog_command() in PRAgent
- Added _format_changelog() for markdown formatting
- Updated PRAgent.can_handle() to route changelog commands
- Added 'changelog' to config.yml commands list

Workflow Safety (prevents duplicate runs):
- Added '@codebot changelog' to ai-comment-reply.yml conditions
- Excluded from ai-chat.yml to prevent duplication
- Only triggers on PR comments (not issues)
- Manual command only (no automatic triggering)

Testing:
- 9 comprehensive tests in TestChangelogGeneration class
- Tests command detection, formatting, config validation
- Verifies prompt formatting and Keep a Changelog structure

Documentation:
- Updated README.md with changelog command and examples
- Added detailed implementation guide in CLAUDE.md
- Included example output and use cases

Related: Milestone 2 feature - PR changelog generation for release notes
This commit is contained in:
2025-12-29 10:52:48 +00:00
parent c6dbb0acf6
commit 15beb0fb5b
8 changed files with 628 additions and 3 deletions

View File

@@ -99,7 +99,8 @@ class PRAgent(BaseAgent):
f"{mention_prefix} review-again" in comment_body.lower()
)
has_summarize = f"{mention_prefix} summarize" in comment_body.lower()
return is_pr and (has_review_again or has_summarize)
has_changelog = f"{mention_prefix} changelog" in comment_body.lower()
return is_pr and (has_review_again or has_summarize or has_changelog)
return False
@@ -113,6 +114,8 @@ class PRAgent(BaseAgent):
)
if f"{mention_prefix} summarize" in comment_body.lower():
return self._handle_summarize_command(context)
elif f"{mention_prefix} changelog" in comment_body.lower():
return self._handle_changelog_command(context)
elif f"{mention_prefix} review-again" in comment_body.lower():
return self._handle_review_again(context)
@@ -1021,3 +1024,190 @@ class PRAgent(BaseAgent):
message=f"Failed to generate PR summary for PR #{pr_number}",
error="Summary generation failed",
)
def _handle_changelog_command(self, context: AgentContext) -> AgentResult:
"""Handle @codebot changelog command from PR comments.
Generates Keep a Changelog format entries for the PR.
Args:
context: Agent context with event data
Returns:
AgentResult with success status and actions taken
"""
issue = context.event_data.get("issue", {})
pr_number = issue.get("number")
comment_author = (
context.event_data.get("comment", {}).get("user", {}).get("login", "user")
)
self.logger.info(f"Generating changelog for PR #{pr_number} at user request")
try:
# Get PR data
pr = self.gitea.get_pull_request(context.owner, context.repo, pr_number)
pr_title = pr.get("title", "")
pr_description = pr.get("body", "")
# Get PR diff
diff = self._get_diff(context.owner, context.repo, pr_number)
if not diff.strip():
error_msg = (
f"@{comment_author}\n\n"
f"{self.AI_DISCLAIMER}\n\n"
"**⚠️ Changelog Generation Failed**\n\n"
"No changes found in this PR to analyze."
)
self.gitea.create_issue_comment(
context.owner, context.repo, pr_number, error_msg
)
return AgentResult(
success=False,
message=f"No diff to generate changelog for PR #{pr_number}",
)
# Load changelog prompt
prompt_template = self.load_prompt("changelog")
prompt = prompt_template.format(
pr_title=pr_title,
pr_description=pr_description or "(No description provided)",
)
prompt = f"{prompt}\n{diff}"
# Call LLM to generate changelog
result = self.call_llm_json(prompt)
# Format the changelog comment
changelog_comment = self._format_changelog(result, pr_number)
# Post changelog comment
self.gitea.create_issue_comment(
context.owner, context.repo, pr_number, changelog_comment
)
return AgentResult(
success=True,
message=f"Generated changelog for PR #{pr_number}",
actions_taken=["Posted changelog comment"],
)
except Exception as e:
self.logger.error(f"Failed to generate changelog: {e}")
# Post error message
error_msg = (
f"@{comment_author}\n\n"
f"{self.AI_DISCLAIMER}\n\n"
"**⚠️ Changelog Generation Failed**\n\n"
f"I encountered an error while generating the changelog: {str(e)}\n\n"
"This could be due to:\n"
"- The PR is too large to analyze\n"
"- The LLM service is temporarily unavailable\n"
"- An unexpected error occurred"
)
self.gitea.create_issue_comment(
context.owner, context.repo, pr_number, error_msg
)
return AgentResult(
success=False,
message=f"Failed to generate changelog for PR #{pr_number}",
error=str(e),
)
def _format_changelog(self, changelog_data: dict, pr_number: int) -> str:
"""Format changelog data into Keep a Changelog format.
Args:
changelog_data: JSON data from LLM containing changelog entries
pr_number: PR number for reference
Returns:
Formatted markdown changelog
"""
lines = [
self.AI_DISCLAIMER,
"",
f"## 📋 Changelog for PR #{pr_number}",
"",
]
changelog = changelog_data.get("changelog", {})
# Added
added = changelog.get("added", [])
if added:
lines.append("### ✨ Added")
for item in added:
lines.append(f"- {item}")
lines.append("")
# Changed
changed = changelog.get("changed", [])
if changed:
lines.append("### 🔄 Changed")
for item in changed:
lines.append(f"- {item}")
lines.append("")
# Deprecated
deprecated = changelog.get("deprecated", [])
if deprecated:
lines.append("### ⚠️ Deprecated")
for item in deprecated:
lines.append(f"- {item}")
lines.append("")
# Removed
removed = changelog.get("removed", [])
if removed:
lines.append("### 🗑️ Removed")
for item in removed:
lines.append(f"- {item}")
lines.append("")
# Fixed
fixed = changelog.get("fixed", [])
if fixed:
lines.append("### 🐛 Fixed")
for item in fixed:
lines.append(f"- {item}")
lines.append("")
# Security
security = changelog.get("security", [])
if security:
lines.append("### 🔒 Security")
for item in security:
lines.append(f"- {item}")
lines.append("")
# Breaking changes
breaking = changelog_data.get("breaking_changes", [])
if breaking:
lines.append("---")
lines.append("")
lines.append("### ⚠️ BREAKING CHANGES")
for item in breaking:
lines.append(f"- **{item}**")
lines.append("")
# Technical details
tech = changelog_data.get("technical_details", {})
if tech:
lines.append("---")
lines.append("")
lines.append("### 📊 Technical Details")
files = tech.get("files_changed", 0)
additions = tech.get("insertions", 0)
deletions = tech.get("deletions", 0)
lines.append(f"- **Files changed:** {files}")
lines.append(f"- **Lines:** +{additions} / -{deletions}")
components = tech.get("main_components", [])
if components:
lines.append(f"- **Main components:** {', '.join(components)}")
return "\n".join(lines)