From d321e3ecbeb2e49191cd6f9e0f173db7782dc701 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:41:49 +0000 Subject: [PATCH 01/11] Initial plan From 63653799678443548e59358b43a72c0c40c38005 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:49:07 +0000 Subject: [PATCH 02/11] Implement automatic changelog generation for PRs Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- .github/scripts/update_changelog.py | 96 ++++++++++++++++++++++++++ .github/workflows/update-changelog.yml | 53 ++++++++++++++ docs/changelog.md | 1 + 3 files changed, 150 insertions(+) create mode 100755 .github/scripts/update_changelog.py create mode 100644 .github/workflows/update-changelog.yml diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py new file mode 100755 index 00000000..2bc9c78a --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Script to automatically update the changelog with PR information. +""" + +import argparse +import re +import sys +from pathlib import Path + + +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(changelog_path, pr_number, pr_title, pr_url): + """Update the changelog with the new PR entry.""" + + # Read the current changelog + with open(changelog_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(changelog_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='Update changelog with PR information') + parser.add_argument('--pr-number', required=True, help='Pull request number') + parser.add_argument('--pr-title', required=True, help='Pull request title') + parser.add_argument('--pr-url', required=True, help='Pull request URL') + parser.add_argument('--changelog-path', default='docs/changelog.md', help='Path to changelog file') + + args = parser.parse_args() + + 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(changelog_path, args.pr_number, args.pr_title, args.pr_url) + + if not success: + sys.exit(1) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 00000000..92d113e2 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -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 .github/scripts/update_changelog.py \ + --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 \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index ddb82166..9de57b6b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ _This project uses semantic versioning_ ## UNRELEASED +- 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. From 6dca75eedfe237849a38920da87a3a39802cf26a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:50:34 +0000 Subject: [PATCH 03/11] Add documentation and tests for changelog automation Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- .github/CHANGELOG_AUTOMATION.md | 36 ++++++++ .github/scripts/test_update_changelog.py | 100 +++++++++++++++++++++++ .github/workflows/update-changelog.yml | 3 + 3 files changed, 139 insertions(+) create mode 100644 .github/CHANGELOG_AUTOMATION.md create mode 100755 .github/scripts/test_update_changelog.py diff --git a/.github/CHANGELOG_AUTOMATION.md b/.github/CHANGELOG_AUTOMATION.md new file mode 100644 index 00000000..ce4e87a5 --- /dev/null +++ b/.github/CHANGELOG_AUTOMATION.md @@ -0,0 +1,36 @@ +# Automatic Changelog Generation + +This repository automatically generates changelog entries for new PRs using a GitHub Action. + +## How it works + +1. **Trigger**: When a PR is opened or edited +2. **Processing**: The action runs a Python script that: + - Parses the `docs/changelog.md` file + - Finds the "## UNRELEASED" section + - Adds a new entry with format: `- PR_TITLE [#PR_NUMBER](PR_URL)` + - Checks for duplicates to avoid repeated entries +3. **Update**: Commits the changes back to the PR branch + +## Files + +- `.github/workflows/update-changelog.yml` - GitHub Action workflow +- `.github/scripts/update_changelog.py` - Python script that updates the changelog + +## Safety features + +- Only runs for PRs from the same repository (not forks) +- Prevents infinite loops by excluding commits made by GitHub Action +- Includes duplicate detection +- Proper error handling and logging + +## Manual usage + +You can also run the script manually: + +```bash +python .github/scripts/update_changelog.py \ + --pr-number="123" \ + --pr-title="My PR Title" \ + --pr-url="https://github.com/egraphs-good/egglog-python/pull/123" +``` \ No newline at end of file diff --git a/.github/scripts/test_update_changelog.py b/.github/scripts/test_update_changelog.py new file mode 100755 index 00000000..b33bcd3b --- /dev/null +++ b/.github/scripts/test_update_changelog.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Simple test for the changelog update script. +""" + +import os +import tempfile +import sys +from pathlib import Path + +# Add the scripts directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) + +from update_changelog import update_changelog, find_unreleased_section + + +def test_find_unreleased_section(): + """Test finding the unreleased section.""" + lines = [ + "# Changelog\n", + "\n", + "## UNRELEASED\n", + "\n", + "- Some existing entry\n", + "\n", + "## 1.0.0\n", + "- Released version\n" + ] + + unreleased_start, content_start = find_unreleased_section(lines) + assert unreleased_start == 2, f"Expected unreleased_start=2, got {unreleased_start}" + assert content_start == 4, f"Expected content_start=4, got {content_start}" + print("✓ find_unreleased_section test passed") + + +def test_update_changelog(): + """Test updating the changelog.""" + # Create a temporary changelog file + changelog_content = """# Changelog + +## UNRELEASED + +- Existing entry + +## 1.0.0 + +- Released version +""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: + f.write(changelog_content) + temp_path = f.name + + try: + # Update the changelog + result = update_changelog(temp_path, "999", "Test PR", "https://example.com/pr/999") + assert result == True, "update_changelog should return True on success" + + # Read the updated content + with open(temp_path, 'r') as f: + updated_content = f.read() + + # Check that the entry was added + assert "- Test PR [#999](https://example.com/pr/999)" in updated_content + + # Check that it was added in the right place (after UNRELEASED) + lines = updated_content.split('\n') + unreleased_idx = lines.index("## UNRELEASED") + entry_idx = None + for i, line in enumerate(lines): + if "Test PR [#999]" in line: + entry_idx = i + break + + assert entry_idx is not None, "Entry should be found" + assert entry_idx > unreleased_idx, "Entry should be after UNRELEASED section" + + # Test duplicate detection + result2 = update_changelog(temp_path, "999", "Test PR", "https://example.com/pr/999") + assert result2 == False, "update_changelog should return False for duplicates" + + print("✓ update_changelog test passed") + + finally: + # Clean up + os.unlink(temp_path) + + +def main(): + """Run all tests.""" + print("Running changelog automation tests...") + + test_find_unreleased_section() + test_update_changelog() + + print("✓ All tests passed!") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 92d113e2..4db64380 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -27,6 +27,9 @@ jobs: with: python-version: '3.12' + - name: Test changelog script + run: python .github/scripts/test_update_changelog.py + - name: Update changelog run: | python .github/scripts/update_changelog.py \ From a1a0255cb3cca26828cfb63cbecb3a7c7923a32a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Aug 2025 18:51:26 +0000 Subject: [PATCH 04/11] Add changelog entry for PR #313 --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index 9de57b6b..3e3898fd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ _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. From d874184a574b2e4a83d62d0cf201e44589f182c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:59:51 +0000 Subject: [PATCH 05/11] Integrate changelog PR automation into increment_version.py script Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- .github/scripts/test_update_changelog.py | 100 --------------- .github/scripts/update_changelog.py | 96 --------------- .github/workflows/update-changelog.yml | 5 +- increment_version.py | 148 +++++++++++++++++++++-- 4 files changed, 140 insertions(+), 209 deletions(-) delete mode 100755 .github/scripts/test_update_changelog.py delete mode 100755 .github/scripts/update_changelog.py diff --git a/.github/scripts/test_update_changelog.py b/.github/scripts/test_update_changelog.py deleted file mode 100755 index b33bcd3b..00000000 --- a/.github/scripts/test_update_changelog.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test for the changelog update script. -""" - -import os -import tempfile -import sys -from pathlib import Path - -# Add the scripts directory to the path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) - -from update_changelog import update_changelog, find_unreleased_section - - -def test_find_unreleased_section(): - """Test finding the unreleased section.""" - lines = [ - "# Changelog\n", - "\n", - "## UNRELEASED\n", - "\n", - "- Some existing entry\n", - "\n", - "## 1.0.0\n", - "- Released version\n" - ] - - unreleased_start, content_start = find_unreleased_section(lines) - assert unreleased_start == 2, f"Expected unreleased_start=2, got {unreleased_start}" - assert content_start == 4, f"Expected content_start=4, got {content_start}" - print("✓ find_unreleased_section test passed") - - -def test_update_changelog(): - """Test updating the changelog.""" - # Create a temporary changelog file - changelog_content = """# Changelog - -## UNRELEASED - -- Existing entry - -## 1.0.0 - -- Released version -""" - - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: - f.write(changelog_content) - temp_path = f.name - - try: - # Update the changelog - result = update_changelog(temp_path, "999", "Test PR", "https://example.com/pr/999") - assert result == True, "update_changelog should return True on success" - - # Read the updated content - with open(temp_path, 'r') as f: - updated_content = f.read() - - # Check that the entry was added - assert "- Test PR [#999](https://example.com/pr/999)" in updated_content - - # Check that it was added in the right place (after UNRELEASED) - lines = updated_content.split('\n') - unreleased_idx = lines.index("## UNRELEASED") - entry_idx = None - for i, line in enumerate(lines): - if "Test PR [#999]" in line: - entry_idx = i - break - - assert entry_idx is not None, "Entry should be found" - assert entry_idx > unreleased_idx, "Entry should be after UNRELEASED section" - - # Test duplicate detection - result2 = update_changelog(temp_path, "999", "Test PR", "https://example.com/pr/999") - assert result2 == False, "update_changelog should return False for duplicates" - - print("✓ update_changelog test passed") - - finally: - # Clean up - os.unlink(temp_path) - - -def main(): - """Run all tests.""" - print("Running changelog automation tests...") - - test_find_unreleased_section() - test_update_changelog() - - print("✓ All tests passed!") - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py deleted file mode 100755 index 2bc9c78a..00000000 --- a/.github/scripts/update_changelog.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to automatically update the changelog with PR information. -""" - -import argparse -import re -import sys -from pathlib import Path - - -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(changelog_path, pr_number, pr_title, pr_url): - """Update the changelog with the new PR entry.""" - - # Read the current changelog - with open(changelog_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(changelog_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='Update changelog with PR information') - parser.add_argument('--pr-number', required=True, help='Pull request number') - parser.add_argument('--pr-title', required=True, help='Pull request title') - parser.add_argument('--pr-url', required=True, help='Pull request URL') - parser.add_argument('--changelog-path', default='docs/changelog.md', help='Path to changelog file') - - args = parser.parse_args() - - 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(changelog_path, args.pr_number, args.pr_title, args.pr_url) - - if not success: - sys.exit(1) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 4db64380..786c8d0a 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -27,12 +27,9 @@ jobs: with: python-version: '3.12' - - name: Test changelog script - run: python .github/scripts/test_update_changelog.py - - name: Update changelog run: | - python .github/scripts/update_changelog.py \ + 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 }}" diff --git a/increment_version.py b/increment_version.py index ce722c7b..15c5fd17 100644 --- a/increment_version.py +++ b/increment_version.py @@ -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 @@ -47,7 +57,29 @@ 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})" @@ -55,12 +87,86 @@ def update_changelog(file_path: Path, new_version: str) -> None: 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") @@ -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() From 7d0f459e054e4dd9e46b08f7439f52025549e795 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:04:58 +0000 Subject: [PATCH 06/11] Move changelog automation docs to contributing guide and make concise Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- .github/CHANGELOG_AUTOMATION.md | 36 --------------------------------- docs/reference/contributing.md | 4 ++++ 2 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 .github/CHANGELOG_AUTOMATION.md diff --git a/.github/CHANGELOG_AUTOMATION.md b/.github/CHANGELOG_AUTOMATION.md deleted file mode 100644 index ce4e87a5..00000000 --- a/.github/CHANGELOG_AUTOMATION.md +++ /dev/null @@ -1,36 +0,0 @@ -# Automatic Changelog Generation - -This repository automatically generates changelog entries for new PRs using a GitHub Action. - -## How it works - -1. **Trigger**: When a PR is opened or edited -2. **Processing**: The action runs a Python script that: - - Parses the `docs/changelog.md` file - - Finds the "## UNRELEASED" section - - Adds a new entry with format: `- PR_TITLE [#PR_NUMBER](PR_URL)` - - Checks for duplicates to avoid repeated entries -3. **Update**: Commits the changes back to the PR branch - -## Files - -- `.github/workflows/update-changelog.yml` - GitHub Action workflow -- `.github/scripts/update_changelog.py` - Python script that updates the changelog - -## Safety features - -- Only runs for PRs from the same repository (not forks) -- Prevents infinite loops by excluding commits made by GitHub Action -- Includes duplicate detection -- Proper error handling and logging - -## Manual usage - -You can also run the script manually: - -```bash -python .github/scripts/update_changelog.py \ - --pr-number="123" \ - --pr-title="My PR Title" \ - --pr-url="https://github.com/egraphs-good/egglog-python/pull/123" -``` \ No newline at end of file diff --git a/docs/reference/contributing.md b/docs/reference/contributing.md index 8108fbc0..11556759 100644 --- a/docs/reference/contributing.md +++ b/docs/reference/contributing.md @@ -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 From 30a865b1e51e7011d4612a80bc5992fb6185404d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:13:14 +0000 Subject: [PATCH 07/11] Refactor increment_version.py to modify_changelog.py with subcommands and tests - Renamed increment_version.py to modify_changelog.py - Consolidated backward compatibility logic into main() - Added subcommands: bump_version and update_changelog - Updated GitHub workflows to use new command format - Added comprehensive tests for both functionalities - Improved argument parsing and error handling Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- .github/workflows/update-changelog.yml | 7 +- .github/workflows/version.yml | 2 +- increment_version.py => modify_changelog.py | 157 +++++----- python/tests/test_modify_changelog.py | 300 ++++++++++++++++++++ 4 files changed, 381 insertions(+), 85 deletions(-) rename increment_version.py => modify_changelog.py (53%) create mode 100644 python/tests/test_modify_changelog.py diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 786c8d0a..b13e32cf 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -29,10 +29,9 @@ jobs: - 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 }}" + python modify_changelog.py update_changelog \ + "${{ github.event.pull_request.number }}" \ + "${{ github.event.pull_request.title }}" - name: Check for changes id: changes diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 2fda944c..7ab0ae62 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -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 diff --git a/increment_version.py b/modify_changelog.py similarity index 53% rename from increment_version.py rename to modify_changelog.py index 15c5fd17..0b286caa 100644 --- a/increment_version.py +++ b/modify_changelog.py @@ -1,33 +1,31 @@ """ -Version Bumper for Cargo.toml and Changelog.md +Changelog Modifier and Version Bumper for Cargo.toml and Changelog.md -This script automates the process of version bumping 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. +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 will also print out the new version number. - -Additionally, this script can add PR entries to the UNRELEASED section of the changelog. +It can also add PR entries to the UNRELEASED section of the changelog. Usage: Version bumping: - $ python increment_version.py [major|minor|patch] + $ python modify_changelog.py bump_version [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) + $ python modify_changelog.py update_changelog -From https://chat.openai.com/share/6b08906d-23a3-4193-9f4e-87076ce56ddb +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 """ @@ -128,48 +126,20 @@ def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: 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) - +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: @@ -179,31 +149,58 @@ def main(): 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) +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__": - # 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() + main() \ No newline at end of file diff --git a/python/tests/test_modify_changelog.py b/python/tests/test_modify_changelog.py new file mode 100644 index 00000000..a3eb9cc1 --- /dev/null +++ b/python/tests/test_modify_changelog.py @@ -0,0 +1,300 @@ +"""Tests for modify_changelog.py script.""" + +import tempfile +import subprocess +import sys +from pathlib import Path +import pytest + + +def test_modify_changelog_help(): + """Test that the script shows help correctly.""" + result = subprocess.run([sys.executable, "modify_changelog.py", "--help"], + capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + assert result.returncode == 0 + assert "Changelog modifier and version bumper" in result.stdout + assert "bump_version" in result.stdout + assert "update_changelog" in result.stdout + + +def test_bump_version_subcommand_help(): + """Test that bump_version subcommand shows help correctly.""" + result = subprocess.run([sys.executable, "modify_changelog.py", "bump_version", "--help"], + capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + assert result.returncode == 0 + assert "Type of version bump" in result.stdout + assert "major" in result.stdout + assert "minor" in result.stdout + assert "patch" in result.stdout + + +def test_update_changelog_subcommand_help(): + """Test that update_changelog subcommand shows help correctly.""" + result = subprocess.run([sys.executable, "modify_changelog.py", "update_changelog", "--help"], + capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + assert result.returncode == 0 + assert "Pull request number" in result.stdout + assert "Pull request title" in result.stdout + + +def test_bump_version_patch(): + """Test version bumping with patch increment.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock Cargo.toml + cargo_content = '''[package] +name = "test-package" +version = "1.2.3" +edition = "2021" +''' + cargo_path = temp_path / "Cargo.toml" + cargo_path.write_text(cargo_content) + + # Create mock changelog + changelog_content = '''# Changelog + +## UNRELEASED + +## 1.2.3 (2024-01-01) + +- Some old change +''' + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text(changelog_content) + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "bump_version", "patch"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 0 + assert result.stdout.strip() == "1.2.4" + + # Check Cargo.toml was updated + updated_cargo = cargo_path.read_text() + assert 'version = "1.2.4"' in updated_cargo + + # Check changelog was updated + updated_changelog = changelog_path.read_text() + assert "## UNRELEASED" in updated_changelog + assert "## 1.2.4 (" in updated_changelog + + +def test_bump_version_minor(): + """Test version bumping with minor increment.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock Cargo.toml + cargo_content = '''[package] +name = "test-package" +version = "1.2.3" +edition = "2021" +''' + cargo_path = temp_path / "Cargo.toml" + cargo_path.write_text(cargo_content) + + # Create mock changelog + changelog_content = '''# Changelog + +## UNRELEASED + +## 1.2.3 (2024-01-01) + +- Some old change +''' + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text(changelog_content) + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "bump_version", "minor"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 0 + assert result.stdout.strip() == "1.3.0" + + # Check Cargo.toml was updated + updated_cargo = cargo_path.read_text() + assert 'version = "1.3.0"' in updated_cargo + + +def test_bump_version_major(): + """Test version bumping with major increment.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock Cargo.toml + cargo_content = '''[package] +name = "test-package" +version = "1.2.3" +edition = "2021" +''' + cargo_path = temp_path / "Cargo.toml" + cargo_path.write_text(cargo_content) + + # Create mock changelog + changelog_content = '''# Changelog + +## UNRELEASED + +## 1.2.3 (2024-01-01) + +- Some old change +''' + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text(changelog_content) + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "bump_version", "major"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 0 + assert result.stdout.strip() == "2.0.0" + + # Check Cargo.toml was updated + updated_cargo = cargo_path.read_text() + assert 'version = "2.0.0"' in updated_cargo + + +def test_update_changelog_new_entry(): + """Test adding a new PR entry to changelog.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock changelog + changelog_content = '''# Changelog + +## UNRELEASED + +## 1.2.3 (2024-01-01) + +- Some old change +''' + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text(changelog_content) + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", "123", "Fix important bug"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 0 + assert "Added changelog entry for PR #123: Fix important bug" in result.stdout + + # Check changelog was updated + updated_changelog = changelog_path.read_text() + assert "- Fix important bug [#123](https://github.com/egraphs-good/egglog-python/pull/123)" in updated_changelog + + +def test_update_changelog_duplicate_entry(): + """Test that duplicate PR entries are not added.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock changelog with existing entry + changelog_content = '''# Changelog + +## UNRELEASED + +- Fix important bug [#123](https://github.com/egraphs-good/egglog-python/pull/123) + +## 1.2.3 (2024-01-01) + +- Some old change +''' + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text(changelog_content) + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", "123", "Fix important bug"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 1 + assert "Changelog entry for PR #123 already exists" in result.stdout + + +def test_update_changelog_missing_file(): + """Test error handling when changelog file is missing.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Run the script without creating changelog file + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", "123", "Fix important bug"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 1 + assert "ERROR: Changelog file not found" in result.stdout + + +def test_bump_version_missing_cargo(): + """Test error handling when Cargo.toml file is missing.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create changelog but no Cargo.toml + docs_dir = temp_path / "docs" + docs_dir.mkdir() + changelog_path = docs_dir / "changelog.md" + changelog_path.write_text("# Changelog\n\n## UNRELEASED\n") + + # Run the script + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "bump_version", "patch"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 1 + assert "ERROR: Cargo.toml not found" in result.stdout + + +def test_custom_changelog_path(): + """Test using custom changelog path.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create mock changelog in custom location + changelog_content = '''# Changelog + +## UNRELEASED + +## 1.2.3 (2024-01-01) + +- Some old change +''' + custom_changelog_path = temp_path / "CHANGELOG.md" + custom_changelog_path.write_text(changelog_content) + + # Run the script with custom path + result = subprocess.run([sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", "456", "Add new feature", + "--changelog-path", "CHANGELOG.md"], + capture_output=True, text=True, cwd=temp_path) + + assert result.returncode == 0 + assert "Added changelog entry for PR #456: Add new feature" in result.stdout + + # Check changelog was updated + updated_changelog = custom_changelog_path.read_text() + assert "- Add new feature [#456](https://github.com/egraphs-good/egglog-python/pull/456)" in updated_changelog \ No newline at end of file From 996332b3024b8a154f3bf22624ab7bf4ea34b10d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:31:03 +0000 Subject: [PATCH 08/11] Combine version bump tests using pytest.param and add changelog entry update functionality Co-authored-by: saulshanabrook <1186124+saulshanabrook@users.noreply.github.com> --- modify_changelog.py | 22 +++-- python/tests/test_modify_changelog.py | 125 ++++++-------------------- 2 files changed, 40 insertions(+), 107 deletions(-) diff --git a/modify_changelog.py b/modify_changelog.py index 0b286caa..e78a436e 100644 --- a/modify_changelog.py +++ b/modify_changelog.py @@ -86,7 +86,7 @@ def update_changelog_version(file_path: Path, new_version: str) -> None: 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.""" + """Update the changelog with the new PR entry. If entry exists, update it; otherwise add new entry.""" # Read the current changelog with open(file_path, 'r', encoding='utf-8') as f: @@ -106,23 +106,29 @@ def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: # 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:]: + # Check if this PR entry already exists and update it if so + existing_entry_index = None + for i, line in enumerate(lines[content_start:], start=content_start): if f"[#{pr_number}]" in line: - print(f"Changelog entry for PR #{pr_number} already exists") - return False + existing_entry_index = i + break # 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) + if existing_entry_index is not None: + # Update existing entry + lines[existing_entry_index] = new_entry + print(f"Updated changelog entry for PR #{pr_number}: {pr_title}") + else: + # Insert the new entry at the beginning of the unreleased content + lines.insert(content_start, new_entry) + print(f"Added changelog entry for PR #{pr_number}: {pr_title}") # 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 diff --git a/python/tests/test_modify_changelog.py b/python/tests/test_modify_changelog.py index a3eb9cc1..8b0958fa 100644 --- a/python/tests/test_modify_changelog.py +++ b/python/tests/test_modify_changelog.py @@ -37,26 +37,31 @@ def test_update_changelog_subcommand_help(): assert "Pull request title" in result.stdout -def test_bump_version_patch(): - """Test version bumping with patch increment.""" +@pytest.mark.parametrize("start_version,bump_type,expected_version", [ + pytest.param("1.2.3", "patch", "1.2.4", id="patch_bump"), + pytest.param("1.2.3", "minor", "1.3.0", id="minor_bump"), + pytest.param("1.2.3", "major", "2.0.0", id="major_bump"), +]) +def test_bump_version(start_version, bump_type, expected_version): + """Test version bumping with different increment types.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Create mock Cargo.toml - cargo_content = '''[package] + cargo_content = f'''[package] name = "test-package" -version = "1.2.3" +version = "{start_version}" edition = "2021" ''' cargo_path = temp_path / "Cargo.toml" cargo_path.write_text(cargo_content) # Create mock changelog - changelog_content = '''# Changelog + changelog_content = f'''# Changelog ## UNRELEASED -## 1.2.3 (2024-01-01) +## {start_version} (2024-01-01) - Some old change ''' @@ -68,104 +73,20 @@ def test_bump_version_patch(): # Run the script result = subprocess.run([sys.executable, str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "bump_version", "patch"], + "bump_version", bump_type], capture_output=True, text=True, cwd=temp_path) assert result.returncode == 0 - assert result.stdout.strip() == "1.2.4" + assert result.stdout.strip() == expected_version # Check Cargo.toml was updated updated_cargo = cargo_path.read_text() - assert 'version = "1.2.4"' in updated_cargo + assert f'version = "{expected_version}"' in updated_cargo # Check changelog was updated updated_changelog = changelog_path.read_text() assert "## UNRELEASED" in updated_changelog - assert "## 1.2.4 (" in updated_changelog - - -def test_bump_version_minor(): - """Test version bumping with minor increment.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create mock Cargo.toml - cargo_content = '''[package] -name = "test-package" -version = "1.2.3" -edition = "2021" -''' - cargo_path = temp_path / "Cargo.toml" - cargo_path.write_text(cargo_content) - - # Create mock changelog - changelog_content = '''# Changelog - -## UNRELEASED - -## 1.2.3 (2024-01-01) - -- Some old change -''' - docs_dir = temp_path / "docs" - docs_dir.mkdir() - changelog_path = docs_dir / "changelog.md" - changelog_path.write_text(changelog_content) - - # Run the script - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "bump_version", "minor"], - capture_output=True, text=True, cwd=temp_path) - - assert result.returncode == 0 - assert result.stdout.strip() == "1.3.0" - - # Check Cargo.toml was updated - updated_cargo = cargo_path.read_text() - assert 'version = "1.3.0"' in updated_cargo - - -def test_bump_version_major(): - """Test version bumping with major increment.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create mock Cargo.toml - cargo_content = '''[package] -name = "test-package" -version = "1.2.3" -edition = "2021" -''' - cargo_path = temp_path / "Cargo.toml" - cargo_path.write_text(cargo_content) - - # Create mock changelog - changelog_content = '''# Changelog - -## UNRELEASED - -## 1.2.3 (2024-01-01) - -- Some old change -''' - docs_dir = temp_path / "docs" - docs_dir.mkdir() - changelog_path = docs_dir / "changelog.md" - changelog_path.write_text(changelog_content) - - # Run the script - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "bump_version", "major"], - capture_output=True, text=True, cwd=temp_path) - - assert result.returncode == 0 - assert result.stdout.strip() == "2.0.0" - - # Check Cargo.toml was updated - updated_cargo = cargo_path.read_text() - assert 'version = "2.0.0"' in updated_cargo + assert f"## {expected_version} (" in updated_changelog def test_update_changelog_new_entry(): @@ -202,7 +123,7 @@ def test_update_changelog_new_entry(): def test_update_changelog_duplicate_entry(): - """Test that duplicate PR entries are not added.""" + """Test that modifying PR title updates the existing changelog entry instead of making a new one.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) @@ -222,14 +143,20 @@ def test_update_changelog_duplicate_entry(): changelog_path = docs_dir / "changelog.md" changelog_path.write_text(changelog_content) - # Run the script + # Run the script with updated title for same PR result = subprocess.run([sys.executable, str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "update_changelog", "123", "Fix important bug"], + "update_changelog", "123", "Fix critical security bug"], capture_output=True, text=True, cwd=temp_path) - assert result.returncode == 1 - assert "Changelog entry for PR #123 already exists" in result.stdout + assert result.returncode == 0 + assert "Updated changelog entry for PR #123: Fix critical security bug" in result.stdout + + # Check that the changelog was updated, not duplicated + updated_changelog = changelog_path.read_text() + assert "- Fix critical security bug [#123](https://github.com/egraphs-good/egglog-python/pull/123)" in updated_changelog + assert "- Fix important bug [#123]" not in updated_changelog # Old entry should be gone + assert updated_changelog.count("[#123]") == 1 # Should only have one entry for PR 123 def test_update_changelog_missing_file(): From e1277298ab60b8301ebeb67218a69419eb42d8d6 Mon Sep 17 00:00:00 2001 From: GitHub Action <action@github.com> Date: Tue, 5 Aug 2025 19:34:06 +0000 Subject: [PATCH 09/11] Add changelog entry for PR #313 --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 3e3898fd..e6aa633c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,7 @@ _This project uses semantic versioning_ ## UNRELEASED -- [WIP] Automatically Generate Changelog Entries for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313) +- Automatically Create Changelog Entry 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. From 9605a91317369ede86d1e52634651d065864cc25 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook <s.shanabrook@gmail.com> Date: Tue, 5 Aug 2025 15:35:40 -0400 Subject: [PATCH 10/11] Update docs/changelog.md --- docs/changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index e6aa633c..1792f2a9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,6 @@ _This project uses semantic versioning_ ## UNRELEASED - Automatically Create Changelog Entry 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. From 55c4fe9f885e8bf64fb5db398a94d50bd2dfda40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:35:47 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- modify_changelog.py | 88 ++++++----- python/tests/test_modify_changelog.py | 210 +++++++++++++++++--------- 2 files changed, 182 insertions(+), 116 deletions(-) diff --git a/modify_changelog.py b/modify_changelog.py index e78a436e..2cdaa959 100644 --- a/modify_changelog.py +++ b/modify_changelog.py @@ -1,8 +1,8 @@ """ 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), +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. @@ -10,7 +10,7 @@ Usage: Version bumping: $ python modify_changelog.py bump_version [major|minor|patch] - + Adding PR entry: $ python modify_changelog.py update_changelog <number> <title> @@ -20,7 +20,7 @@ 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 @@ -59,20 +59,19 @@ 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 - + content_start = i + break + return unreleased_start, content_start @@ -87,25 +86,24 @@ def update_changelog_version(file_path: Path, new_version: str) -> None: 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. If entry exists, update it; otherwise add new entry.""" - # Read the current changelog - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, 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 and update it if so existing_entry_index = None for i, line in enumerate(lines[content_start:], start=content_start): @@ -113,9 +111,9 @@ def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: existing_entry_index = i break # Stop checking when we reach the next section - if line.startswith("## ") and not line.strip() == "## UNRELEASED": + if line.startswith("## ") and line.strip() != "## UNRELEASED": break - + if existing_entry_index is not None: # Update existing entry lines[existing_entry_index] = new_entry @@ -124,11 +122,11 @@ def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: # Insert the new entry at the beginning of the unreleased content lines.insert(content_start, new_entry) print(f"Added changelog entry for PR #{pr_number}: {pr_title}") - + # Write the updated changelog - with open(file_path, 'w', encoding='utf-8') as f: + with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) - + return True @@ -141,7 +139,7 @@ def handle_bump_version(args): 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) @@ -164,49 +162,47 @@ 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')) - + + 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') - + 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') - + 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') - + 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': + + if args.command == "bump_version": handle_bump_version(args) - elif args.command == 'update_changelog': + elif args.command == "update_changelog": handle_update_changelog(args) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/python/tests/test_modify_changelog.py b/python/tests/test_modify_changelog.py index 8b0958fa..7682e829 100644 --- a/python/tests/test_modify_changelog.py +++ b/python/tests/test_modify_changelog.py @@ -1,16 +1,22 @@ """Tests for modify_changelog.py script.""" -import tempfile import subprocess import sys +import tempfile from pathlib import Path + import pytest def test_modify_changelog_help(): """Test that the script shows help correctly.""" - result = subprocess.run([sys.executable, "modify_changelog.py", "--help"], - capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + result = subprocess.run( + [sys.executable, "modify_changelog.py", "--help"], + capture_output=True, + text=True, + cwd=Path(__file__).parent.parent.parent, + check=False, + ) assert result.returncode == 0 assert "Changelog modifier and version bumper" in result.stdout assert "bump_version" in result.stdout @@ -19,8 +25,13 @@ def test_modify_changelog_help(): def test_bump_version_subcommand_help(): """Test that bump_version subcommand shows help correctly.""" - result = subprocess.run([sys.executable, "modify_changelog.py", "bump_version", "--help"], - capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + result = subprocess.run( + [sys.executable, "modify_changelog.py", "bump_version", "--help"], + capture_output=True, + text=True, + cwd=Path(__file__).parent.parent.parent, + check=False, + ) assert result.returncode == 0 assert "Type of version bump" in result.stdout assert "major" in result.stdout @@ -30,23 +41,31 @@ def test_bump_version_subcommand_help(): def test_update_changelog_subcommand_help(): """Test that update_changelog subcommand shows help correctly.""" - result = subprocess.run([sys.executable, "modify_changelog.py", "update_changelog", "--help"], - capture_output=True, text=True, cwd=Path(__file__).parent.parent.parent) + result = subprocess.run( + [sys.executable, "modify_changelog.py", "update_changelog", "--help"], + capture_output=True, + text=True, + cwd=Path(__file__).parent.parent.parent, + check=False, + ) assert result.returncode == 0 assert "Pull request number" in result.stdout assert "Pull request title" in result.stdout -@pytest.mark.parametrize("start_version,bump_type,expected_version", [ - pytest.param("1.2.3", "patch", "1.2.4", id="patch_bump"), - pytest.param("1.2.3", "minor", "1.3.0", id="minor_bump"), - pytest.param("1.2.3", "major", "2.0.0", id="major_bump"), -]) +@pytest.mark.parametrize( + ("start_version", "bump_type", "expected_version"), + [ + pytest.param("1.2.3", "patch", "1.2.4", id="patch_bump"), + pytest.param("1.2.3", "minor", "1.3.0", id="minor_bump"), + pytest.param("1.2.3", "major", "2.0.0", id="major_bump"), + ], +) def test_bump_version(start_version, bump_type, expected_version): """Test version bumping with different increment types.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Create mock Cargo.toml cargo_content = f'''[package] name = "test-package" @@ -55,34 +74,42 @@ def test_bump_version(start_version, bump_type, expected_version): ''' cargo_path = temp_path / "Cargo.toml" cargo_path.write_text(cargo_content) - + # Create mock changelog - changelog_content = f'''# Changelog + changelog_content = f"""# Changelog ## UNRELEASED ## {start_version} (2024-01-01) - Some old change -''' +""" docs_dir = temp_path / "docs" docs_dir.mkdir() changelog_path = docs_dir / "changelog.md" changelog_path.write_text(changelog_content) - + # Run the script - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "bump_version", bump_type], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [ + sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "bump_version", + bump_type, + ], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 0 assert result.stdout.strip() == expected_version - + # Check Cargo.toml was updated updated_cargo = cargo_path.read_text() assert f'version = "{expected_version}"' in updated_cargo - + # Check changelog was updated updated_changelog = changelog_path.read_text() assert "## UNRELEASED" in updated_changelog @@ -93,30 +120,39 @@ def test_update_changelog_new_entry(): """Test adding a new PR entry to changelog.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Create mock changelog - changelog_content = '''# Changelog + changelog_content = """# Changelog ## UNRELEASED ## 1.2.3 (2024-01-01) - Some old change -''' +""" docs_dir = temp_path / "docs" docs_dir.mkdir() changelog_path = docs_dir / "changelog.md" changelog_path.write_text(changelog_content) - + # Run the script - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "update_changelog", "123", "Fix important bug"], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [ + sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", + "123", + "Fix important bug", + ], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 0 assert "Added changelog entry for PR #123: Fix important bug" in result.stdout - + # Check changelog was updated updated_changelog = changelog_path.read_text() assert "- Fix important bug [#123](https://github.com/egraphs-good/egglog-python/pull/123)" in updated_changelog @@ -126,9 +162,9 @@ def test_update_changelog_duplicate_entry(): """Test that modifying PR title updates the existing changelog entry instead of making a new one.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Create mock changelog with existing entry - changelog_content = '''# Changelog + changelog_content = """# Changelog ## UNRELEASED @@ -137,24 +173,36 @@ def test_update_changelog_duplicate_entry(): ## 1.2.3 (2024-01-01) - Some old change -''' +""" docs_dir = temp_path / "docs" docs_dir.mkdir() changelog_path = docs_dir / "changelog.md" changelog_path.write_text(changelog_content) - + # Run the script with updated title for same PR - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "update_changelog", "123", "Fix critical security bug"], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [ + sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", + "123", + "Fix critical security bug", + ], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 0 assert "Updated changelog entry for PR #123: Fix critical security bug" in result.stdout - + # Check that the changelog was updated, not duplicated updated_changelog = changelog_path.read_text() - assert "- Fix critical security bug [#123](https://github.com/egraphs-good/egglog-python/pull/123)" in updated_changelog + assert ( + "- Fix critical security bug [#123](https://github.com/egraphs-good/egglog-python/pull/123)" + in updated_changelog + ) assert "- Fix important bug [#123]" not in updated_changelog # Old entry should be gone assert updated_changelog.count("[#123]") == 1 # Should only have one entry for PR 123 @@ -163,13 +211,22 @@ def test_update_changelog_missing_file(): """Test error handling when changelog file is missing.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Run the script without creating changelog file - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "update_changelog", "123", "Fix important bug"], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [ + sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", + "123", + "Fix important bug", + ], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 1 assert "ERROR: Changelog file not found" in result.stdout @@ -178,19 +235,22 @@ def test_bump_version_missing_cargo(): """Test error handling when Cargo.toml file is missing.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Create changelog but no Cargo.toml docs_dir = temp_path / "docs" docs_dir.mkdir() changelog_path = docs_dir / "changelog.md" changelog_path.write_text("# Changelog\n\n## UNRELEASED\n") - + # Run the script - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "bump_version", "patch"], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [sys.executable, str(Path(__file__).parent.parent.parent / "modify_changelog.py"), "bump_version", "patch"], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 1 assert "ERROR: Cargo.toml not found" in result.stdout @@ -199,29 +259,39 @@ def test_custom_changelog_path(): """Test using custom changelog path.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) - + # Create mock changelog in custom location - changelog_content = '''# Changelog + changelog_content = """# Changelog ## UNRELEASED ## 1.2.3 (2024-01-01) - Some old change -''' +""" custom_changelog_path = temp_path / "CHANGELOG.md" custom_changelog_path.write_text(changelog_content) - + # Run the script with custom path - result = subprocess.run([sys.executable, - str(Path(__file__).parent.parent.parent / "modify_changelog.py"), - "update_changelog", "456", "Add new feature", - "--changelog-path", "CHANGELOG.md"], - capture_output=True, text=True, cwd=temp_path) - + result = subprocess.run( + [ + sys.executable, + str(Path(__file__).parent.parent.parent / "modify_changelog.py"), + "update_changelog", + "456", + "Add new feature", + "--changelog-path", + "CHANGELOG.md", + ], + capture_output=True, + text=True, + cwd=temp_path, + check=False, + ) + assert result.returncode == 0 assert "Added changelog entry for PR #456: Add new feature" in result.stdout - + # Check changelog was updated updated_changelog = custom_changelog_path.read_text() - assert "- Add new feature [#456](https://github.com/egraphs-good/egglog-python/pull/456)" in updated_changelog \ No newline at end of file + assert "- Add new feature [#456](https://github.com/egraphs-good/egglog-python/pull/456)" in updated_changelog