@@ -28,25 +28,6 @@ async function resolveUsername(email) {
2828 }
2929}
3030
31- // Resolve author from a commit hash via git log
32- const authorCache = { }
33- async function resolveAuthorForCommit ( hash ) {
34- if ( authorCache [ hash ] !== undefined ) return authorCache [ hash ]
35-
36- try {
37- const email = execSync ( `git log -1 --format=%ae ${ hash } ` , {
38- encoding : 'utf-8' ,
39- stdio : [ 'pipe' , 'pipe' , 'ignore' ] ,
40- } ) . trim ( )
41- const username = await resolveUsername ( email )
42- authorCache [ hash ] = username
43- return username
44- } catch {
45- authorCache [ hash ] = null
46- return null
47- }
48- }
49-
5031// Resolve author from a PR number via GitHub API
5132const prAuthorCache = { }
5233async function resolveAuthorForPR ( prNumber ) {
@@ -72,39 +53,6 @@ async function resolveAuthorForPR(prNumber) {
7253 }
7354}
7455
75- // Append author info to changelog lines that contain PR refs or commit hashes
76- async function appendAuthors ( content ) {
77- const lines = content . split ( '\n' )
78- const result = [ ]
79-
80- for ( const line of lines ) {
81- if ( ! line . startsWith ( '- ' ) ) {
82- result . push ( line )
83- continue
84- }
85-
86- // Try PR reference first: ([#6891](url))
87- const prMatch = line . match ( / \[ # ( \d + ) \] / )
88- if ( prMatch ) {
89- const username = await resolveAuthorForPR ( prMatch [ 1 ] )
90- result . push ( username ? `${ line } by @${ username } ` : line )
91- continue
92- }
93-
94- // Fall back to commit hash: [`9a4d924`](url)
95- const commitMatch = line . match ( / \[ ` ( [ a - f 0 - 9 ] { 7 , } ) ` \] / )
96- if ( commitMatch ) {
97- const username = await resolveAuthorForCommit ( commitMatch [ 1 ] )
98- result . push ( username ? `${ line } by @${ username } ` : line )
99- continue
100- }
101-
102- result . push ( line )
103- }
104-
105- return result . join ( '\n' )
106- }
107-
10856// Get the previous release commit to diff against.
10957// This script runs right after the "ci: changeset release" commit is pushed,
11058// so HEAD is the release commit.
@@ -167,37 +115,88 @@ for (const relPath of allPkgJsonPaths) {
167115
168116bumpedPackages . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
169117
170- // Extract changelog entries from changeset-generated CHANGELOG.md files.
171- // Changesets writes entries under "## <version>" headers. We extract the
172- // content under the current version header for each bumped package.
118+ // Build changelog from git log between releases (conventional commits)
119+ const rangeFrom = previousRelease || `${ currentRelease } ~1`
120+ const rawLog = execSync (
121+ `git log ${ rangeFrom } ..${ currentRelease } --pretty=format:"%h %ae %s" --no-merges` ,
122+ { encoding : 'utf-8' } ,
123+ ) . trim ( )
124+
125+ const typeOrder = [
126+ 'feat' ,
127+ 'fix' ,
128+ 'perf' ,
129+ 'refactor' ,
130+ 'docs' ,
131+ 'chore' ,
132+ 'test' ,
133+ 'ci' ,
134+ ]
135+ const typeLabels = {
136+ feat : 'Features' ,
137+ fix : 'Fix' ,
138+ perf : 'Performance' ,
139+ refactor : 'Refactor' ,
140+ docs : 'Documentation' ,
141+ chore : 'Chore' ,
142+ test : 'Tests' ,
143+ ci : 'CI' ,
144+ }
145+ const typeIndex = ( t ) => {
146+ const i = typeOrder . indexOf ( t )
147+ return i === - 1 ? 99 : i
148+ }
149+
150+ const groups = { }
151+ const commits = rawLog ? rawLog . split ( '\n' ) : [ ]
152+
153+ for ( const line of commits ) {
154+ const match = line . match ( / ^ ( \w + ) \s + ( \S + ) \s + ( .* ) $ / )
155+ if ( ! match ) continue
156+ const [ , hash , email , subject ] = match
157+
158+ // Skip release commits
159+ if ( subject . startsWith ( 'ci: changeset release' ) ) continue
160+
161+ // Parse conventional commit: type(scope): message
162+ const conventionalMatch = subject . match ( / ^ ( \w + ) (?: \( ( [ ^ ) ] * ) \) ) ? : \s * ( .* ) $ / )
163+ const type = conventionalMatch ? conventionalMatch [ 1 ] : 'other'
164+ const scope = conventionalMatch ? conventionalMatch [ 2 ] || '' : ''
165+ const message = conventionalMatch ? conventionalMatch [ 3 ] : subject
166+
167+ // Extract PR number if present
168+ const prMatch = message . match ( / \( # ( \d + ) \) / )
169+ const prNumber = prMatch ? prMatch [ 1 ] : null
170+
171+ if ( ! groups [ type ] ) groups [ type ] = [ ]
172+ groups [ type ] . push ( { hash, email, scope, message, prNumber } )
173+ }
174+
175+ // Build markdown grouped by conventional commit type
176+ const sortedTypes = Object . keys ( groups ) . sort (
177+ ( a , b ) => typeIndex ( a ) - typeIndex ( b ) ,
178+ )
179+
173180let changelogMd = ''
174- for ( const pkg of bumpedPackages ) {
175- const changelogPath = path . join ( packagesDir , pkg . dir , 'CHANGELOG.md' )
176- if ( ! fs . existsSync ( changelogPath ) ) continue
177-
178- const changelog = fs . readFileSync ( changelogPath , 'utf-8' )
179-
180- // Find the section for the current version: starts with "## <version>"
181- // and ends at the next "## " or end of file
182- const versionHeader = `## ${ pkg . version } `
183- const startIdx = changelog . indexOf ( versionHeader )
184- if ( startIdx === - 1 ) continue
185-
186- const afterHeader = startIdx + versionHeader . length
187- const nextSection = changelog . indexOf ( '\n## ' , afterHeader )
188- const section =
189- nextSection === - 1
190- ? changelog . slice ( afterHeader )
191- : changelog . slice ( afterHeader , nextSection )
192-
193- const content = section . trim ( )
194- if ( content ) {
195- const withAuthors = await appendAuthors ( content )
196- changelogMd += `#### ${ pkg . name } \n\n${ withAuthors } \n\n`
181+ for ( const type of sortedTypes ) {
182+ const label = typeLabels [ type ] || type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 )
183+ changelogMd += `### ${ label } \n\n`
184+
185+ for ( const commit of groups [ type ] ) {
186+ const scopePrefix = commit . scope ? `${ commit . scope } : ` : ''
187+ const cleanMessage = commit . message . replace ( / \s * \( # \d + \) / , '' )
188+ const prRef = commit . prNumber ? ` (#${ commit . prNumber } )` : ''
189+ const username = commit . prNumber
190+ ? await resolveAuthorForPR ( commit . prNumber )
191+ : await resolveUsername ( commit . email )
192+ const authorSuffix = username ? ` by @${ username } ` : ''
193+
194+ changelogMd += `- ${ scopePrefix } ${ cleanMessage } ${ prRef } (${ commit . hash } )${ authorSuffix } \n`
197195 }
196+ changelogMd += '\n'
198197}
199198
200- if ( ! changelogMd ) {
199+ if ( ! changelogMd . trim ( ) ) {
201200 changelogMd = '- No changelog entries\n\n'
202201}
203202
0 commit comments