Skip to content

Commit bf04275

Browse files
Merge pull request #313 from egraphs-good/copilot/fix-312
Automatically Create Changelog Entry for PRs
2 parents 5aa044f + 55c4fe9 commit bf04275

File tree

7 files changed

+563
-80
lines changed

7 files changed

+563
-80
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Update Changelog
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited]
6+
7+
jobs:
8+
update-changelog:
9+
# Only run if this is not a PR from a fork to avoid permission issues
10+
# and not a commit made by GitHub Action to avoid infinite loops
11+
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'github-actions[bot]'
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
# Checkout the PR head ref
22+
ref: ${{ github.event.pull_request.head.ref }}
23+
token: ${{ secrets.GITHUB_TOKEN }}
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.12'
29+
30+
- name: Update changelog
31+
run: |
32+
python modify_changelog.py update_changelog \
33+
"${{ github.event.pull_request.number }}" \
34+
"${{ github.event.pull_request.title }}"
35+
36+
- name: Check for changes
37+
id: changes
38+
run: |
39+
if git diff --quiet docs/changelog.md; then
40+
echo "changed=false" >> $GITHUB_OUTPUT
41+
else
42+
echo "changed=true" >> $GITHUB_OUTPUT
43+
fi
44+
45+
- name: Commit and push changes
46+
if: steps.changes.outputs.changed == 'true'
47+
run: |
48+
git config --local user.email "[email protected]"
49+
git config --local user.name "GitHub Action"
50+
git add docs/changelog.md
51+
git commit -m "Add changelog entry for PR #${{ github.event.pull_request.number }}"
52+
git push

.github/workflows/version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- run: |
4040
git config user.name github-actions[bot]
4141
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
42-
VERSION=$(python increment_version.py $TYPE)
42+
VERSION=$(python modify_changelog.py bump_version $TYPE)
4343
git checkout -b "version-$VERSION"
4444
git commit -am "Version $VERSION"
4545
git push -u origin HEAD

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7+
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
78
- Upgrade egglog which includes new backend.
89
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.
910
Also changes the representation to be an index into a list instead of the ID, making egglog programs more deterministic.

docs/reference/contributing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ or bug fixes.
8484

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

87+
#### Changelog Automation
88+
89+
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.
90+
8791
## Documentation
8892

