mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(release): implement custom changelog generator for merged beta versions
- Create custom-changelog.mjs script to generate single consolidated changelog entry - Disable conventional-changelog's built-in changelog generation (infile: false) - Use custom script in after:bump hook to generate merged changelog - Properly groups all commits from last stable version by category - Filters out beta release commits automatically - Supports breaking changes with detailed descriptions
This commit is contained in:
parent
af101fd6b0
commit
9bd0d53b67
2 changed files with 203 additions and 50 deletions
|
|
@ -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,
|
||||
|
|
|
|||
197
scripts/custom-changelog.mjs
Executable file
197
scripts/custom-changelog.mjs
Executable file
|
|
@ -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 });
|
||||
}
|
||||
Loading…
Reference in a new issue