generate-changelog.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * generate-changelog.js — 从 git log 生成 CHANGELOG.md
  3. *
  4. * 逻辑:
  5. * 1. 读取 LAST_BUILD 文件(记录上次构建的 commit hash)
  6. * 2. 从 LAST_BUILD 到 HEAD 收集 commit messages
  7. * 3. 按 conventional-commit 分类 (Feat/Fix/Refactor/Chore/Docs/…)
  8. * 4. 追加到 CHANGELOG.md 末尾并更新 LAST_BUILD
  9. *
  10. * 用法: node scripts/generate-changelog.js <version>
  11. */
  12. const fs = require('fs');
  13. const path = require('path');
  14. const { execSync } = require('child_process');
  15. const rootDir = path.resolve(__dirname, '..');
  16. const version = process.argv[2];
  17. if (!version) {
  18. console.error('[generate-changelog] 用法: node scripts/generate-changelog.js <version>');
  19. process.exit(1);
  20. }
  21. const lastBuildFile = path.join(rootDir, '.last_build_commit');
  22. const changelogFile = path.join(rootDir, 'public', 'CHANGELOG.md');
  23. // 获取上次构建 commit hash
  24. let fromHash = '';
  25. try {
  26. fromHash = fs.readFileSync(lastBuildFile, 'utf-8').trim();
  27. } catch {
  28. // 首次构建:从第一个 commit 开始
  29. try {
  30. fromHash = execSync('git rev-list --max-parents=0 HEAD', { cwd: rootDir, encoding: 'utf-8' }).trim();
  31. } catch {
  32. fromHash = '';
  33. }
  34. }
  35. // 获取当前 HEAD
  36. const headHash = execSync('git rev-parse HEAD', { cwd: rootDir, encoding: 'utf-8' }).trim();
  37. // 获取 commit 日志
  38. let log = '';
  39. try {
  40. const range = fromHash ? `${fromHash}..HEAD` : 'HEAD';
  41. log = execSync(
  42. `git log --no-merges --pretty=format:"%s|||%b|||%an|||%ai" ${range}`,
  43. { cwd: rootDir, encoding: 'utf-8' }
  44. ).trim();
  45. } catch {
  46. log = '';
  47. }
  48. // 解析 commit
  49. const commits = log ? log.split('\n').filter(Boolean).map(line => {
  50. const [subject, body, author, date] = line.split('|||');
  51. return { subject: subject || '', body: body || '', author: author || '', date: date || '' };
  52. }) : [];
  53. if (commits.length === 0) {
  54. console.log('[generate-changelog] 没有新 commit,跳过 changelog 生成');
  55. // 仍然更新 LAST_BUILD
  56. fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
  57. return;
  58. }
  59. const hasMeaningful = commits.some(c => {
  60. const cat = categorize(c.subject);
  61. return cat !== '工程化';
  62. });
  63. if (!hasMeaningful) {
  64. console.log('[generate-changelog] 仅有工程化变更,跳过 changelog 生成');
  65. fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
  66. return;
  67. }
  68. // 分类
  69. function categorize(subject) {
  70. const s = subject.toLowerCase();
  71. if (s.startsWith('feat') || s.startsWith('feature')) return '新功能';
  72. if (s.startsWith('fix') || s.startsWith('bug')) return 'Bug 修复';
  73. if (s.startsWith('refactor')) return '重构';
  74. if (s.startsWith('docs') || s.startsWith('doc')) return '文档';
  75. if (s.startsWith('chore') || s.startsWith('build') || s.startsWith('ci')) return '工程化';
  76. if (s.startsWith('perf') || s.startsWith('optimize')) return '性能优化';
  77. if (s.startsWith('style')) return '样式';
  78. if (s.startsWith('test')) return '测试';
  79. return '其他';
  80. }
  81. const grouped = {};
  82. commits.forEach(c => {
  83. const cat = categorize(c.subject);
  84. if (!grouped[cat]) grouped[cat] = [];
  85. // 格式化 subject:去掉 feat/fix: 前缀
  86. let desc = c.subject.replace(/^(feat|feature|fix|refactor|docs?|chore|build|ci|perf|style|test)(\([^)]*\))?[:\s]*/i, '');
  87. if (!desc) desc = c.subject;
  88. grouped[cat].push(`- ${desc}`);
  89. });
  90. // 构建新版本 changelog 块
  91. const now = new Date().toISOString().slice(0, 10);
  92. let block = `\n## v${version} (${now})\n\n`;
  93. for (const [cat, items] of Object.entries(grouped)) {
  94. if (cat === '工程化') continue;
  95. block += `### ${cat}\n${items.join('\n')}\n\n`;
  96. }
  97. // 追加或新建 CHANGELOG.md
  98. let existing = '';
  99. try {
  100. existing = fs.readFileSync(changelogFile, 'utf-8');
  101. } catch {
  102. existing = '# 更新日志 (Changelog)\n\n此文件记录所有构建版本的变更。\n';
  103. }
  104. // 新版本插入到标题之后、旧内容之前
  105. const headerEnd = existing.indexOf('\n## ');
  106. if (headerEnd === -1) {
  107. existing = existing.trimEnd() + block;
  108. } else {
  109. existing = existing.slice(0, headerEnd) + block + existing.slice(headerEnd);
  110. }
  111. fs.writeFileSync(changelogFile, existing, 'utf-8');
  112. // 更新 LAST_BUILD
  113. fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
  114. console.log(`[generate-changelog] CHANGELOG.md 已更新,新增 ${commits.length} 条记录`);