Skip to content

Automatically Create Changelog Entry for PRs #313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
53 changes: 53 additions & 0 deletions .github/workflows/update-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Update Changelog

on:
pull_request:
types: [opened, edited]

jobs:
update-changelog:
# Only run if this is not a PR from a fork to avoid permission issues
# and not a commit made by GitHub Action to avoid infinite loops
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'github-actions[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Checkout the PR head ref
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Update changelog
run: |
python increment_version.py --add-pr \
--pr-number="${{ github.event.pull_request.number }}" \
--pr-title="${{ github.event.pull_request.title }}" \
--pr-url="${{ github.event.pull_request.html_url }}"

- name: Check for changes
id: changes
run: |
if git diff --quiet docs/changelog.md; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi

- name: Commit and push changes
if: steps.changes.outputs.changed == 'true'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add docs/changelog.md
git commit -m "Add changelog entry for PR #${{ github.event.pull_request.number }}"
git push
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ _This project uses semantic versioning_

## UNRELEASED

- [WIP] Automatically Generate Changelog Entries for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
- Automatically Generate Changelog Entries for PRs [#312](https://github.com/egraphs-good/egglog-python/pull/312)
- Upgrade egglog which includes new backend.
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.
Also changes the representation to be an index into a list instead of the ID, making egglog programs more deterministic.
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ or bug fixes.

When you are ready to submit your changes, please open a pull request. The CI will run the tests and check the code style.

#### Changelog Automation

When you open a pull request, a GitHub Action automatically adds an entry to the UNRELEASED section of the changelog using your PR title and number. This ensures the changelog stays up-to-date without manual intervention.

## Documentation

We use the [Diátaxis framework](https://diataxis.fr/) to organize our documentation. The "explanation" section has
Expand Down
148 changes: 139 additions & 9 deletions increment_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@

It will also print out the new version number.

Additionally, this script can add PR entries to the UNRELEASED section of the changelog.

Usage:
Run the script from the command line, specifying the type of version increment as an argument:
$ python bump_version.py [major|minor|patch]
Version bumping:
$ python increment_version.py [major|minor|patch]

Adding PR entry:
$ python increment_version.py --add-pr --pr-number=123 --pr-title="Fix bug" --pr-url="https://github.com/..."

Arguments:
---------
major - Increments the major component of the version, sets minor and patch to 0
minor - Increments the minor component of the version, sets patch to 0
patch - Increments the patch component of the version
--add-pr - Add a PR entry to the UNRELEASED section
--pr-number - PR number (required with --add-pr)
--pr-title - PR title (required with --add-pr)
--pr-url - PR URL (required with --add-pr)

From https://chat.openai.com/share/6b08906d-23a3-4193-9f4e-87076ce56ddb


"""

import argparse
import datetime
import re
import sys
Expand All @@ -47,20 +57,116 @@ def update_cargo_toml(file_path: Path, new_version: str) -> None:
file_path.write_text(content)


def update_changelog(file_path: Path, new_version: str) -> None:
def find_unreleased_section(lines):
"""Find the line number where UNRELEASED section starts and ends."""
unreleased_start = None
content_start = None

for i, line in enumerate(lines):
if line.strip() == "## UNRELEASED":
unreleased_start = i
continue

if unreleased_start is not None and content_start is None:
# Skip empty lines after ## UNRELEASED
if line.strip() == "":
continue
else:
content_start = i
break

return unreleased_start, content_start


def update_changelog_version(file_path: Path, new_version: str) -> None:
"""Update changelog for version bump - replaces UNRELEASED with versioned section."""
today = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d")
content = file_path.read_text()
new_section = f"## UNRELEASED\n\n## {new_version} ({today})"
content = content.replace("## UNRELEASED", new_section, 1)
file_path.write_text(content)


if __name__ == "__main__":
if len(sys.argv) != 2 or sys.argv[1] not in ("major", "minor", "patch"):
print("Usage: python bump_version.py [major|minor|patch]")
def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: str) -> bool:
"""Update the changelog with the new PR entry. Returns True if successful, False if entry already exists."""

# Read the current changelog
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()

# Find the UNRELEASED section
unreleased_start, content_start = find_unreleased_section(lines)

if unreleased_start is None:
print("ERROR: Could not find '## UNRELEASED' section in changelog")
return False

if content_start is None:
print("ERROR: Could not find content start after UNRELEASED section")
return False

# Create the new entry
new_entry = f"- {pr_title} [#{pr_number}]({pr_url})\n"

# Check if this PR entry already exists to avoid duplicates
for line in lines[content_start:]:
if f"[#{pr_number}]" in line:
print(f"Changelog entry for PR #{pr_number} already exists")
return False
# Stop checking when we reach the next section
if line.startswith("## ") and not line.strip() == "## UNRELEASED":
break

# Insert the new entry at the beginning of the unreleased content
lines.insert(content_start, new_entry)

# Write the updated changelog
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)

print(f"Added changelog entry for PR #{pr_number}: {pr_title}")
return True


def main():
parser = argparse.ArgumentParser(description='Version bumper and changelog updater')

# Create mutually exclusive group for version bump vs PR add
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('bump_type', nargs='?', choices=['major', 'minor', 'patch'],
help='Type of version bump')
group.add_argument('--add-pr', action='store_true', help='Add PR entry to changelog')

# PR-specific arguments
parser.add_argument('--pr-number', help='Pull request number (required with --add-pr)')
parser.add_argument('--pr-title', help='Pull request title (required with --add-pr)')
parser.add_argument('--pr-url', help='Pull request URL (required with --add-pr)')
parser.add_argument('--changelog-path', default='docs/changelog.md', help='Path to changelog file')

args = parser.parse_args()

# Handle PR addition
if args.add_pr:
if not all([args.pr_number, args.pr_title, args.pr_url]):
print("ERROR: --pr-number, --pr-title, and --pr-url are required with --add-pr")
sys.exit(1)

changelog_path = Path(args.changelog_path)
if not changelog_path.exists():
print(f"ERROR: Changelog file not found: {changelog_path}")
sys.exit(1)

success = update_changelog_pr(changelog_path, args.pr_number, args.pr_title, args.pr_url)
if not success:
sys.exit(1)
return

# Handle version bump (existing functionality)
if not args.bump_type:
print("ERROR: Either specify bump type (major|minor|patch) or use --add-pr")
sys.exit(1)

part = sys.argv[1]
part = args.bump_type
cargo_path = Path("Cargo.toml")
changelog_path = Path("docs/changelog.md")

Expand All @@ -75,5 +181,29 @@ def update_changelog(file_path: Path, new_version: str) -> None:
new_version = bump_version(major, minor, patch, part)
old_version = f"{major}.{minor}.{patch}"
update_cargo_toml(cargo_path, new_version)
update_changelog(changelog_path, new_version)
update_changelog_version(changelog_path, new_version)
print(new_version)


if __name__ == "__main__":
# For backward compatibility, support old command line format
if len(sys.argv) == 2 and sys.argv[1] in ("major", "minor", "patch"):
part = sys.argv[1]
cargo_path = Path("Cargo.toml")
changelog_path = Path("docs/changelog.md")

cargo_content = cargo_path.read_text()
version_match = re.search(r'version = "(\d+)\.(\d+)\.(\d+)"', cargo_content)
if version_match:
major, minor, patch = map(int, version_match.groups())
else:
print("Current version not found in cargo.toml.")
sys.exit(1)

new_version = bump_version(major, minor, patch, part)
old_version = f"{major}.{minor}.{patch}"
update_cargo_toml(cargo_path, new_version)
update_changelog_version(changelog_path, new_version)
print(new_version)
else:
main()
Loading