Skip to content

Commit 9482315

Browse files
Copilotanupriya13
andcommitted
Add @rnw-scripts/generate-release-notes package with updated version and dates
Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com>
1 parent a4f0072 commit 9482315

6 files changed

Lines changed: 274 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add a new \"yarn release-notes\" script to generate release notes",
4+
"packageName": "@rnw-scripts/generate-release-notes",
5+
"email": "copilot@example.com",
6+
"dependentChangeType": "patch"
7+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"format": "format-files -i -style=file",
1313
"format:verify": "format-files -i -style=file -verify",
1414
"postinstall": "yarn build",
15+
"release-notes": "yarn workspace @rnw-scripts/generate-release-notes release-notes",
1516
"spellcheck": "npx cspell",
1617
"test": "lage test --verbose --passWithNoTests",
1718
"validate-overrides": "react-native-platform-override validate"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
release_notes.md
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
### Type of Change
2+
Automate release notes creation by adding a new yarn script. Automating the process of creating release notes so that we don't have to manually copy paste the commits.
3+
4+
5+
### Why
6+
To save us some time when generating release notes. Fetches commit from start and end date range, ignores bots and creates the release notes md file. It also categorizes the commits. Please cross-check the generated release-notes.md file and update it manually if required like regrouping commits or updating the Summary/Explanation for the PR commit.
7+
8+
## Format
9+
10+
`Explanation. [PRName (#11168) · microsoft/react-native-windows@aaaaaaa (github.com)](link)`
11+
12+
### Steps to follow
13+
14+
#### 1. Set up your personal access token
15+
16+
- Go to GitHub and log in: https://github.com/
17+
- Click on your profile picture (top-right corner), then click Settings
18+
- On the left sidebar, click Developer settings
19+
- Then click Personal access tokens > Tokens (classic)
20+
- Click Generate new token > Generate new token (classic)
21+
- Give it a name like "Release Notes Script"
22+
- Set an expiration (choose less than 90 days)
23+
- Under Scopes, select the permissions your script needs. For fetching commits and repo info, you typically need:
24+
repo (full control of private repositories)
25+
or at least repo:status, repo_deployment, public_repo (for public repos)
26+
- Click Generate token
27+
- Find the token you're using (whichever token you created).
28+
- You should see a message or option to "Grant access to your organization" or "Authorize SAML SSO" for your token.
29+
- Click that button to authorize the token with the organization.
30+
- Copy the generated token
31+
32+
#### 2. Set env variables at root of the repo
33+
34+
```
35+
set GITHUB_TOKEN=<your-personal-access-token>
36+
set RELEASE_TAG=0.80.0
37+
set START_DATE=2025-06-01
38+
set END_DATE=2025-07-16
39+
40+
```
41+
#### 3. Run "`yarn release-notes`" at the root of the repo
42+
43+
#### 4. You will see a release-notes.md file generated at packages\@rnw-scripts\generate-release-notes\release_notes.md which will have all the data you need.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import fetch from 'node-fetch';
2+
import fs from 'fs';
3+
import process from 'process';
4+
5+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
6+
const REPO = 'microsoft/react-native-windows';
7+
const RELEASE_TAG = process.env.RELEASE_TAG || 'Unreleased';
8+
const START_DATE = process.env.START_DATE;
9+
const END_DATE = process.env.END_DATE;
10+
11+
if (!GITHUB_TOKEN) {
12+
console.error('GITHUB_TOKEN is not set. Please set it before running.');
13+
process.exit(1);
14+
}
15+
if (!START_DATE || !END_DATE) {
16+
console.error('START_DATE and END_DATE are required.');
17+
process.exit(1);
18+
}
19+
20+
console.log(`Generating release notes for ${REPO} from ${START_DATE} to ${END_DATE}...`);
21+
22+
const HEADERS = {
23+
Authorization: `token ${GITHUB_TOKEN}`,
24+
Accept: 'application/vnd.github+json',
25+
};
26+
27+
function parseDate(dateStr) {
28+
return dateStr ? new Date(dateStr) : null;
29+
}
30+
31+
const START = parseDate(START_DATE);
32+
const END = parseDate(END_DATE);
33+
34+
function isBotCommit(commit) {
35+
const author = commit.author;
36+
const commitAuthorName = (commit.commit.author.name || '').toLowerCase();
37+
const authorLogin = (author?.login || '').toLowerCase();
38+
const botIndicators = ['bot', 'dependabot', 'actions-user'];
39+
const msg = commit.commit.message.toLowerCase();
40+
41+
if (
42+
botIndicators.some(
43+
(bot) => authorLogin.includes(bot) || commitAuthorName.includes(bot)
44+
)
45+
)
46+
return true;
47+
if (['bump', 'applying package updates', 'no_ci', 'no ci'].some((k) => msg.includes(k)))
48+
return true;
49+
return false;
50+
}
51+
52+
function formatDate(date) {
53+
return date ? new Date(date).toLocaleDateString('en-US') : 'N/A';
54+
}
55+
56+
async function fetchCommits() {
57+
const commits = [];
58+
let page = 1;
59+
const perPage = 100;
60+
61+
while (true) {
62+
const url = new URL(`https://api.github.com/repos/${REPO}/commits`);
63+
url.searchParams.set('per_page', perPage);
64+
url.searchParams.set('page', page);
65+
if (START_DATE) url.searchParams.set('since', START_DATE + 'T00:00:00Z');
66+
if (END_DATE) url.searchParams.set('until', END_DATE + 'T23:59:59Z');
67+
68+
console.log(`Fetching commits from: ${url.toString()}`);
69+
70+
const res = await fetch(url, { headers: HEADERS });
71+
72+
if (!res.ok) {
73+
console.error(`GitHub API request failed: ${res.status} ${res.statusText}`);
74+
const errText = await res.text();
75+
console.error('Response body:', errText);
76+
break;
77+
}
78+
79+
const data = await res.json();
80+
81+
if (!Array.isArray(data)) {
82+
console.error('Unexpected response format:', data);
83+
break;
84+
}
85+
86+
console.log(`Fetched page ${page} with ${data.length} commits.`);
87+
88+
if (data.length === 0) break;
89+
90+
commits.push(...data);
91+
page++;
92+
}
93+
94+
console.log(`Total commits fetched: ${commits.length}`);
95+
return commits;
96+
}
97+
98+
function filterCommitsByDate(commits) {
99+
return commits.filter((c) => {
100+
if (isBotCommit(c)) return false;
101+
const commitDate = new Date(c.commit.author.date);
102+
if (START && commitDate < START) return false;
103+
if (END && commitDate > END) return false;
104+
return true;
105+
});
106+
}
107+
108+
function categorizeCommits(commits) {
109+
// TODO: Update logic for commits categorisation, refer 'All Commits' section for all the commits.
110+
const categories = {
111+
'All Commits': [],
112+
'Breaking Changes': [],
113+
'New Features': [],
114+
'Reliability': [],
115+
'New Architecture-specific changes': [],
116+
Other: [],
117+
};
118+
119+
const keywords = {
120+
'All Commits': [],
121+
'Breaking Changes': [
122+
'break',
123+
'remove',
124+
'deprecated',
125+
'incompatible',
126+
'remove support',
127+
'change api',
128+
'breaking',
129+
],
130+
'New Features': ['feature', 'add', 'introduce', 'support', 'enable'],
131+
'Reliability': ['fix', 'bug', 'error', 'issue', 'crash', 'fault', 'defect', 'patch'],
132+
'New Architecture-specific changes': [
133+
'implement',
134+
'new',
135+
'fabric',
136+
'arch',
137+
'modal',
138+
'architecture',
139+
'refactor',
140+
'restructure',
141+
'modularize',
142+
],
143+
};
144+
145+
for (const c of commits) {
146+
const msg = c.commit.message;
147+
const lowerMsg = msg.toLowerCase();
148+
const sha = c.sha.slice(0, 7);
149+
const url = c.html_url;
150+
const entry = `- ${msg.split('\n')[0]} [${msg.split('\n')[0]} · ${REPO}@${sha} (github.com)](${url})`;
151+
152+
const matched = Object.keys(keywords).filter((k) =>
153+
keywords[k].some((word) => lowerMsg.includes(word))
154+
);
155+
const category = matched.includes('Breaking Changes')
156+
? 'Breaking Changes'
157+
: matched.includes('New Features')
158+
? 'New Features'
159+
: matched.includes('Reliability')
160+
? 'Reliability'
161+
: matched.includes('New Architecture-specific changes')
162+
? 'New Architecture-specific changes'
163+
: 'Other';
164+
165+
categories['All Commits'].push(entry);
166+
categories[category].push(entry);
167+
}
168+
169+
return categories;
170+
}
171+
172+
function generateReleaseNotes(commits, categories) {
173+
const start = formatDate(START || new Date(commits[0]?.commit.author.date));
174+
const end = formatDate(END || new Date(commits.at(-1)?.commit.author.date));
175+
176+
const lines = [];
177+
lines.push(`${RELEASE_TAG} Release Notes\n`);
178+
lines.push(
179+
`We're excited to release React Native Windows ${RELEASE_TAG} targeting React Native ${RELEASE_TAG}!`
180+
);
181+
lines.push(`This release includes the commits to React Native Windows from ${start} - ${end}.\n`);
182+
lines.push('## How to upgrade');
183+
lines.push(
184+
'You can view the changes made to the default new React Native Windows applications for C++ and C# using React Native Upgrade Helper. See this [document](https://microsoft.github.io/react-native-windows/docs/upgrade-app) for more details.\n'
185+
);
186+
187+
for (const [category, entries] of Object.entries(categories)) {
188+
if (entries.length > 0) {
189+
lines.push(`## ${category}`);
190+
lines.push(...entries);
191+
lines.push('');
192+
}
193+
}
194+
195+
return lines.join('\n');
196+
}
197+
198+
async function main() {
199+
const commits = await fetchCommits();
200+
const filtered = filterCommitsByDate(commits);
201+
const categories = categorizeCommits(filtered);
202+
const notes = generateReleaseNotes(filtered, categories);
203+
fs.writeFileSync('release_notes.md', notes, 'utf8');
204+
}
205+
206+
main().catch((err) => {
207+
console.error('Failed to generate release notes:', err);
208+
process.exit(1);
209+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@rnw-scripts/generate-release-notes",
3+
"version": "1.0.0",
4+
"description": "Generates release notes for React Native Windows",
5+
"main": "generate-release-notes.js",
6+
"scripts": {
7+
"release-notes": "node generate-release-notes.js"
8+
},
9+
"dependencies": {
10+
"node-fetch": "^3.3.2"
11+
},
12+
"type": "module"
13+
}

0 commit comments

Comments
 (0)