From a4f007284fcc3e1d89b075c3e1cb0942a741748b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 05:45:39 +0000 Subject: [PATCH 1/7] Initial plan From 9482315a521fa815e760970bf9ff44a608c7b356 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 05:56:44 +0000 Subject: [PATCH 2/7] Add @rnw-scripts/generate-release-notes package with updated version and dates Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com> --- ...pts-generate-release-notes-automation.json | 7 + package.json | 1 + .../generate-release-notes/.gitignore | 1 + .../generate-release-notes/ReadMe.md | 43 ++++ .../generate-release-notes.js | 209 ++++++++++++++++++ .../generate-release-notes/package.json | 13 ++ 6 files changed, 274 insertions(+) create mode 100644 change/@rnw-scripts-generate-release-notes-automation.json create mode 100644 packages/@rnw-scripts/generate-release-notes/.gitignore create mode 100644 packages/@rnw-scripts/generate-release-notes/ReadMe.md create mode 100644 packages/@rnw-scripts/generate-release-notes/generate-release-notes.js create mode 100644 packages/@rnw-scripts/generate-release-notes/package.json diff --git a/change/@rnw-scripts-generate-release-notes-automation.json b/change/@rnw-scripts-generate-release-notes-automation.json new file mode 100644 index 00000000000..d86570e8ebf --- /dev/null +++ b/change/@rnw-scripts-generate-release-notes-automation.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add a new \"yarn release-notes\" script to generate release notes", + "packageName": "@rnw-scripts/generate-release-notes", + "email": "copilot@example.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/package.json b/package.json index 861e3ef8ee9..35deae932aa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "format": "format-files -i -style=file", "format:verify": "format-files -i -style=file -verify", "postinstall": "yarn build", + "release-notes": "yarn workspace @rnw-scripts/generate-release-notes release-notes", "spellcheck": "npx cspell", "test": "lage test --verbose --passWithNoTests", "validate-overrides": "react-native-platform-override validate" diff --git a/packages/@rnw-scripts/generate-release-notes/.gitignore b/packages/@rnw-scripts/generate-release-notes/.gitignore new file mode 100644 index 00000000000..a7d1a810b89 --- /dev/null +++ b/packages/@rnw-scripts/generate-release-notes/.gitignore @@ -0,0 +1 @@ +release_notes.md \ No newline at end of file diff --git a/packages/@rnw-scripts/generate-release-notes/ReadMe.md b/packages/@rnw-scripts/generate-release-notes/ReadMe.md new file mode 100644 index 00000000000..a6cf3680be8 --- /dev/null +++ b/packages/@rnw-scripts/generate-release-notes/ReadMe.md @@ -0,0 +1,43 @@ +### Type of Change +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. + + +### Why +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. + +## Format + +`Explanation. [PRName (#11168) · microsoft/react-native-windows@aaaaaaa (github.com)](link)` + +### Steps to follow + +#### 1. Set up your personal access token + +- Go to GitHub and log in: https://github.com/ +- Click on your profile picture (top-right corner), then click Settings +- On the left sidebar, click Developer settings +- Then click Personal access tokens > Tokens (classic) +- Click Generate new token > Generate new token (classic) +- Give it a name like "Release Notes Script" +- Set an expiration (choose less than 90 days) +- Under Scopes, select the permissions your script needs. For fetching commits and repo info, you typically need: +repo (full control of private repositories) +or at least repo:status, repo_deployment, public_repo (for public repos) +- Click Generate token +- Find the token you're using (whichever token you created). +- You should see a message or option to "Grant access to your organization" or "Authorize SAML SSO" for your token. +- Click that button to authorize the token with the organization. +- Copy the generated token + +#### 2. Set env variables at root of the repo + +``` +set GITHUB_TOKEN= +set RELEASE_TAG=0.80.0 +set START_DATE=2025-06-01 +set END_DATE=2025-07-16 + +``` +#### 3. Run "`yarn release-notes`" at the root of the repo + +#### 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. \ No newline at end of file diff --git a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js new file mode 100644 index 00000000000..1a659fea5ce --- /dev/null +++ b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js @@ -0,0 +1,209 @@ +import fetch from 'node-fetch'; +import fs from 'fs'; +import process from 'process'; + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const REPO = 'microsoft/react-native-windows'; +const RELEASE_TAG = process.env.RELEASE_TAG || 'Unreleased'; +const START_DATE = process.env.START_DATE; +const END_DATE = process.env.END_DATE; + +if (!GITHUB_TOKEN) { + console.error('GITHUB_TOKEN is not set. Please set it before running.'); + process.exit(1); +} +if (!START_DATE || !END_DATE) { + console.error('START_DATE and END_DATE are required.'); + process.exit(1); +} + +console.log(`Generating release notes for ${REPO} from ${START_DATE} to ${END_DATE}...`); + +const HEADERS = { + Authorization: `token ${GITHUB_TOKEN}`, + Accept: 'application/vnd.github+json', +}; + +function parseDate(dateStr) { + return dateStr ? new Date(dateStr) : null; +} + +const START = parseDate(START_DATE); +const END = parseDate(END_DATE); + +function isBotCommit(commit) { + const author = commit.author; + const commitAuthorName = (commit.commit.author.name || '').toLowerCase(); + const authorLogin = (author?.login || '').toLowerCase(); + const botIndicators = ['bot', 'dependabot', 'actions-user']; + const msg = commit.commit.message.toLowerCase(); + + if ( + botIndicators.some( + (bot) => authorLogin.includes(bot) || commitAuthorName.includes(bot) + ) + ) + return true; + if (['bump', 'applying package updates', 'no_ci', 'no ci'].some((k) => msg.includes(k))) + return true; + return false; +} + +function formatDate(date) { + return date ? new Date(date).toLocaleDateString('en-US') : 'N/A'; +} + +async function fetchCommits() { + const commits = []; + let page = 1; + const perPage = 100; + + while (true) { + const url = new URL(`https://api.github.com/repos/${REPO}/commits`); + url.searchParams.set('per_page', perPage); + url.searchParams.set('page', page); + if (START_DATE) url.searchParams.set('since', START_DATE + 'T00:00:00Z'); + if (END_DATE) url.searchParams.set('until', END_DATE + 'T23:59:59Z'); + + console.log(`Fetching commits from: ${url.toString()}`); + + const res = await fetch(url, { headers: HEADERS }); + + if (!res.ok) { + console.error(`GitHub API request failed: ${res.status} ${res.statusText}`); + const errText = await res.text(); + console.error('Response body:', errText); + break; + } + + const data = await res.json(); + + if (!Array.isArray(data)) { + console.error('Unexpected response format:', data); + break; + } + + console.log(`Fetched page ${page} with ${data.length} commits.`); + + if (data.length === 0) break; + + commits.push(...data); + page++; + } + + console.log(`Total commits fetched: ${commits.length}`); + return commits; +} + +function filterCommitsByDate(commits) { + return commits.filter((c) => { + if (isBotCommit(c)) return false; + const commitDate = new Date(c.commit.author.date); + if (START && commitDate < START) return false; + if (END && commitDate > END) return false; + return true; + }); +} + +function categorizeCommits(commits) { +// TODO: Update logic for commits categorisation, refer 'All Commits' section for all the commits. + const categories = { + 'All Commits': [], + 'Breaking Changes': [], + 'New Features': [], + 'Reliability': [], + 'New Architecture-specific changes': [], + Other: [], + }; + + const keywords = { + 'All Commits': [], + 'Breaking Changes': [ + 'break', + 'remove', + 'deprecated', + 'incompatible', + 'remove support', + 'change api', + 'breaking', + ], + 'New Features': ['feature', 'add', 'introduce', 'support', 'enable'], + 'Reliability': ['fix', 'bug', 'error', 'issue', 'crash', 'fault', 'defect', 'patch'], + 'New Architecture-specific changes': [ + 'implement', + 'new', + 'fabric', + 'arch', + 'modal', + 'architecture', + 'refactor', + 'restructure', + 'modularize', + ], + }; + + for (const c of commits) { + const msg = c.commit.message; + const lowerMsg = msg.toLowerCase(); + const sha = c.sha.slice(0, 7); + const url = c.html_url; + const entry = `- ${msg.split('\n')[0]} [${msg.split('\n')[0]} · ${REPO}@${sha} (github.com)](${url})`; + + const matched = Object.keys(keywords).filter((k) => + keywords[k].some((word) => lowerMsg.includes(word)) + ); + const category = matched.includes('Breaking Changes') + ? 'Breaking Changes' + : matched.includes('New Features') + ? 'New Features' + : matched.includes('Reliability') + ? 'Reliability' + : matched.includes('New Architecture-specific changes') + ? 'New Architecture-specific changes' + : 'Other'; + + categories['All Commits'].push(entry); + categories[category].push(entry); + } + + return categories; +} + +function generateReleaseNotes(commits, categories) { + const start = formatDate(START || new Date(commits[0]?.commit.author.date)); + const end = formatDate(END || new Date(commits.at(-1)?.commit.author.date)); + + const lines = []; + lines.push(`${RELEASE_TAG} Release Notes\n`); + lines.push( + `We're excited to release React Native Windows ${RELEASE_TAG} targeting React Native ${RELEASE_TAG}!` + ); + lines.push(`This release includes the commits to React Native Windows from ${start} - ${end}.\n`); + lines.push('## How to upgrade'); + lines.push( + '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' + ); + + for (const [category, entries] of Object.entries(categories)) { + if (entries.length > 0) { + lines.push(`## ${category}`); + lines.push(...entries); + lines.push(''); + } + } + + return lines.join('\n'); +} + +async function main() { + const commits = await fetchCommits(); + const filtered = filterCommitsByDate(commits); + const categories = categorizeCommits(filtered); + const notes = generateReleaseNotes(filtered, categories); + fs.writeFileSync('release_notes.md', notes, 'utf8'); +} + +main().catch((err) => { + console.error('Failed to generate release notes:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/packages/@rnw-scripts/generate-release-notes/package.json b/packages/@rnw-scripts/generate-release-notes/package.json new file mode 100644 index 00000000000..bc274891048 --- /dev/null +++ b/packages/@rnw-scripts/generate-release-notes/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rnw-scripts/generate-release-notes", + "version": "1.0.0", + "description": "Generates release notes for React Native Windows", + "main": "generate-release-notes.js", + "scripts": { + "release-notes": "node generate-release-notes.js" + }, + "dependencies": { + "node-fetch": "^3.3.2" + }, + "type": "module" +} \ No newline at end of file From 76db37e269bf69142365a15b731ec1f99d11944d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 05:59:00 +0000 Subject: [PATCH 3/7] Update package.json with proper repository info and improve .gitignore Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com> --- packages/@rnw-scripts/generate-release-notes/.gitignore | 3 ++- .../@rnw-scripts/generate-release-notes/package.json | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/@rnw-scripts/generate-release-notes/.gitignore b/packages/@rnw-scripts/generate-release-notes/.gitignore index a7d1a810b89..1ddabf0771e 100644 --- a/packages/@rnw-scripts/generate-release-notes/.gitignore +++ b/packages/@rnw-scripts/generate-release-notes/.gitignore @@ -1 +1,2 @@ -release_notes.md \ No newline at end of file +release_notes.md +sample_release_notes.md \ No newline at end of file diff --git a/packages/@rnw-scripts/generate-release-notes/package.json b/packages/@rnw-scripts/generate-release-notes/package.json index bc274891048..265530bd2a4 100644 --- a/packages/@rnw-scripts/generate-release-notes/package.json +++ b/packages/@rnw-scripts/generate-release-notes/package.json @@ -2,6 +2,12 @@ "name": "@rnw-scripts/generate-release-notes", "version": "1.0.0", "description": "Generates release notes for React Native Windows", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/react-native-windows", + "directory": "packages/@rnw-scripts/generate-release-notes" + }, "main": "generate-release-notes.js", "scripts": { "release-notes": "node generate-release-notes.js" @@ -9,5 +15,8 @@ "dependencies": { "node-fetch": "^3.3.2" }, + "engines": { + "node": ">= 18" + }, "type": "module" } \ No newline at end of file From 5e335dc8b59574c83b8c5c252fa308940dfadc5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 06:25:02 +0000 Subject: [PATCH 4/7] Improve commit summaries by extracting release notes from PR descriptions Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com> --- .../generate-release-notes.js | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js index 1a659fea5ce..1a5125accc3 100644 --- a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js +++ b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js @@ -105,7 +105,53 @@ function filterCommitsByDate(commits) { }); } -function categorizeCommits(commits) { +function extractPRNumber(commitMessage) { + // Extract PR number from commit message like "(#14813)" + const match = commitMessage.match(/\(#(\d+)\)/); + return match ? parseInt(match[1]) : null; +} + +async function fetchPRDetails(prNumber) { + if (!prNumber) return null; + + try { + const url = `https://api.github.com/repos/${REPO}/pulls/${prNumber}`; + const res = await fetch(url, { headers: HEADERS }); + + if (!res.ok) { + console.warn(`Failed to fetch PR #${prNumber}: ${res.status}`); + return null; + } + + return await res.json(); + } catch (error) { + console.warn(`Error fetching PR #${prNumber}:`, error.message); + return null; + } +} + +function extractReleaseNotesSummary(prDescription) { + if (!prDescription) return null; + + // Look for the release notes summary marker + const marker = 'Add a brief summary of the change to use in the release notes for the next release.'; + const markerIndex = prDescription.indexOf(marker); + + if (markerIndex === -1) return null; + + // Get text after the marker + const afterMarker = prDescription.substring(markerIndex + marker.length); + + // Extract the next line or paragraph after the marker + const lines = afterMarker.split('\n').map(line => line.trim()).filter(line => line.length > 0); + + if (lines.length === 0) return null; + + // Return the first non-empty line after the marker + return lines[0]; +} + +async function categorizeCommits(commits) { // TODO: Update logic for commits categorisation, refer 'All Commits' section for all the commits. const categories = { 'All Commits': [], @@ -147,7 +193,25 @@ function categorizeCommits(commits) { const lowerMsg = msg.toLowerCase(); const sha = c.sha.slice(0, 7); const url = c.html_url; - const entry = `- ${msg.split('\n')[0]} [${msg.split('\n')[0]} · ${REPO}@${sha} (github.com)](${url})`; + const commitTitle = msg.split('\n')[0]; + + // Try to get a better summary from PR description + const prNumber = extractPRNumber(commitTitle); + let summary = commitTitle; + + if (prNumber) { + console.log(`Fetching PR details for #${prNumber}...`); + const prDetails = await fetchPRDetails(prNumber); + if (prDetails) { + const releaseNotesSummary = extractReleaseNotesSummary(prDetails.body); + if (releaseNotesSummary) { + summary = releaseNotesSummary; + console.log(`Found release notes summary for PR #${prNumber}: ${summary}`); + } + } + } + + const entry = `- ${summary} [${commitTitle} · ${REPO}@${sha} (github.com)](${url})`; const matched = Object.keys(keywords).filter((k) => keywords[k].some((word) => lowerMsg.includes(word)) @@ -198,7 +262,7 @@ function generateReleaseNotes(commits, categories) { async function main() { const commits = await fetchCommits(); const filtered = filterCommitsByDate(commits); - const categories = categorizeCommits(filtered); + const categories = await categorizeCommits(filtered); const notes = generateReleaseNotes(filtered, categories); fs.writeFileSync('release_notes.md', notes, 'utf8'); } From c6e5c93cff0a57dc172f54d00cc5d54abd21ff66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 06:36:50 +0000 Subject: [PATCH 5/7] Add PR inclusion filtering and Microsoft Reviewers cleanup logic Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com> --- .../generate-release-notes.js | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js index 1a5125accc3..63653a545fa 100644 --- a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js +++ b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js @@ -130,6 +130,28 @@ async function fetchPRDetails(prNumber) { } } +function shouldIncludeInReleaseNotes(prDescription) { + if (!prDescription) return false; + + // Look for the inclusion marker + const marker = 'Should this change be included in the release notes:'; + const markerIndex = prDescription.indexOf(marker); + + if (markerIndex === -1) return false; + + // Get text after the marker + const afterMarker = prDescription.substring(markerIndex + marker.length); + + // Extract the next line or paragraph after the marker + const lines = afterMarker.split('\n').map(line => line.trim()).filter(line => line.length > 0); + + if (lines.length === 0) return false; + + // Check if the first non-empty line contains "yes" or "_yes_" + const firstLine = lines[0].toLowerCase(); + return firstLine.includes('yes') || firstLine.includes('_yes_'); +} + function extractReleaseNotesSummary(prDescription) { if (!prDescription) return null; @@ -142,13 +164,36 @@ function extractReleaseNotesSummary(prDescription) { // Get text after the marker const afterMarker = prDescription.substring(markerIndex + marker.length); - // Extract the next line or paragraph after the marker - const lines = afterMarker.split('\n').map(line => line.trim()).filter(line => line.length > 0); + // Split into lines and get all non-empty lines + const lines = afterMarker.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); if (lines.length === 0) return null; - // Return the first non-empty line after the marker - return lines[0]; + // Get the first non-empty line after the marker + let summary = lines[0]; + + // Remove Microsoft Reviewers text if it exists anywhere in the summary + const reviewersMarker = 'Microsoft Reviewers: [Open in CodeFlow'; + if (summary.includes(reviewersMarker)) { + const reviewersIndex = summary.indexOf(reviewersMarker); + summary = summary.substring(0, reviewersIndex).trim(); + } + + // Filter out lines that contain Microsoft Reviewers text + if (!summary || summary.length === 0) { + // Try the next lines if the first one was entirely Microsoft Reviewers text + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (!line.includes('Microsoft Reviewers: [Open in CodeFlow')) { + summary = line; + break; + } + } + } + + return summary && summary.length > 0 ? summary : null; } async function categorizeCommits(commits) { @@ -198,15 +243,24 @@ async function categorizeCommits(commits) { // Try to get a better summary from PR description const prNumber = extractPRNumber(commitTitle); let summary = commitTitle; + let shouldInclude = true; // Default to include if we can't determine if (prNumber) { console.log(`Fetching PR details for #${prNumber}...`); const prDetails = await fetchPRDetails(prNumber); if (prDetails) { - const releaseNotesSummary = extractReleaseNotesSummary(prDetails.body); - if (releaseNotesSummary) { - summary = releaseNotesSummary; - console.log(`Found release notes summary for PR #${prNumber}: ${summary}`); + // Check if this PR should be included in release notes + shouldInclude = shouldIncludeInReleaseNotes(prDetails.body); + + if (shouldInclude) { + const releaseNotesSummary = extractReleaseNotesSummary(prDetails.body); + if (releaseNotesSummary) { + summary = releaseNotesSummary; + console.log(`Found release notes summary for PR #${prNumber}: ${summary}`); + } + } else { + console.log(`Skipping PR #${prNumber} - not marked for inclusion in release notes`); + continue; // Skip this commit } } } From cbf9e134efe9ec21ec6cead7f59003962a538a2e Mon Sep 17 00:00:00 2001 From: Anupriya Verma <54227869+anupriya13@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:29:18 +0530 Subject: [PATCH 6/7] update logic --- .../generate-release-notes/generate-release-notes.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js index 63653a545fa..3b50f46ebe2 100644 --- a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js +++ b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js @@ -137,7 +137,7 @@ function shouldIncludeInReleaseNotes(prDescription) { const marker = 'Should this change be included in the release notes:'; const markerIndex = prDescription.indexOf(marker); - if (markerIndex === -1) return false; + if (markerIndex === -1) return true; // Get text after the marker const afterMarker = prDescription.substring(markerIndex + marker.length); @@ -147,9 +147,9 @@ function shouldIncludeInReleaseNotes(prDescription) { if (lines.length === 0) return false; - // Check if the first non-empty line contains "yes" or "_yes_" + // Check if the first non-empty line contains "no" or "_no_" const firstLine = lines[0].toLowerCase(); - return firstLine.includes('yes') || firstLine.includes('_yes_'); + return !(firstLine.includes('no') || firstLine.includes('_no_')); } function extractReleaseNotesSummary(prDescription) { From 56f0317220be8a97e8602988bf17c165b9303ab9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 07:13:53 +0000 Subject: [PATCH 7/7] Improve categorization logic based on PR Type of Change section Co-authored-by: anupriya13 <54227869+anupriya13@users.noreply.github.com> --- .../generate-release-notes.js | 102 +++++++++++------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js index 3b50f46ebe2..0e08549a997 100644 --- a/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js +++ b/packages/@rnw-scripts/generate-release-notes/generate-release-notes.js @@ -196,8 +196,38 @@ function extractReleaseNotesSummary(prDescription) { return summary && summary.length > 0 ? summary : null; } +function extractTypeOfChange(prDescription) { + if (!prDescription) return null; + + // Look for the "### Type of Change" section + const marker = '### Type of Change'; + const markerIndex = prDescription.indexOf(marker); + + if (markerIndex === -1) return null; + + // Get text after the marker until the next section (###) or end + const afterMarker = prDescription.substring(markerIndex + marker.length); + const nextSectionIndex = afterMarker.indexOf('###'); + const sectionText = nextSectionIndex !== -1 + ? afterMarker.substring(0, nextSectionIndex) + : afterMarker; + + // Convert to lowercase for easier matching + const lowerSectionText = sectionText.toLowerCase(); + + // Check for each type of change + if (lowerSectionText.includes('bug fix')) { + return 'Bug fix'; + } else if (lowerSectionText.includes('new feature')) { + return 'New feature'; + } else if (lowerSectionText.includes('breaking change')) { + return 'Breaking change'; + } + + return null; +} + async function categorizeCommits(commits) { -// TODO: Update logic for commits categorisation, refer 'All Commits' section for all the commits. const categories = { 'All Commits': [], 'Breaking Changes': [], @@ -207,35 +237,8 @@ async function categorizeCommits(commits) { Other: [], }; - const keywords = { - 'All Commits': [], - 'Breaking Changes': [ - 'break', - 'remove', - 'deprecated', - 'incompatible', - 'remove support', - 'change api', - 'breaking', - ], - 'New Features': ['feature', 'add', 'introduce', 'support', 'enable'], - 'Reliability': ['fix', 'bug', 'error', 'issue', 'crash', 'fault', 'defect', 'patch'], - 'New Architecture-specific changes': [ - 'implement', - 'new', - 'fabric', - 'arch', - 'modal', - 'architecture', - 'refactor', - 'restructure', - 'modularize', - ], - }; - for (const c of commits) { const msg = c.commit.message; - const lowerMsg = msg.toLowerCase(); const sha = c.sha.slice(0, 7); const url = c.html_url; const commitTitle = msg.split('\n')[0]; @@ -244,6 +247,7 @@ async function categorizeCommits(commits) { const prNumber = extractPRNumber(commitTitle); let summary = commitTitle; let shouldInclude = true; // Default to include if we can't determine + let category = 'Other'; // Default category if (prNumber) { console.log(`Fetching PR details for #${prNumber}...`); @@ -258,6 +262,35 @@ async function categorizeCommits(commits) { summary = releaseNotesSummary; console.log(`Found release notes summary for PR #${prNumber}: ${summary}`); } + + // Determine category based on PR description "Type of Change" + const typeOfChange = extractTypeOfChange(prDetails.body); + const prTitle = prDetails.title || ''; + const prDescription = prDetails.body || ''; + + // Check for special architecture keywords first + const lowerTitle = prTitle.toLowerCase(); + const lowerDescription = prDescription.toLowerCase(); + const hasArchKeywords = lowerTitle.includes('fabric') || + lowerTitle.includes('implement') || + lowerTitle.includes('prop') || + lowerDescription.includes('fabric') || + lowerDescription.includes('implement') || + lowerDescription.includes('prop'); + + if (hasArchKeywords) { + category = 'New Architecture-specific changes'; + } else if (typeOfChange === 'Bug fix') { + category = 'Reliability'; + } else if (typeOfChange === 'New feature') { + category = 'New Features'; + } else if (typeOfChange === 'Breaking change') { + category = 'Breaking Changes'; + } else { + category = 'Other'; + } + + console.log(`PR #${prNumber}: Type of Change = "${typeOfChange}", Category = "${category}"`); } else { console.log(`Skipping PR #${prNumber} - not marked for inclusion in release notes`); continue; // Skip this commit @@ -267,19 +300,6 @@ async function categorizeCommits(commits) { const entry = `- ${summary} [${commitTitle} · ${REPO}@${sha} (github.com)](${url})`; - const matched = Object.keys(keywords).filter((k) => - keywords[k].some((word) => lowerMsg.includes(word)) - ); - const category = matched.includes('Breaking Changes') - ? 'Breaking Changes' - : matched.includes('New Features') - ? 'New Features' - : matched.includes('Reliability') - ? 'Reliability' - : matched.includes('New Architecture-specific changes') - ? 'New Architecture-specific changes' - : 'Other'; - categories['All Commits'].push(entry); categories[category].push(entry); }