diff --git a/.release-it.cjs b/.release-it.cjs index b1e2b5de..259d69f0 100644 --- a/.release-it.cjs +++ b/.release-it.cjs @@ -69,6 +69,8 @@ module.exports = { "after:bump": [ "node esbuild.config.mjs production", "node ./scripts/zip.mjs", + // 使用自定义 changelog 生成器替代 conventional-changelog + "node ./scripts/custom-changelog.mjs ${version}", "git add .", ], "after:release": @@ -98,56 +100,10 @@ module.exports = { {type: "revert", section: "Reverts"} ] }, - infile: "CHANGELOG.md", - header: "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n", - // 只生成当前版本的 changelog,但包含从上一个正式版以来的所有提交 - releaseCount: 1, - // 限制 git log 的提交范围,从上一个正式版开始(排除 beta 版本) - gitRawCommitsOpts: { - from: lastStableTag // 使用已经获取的值 - }, - writerOpts: { - // 自定义提交转换,过滤掉 beta 相关的 chore commits - transform: (commit, context) => { - // 过滤掉 beta 版本的 release commits - if (commit.type === 'chore' && commit.subject) { - // 过滤包含 beta 的 release commits - if (commit.subject.includes('beta') || - commit.subject.includes('-beta.') || - commit.subject.match(/v\d+\.\d+\.\d+-beta/)) { - return null; - } - // 过滤 release scope 的 commits(通常是版本发布相关) - if (commit.scope === 'release' && - (commit.subject.includes('bump version') || - commit.subject.includes('v9.8.0'))) { - return null; - } - } - - // 创建一个新的 commit 对象副本,避免修改原始对象 - const transformedCommit = Object.assign({}, commit); - - // 确保 commit 有短 hash 用于显示 - if (transformedCommit.hash && !transformedCommit.shortHash) { - transformedCommit.shortHash = transformedCommit.hash.substring(0, 7); - } - - // 返回转换后的提交 - return transformedCommit; - }, - // 确保比较链接使用正确的版本范围 - finalizeContext: (context) => { - // 使用已经获取的值,避免重复调用 - if (lastStableTag) { - context.previousTag = lastStableTag; - context.currentTag = context.version; - // 更新比较 URL - context.compareUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/compare/${lastStableTag}...${context.version}`; - } - return context; - } - } + // 禁用 conventional-changelog 的 changelog 生成,使用我们的自定义脚本 + infile: false, + // 仍然使用 conventional commits 来推荐版本号 + strictSemVer: false }, "./scripts/ob-bumper.mjs": { indent: 2, diff --git a/scripts/custom-changelog.mjs b/scripts/custom-changelog.mjs new file mode 100755 index 00000000..9a866384 --- /dev/null +++ b/scripts/custom-changelog.mjs @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import { readFileSync, writeFileSync } from 'fs'; +import semver from 'semver'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * 自定义 changelog 生成器,合并所有 beta 版本的提交到单个版本条目 + */ +export function generateMergedChangelog(targetVersion, options = {}) { + const { dryRun = false } = options; + + // 获取上一个正式版本标签 + function getLastStableTag() { + try { + const allTags = execSync('git tag -l', { encoding: 'utf8' }) + .trim() + .split('\n') + .filter(Boolean); + + const stableTags = []; + for (const tag of allTags) { + try { + execSync(`git merge-base --is-ancestor ${tag} HEAD`, { encoding: 'utf8' }); + const versionString = tag.replace(/^v/, ''); + const version = semver.valid(versionString); + if (version && !semver.prerelease(version)) { + stableTags.push({ tag, version }); + } + } catch (e) { + // 标签不在当前分支历史中,跳过 + } + } + + if (stableTags.length === 0) { + return 'HEAD~30'; + } + + const sortedTags = stableTags.sort((a, b) => { + return semver.rcompare(a.version, b.version); + }); + + return sortedTags[0].tag; + } catch (error) { + return 'HEAD~30'; + } + } + + const lastStableTag = getLastStableTag(); + console.log(`📦 Generating changelog from ${lastStableTag} to ${targetVersion}`); + + // 获取所有提交 + const rawCommits = execSync( + `git log ${lastStableTag}..HEAD --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ad" --no-merges`, + { encoding: 'utf8' } + ).trim(); + + const commits = rawCommits ? rawCommits.split('\n').filter(Boolean) : []; + + // 按类型分组提交 + const groupedCommits = { + 'Features': [], + 'Bug Fixes': [], + 'Performance': [], + 'Refactors': [], + 'Documentation': [], + 'Styles': [], + 'Tests': [], + 'Reverts': [], + 'Breaking Changes': [] + }; + + const typeMap = { + 'feat': 'Features', + 'fix': 'Bug Fixes', + 'perf': 'Performance', + 'refactor': 'Refactors', + 'docs': 'Documentation', + 'style': 'Styles', + 'test': 'Tests', + 'revert': 'Reverts' + }; + + // 解析提交 + commits.forEach(commit => { + const parts = commit.split('|||'); + if (parts.length < 2) return; + + const [hash, subject, body] = parts; + if (!subject) return; + + // 解析 conventional commit 格式 + const match = subject.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/); + if (!match) return; + + const [, type, scope, description] = match; + + // 过滤掉 beta release commits + if (type === 'chore' && description && ( + description.includes('beta') || + description.includes('-beta.') || + description.match(/v\d+\.\d+\.\d+-beta/))) { + return; + } + + // 检查是否有 BREAKING CHANGE + if (body && body.includes('BREAKING CHANGE')) { + groupedCommits['Breaking Changes'].push({ + hash: hash.substring(0, 7), + scope, + description, + body + }); + } + + const section = typeMap[type]; + if (section) { + groupedCommits[section].push({ + hash: hash.substring(0, 7), + scope, + description + }); + } + }); + + // 生成 changelog 内容 + const date = new Date().toISOString().split('T')[0]; + const compareUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/compare/${lastStableTag}...${targetVersion}`; + + let newChangelog = `## [${targetVersion}](${compareUrl}) (${date})\n\n`; + + // 按顺序输出各个分组(Breaking Changes 优先) + const sectionOrder = ['Breaking Changes', 'Features', 'Bug Fixes', 'Performance', 'Refactors', 'Documentation', 'Tests', 'Styles', 'Reverts']; + + sectionOrder.forEach(section => { + const sectionCommits = groupedCommits[section]; + if (sectionCommits && sectionCommits.length > 0) { + newChangelog += `### ${section}\n\n`; + sectionCommits.forEach(commit => { + const commitUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/commit/${commit.hash}`; + const scopePrefix = commit.scope ? `**${commit.scope}:** ` : ''; + newChangelog += `* ${scopePrefix}${commit.description} ([${commit.hash}](${commitUrl}))\n`; + if (commit.body && section === 'Breaking Changes') { + // 添加 breaking change 详细说明 + const breakingDetail = commit.body.split('BREAKING CHANGE:')[1]?.trim(); + if (breakingDetail) { + newChangelog += ` ${breakingDetail}\n`; + } + } + }); + newChangelog += '\n'; + } + }); + + // 读取现有的 changelog + const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md'); + let existingChangelog = ''; + try { + existingChangelog = readFileSync(changelogPath, 'utf8'); + } catch (e) { + // 文件不存在,创建新的 + existingChangelog = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n'; + } + + // 确保不重复添加相同版本 + if (existingChangelog.includes(`## [${targetVersion}]`)) { + console.log(`⚠️ Version ${targetVersion} already exists in CHANGELOG.md`); + return existingChangelog; + } + + // 插入新的 changelog 到文件开头(在 header 之后) + const header = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n'; + const restContent = existingChangelog.replace(header, '').trim(); + const updatedChangelog = header + '\n' + newChangelog + restContent; + + if (!dryRun) { + writeFileSync(changelogPath, updatedChangelog); + console.log(`✅ CHANGELOG.md updated with version ${targetVersion}`); + } else { + console.log('📋 Preview (dry-run mode):'); + console.log(newChangelog); + } + + return updatedChangelog; +} + +// 如果直接运行此脚本 +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const version = process.argv[2] || '9.8.0'; + const dryRun = process.argv.includes('--dry-run'); + generateMergedChangelog(version, { dryRun }); +} \ No newline at end of file