mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
- 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
197 lines
No EOL
5.7 KiB
JavaScript
Executable file
197 lines
No EOL
5.7 KiB
JavaScript
Executable file
#!/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 });
|
||
} |