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 7 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
52 changes: 52 additions & 0 deletions .github/workflows/update-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 modify_changelog.py update_changelog \
"${{ github.event.pull_request.number }}" \
"${{ github.event.pull_request.title }}"

- 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: 1 addition & 1 deletion .github/workflows/version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
VERSION=$(python increment_version.py $TYPE)
VERSION=$(python modify_changelog.py bump_version $TYPE)
git checkout -b "version-$VERSION"
git commit -am "Version $VERSION"
git push -u origin HEAD
Expand Down
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
79 changes: 0 additions & 79 deletions increment_version.py

This file was deleted.

206 changes: 206 additions & 0 deletions modify_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"""
Changelog Modifier and Version Bumper for Cargo.toml and Changelog.md

This script automates the process of version bumping and changelog updates for Rust projects managed with Cargo.
It reads the version from the cargo.toml file, increments it based on the specified component (major, minor, or patch),
and updates both the cargo.toml and changelog.md files accordingly.

It can also add PR entries to the UNRELEASED section of the changelog.

Usage:
Version bumping:
$ python modify_changelog.py bump_version [major|minor|patch]

Adding PR entry:
$ python modify_changelog.py update_changelog <number> <title>

Subcommands:
-----------
bump_version - Increments version and updates changelog
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

update_changelog - Add a PR entry to the UNRELEASED section
number - PR number
title - PR title

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

"""

import argparse
import datetime
import re
import sys
from pathlib import Path


def bump_version(major: int, minor: int, patch: int, part: str) -> str:
if part == "major":
major += 1
minor = 0
patch = 0
elif part == "minor":
minor += 1
patch = 0
elif part == "patch":
patch += 1
return f"{major}.{minor}.{patch}"


def update_cargo_toml(file_path: Path, new_version: str) -> None:
content = file_path.read_text()
content = re.sub(r'version = "(\d+\.\d+\.\d+)"', f'version = "{new_version}"', content, count=1)
file_path.write_text(content)


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)


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 handle_bump_version(args):
"""Handle version bump subcommand."""
part = args.bump_type
cargo_path = Path("Cargo.toml")
changelog_path = Path("docs/changelog.md")

if not cargo_path.exists():
print("ERROR: Cargo.toml not found.")
sys.exit(1)

if not changelog_path.exists():
print("ERROR: Changelog file not found.")
sys.exit(1)

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)
update_cargo_toml(cargo_path, new_version)
update_changelog_version(changelog_path, new_version)
print(new_version)


def handle_update_changelog(args):
"""Handle update changelog subcommand."""
pr_number = args.number
pr_title = args.title

# Construct PR URL from repository info and PR number
# Default to the egglog-python repository
pr_url = f"https://github.com/egraphs-good/egglog-python/pull/{pr_number}"

changelog_path = Path(getattr(args, 'changelog_path', 'docs/changelog.md'))

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

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


def main():
parser = argparse.ArgumentParser(description='Changelog modifier and version bumper')
subparsers = parser.add_subparsers(dest='command', help='Available commands')

# Bump version subcommand
bump_parser = subparsers.add_parser('bump_version', help='Bump version and update changelog')
bump_parser.add_argument('bump_type', choices=['major', 'minor', 'patch'],
help='Type of version bump')

# Update changelog subcommand
changelog_parser = subparsers.add_parser('update_changelog', help='Add PR entry to changelog')
changelog_parser.add_argument('number', help='Pull request number')
changelog_parser.add_argument('title', help='Pull request title')
changelog_parser.add_argument('--changelog-path', default='docs/changelog.md',
help='Path to changelog file')

args = parser.parse_args()

if not args.command:
parser.print_help()
sys.exit(1)

if args.command == 'bump_version':
handle_bump_version(args)
elif args.command == 'update_changelog':
handle_update_changelog(args)


if __name__ == "__main__":
main()
Loading
Loading