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
1 change: 0 additions & 1 deletion .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,3 @@ jobs:
on-missing-entry: generate
claude-token: ${{ secrets.CLAUDE_API_KEY }}
skip-files-regex: '^(README\.md|docs/|\.github/)'
legacy-changelog-paths: CHANGELOG.md,HISTORY.txt
10 changes: 3 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PR comment bot that aggressively reminds you to update CHANGELOG.md (#178) (Michael Zhang)
- Changelog formatter that converts "fixed shit" into "resolved critical infrastructure issue" (#356) (Riley Cross)
- GitHub Actions integration that blocks your PR until changelog is perfect (#489) (Dana Torres)
- 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)

### Changed
- Complete rewrite of changelog parser to handle developers' stream-of-consciousness commit messages (#612) (Kevin O'Connor)
Expand All @@ -44,11 +41,10 @@ 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)


### Security
- Added detection for when developers try to hide breaking changes in the changelog (#567) (Lisa Wong)
- Implemented strict enforcement that prevents lying in your changelog entries (#391) (Oscar Hernandez)
- Lorem ipsum dolor
this should be suggested removed
even if it's many lines (#111) (Jon Blow, Jane Snow)

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
even if it's many lines (#111) (Jon Blow, Jane Snow)

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ docker run -e GITHUB_EVENT_NAME=pull_request logchange-action:test

## Security

See [SECURITY.md](SECURITY.md) for information about:
See [DEPLOYMENT.md](DEPLOYMENT.md#security-considerations) for information about:
- How this action handles fork pull requests securely
- Why we use `pull_request_target` event
- Security safeguards in place
Expand Down
75 changes: 75 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,81 @@ To enable this workflow:
- Ensure the tag matches exactly in `action.yml`
- Try rebuilding and pushing with verbose output: `docker push -verbose`

## Security Considerations

### Workflow Security

This action uses GitHub Actions' `pull_request_target` event to enable commenting on pull requests from forks. This approach requires careful consideration of security implications.

#### Why pull_request_target?

Standard `pull_request` events on fork PRs cannot:
- Comment on the PR
- Create review comments
- Access secrets with full scope

This action needs these capabilities to provide changelog validation and AI-generated suggestions.

#### Security Safeguards

**✅ Safe by design:**

1. **Explicit checkout of PR code**: The action checks out the PR's actual head commit (`head.sha`), not untrusted base code:
```yaml
ref: ${{ github.event.pull_request.head.sha }}
```

2. **Limited token scope**: Workflows only receive `pull-requests:write` permission:
```yaml
permissions:
contents: read
pull-requests: write
```
This prevents malicious code from modifying the repository.

3. **Non-destructive validation**: The action reads and validates changelog format—it doesn't execute arbitrary PR code.

4. **Containerized execution**: The action runs in an isolated Docker container.

5. **No secret exfiltration**: Sensitive values (tokens, API keys) are never printed to logs.

#### What This Means

- ✅ Fork PRs can receive changelog validation and AI suggestions
- ✅ Malicious PRs cannot modify your repository
- ✅ Malicious PRs cannot access repository secrets with write scope
- ⚠️ Untrusted code from PRs runs in the action's environment (read-only context)

#### Recommendations for Repository Owners

**For public projects accepting community PRs:**

1. Keep `pull_request_target` enabled to support fork contributions
2. Trust the validation performed by GitHub Actions (code review is still recommended)
3. Monitor action logs for unusual activity

**For private projects or additional security:**

1. Switch to standard `pull_request` (fork PRs won't get comments, but only internal PRs will run)
2. Use branch protection rules requiring maintainer approval before workflows run
3. Implement manual approval workflows for external contributions

#### Best Practices When Using This Action

1. **Keep dependencies updated**: Regularly update GitHub Actions and Docker images
2. **Review logs**: Check workflow logs for unexpected behavior
3. **Use specific action versions**: Pin to a specific version tag rather than `@main` or `@latest`
4. **Validate changelog entries**: Even with automation, human review of changelog entries is recommended
5. **Restrict secrets**: Only provide necessary secrets (GITHUB_TOKEN is automatically provided)

#### Reporting Security Issues

If you discover a security vulnerability in this action:

1. **Do not** open a public GitHub issue
2. Report to the repository maintainers privately
3. Include details about the vulnerability and potential impact

## Support

For issues with:
Expand Down
74 changes: 0 additions & 74 deletions SECURITY.md

This file was deleted.

6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ inputs:
default: ''

legacy-changelog-paths:
description: 'Comma-separated list of legacy changelog file paths to detect (e.g., CHANGELOG.md,HISTORY.txt). DISABLED by default (empty string). Set to enable legacy changelog detection and conversion.'
description: 'Comma-separated list of legacy changelog file paths to detect (e.g., CHANGELOG.md,HISTORY.txt). Defaults to CHANGELOG.md. Set to empty string to disable legacy changelog detection.'
required: false
default: ''
default: 'CHANGELOG.md'

on-legacy-entry:
description: 'Action when legacy changelog entry found without logchange entry: "convert", "warn", or "fail"'
description: 'Action when legacy changelog entry is found. Always fails the action with different levels of help: "fail" (just fail), "warn" (fail + comment), "remove" (fail + removal suggestions), "convert" (fail + removal suggestions + LLM conversion to logchange)'
required: false
default: 'convert'

Expand Down
9 changes: 7 additions & 2 deletions action/src/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,13 @@ def _build_user_message(
{pr_diff}
```

Based on the above information, generate a valid logchange YAML entry that accurately describes this change.
Make sure the generated YAML is valid and can be parsed directly. Output ONLY the YAML with no additional text."""
**IMPORTANT INSTRUCTIONS:**
1. Always include the "authors" field with at least the primary author ({pr_author})
2. The authors section must contain the PR author information
3. Extract any additional authors from commit authors if available
4. Generate a valid logchange YAML entry that accurately describes this change
5. Make sure the generated YAML is valid and can be parsed directly
6. Output ONLY the YAML with no additional text"""

return message

Expand Down
8 changes: 4 additions & 4 deletions action/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ def __init__(self):
self.forbidden_fields = self._parse_list_input("forbidden-fields", "")
self.optional_fields = self._parse_list_input("optional-fields", "")

# Legacy changelog configuration (disabled by default)
# Legacy changelog configuration (enabled by default with CHANGELOG.md)
self.legacy_changelog_paths = self._parse_list_input(
"legacy-changelog-paths", ""
"legacy-changelog-paths", "CHANGELOG.md"
)
self.on_legacy_entry = self._get_input("on-legacy-entry", "convert").lower()
self.on_legacy_and_logchange = self._get_input(
Expand Down Expand Up @@ -173,10 +173,10 @@ def _validate_config(self) -> None:
)

# Validate on_legacy_entry mode
if self.on_legacy_entry not in ("convert", "warn", "fail"):
if self.on_legacy_entry not in ("convert", "warn", "fail", "remove"):
raise ConfigurationError(
f"Invalid on-legacy-entry mode: {self.on_legacy_entry}. "
"Must be one of: convert, warn, fail"
"Must be one of: fail, warn, remove, convert"
)

# Validate on_legacy_and_logchange mode
Expand Down
10 changes: 6 additions & 4 deletions action/src/github_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,21 @@ def create_review_comment_with_suggestion(
body: str,
suggestion: str = "",
start_line: int = None,
side: str = "RIGHT",
) -> bool:
"""
Create a review comment on a specific line with optional suggested changes.
Supports multi-line comments.
Supports multi-line comments and comments on removed lines.

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, or end line for multi-line)
line: Line number (in the new version for RIGHT, old version for LEFT)
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)
side: "RIGHT" for added/modified lines, "LEFT" for removed lines

Returns:
True if successful, False otherwise
Expand All @@ -294,13 +296,13 @@ def create_review_comment_with_suggestion(
"commit_id": commit_sha,
"path": file_path,
"line": line,
"side": "RIGHT", # Comment on the new version of the file
"side": side,
}

# 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"
comment_data["start_side"] = side

try:
response = self.session.post(url, json=comment_data)
Expand Down
61 changes: 61 additions & 0 deletions action/src/legacy_changelog_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,59 @@ def extract_added_lines_with_positions(

return added_lines_with_pos

def extract_removed_lines_with_positions(
self, diff_content: str, legacy_file: str
) -> List[Tuple[int, str]]:
"""
Extract removed changelog lines from diff with their line numbers.

Args:
diff_content: The diff output for the changelog file
legacy_file: The legacy file path (for matching the diff)

Returns:
List of tuples (line_number, line_content) for removed lines in the old version
"""
lines = diff_content.split("\n")
removed_lines_with_pos = []
current_old_line = 0
in_hunk = False

for line in lines:
# Skip lines until we find the hunk header
if line.startswith("@@"):
# Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
try:
parts = line.split(" ")
old_range = parts[1] # -old_start,old_count
old_start = int(old_range.split(",")[0].lstrip("-"))
current_old_line = old_start
except (IndexError, ValueError):
pass
in_hunk = True
continue

if not in_hunk:
continue

# Process lines in hunk
if line.startswith("@@"):
# New hunk, reset
continue
elif line.startswith("-") and not line.startswith("---"):
# Removed line - record it with position
content = line[1:] # Remove '-' prefix
removed_lines_with_pos.append((current_old_line, content))
current_old_line += 1
elif line.startswith("+"):
# Added line - don't increment old line counter
pass
elif not line.startswith("\\"):
# Context line (unchanged) - increment counter
current_old_line += 1

return removed_lines_with_pos

def group_consecutive_lines(
self, added_lines: List[Tuple[int, str]]
) -> List[Tuple[int, int, List[str]]]:
Expand Down Expand Up @@ -289,6 +342,7 @@ def create_conversion_prompt(
The user prompt for Claude
"""
pr_title = pr_info.get("title", "")
pr_author = pr_info.get("user", {}).get("login", "unknown")
entry_type = context.get("type", "unknown")

# Use provided types or defaults
Expand Down Expand Up @@ -355,10 +409,17 @@ def create_conversion_prompt(

PR Title: {pr_title}

PR Author: {pr_author}

Entry Type Detected: {entry_type}

{validation_section}

**IMPORTANT: Always include the authors field**
- The authors field is REQUIRED and must include at least the PR author ({pr_author})
- Extract any additional authors from the legacy entry text if mentioned
- Format: authors: [{{name: "Author Name"}}]

Now convert this into logchange format, validating that it's relevant to the code changes:"""

return prompt
Expand Down
Loading
Loading