generate-changelog.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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. // 分类
  60. function categorize(subject) {
  61. const s = subject.toLowerCase();
  62. if (s.startsWith('feat') || s.startsWith('feature')) return '新功能';
  63. if (s.startsWith('fix') || s.startsWith('bug')) return 'Bug 修复';
  64. if (s.startsWith('refactor')) return '重构';
  65. if (s.startsWith('docs') || s.startsWith('doc')) return '文档';
  66. if (s.startsWith('chore') || s.startsWith('build') || s.startsWith('ci')) return '工程化';
  67. if (s.startsWith('perf') || s.startsWith('optimize')) return '性能优化';
  68. if (s.startsWith('style')) return '样式';
  69. if (s.startsWith('test')) return '测试';
  70. return '其他';
  71. }
  72. const grouped = {};
  73. commits.forEach(c => {
  74. const cat = categorize(c.subject);
  75. if (!grouped[cat]) grouped[cat] = [];
  76. // 格式化 subject:去掉 feat/fix: 前缀
  77. let desc = c.subject.replace(/^(feat|feature|fix|refactor|docs?|chore|build|ci|perf|style|test)(\([^)]*\))?[:\s]*/i, '');
  78. if (!desc) desc = c.subject;
  79. grouped[cat].push(`- ${desc}`);
  80. });
  81. // 构建新版本 changelog 块
  82. const now = new Date().toISOString().slice(0, 10);
  83. let block = `\n## v${version} (${now})\n\n`;
  84. for (const [cat, items] of Object.entries(grouped)) {
  85. block += `### ${cat}\n${items.join('\n')}\n\n`;
  86. }
  87. // 追加或新建 CHANGELOG.md
  88. let existing = '';
  89. try {
  90. existing = fs.readFileSync(changelogFile, 'utf-8');
  91. } catch {
  92. existing = '# 更新日志 (Changelog)\n\n此文件记录所有构建版本的变更。\n';
  93. }
  94. // 新版本插入到标题之后、旧内容之前
  95. const headerEnd = existing.indexOf('\n## ');
  96. if (headerEnd === -1) {
  97. existing = existing.trimEnd() + block;
  98. } else {
  99. existing = existing.slice(0, headerEnd) + block + existing.slice(headerEnd);
  100. }
  101. fs.writeFileSync(changelogFile, existing, 'utf-8');
  102. // 更新 LAST_BUILD
  103. fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
  104. console.log(`[generate-changelog] CHANGELOG.md 已更新,新增 ${commits.length} 条记录`);