Skip to content

Commit 1fb0cc3

Browse files
committed
Add script to generate release notes from merged PRs
1 parent 2a952b3 commit 1fb0cc3

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (C) 2019 Intel Corporation. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3+
4+
# get the last release tag from git, and use it to find all merged PRs since
5+
# that tag. extract their titles, labels and PR numbers and classify them into
6+
# break changes, new # features, enhancements, bug fixes, and others based on
7+
# their labels.
8+
#
9+
# The release version is generated based on the last release tag. The tag
10+
# should be in the format of "WAMR-major.minor.patch", where major, minor,
11+
# and patch are numbers. If there is new feature in merged PRs, the minor
12+
# version should be increased by 1, and the patch version should be reset to 0.
13+
# If there is no new feature, the patch version should be increased by 1.
14+
#
15+
# new content should be inserted into the beginning of the RELEASE_NOTES.md file.
16+
# in a form like:
17+
#
18+
# ``` markdown
19+
# ## WAMR-major.minor.patch
20+
#
21+
# ### Breaking Changes
22+
#
23+
# ### New Features
24+
#
25+
# ### Bug Fixes
26+
#
27+
# ### Enhancements
28+
#
29+
# ### Others
30+
# ```
31+
# The path of RELEASE_NOTES.md is passed in as an command line argument.
32+
33+
import json
34+
import os
35+
import subprocess
36+
import sys
37+
38+
39+
def run_cmd(cmd):
40+
result = subprocess.run(
41+
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
42+
)
43+
if result.returncode != 0:
44+
print(f"Error running command: {cmd}\n{result.stderr}")
45+
sys.exit(1)
46+
return result.stdout.strip()
47+
48+
49+
def get_last_release_tag():
50+
tags = run_cmd("git tag --sort=-creatordate").splitlines()
51+
for tag in tags:
52+
if tag.startswith("WAMR-"):
53+
return tag
54+
return None
55+
56+
57+
def get_merged_prs_since(tag):
58+
# Get commits since the last release tag
59+
log_cmd = f'git log {tag}..HEAD --pretty=format:"%s"'
60+
logs = run_cmd(log_cmd).splitlines()
61+
62+
print(f"Found {len(logs)} merge commits since last tag '{tag}'.")
63+
64+
pr_numbers = []
65+
for line in logs:
66+
# assume the commit message ends with "(#PR_NUMBER)"
67+
if not line.endswith(")"):
68+
continue
69+
70+
# Extract PR number
71+
parts = line.split("(#")
72+
if len(parts) < 2:
73+
continue
74+
75+
# PR_NUMBER) -> PR_NUMBER
76+
pr_num = parts[1][:-1]
77+
pr_numbers.append(pr_num)
78+
return pr_numbers
79+
80+
81+
def get_pr_info(pr_number):
82+
# Use GitHub CLI to get PR info
83+
pr_json = run_cmd(f"gh pr view {pr_number} --json title,labels,url")
84+
pr_data = json.loads(pr_json)
85+
title = pr_data.get("title", "")
86+
labels = [label["name"] for label in pr_data.get("labels", [])]
87+
url = pr_data.get("url", "")
88+
return title, labels, url
89+
90+
91+
def classify_pr(title, labels, url):
92+
entry = f"- {title} (#{url.split('/')[-1]})"
93+
if "breaking-change" in labels:
94+
return "Breaking Changes", entry
95+
elif "new feature" in labels:
96+
return "New Features", entry
97+
elif "enhancement" in labels:
98+
return "Enhancements", entry
99+
elif "bug-fix" in labels:
100+
return "Bug Fixes", entry
101+
else:
102+
return "Others", entry
103+
104+
105+
def generate_release_notes(pr_numbers):
106+
sections = {
107+
"Breaking Changes": [],
108+
"New Features": [],
109+
"Bug Fixes": [],
110+
"Enhancements": [],
111+
"Others": [],
112+
}
113+
for pr_num in pr_numbers:
114+
title, labels, url = get_pr_info(pr_num)
115+
section, entry = classify_pr(title, labels, url)
116+
sections[section].append(entry)
117+
return sections
118+
119+
120+
def generate_version_string(last_tag, sections):
121+
last_tag_parts = last_tag.split("-")[-1]
122+
major, minor, patch = map(int, last_tag_parts.split("."))
123+
124+
if sections["New Features"]:
125+
minor += 1
126+
patch = 0
127+
else:
128+
patch += 1
129+
130+
return f"WAMR-{major}.{minor}.{patch}"
131+
132+
133+
def format_release_notes(version, sections):
134+
notes = [f"## {version}\n"]
135+
for section in [
136+
"Breaking Changes",
137+
"New Features",
138+
"Bug Fixes",
139+
"Enhancements",
140+
"Others",
141+
]:
142+
notes.append(f"### {section}\n")
143+
if sections[section]:
144+
notes.extend(sections[section])
145+
else:
146+
notes.append("")
147+
notes.append("")
148+
return "\n".join(notes)
149+
150+
151+
def insert_release_notes(notes, RELEASE_NOTES_FILE):
152+
with open(RELEASE_NOTES_FILE, "r", encoding="utf-8") as f:
153+
old_content = f.read()
154+
with open(RELEASE_NOTES_FILE, "w", encoding="utf-8") as f:
155+
f.write(notes + old_content)
156+
157+
158+
def set_action_output(name, value):
159+
"""Set the output for GitHub Actions."""
160+
if not os.getenv("GITHUB_OUTPUT"):
161+
return
162+
163+
print(f"{name}={value}")
164+
165+
166+
def main(RELEASE_NOTES_FILE):
167+
last_tag = get_last_release_tag()
168+
if not last_tag:
169+
print("No release tag found.")
170+
sys.exit(1)
171+
172+
print(f"Last release tag: {last_tag}")
173+
174+
pr_numbers = get_merged_prs_since(last_tag)
175+
if not pr_numbers:
176+
print("No merged PRs since last release.")
177+
sys.exit(0)
178+
179+
print(f"Found {len(pr_numbers)} merged PRs since last release.")
180+
print(f"PR numbers: {', '.join(pr_numbers)}")
181+
182+
sections = generate_release_notes(pr_numbers)
183+
184+
next_version = generate_version_string(last_tag, sections)
185+
print(f"Next version will be: {next_version}")
186+
187+
notes = format_release_notes(next_version, sections)
188+
insert_release_notes(notes, RELEASE_NOTES_FILE)
189+
print(f"Release notes for {next_version} generated and inserted.")
190+
191+
set_action_output("next_version", next_version)
192+
193+
194+
if __name__ == "__main__":
195+
if len(sys.argv) > 1:
196+
RELEASE_NOTES_FILE = sys.argv[1]
197+
else:
198+
RELEASE_NOTES_FILE = os.path.join(
199+
os.path.dirname(__file__), "../../RELEASE_NOTES.md"
200+
)
201+
202+
main(RELEASE_NOTES_FILE)

0 commit comments

Comments
 (0)