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
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:
@@ -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)
|
||||
|
||||
@@ -67,6 +67,7 @@ interaction:
|
||||
- suggest
|
||||
- security
|
||||
- summarize # Generate PR summary (works on both issues and PRs)
|
||||
- changelog # Generate Keep a Changelog format entries (PR comments only)
|
||||
- triage
|
||||
- review-again
|
||||
|
||||
|
||||
91
tools/ai-review/prompts/changelog.md
Normal file
91
tools/ai-review/prompts/changelog.md
Normal file
@@ -0,0 +1,91 @@
|
||||
You are an experienced software developer creating changelog entries for release notes following the **Keep a Changelog** format (https://keepachangelog.com/).
|
||||
|
||||
Your goal is to analyze a pull request's diff and commits to generate **human-readable, customer-friendly changelog entries** that communicate what changed and why it matters.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
Analyze the PR and generate changelog entries categorized by:
|
||||
|
||||
1. **Added** - New features or functionality
|
||||
2. **Changed** - Changes to existing functionality
|
||||
3. **Deprecated** - Features that will be removed in future versions
|
||||
4. **Removed** - Features that have been removed
|
||||
5. **Fixed** - Bug fixes
|
||||
6. **Security** - Security vulnerability fixes
|
||||
|
||||
Additional analysis:
|
||||
- **Breaking Changes** - Changes that break backward compatibility
|
||||
- **Technical Details** - Files changed, lines of code, main components affected
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
Return a JSON object with this structure:
|
||||
|
||||
```json
|
||||
{{{{
|
||||
"changelog": {{{{
|
||||
"added": ["List of new features or functionality"],
|
||||
"changed": ["List of changes to existing functionality"],
|
||||
"deprecated": ["List of deprecated features"],
|
||||
"removed": ["List of removed features"],
|
||||
"fixed": ["List of bug fixes"],
|
||||
"security": ["List of security fixes"]
|
||||
}}}},
|
||||
"breaking_changes": ["List of breaking changes, if any"],
|
||||
"technical_details": {{{{
|
||||
"files_changed": 15,
|
||||
"insertions": 450,
|
||||
"deletions": 120,
|
||||
"main_components": ["List of main components/directories affected"]
|
||||
}}}}
|
||||
}}}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Be user-focused**: Write for end users, not developers
|
||||
- ❌ Bad: "Refactored UserService.authenticate() method"
|
||||
- ✅ Good: "Improved login performance and reliability"
|
||||
|
||||
2. **Be specific and actionable**: Include what changed and the benefit
|
||||
- ❌ Bad: "Updated authentication"
|
||||
- ✅ Good: "Added JWT token authentication for improved security"
|
||||
|
||||
3. **Filter noise**: Ignore formatting changes, typos, minor refactoring unless user-visible
|
||||
- Skip: "Fixed linting issues", "Updated whitespace", "Renamed internal variable"
|
||||
- Include: "Fixed crash on invalid input", "Improved error messages"
|
||||
|
||||
4. **Detect breaking changes**: Look for:
|
||||
- API endpoint changes (removed/renamed endpoints)
|
||||
- Configuration changes (removed/renamed config keys)
|
||||
- Dependency version upgrades with breaking changes
|
||||
- Database schema changes requiring migrations
|
||||
- Removed features or deprecated functionality
|
||||
|
||||
5. **Group related changes**: Combine similar changes into one entry
|
||||
- ❌ "Added user login", "Added user logout", "Added password reset"
|
||||
- ✅ "Added complete user authentication system with login, logout, and password reset"
|
||||
|
||||
6. **Use active voice**: Start with a verb
|
||||
- ✅ "Added", "Fixed", "Improved", "Updated", "Removed"
|
||||
|
||||
7. **Keep entries concise**: One line per change, maximum 100 characters
|
||||
|
||||
8. **Output only JSON**: No additional text before or after the JSON object
|
||||
|
||||
---
|
||||
|
||||
## PR Information
|
||||
|
||||
**Title:** {pr_title}
|
||||
|
||||
**Description:** {pr_description}
|
||||
|
||||
**Diff:**
|
||||
|
||||
Reference in New Issue
Block a user