@@ -15,7 +15,6 @@ import { $ } from 'execa';
1515import gitUrlParse from 'git-url-parse' ;
1616import * as semver from 'semver' ;
1717
18- import { fetchCommitsBetweenRefs } from '../utils/changelog.mjs' ;
1918import {
2019 getCurrentGitSha ,
2120 getPackageVersionInfo ,
@@ -67,112 +66,97 @@ async function getRepositoryInfo() {
6766
6867/**
6968 * Extract package name from npm package name for label matching
70- * @param {string } npmPackageName - npm package name (e.g., '@mui/internal-code-infra')
71- * @returns {string } Package name for label (e.g., 'code-infra')
69+ * @param {string } commitHash - Commit hash to check changed files
70+ * @param {PublicPackage[] } allPackages - List of all package names
71+ * @returns {Promise<string[]> } Affected package names
7272 */
73- function extractPackageNameForLabel ( npmPackageName ) {
74- // For scoped packages like @mui /internal-code-infra, extract the last part after 'internal-'
75- if ( npmPackageName . startsWith ( '@' ) ) {
76- const parts = npmPackageName . split ( '/' ) ;
77- if ( parts . length >= 2 ) {
78- const packageName = parts [ 1 ] ;
79- // Remove 'internal-' prefix if present
80- return packageName . replace ( / ^ i n t e r n a l - / , '' ) ;
73+ async function getAffectedPkgsForCommit ( commitHash , allPackages ) {
74+ const { stdout } = await $ `git diff-tree --no-commit-id --name-only -r ${ commitHash } ` ;
75+ const affectedFiles = stdout . trim ( ) . split ( '\n' ) ;
76+ /**
77+ * @type {Set<string> }
78+ */
79+ const affectedPackages = new Set ( ) ;
80+
81+ for ( const filePath of affectedFiles ) {
82+ for ( const pkg of allPackages ) {
83+ const relativePath = path . relative ( pkg . path , filePath ) ;
84+ if ( ! relativePath . startsWith ( '..' ) && ! path . isAbsolute ( relativePath ) ) {
85+ affectedPackages . add ( pkg . name ) ;
86+ }
8187 }
8288 }
83- return npmPackageName ;
84- }
8589
86- /**
87- * Get merged PRs since the last canary tag
88- * @param {string } owner - Repository owner
89- * @param {string } repo - Repository name
90- * @returns {Promise<Array<import('../utils/changelog.mjs').FetchedCommitDetails>> } List of merged PRs
91- */
92- async function getMergedPRsSinceTag ( owner , repo ) {
93- const octokit = getOctokit ( ) ;
94- const changelogs = await fetchCommitsBetweenRefs ( {
95- octokit,
96- lastRelease : CANARY_TAG ,
97- release : 'master' ,
98- repo,
99- org : owner ,
100- } ) ;
101- return changelogs ;
90+ return Array . from ( affectedPackages ) ;
10291}
10392
10493/**
105- * Generate changelog for a package based on PRs
106- * @param {string } packageName - Package name (e.g., 'code-infra')
107- * @param {Array<import('../utils/changelog.mjs').FetchedCommitDetails> } allPRs - All merged PRs
108- * @returns {string } Generated changelog content
94+ *
95+ * @param {Object } param0
96+ * @param {string } param0.repo
97+ * @param {string } param0.owner
98+ * @returns {Promise<Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits']> }
10999 */
110- function generateChangelogForPackage ( packageName , allPRs ) {
111- const scopeLabel = `scope: ${ packageName } ` ;
112-
113- // Filter PRs that have the matching scope label
114- const relevantPRs = allPRs . filter ( ( pr ) =>
115- pr . labels . some ( ( label ) => label . toLowerCase ( ) === scopeLabel . toLowerCase ( ) ) ,
100+ async function fetchCommitsBetweenRefs ( { repo, owner } ) {
101+ const octokit = getOctokit ( ) ;
102+ /**
103+ * @typedef {Awaited<ReturnType<Octokit['repos']['compareCommits']>>['data']['commits'] } Commits
104+ */
105+ /**
106+ * @type {Commits }
107+ */
108+ const results = [ ] ;
109+ /**
110+ * @type {any }
111+ */
112+ const timeline = octokit . paginate . iterator (
113+ octokit . repos . compareCommitsWithBasehead . endpoint . merge ( {
114+ owner,
115+ repo,
116+ basehead : `${ CANARY_TAG } ...master` ,
117+ } ) ,
116118 ) ;
117-
118- if ( relevantPRs . length === 0 ) {
119- return '' ;
119+ for await ( const response of timeline ) {
120+ results . push ( ...response . data . commits ) ;
120121 }
121-
122- // Generate changelog content
123- const changelogLines = relevantPRs . map ( ( pr ) => {
124- // Check if PR has 'breaking' label (case-insensitive)
125- const hasBreakingLabel = pr . labels . some ( ( label ) =>
126- [ 'breaking' , 'breaking change' , 'breaking-change' ] . includes ( label . toLowerCase ( ) ) ,
127- ) ;
128- // Check if title contains [breaking] (case-insensitive)
129- const hasBreakingInTitle = / \[ ( b r e a k i n g (?: \s | - ) ? (?: c h a n g e ) ? ) \] / i. test ( pr . message ) ;
130- const isBreaking = hasBreakingLabel || hasBreakingInTitle ;
131-
132- // Add "Breaking: " prefix if breaking change
133- const prefix = isBreaking ? 'Breaking: ' : '' ;
134- return `- ${ prefix } ${ pr . message } (#${ pr . prNumber } )` ;
135- } ) ;
136-
137- return changelogLines . join ( '\n' ) ;
122+ return results ;
138123}
139124
140125/**
141126 * Prepare changelog data for packages using GitHub API
142127 * @param {PublicPackage[] } packagesToPublish - Packages that will be published
143128 * @param {Map<string, string> } canaryVersions - Map of package names to their canary versions
144129 * @param {{owner: string, repo: string} } repoInfo - Repository information
145- * @returns {Promise<Map<string, string>> } Map of package names to their changelogs
130+ * @returns {Promise<Map<string, string[] >> } Map of package names to their changelogs
146131 */
147132async function prepareChangelogsFromGitHub ( packagesToPublish , canaryVersions , repoInfo ) {
148- console . log ( '🔍 Fetching merged PRs from GitHub API...' ) ;
149- const allCommitPRs = await getMergedPRsSinceTag ( repoInfo . owner , repoInfo . repo ) ;
150- console . log ( `📋 Found ${ allCommitPRs . length } merged PRs since last canary tag` ) ;
133+ console . log ( '🔍 Fetching merged commits from GitHub API...' ) ;
134+ const commits = await fetchCommitsBetweenRefs ( repoInfo ) ;
135+ console . log ( `📋 Found ${ commits . length } merged commits since last canary tag` ) ;
151136
152137 /**
153- * @type {Map<string, string> }
138+ * @type {Map<string, string[] > }
154139 */
155140 const changelogs = new Map ( ) ;
156-
157- for ( const pkg of packagesToPublish ) {
158- const version = canaryVersions . get ( pkg . name ) ;
159- if ( ! version ) {
160- continue ;
141+ for ( const commit of commits ) {
142+ // eslint-disable-next-line no-await-in-loop
143+ const affectedPackages = await getAffectedPkgsForCommit ( commit . sha , packagesToPublish ) ;
144+ for ( const pkgName of affectedPackages ) {
145+ const existingChangelogs = changelogs . get ( pkgName ) || [ ] ;
146+ existingChangelogs . push (
147+ `- ${ commit . commit . message . split ( '\n' ) [ 0 ] } (${ commit . sha . slice ( 0 , 7 ) } )` ,
148+ ) ;
149+ changelogs . set ( pkgName , existingChangelogs ) ;
161150 }
162-
163- const packageLabel = extractPackageNameForLabel ( pkg . name ) ;
164- const changelog = generateChangelogForPackage ( packageLabel , allCommitPRs ) ;
165- changelogs . set ( pkg . name , changelog ) ;
166151 }
167-
168152 return changelogs ;
169153}
170154
171155/**
172156 * Prepare changelog data for packages
173157 * @param {PublicPackage[] } packagesToPublish - Packages that will be published
174158 * @param {Map<string, string> } canaryVersions - Map of package names to their canary versions
175- * @returns {Promise<Map<string, string>> } Map of package names to their changelogs
159+ * @returns {Promise<Map<string, string[] >> } Map of package names to their changelogs
176160 */
177161async function prepareChangelogsForPackages ( packagesToPublish , canaryVersions ) {
178162 console . log ( '\n📝 Preparing changelogs for packages...' ) ;
@@ -181,7 +165,7 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
181165 console . log ( `📂 Repository: ${ repoInfo . owner } /${ repoInfo . repo } ` ) ;
182166
183167 /**
184- * @type {Map<string, string> }
168+ * @type {Map<string, string[] > }
185169 */
186170 let changelogs = new Map ( ) ;
187171
@@ -194,14 +178,11 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
194178 continue ;
195179 }
196180
197- const changelog = changelogs . get ( pkg . name ) || '' ;
181+ const changelog = changelogs . get ( pkg . name ) || [ ] ;
198182 console . log ( `\n📦 ${ pkg . name } @${ version } ` ) ;
199183 if ( changelog ) {
200184 console . log (
201- ` Changelog:\n${ changelog
202- . split ( '\n' )
203- . map ( ( /** @type {string } */ line ) => ` ${ line } ` )
204- . join ( '\n' ) } `,
185+ ` Changelog:\n${ changelog . map ( ( /** @type {string } */ line ) => ` ${ line } ` ) . join ( '\n' ) } ` ,
205186 ) ;
206187 } else {
207188 console . log ( ' Changelog: No changes with scope labels found for this package.' ) ;
@@ -216,7 +197,7 @@ async function prepareChangelogsForPackages(packagesToPublish, canaryVersions) {
216197 * Create GitHub releases and tags for published packages
217198 * @param {PublicPackage[] } publishedPackages - Packages that were published
218199 * @param {Map<string, string> } canaryVersions - Map of package names to their canary versions
219- * @param {Map<string, string> } changelogs - Map of package names to their changelogs
200+ * @param {Map<string, string[] > } changelogs - Map of package names to their changelogs
220201 * @param {{dryRun?: boolean} } options - Publishing options
221202 * @returns {Promise<void> }
222203 */
@@ -228,33 +209,6 @@ async function createGitHubReleasesForPackages(
228209) {
229210 console . log ( '\n🚀 Creating GitHub releases and tags for published packages...' ) ;
230211
231- if ( options . dryRun ) {
232- console . log ( '🧪 Dry-run mode: Would create release(s) and tag(s) for:' ) ;
233- for ( const pkg of publishedPackages ) {
234- const version = canaryVersions . get ( pkg . name ) ;
235- if ( ! version ) {
236- continue ;
237- }
238- const changelog = changelogs . get ( pkg . name ) ;
239- const tagName = `${ pkg . name } @${ version } ${ changelog ? ' (with changelog):' : ' (no changelog)' } ` ;
240- console . log ( ` • ${ tagName } ` ) ;
241- if ( changelog ) {
242- // Draw changelog in an ASCII rectangle
243- const lines = changelog . split ( '\n' ) ;
244- const maxLength = Math . max ( Math . max ( ...lines . map ( ( line ) => line . length ) ) , 60 ) ;
245- const border = `┌${ '─' . repeat ( maxLength + 2 ) } ┐` ;
246- const footer = `└${ '─' . repeat ( maxLength + 2 ) } ┘` ;
247- console . log ( ' Changelog:' ) ;
248- console . log ( ` ${ border } ` ) ;
249- lines . forEach ( ( line ) => {
250- console . log ( ` │ ${ line . padEnd ( maxLength ) } │` ) ;
251- } ) ;
252- console . log ( ` ${ footer } ` ) ;
253- }
254- }
255- return ;
256- }
257-
258212 const repoInfo = await getRepositoryInfo ( ) ;
259213 const gitSha = await getCurrentGitSha ( ) ;
260214 const octokit = getOctokit ( ) ;
@@ -277,33 +231,41 @@ async function createGitHubReleasesForPackages(
277231 console . log ( `\n📦 Processing ${ pkg . name } @${ version } ...` ) ;
278232
279233 // Create git tag
280- // eslint-disable-next-line no-await-in-loop
281- await $ ( {
282- env : {
283- ...process . env ,
284- GIT_COMMITTER_NAME : 'Code infra' ,
285- GIT_COMMITTER_EMAIL : 'code-infra@mui.com' ,
286- } ,
287- } ) `git tag -a ${ tagName } -m ${ `Canary release ${ pkg . name } @${ version } ` } ` ;
288-
289- // eslint-disable-next-line no-await-in-loop
290- await $ `git push origin ${ tagName } ` ;
291- console . log ( `✅ Created and pushed git tag: ${ tagName } ` ) ;
292-
293- // Create GitHub release
294- // eslint-disable-next-line no-await-in-loop
295- const res = await octokit . repos . createRelease ( {
296- owner : repoInfo . owner ,
297- repo : repoInfo . repo ,
298- tag_name : tagName ,
299- target_commitish : gitSha ,
300- name : releaseName ,
301- body : changelog ,
302- draft : false ,
303- prerelease : true , // Mark as prerelease since these are canary versions
304- } ) ;
234+ if ( options . dryRun ) {
235+ console . log ( `🏷️ Would create and push git tag: ${ tagName } (dry-run)` ) ;
236+ console . log ( `📝 Would publish a Github release:` ) ;
237+ console . log ( ` - Name: ${ releaseName } ` ) ;
238+ console . log ( ` - Tag: ${ tagName } ` ) ;
239+ console . log ( ` - Body:\n${ changelog . join ( '\n' ) } ` ) ;
240+ } else {
241+ // eslint-disable-next-line no-await-in-loop
242+ await $ ( {
243+ env : {
244+ ...process . env ,
245+ GIT_COMMITTER_NAME : 'Code infra' ,
246+ GIT_COMMITTER_EMAIL : 'code-infra@mui.com' ,
247+ } ,
248+ } ) `git tag -a ${ tagName } -m ${ `Canary release ${ pkg . name } @${ version } ` } ` ;
249+
250+ // eslint-disable-next-line no-await-in-loop
251+ await $ `git push origin ${ tagName } ` ;
252+ console . log ( `✅ Created and pushed git tag: ${ tagName } ` ) ;
253+
254+ // Create GitHub release
255+ // eslint-disable-next-line no-await-in-loop
256+ const res = await octokit . repos . createRelease ( {
257+ owner : repoInfo . owner ,
258+ repo : repoInfo . repo ,
259+ tag_name : tagName ,
260+ target_commitish : gitSha ,
261+ name : releaseName ,
262+ body : changelog . join ( '\n' ) ,
263+ draft : false ,
264+ prerelease : true , // Mark as prerelease since these are canary versions
265+ } ) ;
305266
306- console . log ( `✅ Created GitHub release: ${ releaseName } at ${ res . data . html_url } ` ) ;
267+ console . log ( `✅ Created GitHub release: ${ releaseName } at ${ res . data . html_url } ` ) ;
268+ }
307269 }
308270
309271 console . log ( '\n✅ Finished creating GitHub releases' ) ;
@@ -424,6 +386,9 @@ async function publishCanaryVersions(
424386 const updateResults = await Promise . all ( packageUpdatePromises ) ;
425387
426388 // Prepare changelogs before building and publishing (so it can error out early if there are issues)
389+ /**
390+ * @type {Map<string, string[]> }
391+ */
427392 let changelogs = new Map ( ) ;
428393 if ( options . githubRelease ) {
429394 changelogs = await prepareChangelogsForPackages ( packagesToPublish , canaryVersions ) ;
0 commit comments