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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Support for developers who refuse to write changelog entries (tough love) (#298) (Nina Gupta)
- Legacy "I forgot the changelog" excuse handling (#445) (Raj Patel)
- Irrelevant multi-line entry that just discusses elephants and other completely
unrelated topics to the actual change in the PR. Let's see how Claude handles
this then (#111) (Jon Blow, Jane Snow)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was converted to logchange format. Let's remove it.

Suggested change
this then (#111) (Jon Blow, Jane Snow)


### Security
- Added detection for when developers try to hide breaking changes in the changelog (#567) (Lisa Wong)
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Multi-stage build for optimal image size
FROM python:3.11-slim AS builder
FROM python:3-slim AS builder

WORKDIR /build

Expand All @@ -8,7 +8,7 @@ COPY action/src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Final stage
FROM python:3.11-slim
FROM python:3-slim

WORKDIR /action

Expand Down
16 changes: 14 additions & 2 deletions action/src/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ def generate_with_validation(
else:
return None, False, "Failed to generate valid YAML after retries"

# Log the generated entry for debugging
logger.debug(f"Generated entry (attempt {attempt}):\n{generated_entry}")

# Validate the generated entry
is_valid, validation_errors = validator.validate(generated_entry)

Expand All @@ -408,11 +411,17 @@ def generate_with_validation(
# Entry invalid - log errors and retry if attempts remain
error_message = "; ".join(validation_errors)
logger.warning(f"Validation failed on attempt {attempt}: {error_message}")
logger.warning(
f"Generated YAML that failed validation (attempt {attempt}):\n{generated_entry}"
)

if attempt <= max_retries:
logger.info(
f"Retrying with validation feedback... (attempt {attempt + 1})"
)
logger.info(
f"Validation feedback for retry: {'; '.join(validation_errors)}"
)
continue
else:
# All retries exhausted
Expand Down Expand Up @@ -453,14 +462,14 @@ def _build_retry_prompt(

if original_prompt:
# If there was a custom prompt, append retry context to it
return f"{original_prompt}\n\n{retry_context}"
retry_prompt = f"{original_prompt}\n\n{retry_context}"
else:
# Otherwise, build a complete prompt with the diff and retry context
pr_title = pr_info.get("title", "")
pr_body = pr_info.get("body", "")
pr_author = pr_info.get("user", {}).get("login", "unknown")

return f"""Generate a logchange-formatted YAML changelog entry for this PR.
retry_prompt = f"""Generate a logchange-formatted YAML changelog entry for this PR.

PR Title: {pr_title}

Expand All @@ -478,6 +487,9 @@ def _build_retry_prompt(

Output ONLY the corrected YAML with no additional text."""

logger.debug(f"Retry prompt:\n{retry_prompt}")
return retry_prompt

def _extract_yaml(self, text: str) -> str:
"""Extract YAML from markdown code blocks if present"""
# Try to extract from ```yaml ... ``` or ``` ... ``` block
Expand Down
25 changes: 16 additions & 9 deletions action/src/github_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,20 @@ def create_review_comment_with_suggestion(
line: int,
body: str,
suggestion: str = "",
start_line: int = None,
) -> bool:
"""
Create a review comment on a specific line with optional suggested changes.
Supports multi-line comments.

Args:
commit_sha: The commit SHA where the comment should be posted
file_path: Path to the file in the PR
line: Line number (in the new version of the file)
line: Line number (in the new version of the file, or end line for multi-line)
body: The comment text (markdown format with suggestion syntax if applicable)
suggestion: Optional suggested replacement text (for "suggest edits" feature).
Use "```suggestion\n<content>\n```" format in body instead.
start_line: Optional start line for multi-line comments (if None, single line comment)

Returns:
True if successful, False otherwise
Expand All @@ -283,23 +286,24 @@ def create_review_comment_with_suggestion(
logger.warning("No PR number found")
return False

url = f"{self.api_url}/repos/{self.repo_owner}/{self.repo_name}/pulls/{self.pr_number}/reviews"
url = f"{self.api_url}/repos/{self.repo_owner}/{self.repo_name}/pulls/{self.pr_number}/comments"

# Build the comment payload
comment_data = {
"body": body,
"commit_id": commit_sha,
"path": file_path,
"line": line,
"body": body,
"side": "RIGHT", # Comment on the new version of the file
}

payload = {
"body": "Review with suggested changes",
"event": "COMMENT",
"comments": [comment_data],
}
# Add start_line and start_side for multi-line comments
if start_line is not None:
comment_data["start_line"] = start_line
comment_data["start_side"] = "RIGHT"

try:
response = self.session.post(url, json=payload)
response = self.session.post(url, json=comment_data)
response.raise_for_status()
logger.info(
f"Successfully created review comment with suggestion on line {line}"
Expand All @@ -308,4 +312,7 @@ def create_review_comment_with_suggestion(

except requests.exceptions.RequestException as e:
logger.error(f"Failed to create review comment: {e}")
# Log response content for debugging errors
if hasattr(e.response, 'text'):
logger.debug(f"GitHub API response: {e.response.text}")
return False
81 changes: 53 additions & 28 deletions action/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@ def run(self) -> int:

logger.info("Starting logchange action")

# Early exit: if generation is enabled, check for existing suggestion before
# doing expensive file operations
if (
self.on_missing_entry == "generate"
and self._has_existing_suggestion()
):
logger.info(
"Changelog suggestion already exists on this PR, skipping all operations"
)
self.set_output("changelog-found", "false")
self.set_output("changelog-generated", "false")
return 0

# Get PR files
pr_files = self.github_client.get_pr_files()
logger.info(f"Found {len(pr_files)} files in PR")
Expand Down Expand Up @@ -380,7 +393,6 @@ def _generate_and_validate(
- Calling generate_with_validation
- Checking for generation failures
- Checking for validation failures
- Checking for duplicate suggestions
- Posting error comments
- Setting error outputs

Expand All @@ -395,7 +407,8 @@ def _generate_and_validate(
if not self.generator:
logger.warning(f"Generator not available for {context_name}")
self.github_client.comment_on_pr(
f"⚠️ {context_name} generation requested but generator not available"
"⚠️ Changelog generation is currently unavailable.\n\n"
"Please try again with your next commit."
)
self.set_output("generation-error", "Generator not available")
return None
Expand All @@ -420,32 +433,29 @@ def _generate_and_validate(
f"{context_name} generation failed: {validation_message}"
)
self.github_client.comment_on_pr(
f"⚠️ {context_name} generation failed\n\n"
f"*(Reason: {validation_message})*"
"⚠️ Changelog generation encountered some issues.\n\n"
"No worries! Please try again with your next commit."
)
self.set_output("generation-error", validation_message)
return None

if not is_valid:
logger.error(f"Generated {context_name} invalid: {validation_message}")
self.github_client.comment_on_pr(
f"⚠️ Generated {context_name} failed validation"
"⚠️ Changelog generation encountered some issues.\n\n"
"No worries! Please try again with your next commit."
)
self.set_output("generation-error", "Validation failed")
return None

# Check for duplicate suggestion
if self._has_existing_suggestion():
logger.info(f"{context_name} suggestion already exists, skipping")
return None

logger.info(f"{context_name} generated and validated successfully")
return generated_entry

except GenerationError as e:
logger.warning(f"{context_name} generation error: {e}")
self.github_client.comment_on_pr(
f"⚠️ {context_name} generation failed: {str(e)}"
"⚠️ Changelog generation encountered some issues.\n\n"
"No worries! Please try again with your next commit."
)
self.set_output("generation-error", str(e))
return None
Expand Down Expand Up @@ -647,19 +657,34 @@ def _convert_legacy_to_logchange(
)

for start_line, end_line, group_content in line_groups:
# Format the suggestion with markdown syntax for removal
suggestion_body = (
"This was converted to logchange format. Let's remove it.\n\n"
"```suggestion\n"
"```"
)

self.github_client.create_review_comment_with_suggestion(
commit_sha=commit_sha,
file_path=legacy_file,
line=end_line, # Post on the last line of the group
body=suggestion_body,
)
is_single_line = start_line == end_line

if is_single_line:
# For single-line removals, use GitHub's suggestion syntax
suggestion_body = (
"This was converted to logchange format. Let's remove it.\n\n"
"```suggestion\n"
"```"
)
self.github_client.create_review_comment_with_suggestion(
commit_sha=commit_sha,
file_path=legacy_file,
line=end_line,
body=suggestion_body,
)
else:
# For multi-line removals, post a regular comment on the last line
# (GitHub's multi-line suggestion API has limitations)
suggestion_body = (
f"Lines {start_line}-{end_line}: This was converted to logchange format. "
"Please remove these lines."
)
self.github_client.create_review_comment_with_suggestion(
commit_sha=commit_sha,
file_path=legacy_file,
line=end_line,
body=suggestion_body,
)

# Post the converted entry as a regular comment
suggestion_comment = self._format_legacy_conversion_comment(
Expand Down Expand Up @@ -691,9 +716,9 @@ def _format_legacy_conversion_comment(
filename = generate_changelog_slug(pr_number, pr_title)
file_path = f"{self.changelog_path}/{filename}"

return f"""🔄 **I've converted the legacy changelog entry to logchange format!**
return f"""🔄 **I've converted the changelog entry to logchange format!**

I detected a change to `{legacy_file}` and converted it to the logchange format below.
I detected changes to `{legacy_file}` and converted them to the logchange format below.

**Suggested Logchange Entry** for `{file_path}`:

Expand All @@ -707,7 +732,7 @@ def _format_legacy_conversion_comment(
- Create a new file at `{file_path}`
- Copy the YAML above into it

2. ⚠️ **Remove** the legacy changes:
2. ⚠️ **Remove** the original entry:
- I've added suggested edits to remove the lines you added to `{legacy_file}`
- Review the suggestions in the PR review and accept them

Expand All @@ -717,7 +742,7 @@ def _format_legacy_conversion_comment(
- Feel free to edit the logchange YAML

**Why?**
This project uses logchange format for changelog entries, not the traditional {legacy_file} format. Using logchange ensures consistency and better tooling support.
This project uses logchange format for changelog entries. Using logchange ensures consistency and better tooling support across all changelog entries.
"""


Expand Down
Loading