mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(release): include commit body in changelog output
This commit is contained in:
parent
694898644c
commit
b1e3c22082
1 changed files with 137 additions and 90 deletions
|
|
@ -1,10 +1,10 @@
|
|||
#!/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';
|
||||
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);
|
||||
|
|
@ -14,20 +14,22 @@ const __dirname = path.dirname(__filename);
|
|||
*/
|
||||
export function generateMergedChangelog(targetVersion, options = {}) {
|
||||
const { dryRun = false } = options;
|
||||
|
||||
|
||||
// 获取上一个正式版本标签
|
||||
function getLastStableTag() {
|
||||
try {
|
||||
const allTags = execSync('git tag -l', { encoding: 'utf8' })
|
||||
const allTags = execSync("git tag -l", { encoding: "utf8" })
|
||||
.trim()
|
||||
.split('\n')
|
||||
.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/, '');
|
||||
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 });
|
||||
|
|
@ -36,162 +38,207 @@ export function generateMergedChangelog(targetVersion, options = {}) {
|
|||
// 标签不在当前分支历史中,跳过
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (stableTags.length === 0) {
|
||||
return 'HEAD~30';
|
||||
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';
|
||||
return "HEAD~30";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const lastStableTag = getLastStableTag();
|
||||
console.log(`📦 Generating changelog from ${lastStableTag} to ${targetVersion}`);
|
||||
|
||||
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' }
|
||||
{ encoding: "utf8" },
|
||||
).trim();
|
||||
|
||||
const commits = rawCommits ? rawCommits.split('\n').filter(Boolean) : [];
|
||||
|
||||
|
||||
const commits = rawCommits ? rawCommits.split("\n").filter(Boolean) : [];
|
||||
|
||||
// 按类型分组提交
|
||||
const groupedCommits = {
|
||||
'Features': [],
|
||||
'Bug Fixes': [],
|
||||
'Performance': [],
|
||||
'Refactors': [],
|
||||
'Documentation': [],
|
||||
'Styles': [],
|
||||
'Tests': [],
|
||||
'Reverts': [],
|
||||
'Breaking Changes': []
|
||||
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'
|
||||
feat: "Features",
|
||||
fix: "Bug Fixes",
|
||||
perf: "Performance",
|
||||
refactor: "Refactors",
|
||||
docs: "Documentation",
|
||||
style: "Styles",
|
||||
test: "Tests",
|
||||
revert: "Reverts",
|
||||
};
|
||||
|
||||
|
||||
// 解析提交
|
||||
commits.forEach(commit => {
|
||||
const parts = commit.split('|||');
|
||||
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/))) {
|
||||
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({
|
||||
if (body && body.includes("BREAKING CHANGE")) {
|
||||
groupedCommits["Breaking Changes"].push({
|
||||
hash: hash.substring(0, 7),
|
||||
scope,
|
||||
description,
|
||||
body
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const section = typeMap[type];
|
||||
if (section) {
|
||||
groupedCommits[section].push({
|
||||
hash: hash.substring(0, 7),
|
||||
scope,
|
||||
description
|
||||
description,
|
||||
body: body?.trim() || "",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 生成 changelog 内容
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
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 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 => {
|
||||
sectionCommits.forEach((commit) => {
|
||||
const commitUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/commit/${commit.hash}`;
|
||||
const scopePrefix = commit.scope ? `**${commit.scope}:** ` : '';
|
||||
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`;
|
||||
|
||||
// Add commit body as sub-items if present
|
||||
if (commit.body) {
|
||||
if (section === "Breaking Changes") {
|
||||
// Handle BREAKING CHANGE specially
|
||||
const breakingDetail = commit.body
|
||||
.split("BREAKING CHANGE:")[1]
|
||||
?.trim();
|
||||
if (breakingDetail) {
|
||||
newChangelog += ` ${breakingDetail}\n`;
|
||||
}
|
||||
} else {
|
||||
// Parse body lines and add as sub-list items
|
||||
const bodyLines = commit.body
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(
|
||||
(line) =>
|
||||
line && !line.startsWith("BREAKING CHANGE"),
|
||||
);
|
||||
|
||||
if (bodyLines.length > 0) {
|
||||
bodyLines.forEach((line) => {
|
||||
// Remove leading "- " if present to avoid double bullets
|
||||
const cleanLine = line.replace(/^[-*]\s*/, "");
|
||||
if (cleanLine) {
|
||||
newChangelog += ` - ${cleanLine}\n`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
newChangelog += '\n';
|
||||
newChangelog += "\n";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 读取现有的 changelog
|
||||
const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
|
||||
let existingChangelog = '';
|
||||
const changelogPath = path.join(__dirname, "..", "CHANGELOG.md");
|
||||
let existingChangelog = "";
|
||||
try {
|
||||
existingChangelog = readFileSync(changelogPath, 'utf8');
|
||||
existingChangelog = readFileSync(changelogPath, "utf8");
|
||||
} catch (e) {
|
||||
// 文件不存在,创建新的
|
||||
existingChangelog = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n';
|
||||
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`);
|
||||
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;
|
||||
|
||||
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("📋 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');
|
||||
const version = process.argv[2] || "9.8.0";
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
generateMergedChangelog(version, { dryRun });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue