taskgenius_taskgenius-plugin/scripts/custom-changelog.mjs
Quorafind 9bd0d53b67 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
2025-09-04 22:48:17 +08:00

197 lines
No EOL
5.7 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 });
}