Skip to content

Commit 420857a

Browse files
authored
Automate Release Notes Creation (#14675)
1 parent 99e1c75 commit 420857a

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import os
2+
import requests
3+
import re
4+
from datetime import datetime
5+
from collections import defaultdict
6+
from datetime import timezone
7+
8+
GITHUB_TOKEN = os.environ['GITHUB_TOKEN']
9+
REPO = os.environ['GITHUB_REPOSITORY']
10+
RELEASE_TAG = os.environ.get('RELEASE_TAG', 'Unreleased')
11+
START_DATE_STR = os.environ.get('START_DATE') # expected format: yyyy-mm-dd
12+
END_DATE_STR = os.environ.get('END_DATE') # expected format: yyyy-mm-dd
13+
14+
def parse_date_aware(date_str):
15+
# append UTC offset if missing and parse
16+
if date_str and 'T' not in date_str:
17+
date_str = date_str + "T00:00:00+00:00"
18+
elif date_str and date_str.endswith('Z'):
19+
date_str = date_str.replace('Z', '+00:00')
20+
return datetime.fromisoformat(date_str) if date_str else None
21+
22+
START_DATE = parse_date_aware(START_DATE_STR)
23+
END_DATE = parse_date_aware(END_DATE_STR)
24+
25+
API_URL = f"https://api.github.com/repos/{REPO}/commits"
26+
HEADERS = {"Authorization": f"token {GITHUB_TOKEN}"}
27+
28+
def fetch_commits():
29+
commits = []
30+
page = 1
31+
params = {
32+
"per_page": 100,
33+
}
34+
if START_DATE_STR:
35+
params["since"] = START_DATE_STR + "T00:00:00Z"
36+
if END_DATE_STR:
37+
params["until"] = END_DATE_STR + "T23:59:59Z"
38+
39+
while True:
40+
params["page"] = page
41+
response = requests.get(API_URL, headers=HEADERS, params=params)
42+
data = response.json()
43+
if not data or 'message' in data:
44+
break
45+
commits.extend(data)
46+
page += 1
47+
return commits
48+
49+
def is_bot_commit(commit):
50+
# Same as before, skip commits by known bots (dependabot, etc.)
51+
author = commit.get('author')
52+
commit_author_name = commit['commit']['author']['name'].lower() if commit['commit']['author']['name'] else ''
53+
author_login = author.get('login', '').lower() if author else ''
54+
bot_indicators = ['bot', 'dependabot', 'actions-user']
55+
if any(bot_name in author_login for bot_name in bot_indicators):
56+
return True
57+
if any(bot_name in commit_author_name for bot_name in bot_indicators):
58+
return True
59+
60+
# Also skip commits with messages indicating package bumps or updates
61+
message = commit['commit']['message'].lower()
62+
63+
# List of keywords to detect in commit messages
64+
keywords = ['bump', 'applying package updates', 'no_ci', 'no ci']
65+
66+
for keyword in keywords:
67+
if keyword in message:
68+
return True
69+
70+
return False
71+
72+
def filter_commits_by_date(commits):
73+
if not START_DATE and not END_DATE:
74+
filtered = []
75+
for c in commits:
76+
if not is_bot_commit(c):
77+
filtered.append(c)
78+
return filtered
79+
80+
filtered = []
81+
for commit in commits:
82+
if is_bot_commit(commit):
83+
continue
84+
commit_date = datetime.fromisoformat(commit['commit']['author']['date'].replace("Z", "+00:00"))
85+
if START_DATE and commit_date < START_DATE:
86+
continue
87+
if END_DATE and commit_date > END_DATE:
88+
continue
89+
filtered.append(commit)
90+
return filtered
91+
92+
def categorize_commits(commits):
93+
categories = {
94+
"Reliability": [],
95+
"New Features": [],
96+
"Breaking Changes": [],
97+
"New Architecture-specific changes": [],
98+
"Other": []
99+
}
100+
101+
# Keywords for each category (lowercase)
102+
keywords = {
103+
"Reliability": ["fix", "bug", "error", "issue", "crash", "fault", "defect", "patch"],
104+
"New Features": ["feature", "add", "introduce", "support", "enable"],
105+
"Breaking Changes": ["break", "remove", "deprecated", "incompatible", "remove support", "change api", "breaking"],
106+
"New Architecture-specific changes": ["implement", "new", "fabric", "arch", "modal", "architecture", "refactor", "restructure", "modularize"]
107+
}
108+
109+
for commit in commits:
110+
message = commit['commit']['message']
111+
sha = commit['sha']
112+
url = commit['html_url']
113+
entry = f"- {message.splitlines()[0]} [{message.splitlines()[0]} · microsoft/react-native-windows@{sha[:7]} (github.com)]({url})"
114+
msg_lower = message.lower()
115+
116+
# Track which categories matched to avoid multiple assignments
117+
matched_categories = []
118+
119+
for category, keys in keywords.items():
120+
if any(key in msg_lower for key in keys):
121+
matched_categories.append(category)
122+
123+
# Prioritize categories by order: Breaking > New Features > Reliability > Architecture > Other
124+
if "Breaking Changes" in matched_categories:
125+
categories["Breaking Changes"].append(entry)
126+
elif "New Features" in matched_categories:
127+
categories["New Features"].append(entry)
128+
elif "Reliability" in matched_categories:
129+
categories["Reliability"].append(entry)
130+
elif "New Architecture-specific changes" in matched_categories:
131+
categories["New Architecture-specific changes"].append(entry)
132+
else:
133+
categories["Other"].append(entry)
134+
135+
return categories
136+
137+
def generate_release_notes(commits, categories):
138+
if commits:
139+
# Use input dates if provided, else fallback to commit dates
140+
start_date = START_DATE_STR or datetime.fromisoformat(
141+
commits[0]['commit']['author']['date'].replace("Z", "+00:00")).strftime("%Y-%m-%d")
142+
end_date = END_DATE_STR or datetime.fromisoformat(
143+
commits[-1]['commit']['author']['date'].replace("Z", "+00:00")).strftime("%Y-%m-%d")
144+
# Format to mm/dd/yyyy for release notes
145+
start_date_fmt = datetime.fromisoformat(start_date).strftime("%m/%d/%Y")
146+
end_date_fmt = datetime.fromisoformat(end_date).strftime("%m/%d/%Y")
147+
else:
148+
start_date_fmt = START_DATE_STR or "N/A"
149+
end_date_fmt = END_DATE_STR or "N/A"
150+
151+
notes = []
152+
notes.append(f"{RELEASE_TAG} Release Notes")
153+
notes.append("")
154+
notes.append(
155+
f"We're excited to release React Native Windows {RELEASE_TAG} targeting React Native {RELEASE_TAG}! "
156+
f"There have been many changes to both react-native-windows and react-native itself, and we would love your "
157+
f"feedback on anything that doesn't work as expected. This release includes the commits to React Native Windows "
158+
f"from {start_date_fmt} - {end_date_fmt}."
159+
)
160+
notes.append("")
161+
notes.append("## How to upgrade")
162+
notes.append("You can view the changes made to the default new React Native Windows applications for C++ and C# "
163+
"using React Native Upgrade Helper. See this [document](https://microsoft.github.io/react-native-windows/docs/upgrade-app) for more details.")
164+
notes.append("")
165+
for category, entries in categories.items():
166+
if entries:
167+
notes.append(f"## {category}")
168+
notes.extend(entries)
169+
notes.append("")
170+
return "\n".join(notes)
171+
172+
def main():
173+
commits = fetch_commits()
174+
commits = filter_commits_by_date(commits)
175+
categories = categorize_commits(commits)
176+
release_notes = generate_release_notes(commits, categories)
177+
with open("release_notes.md", "w", encoding="utf-8") as f:
178+
f.write(release_notes)
179+
180+
if __name__ == "__main__":
181+
main()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Generate Release Notes
2+
3+
# commenting out the trigger for now, find way to run this script in some 'yarn release-notes' command
4+
# on:
5+
# push:
6+
# paths:
7+
# - '.github/workflows/generate-release-notes.yml'
8+
# pull_request:
9+
# paths:
10+
# - '.github/workflows/generate-release-notes.yml'
11+
12+
jobs:
13+
generate-release-notes:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
with:
20+
repository: anupriya13/react-native-windows
21+
fetch-depth: 0
22+
ref: ${{ github.head_ref != '' && github.head_ref || github.ref_name }}
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v4
26+
with:
27+
python-version: 3.11.12
28+
29+
- name: Install dependencies
30+
run: pip install requests
31+
32+
- name: Generate release notes file
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
RELEASE_TAG: '0.79.0' # Adjust this release as needed
36+
START_DATE: '2025-05-06' # Adjust this date as needed
37+
END_DATE: '2025-05-30' # Adjust this date as needed
38+
run: |
39+
python .github/scripts/generate_release_notes.py > release_notes.md
40+
mkdir -p .github/release_notes
41+
mv release_notes.md .github/release_notes/release_notes.md
42+
43+
- name: Upload release notes
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: release-notes
47+
path: .github/release_notes/release_notes.md

0 commit comments

Comments
 (0)