Skip to content

Commit daa2ab9

Browse files
authored
use semantic commit for improved github release changelog (#6909)
1 parent 576b5ac commit daa2ab9

1 file changed

Lines changed: 78 additions & 79 deletions

File tree

scripts/create-github-release.mjs

Lines changed: 78 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5132
const prAuthorCache = {}
5233
async 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-f0-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

168116
bumpedPackages.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+
173180
let 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

Comments
 (0)