8993
We use the [Diátaxis framework](https://diataxis.fr/) to organize our documentation. The "explanation" section has

increment_version.py

Lines changed: 0 additions & 79 deletions
This file was deleted.

modify_changelog.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""
2+
Changelog Modifier and Version Bumper for Cargo.toml and Changelog.md
3+
4+
This script automates the process of version bumping and changelog updates for Rust projects managed with Cargo.
5+
It reads the version from the cargo.toml file, increments it based on the specified component (major, minor, or patch),
6+
and updates both the cargo.toml and changelog.md files accordingly.
7+
8+
It can also add PR entries to the UNRELEASED section of the changelog.
9+
10+
Usage:
11+
Version bumping:
12+
$ python modify_changelog.py bump_version [major|minor|patch]
13+
14+
Adding PR entry:
15+
$ python modify_changelog.py update_changelog <number> <title>
16+
17+
Subcommands:
18+
-----------
19+
bump_version - Increments version and updates changelog
20+
major - Increments the major component of the version, sets minor and patch to 0
21+
minor - Increments the minor component of the version, sets patch to 0
22+
patch - Increments the patch component of the version
23+
24+
update_changelog - Add a PR entry to the UNRELEASED section
25+
number - PR number
26+
title - PR title
27+
28+
From https://chat.openai.com/share/6b08906d-23a3-4193-9f4e-87076ce56ddb
29+
30+
"""
31+
32+
import argparse
33+
import datetime
34+
import re
35+
import sys
36+
from pathlib import Path
37+
38+
39+
def bump_version(major: int, minor: int, patch: int, part: str) -> str:
40+
if part == "major":
41+
major += 1
42+
minor = 0
43+
patch = 0
44+
elif part == "minor":
45+
minor += 1
46+
patch = 0
47+
elif part == "patch":
48+
patch += 1
49+
return f"{major}.{minor}.{patch}"
50+
51+
52+
def update_cargo_toml(file_path: Path, new_version: str) -> None:
53+
content = file_path.read_text()
54+
content = re.sub(r'version = "(\d+\.\d+\.\d+)"', f'version = "{new_version}"', content, count=1)
55+
file_path.write_text(content)
56+
57+
58+
def find_unreleased_section(lines):
59+
"""Find the line number where UNRELEASED section starts and ends."""
60+
unreleased_start = None
61+
content_start = None
62+
63+
for i, line in enumerate(lines):
64+
if line.strip() == "## UNRELEASED":
65+
unreleased_start = i
66+
continue
67+
68+
if unreleased_start is not None and content_start is None:
69+
# Skip empty lines after ## UNRELEASED
70+
if line.strip() == "":
71+
continue
72+
content_start = i
73+
break
74+
75+
return unreleased_start, content_start
76+
77+
78+
def update_changelog_version(file_path: Path, new_version: str) -> None:
79+
"""Update changelog for version bump - replaces UNRELEASED with versioned section."""
80+
today = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d")
81+
content = file_path.read_text()
82+
new_section = f"## UNRELEASED\n\n## {new_version} ({today})"
83+
content = content.replace("## UNRELEASED", new_section, 1)
84+
file_path.write_text(content)
85+
86+
87+
def update_changelog_pr(file_path: Path, pr_number: str, pr_title: str, pr_url: str) -> bool:
88+
"""Update the changelog with the new PR entry. If entry exists, update it; otherwise add new entry."""
89+
# Read the current changelog
90+
with open(file_path, encoding="utf-8") as f:
91+
lines = f.readlines()
92+
93+
# Find the UNRELEASED section
94+
unreleased_start, content_start = find_unreleased_section(lines)
95+
96+
if unreleased_start is None:
97+
print("ERROR: Could not find '## UNRELEASED' section in changelog")
98+
return False
99+
100+
if content_start is None:
101+
print("ERROR: Could not find content start after UNRELEASED section")
102+
return False
103+
104+
# Create the new entry
105+
new_entry = f"- {pr_title} [#{pr_number}]({pr_url})\n"
106+
107+
# Check if this PR entry already exists and update it if so
108+
existing_entry_index = None
109+
for i, line in enumerate(lines[content_start:], start=content_start):
110+
if f"[#{pr_number}]" in line:
111+
existing_entry_index = i
112+
break
113+
# Stop checking when we reach the next section
114+
if line.startswith("## ") and line.strip() != "## UNRELEASED":
115+
break
116+
117+
if existing_entry_index is not None:
118+
# Update existing entry
119+
lines[existing_entry_index] = new_entry
120+
print(f"Updated changelog entry for PR #{pr_number}: {pr_title}")
121+
else:
122+
# Insert the new entry at the beginning of the unreleased content
123+
lines.insert(content_start, new_entry)
124+
print(f"Added changelog entry for PR #{pr_number}: {pr_title}")
125+
126+
# Write the updated changelog
127+
with open(file_path, "w", encoding="utf-8") as f:
128+
f.writelines(lines)
129+
130+
return True
131+
132+
133+
def handle_bump_version(args):
134+
"""Handle version bump subcommand."""
135+
part = args.bump_type
136+
cargo_path = Path("Cargo.toml")
137+
changelog_path = Path("docs/changelog.md")
138+
139+
if not cargo_path.exists():
140+
print("ERROR: Cargo.toml not found.")
141+
sys.exit(1)
142+
143+
if not changelog_path.exists():
144+
print("ERROR: Changelog file not found.")
145+
sys.exit(1)
146+
147+
cargo_content = cargo_path.read_text()
148+
version_match = re.search(r'version = "(\d+)\.(\d+)\.(\d+)"', cargo_content)
149+
if version_match:
150+
major, minor, patch = map(int, version_match.groups())
151+
else:
152+
print("Current version not found in cargo.toml.")
153+
sys.exit(1)
154+
155+
new_version = bump_version(major, minor, patch, part)
156+
update_cargo_toml(cargo_path, new_version)
157+
update_changelog_version(changelog_path, new_version)
158+
print(new_version)
159+
160+
161+
def handle_update_changelog(args):
162+
"""Handle update changelog subcommand."""
163+
pr_number = args.number
164+
pr_title = args.title
165+
166+
# Construct PR URL from repository info and PR number
167+
# Default to the egglog-python repository
168+
pr_url = f"https://github.com/egraphs-good/egglog-python/pull/{pr_number}"
169+
170+
changelog_path = Path(getattr(args, "changelog_path", "docs/changelog.md"))
171+
172+
if not changelog_path.exists():
173+
print(f"ERROR: Changelog file not found: {changelog_path}")
174+
sys.exit(1)
175+
176+
success = update_changelog_pr(changelog_path, pr_number, pr_title, pr_url)
177+
if not success:
178+
sys.exit(1)
179+
180+
181+
def main():
182+
parser = argparse.ArgumentParser(description="Changelog modifier and version bumper")
183+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
184+
185+
# Bump version subcommand
186+
bump_parser = subparsers.add_parser("bump_version", help="Bump version and update changelog")
187+
bump_parser.add_argument("bump_type", choices=["major", "minor", "patch"], help="Type of version bump")
188+
189+
# Update changelog subcommand
190+
changelog_parser = subparsers.add_parser("update_changelog", help="Add PR entry to changelog")
191+
changelog_parser.add_argument("number", help="Pull request number")
192+
changelog_parser.add_argument("title", help="Pull request title")
193+
changelog_parser.add_argument("--changelog-path", default="docs/changelog.md", help="Path to changelog file")
194+
195+
args = parser.parse_args()
196+
197+
if not args.command:
198+
parser.print_help()
199+
sys.exit(1)
200+
201+
if args.command == "bump_version":
202+
handle_bump_version(args)
203+
elif args.command == "update_changelog":
204+
handle_update_changelog(args)
205+
206+
207+
if __name__ == "__main__":
208+
main()

0 commit comments

Comments
 (0)