release.cjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. #!/usr/bin/env node
  2. /**
  3. * 发布脚本
  4. * Release script for Claude AI Installer
  5. *
  6. * 功能:
  7. * - 自动更新版本号
  8. * - 构建所有平台的安装包
  9. * - 生成发布说明
  10. * - 可选: 创建 Git tag 并推送
  11. *
  12. * 用法 / Usage:
  13. * node scripts/release.js [version] [options]
  14. *
  15. * 示例:
  16. * node scripts/release.js patch # 0.0.1 -> 0.0.2
  17. * node scripts/release.js minor # 0.0.1 -> 0.1.0
  18. * node scripts/release.js major # 0.0.1 -> 1.0.0
  19. * node scripts/release.js 1.2.3 # 设置为指定版本
  20. * node scripts/release.js patch --tag # 更新版本并创建 Git tag
  21. */
  22. const { execSync } = require('child_process');
  23. const path = require('path');
  24. const fs = require('fs');
  25. // 颜色输出
  26. const colors = {
  27. reset: '\x1b[0m',
  28. bright: '\x1b[1m',
  29. red: '\x1b[31m',
  30. green: '\x1b[32m',
  31. yellow: '\x1b[33m',
  32. blue: '\x1b[34m',
  33. cyan: '\x1b[36m',
  34. magenta: '\x1b[35m',
  35. };
  36. const log = {
  37. info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
  38. success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`),
  39. warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
  40. error: (msg) => console.log(`${colors.red}✖${colors.reset} ${msg}`),
  41. step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`),
  42. };
  43. const projectRoot = path.resolve(__dirname, '..');
  44. const packageJsonPath = path.join(projectRoot, 'package.json');
  45. // 解析命令行参数
  46. function parseArgs() {
  47. const args = process.argv.slice(2);
  48. // 每个平台默认只构建对应的版本,避免跨平台构建失败
  49. const platformMap = { win32: 'win', darwin: 'mac', linux: 'linux' };
  50. const currentPlatform = platformMap[process.platform] || 'linux';
  51. const defaultPlatforms = [currentPlatform];
  52. const options = {
  53. version: null,
  54. tag: false,
  55. push: false,
  56. platforms: defaultPlatforms,
  57. dryRun: false,
  58. help: false,
  59. };
  60. for (let i = 0; i < args.length; i++) {
  61. const arg = args[i];
  62. switch (arg) {
  63. case '--tag':
  64. case '-t':
  65. options.tag = true;
  66. break;
  67. case '--push':
  68. options.push = true;
  69. break;
  70. case '--platform':
  71. case '-p':
  72. options.platforms = args[++i].split(',');
  73. break;
  74. case '--dry-run':
  75. options.dryRun = true;
  76. break;
  77. case '--help':
  78. case '-h':
  79. options.help = true;
  80. break;
  81. default:
  82. if (!arg.startsWith('-') && !options.version) {
  83. options.version = arg;
  84. }
  85. }
  86. }
  87. return options;
  88. }
  89. // 显示帮助信息
  90. function showHelp() {
  91. console.log(`
  92. ${colors.bright}Claude AI Installer 发布脚本${colors.reset}
  93. ${colors.cyan}用法:${colors.reset}
  94. node scripts/release.js [version] [options]
  95. ${colors.cyan}版本参数:${colors.reset}
  96. patch 补丁版本 (0.0.1 -> 0.0.2)
  97. minor 次版本 (0.0.1 -> 0.1.0)
  98. major 主版本 (0.0.1 -> 1.0.0)
  99. x.y.z 指定版本号
  100. ${colors.cyan}选项:${colors.reset}
  101. --tag, -t 创建 Git tag
  102. --push 推送 tag 到远程仓库
  103. --platform, -p 指定平台 (逗号分隔): win,mac,linux
  104. --dry-run 仅显示将要执行的操作,不实际执行
  105. --help, -h 显示此帮助信息
  106. ${colors.cyan}示例:${colors.reset}
  107. node scripts/release.js patch # 更新补丁版本并构建
  108. node scripts/release.js 1.0.0 --tag # 设置版本为 1.0.0 并创建 tag
  109. node scripts/release.js minor -p win,mac # 更新次版本,仅构建 Win 和 Mac
  110. node scripts/release.js patch --tag --push # 更新版本、创建 tag 并推送
  111. `);
  112. }
  113. // 读取 package.json
  114. function readPackageJson() {
  115. return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
  116. }
  117. // 写入 package.json
  118. function writePackageJson(pkg) {
  119. fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
  120. }
  121. // 计算新版本号
  122. function calculateNewVersion(currentVersion, versionArg) {
  123. if (!versionArg) {
  124. return currentVersion;
  125. }
  126. // 如果是具体版本号
  127. if (/^\d+\.\d+\.\d+/.test(versionArg)) {
  128. return versionArg;
  129. }
  130. const parts = currentVersion.split('.').map(Number);
  131. switch (versionArg) {
  132. case 'major':
  133. return `${parts[0] + 1}.0.0`;
  134. case 'minor':
  135. return `${parts[0]}.${parts[1] + 1}.0`;
  136. case 'patch':
  137. return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
  138. default:
  139. throw new Error(`无效的版本参数: ${versionArg}`);
  140. }
  141. }
  142. // 执行命令
  143. function exec(command, options = {}) {
  144. log.info(`执行: ${command}`);
  145. try {
  146. return execSync(command, {
  147. stdio: options.silent ? 'pipe' : 'inherit',
  148. cwd: projectRoot,
  149. encoding: 'utf-8',
  150. ...options,
  151. });
  152. } catch (error) {
  153. if (!options.ignoreError) {
  154. throw error;
  155. }
  156. return null;
  157. }
  158. }
  159. // 检查 Git 状态
  160. function checkGitStatus() {
  161. log.step('检查 Git 状态...');
  162. try {
  163. const status = execSync('git status --porcelain', {
  164. cwd: projectRoot,
  165. encoding: 'utf-8',
  166. });
  167. if (status.trim()) {
  168. log.warn('工作目录有未提交的更改:');
  169. console.log(status);
  170. return false;
  171. }
  172. log.success('Git 工作目录干净');
  173. return true;
  174. } catch (error) {
  175. log.warn('无法检查 Git 状态');
  176. return true;
  177. }
  178. }
  179. // 更新版本号
  180. function updateVersion(newVersion, dryRun) {
  181. log.step(`更新版本号到 ${newVersion}...`);
  182. if (dryRun) {
  183. log.info('[DRY RUN] 将更新 package.json 版本号');
  184. return;
  185. }
  186. const pkg = readPackageJson();
  187. pkg.version = newVersion;
  188. writePackageJson(pkg);
  189. log.success(`版本号已更新: ${newVersion}`);
  190. }
  191. // 构建所有平台
  192. function buildAllPlatforms(platforms, dryRun) {
  193. log.step('构建应用...');
  194. for (const platform of platforms) {
  195. log.info(`构建 ${platform} 平台...`);
  196. if (dryRun) {
  197. log.info(`[DRY RUN] 将构建 ${platform} 平台`);
  198. continue;
  199. }
  200. exec(`node scripts/build.js -p ${platform}`);
  201. }
  202. log.success('所有平台构建完成');
  203. }
  204. // 创建 Git tag
  205. function createGitTag(version, dryRun) {
  206. log.step(`创建 Git tag v${version}...`);
  207. if (dryRun) {
  208. log.info(`[DRY RUN] 将创建 tag: v${version}`);
  209. return;
  210. }
  211. // 提交版本更新
  212. exec('git add package.json');
  213. exec(`git commit -m "chore: release v${version}"`);
  214. // 创建 tag
  215. exec(`git tag -a v${version} -m "Release v${version}"`);
  216. log.success(`Git tag v${version} 已创建`);
  217. }
  218. // 推送到远程
  219. function pushToRemote(version, dryRun) {
  220. log.step('推送到远程仓库...');
  221. if (dryRun) {
  222. log.info('[DRY RUN] 将推送 commits 和 tags');
  223. return;
  224. }
  225. exec('git push');
  226. exec('git push --tags');
  227. log.success('已推送到远程仓库');
  228. }
  229. // 生成发布说明
  230. function generateReleaseNotes(version) {
  231. log.step('生成发布说明...');
  232. const releaseDir = path.join(projectRoot, 'release');
  233. const notesPath = path.join(releaseDir, `RELEASE_NOTES_v${version}.md`);
  234. // 获取最近的 commits
  235. let commits = '';
  236. try {
  237. commits = execSync('git log --oneline -20', {
  238. cwd: projectRoot,
  239. encoding: 'utf-8',
  240. });
  241. } catch (e) {
  242. commits = '无法获取提交历史';
  243. }
  244. // 获取构建产物列表
  245. let artifacts = [];
  246. if (fs.existsSync(releaseDir)) {
  247. artifacts = fs.readdirSync(releaseDir).filter(f => {
  248. const ext = path.extname(f).toLowerCase();
  249. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  250. });
  251. }
  252. const notes = `# Claude AI Installer v${version}
  253. ## 发布日期
  254. ${new Date().toISOString().split('T')[0]}
  255. ## 下载
  256. ${artifacts.map(a => `- ${a}`).join('\n') || '暂无构建产物'}
  257. ## 平台支持
  258. | 平台 | 文件格式 | 架构 |
  259. |------|----------|------|
  260. | Windows | NSIS 安装包 (.exe) | x64 |
  261. | macOS | DMG 镜像 (.dmg) | x64, arm64 |
  262. | Linux | AppImage (.AppImage) | x64 |
  263. ## 更新内容
  264. 请查看提交历史了解详细更新内容。
  265. ## 最近提交
  266. \`\`\`
  267. ${commits}
  268. \`\`\`
  269. ## 安装说明
  270. ### Windows
  271. 1. 下载 \`.exe\` 安装包
  272. 2. 双击运行安装程序
  273. 3. 按照向导完成安装
  274. ### macOS
  275. 1. 下载 \`.dmg\` 文件
  276. 2. 双击打开 DMG 镜像
  277. 3. 将应用拖拽到 Applications 文件夹
  278. ### Linux
  279. 1. 下载 \`.AppImage\` 文件
  280. 2. 添加执行权限: \`chmod +x *.AppImage\`
  281. 3. 双击运行或在终端执行
  282. ---
  283. 🤖 Generated with Claude AI Installer Build System
  284. `;
  285. fs.writeFileSync(notesPath, notes);
  286. log.success(`发布说明已生成: ${notesPath}`);
  287. }
  288. // 显示发布摘要
  289. function showSummary(version, platforms) {
  290. const releaseDir = path.join(projectRoot, 'release');
  291. console.log(`
  292. ${colors.bright}╔════════════════════════════════════════════╗
  293. ║ 发布摘要 - v${version.padEnd(20)}║
  294. ╚════════════════════════════════════════════╝${colors.reset}
  295. `);
  296. if (fs.existsSync(releaseDir)) {
  297. const files = fs.readdirSync(releaseDir).filter(f => {
  298. const ext = path.extname(f).toLowerCase();
  299. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  300. });
  301. if (files.length > 0) {
  302. console.log(`${colors.cyan}构建产物:${colors.reset}`);
  303. for (const file of files) {
  304. const filePath = path.join(releaseDir, file);
  305. const stats = fs.statSync(filePath);
  306. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  307. console.log(` ${colors.green}•${colors.reset} ${file} (${sizeMB} MB)`);
  308. }
  309. }
  310. }
  311. console.log(`
  312. ${colors.cyan}输出目录:${colors.reset} ${releaseDir}
  313. ${colors.cyan}版本号:${colors.reset} v${version}
  314. ${colors.cyan}构建平台:${colors.reset} ${platforms.join(', ')}
  315. `);
  316. }
  317. // 主函数
  318. async function main() {
  319. const options = parseArgs();
  320. if (options.help) {
  321. showHelp();
  322. process.exit(0);
  323. }
  324. console.log(`
  325. ${colors.bright}${colors.magenta}╔════════════════════════════════════════════╗
  326. ║ Claude AI Installer 发布脚本 ║
  327. ╚════════════════════════════════════════════╝${colors.reset}
  328. `);
  329. if (options.dryRun) {
  330. log.warn('DRY RUN 模式 - 不会执行实际操作');
  331. }
  332. const startTime = Date.now();
  333. try {
  334. // 读取当前版本
  335. const pkg = readPackageJson();
  336. const currentVersion = pkg.version;
  337. log.info(`当前版本: ${currentVersion}`);
  338. // 计算新版本
  339. const newVersion = calculateNewVersion(currentVersion, options.version);
  340. log.info(`目标版本: ${newVersion}`);
  341. // 检查 Git 状态
  342. if (options.tag && !options.dryRun) {
  343. const isClean = checkGitStatus();
  344. if (!isClean) {
  345. log.warn('建议先提交或暂存更改');
  346. }
  347. }
  348. // 更新版本号
  349. if (options.version) {
  350. updateVersion(newVersion, options.dryRun);
  351. }
  352. // 构建所有平台
  353. buildAllPlatforms(options.platforms, options.dryRun);
  354. // 生成发布说明
  355. if (!options.dryRun) {
  356. generateReleaseNotes(newVersion);
  357. }
  358. // 创建 Git tag
  359. if (options.tag) {
  360. createGitTag(newVersion, options.dryRun);
  361. }
  362. // 推送到远程
  363. if (options.push) {
  364. pushToRemote(newVersion, options.dryRun);
  365. }
  366. // 显示摘要
  367. if (!options.dryRun) {
  368. showSummary(newVersion, options.platforms);
  369. }
  370. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  371. log.success(`发布完成! 耗时: ${duration}s`);
  372. } catch (error) {
  373. log.error(error.message);
  374. process.exit(1);
  375. }
  376. }
  377. main();