Skip to content

Commit dbe698e

Browse files
authored
llama-dev: add changelog generation command (#19925)
1 parent 8ba84e9 commit dbe698e

File tree

3 files changed

+114
-0
lines changed

3 files changed

+114
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ChangeLog
22

3+
<!--- generated changelog --->
4+
35
## [2025-09-15]
46

57
### `llama-index-core` [0.14.2]

llama-dev/llama_dev/release/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import click
22

3+
from .changelog import changelog
34
from .check import check
45

56

@@ -9,3 +10,4 @@ def release():
910

1011

1112
release.add_command(check)
13+
release.add_command(changelog)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import json
2+
import re
3+
import subprocess
4+
from datetime import date
5+
6+
import click
7+
8+
from llama_dev.utils import find_all_packages, get_changed_packages, load_pyproject
9+
10+
11+
def run_command(command: str) -> str:
12+
"""Helper to run a shell command and return the output."""
13+
result = subprocess.run(command, shell=True, capture_output=True, text=True)
14+
if result.returncode != 0:
15+
raise RuntimeError(f"Command failed: {command}\n{result.stderr}")
16+
return result.stdout.strip()
17+
18+
19+
@click.command(short_help="Generate the changelog from the previous release tag")
20+
@click.option(
21+
"--dry-run",
22+
is_flag=True,
23+
help="Show the changelog text without altering the CHANGELOG.md file",
24+
)
25+
@click.pass_obj
26+
def changelog(obj: dict, dry_run: bool) -> None:
27+
"""
28+
Generate the changelog in markdown syntax.
29+
30+
\b
31+
This command will:
32+
- get the list of GitHub PRs that happened since the last release tag
33+
- create a bullet list in Markdown syntax using the PR titles
34+
- group the changes per package, depending on the path that changed
35+
""" # noqa
36+
console = obj["console"]
37+
repo_root = obj["repo_root"]
38+
all_packages = find_all_packages(repo_root)
39+
40+
try:
41+
# Get the latest git tag
42+
latest_tag = run_command('git describe --tags --match "v[0-9]*" --abbrev=0')
43+
console.print(f"Generating changelog since tag '{latest_tag}'...")
44+
45+
# Get commits since the last tag and extract PR numbers
46+
log_output = run_command(f'git log {latest_tag}..HEAD --pretty="format:%H %s"')
47+
pr_numbers = set()
48+
pr_pattern = re.compile(r"\(#(\d+)\)")
49+
for line in log_output.splitlines():
50+
match = pr_pattern.search(line)
51+
if match:
52+
pr_numbers.add(match.group(1))
53+
54+
if not pr_numbers:
55+
raise click.ClickException("No pull requests found since the last tag.")
56+
57+
package_prs = {}
58+
package_versions = {}
59+
60+
with click.progressbar(sorted(pr_numbers), label="Fetching PR details") as bar:
61+
for pr_number in bar:
62+
try:
63+
pr_json_str = run_command(
64+
f"gh pr view {pr_number} --json number,title,url,files"
65+
)
66+
pr_data = json.loads(pr_json_str)
67+
files = [repo_root / f["path"] for f in pr_data.get("files", [])]
68+
69+
changed_packages = get_changed_packages(files, all_packages)
70+
for pkg in changed_packages:
71+
pkg_name = pkg.name
72+
if pkg_name not in package_prs:
73+
package_prs[pkg_name] = []
74+
package_data = load_pyproject(pkg)
75+
ver = package_data["project"]["version"]
76+
package_versions[pkg_name] = ver
77+
78+
package_prs[pkg_name].append(pr_data)
79+
80+
except Exception as e:
81+
console.print(
82+
f"Warning: Could not fetch details for PR #{pr_number}. {e}",
83+
style="error",
84+
)
85+
86+
# Generate the markdown output
87+
changelog_text = f"<!--- generated changelog --->\n\n## [{date.today().strftime('%Y-%m-%d')}]"
88+
sorted_pkgs = sorted(package_prs.keys())
89+
for pkg in sorted_pkgs:
90+
changelog_text += f"\n\n### {pkg} [{package_versions[pkg]}]\n"
91+
prs = sorted(package_prs[pkg], key=lambda p: p["number"])
92+
for pr in prs:
93+
changelog_text += f"- {pr['title']} ([#{pr['number']}]({pr['url']}))"
94+
95+
if dry_run:
96+
click.echo(changelog_text)
97+
else:
98+
with open(repo_root / "CHANGELOG.md", "r+") as f:
99+
content = f.read()
100+
f.seek(0)
101+
f.truncate()
102+
f.write(
103+
content.replace("<!--- generated changelog --->", changelog_text)
104+
)
105+
106+
except FileNotFoundError:
107+
click.ClickException(
108+
"Error: 'gh' command not found. "
109+
"Please ensure it's installed and in your PATH."
110+
)

0 commit comments

Comments
 (0)