1
0

release.cjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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. platforms: defaultPlatforms,
  55. dryRun: false,
  56. help: false,
  57. };
  58. for (let i = 0; i < args.length; i++) {
  59. const arg = args[i];
  60. switch (arg) {
  61. case '--platform':
  62. case '-p':
  63. options.platforms = args[++i].split(',');
  64. break;
  65. case '--dry-run':
  66. options.dryRun = true;
  67. break;
  68. case '--help':
  69. case '-h':
  70. options.help = true;
  71. break;
  72. default:
  73. if (!arg.startsWith('-') && !options.version) {
  74. options.version = arg;
  75. }
  76. }
  77. }
  78. return options;
  79. }
  80. // 显示帮助信息
  81. function showHelp() {
  82. console.log(`
  83. ${colors.bright}Claude AI Installer 发布脚本${colors.reset}
  84. ${colors.cyan}用法:${colors.reset}
  85. node scripts/release.cjs [version] [options]
  86. ${colors.cyan}版本参数:${colors.reset}
  87. patch 补丁版本 (0.0.1 -> 0.0.2)
  88. minor 次版本 (0.0.1 -> 0.1.0)
  89. major 主版本 (0.0.1 -> 1.0.0)
  90. x.y.z 指定版本号
  91. ${colors.cyan}选项:${colors.reset}
  92. --platform, -p 指定平台 (逗号分隔): win,mac,linux
  93. --dry-run 仅显示将要执行的操作,不实际执行
  94. --help, -h 显示此帮助信息
  95. ${colors.cyan}示例:${colors.reset}
  96. node scripts/release.cjs patch # 更新补丁版本并构建
  97. node scripts/release.cjs 1.0.0 # 设置版本为 1.0.0 并构建
  98. node scripts/release.cjs minor -p win,mac # 更新次版本,仅构建 Win 和 Mac
  99. node scripts/release.cjs --dry-run # 预览发布操作
  100. `);
  101. }
  102. // 读取 package.json
  103. function readPackageJson() {
  104. return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
  105. }
  106. // 写入 package.json
  107. function writePackageJson(pkg) {
  108. fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
  109. }
  110. // 计算新版本号(调用 bump-version.mjs --dry-run)
  111. function calculateNewVersion(currentVersion, versionArg) {
  112. if (!versionArg) {
  113. return currentVersion;
  114. }
  115. // 调用 bump-version.mjs --dry-run 获取计算后的版本号
  116. const result = execSync(`node scripts/bump-version.mjs ${versionArg} --dry-run`, {
  117. cwd: projectRoot,
  118. encoding: 'utf-8',
  119. });
  120. return result.trim();
  121. }
  122. // 执行命令
  123. function exec(command, options = {}) {
  124. log.info(`执行: ${command}`);
  125. try {
  126. return execSync(command, {
  127. stdio: options.silent ? 'pipe' : 'inherit',
  128. cwd: projectRoot,
  129. encoding: 'utf-8',
  130. ...options,
  131. });
  132. } catch (error) {
  133. if (!options.ignoreError) {
  134. throw error;
  135. }
  136. return null;
  137. }
  138. }
  139. // 更新版本号(调用 bump-version.mjs 脚本)
  140. function updateVersion(newVersion, dryRun) {
  141. log.step(`更新版本号到 ${newVersion}...`);
  142. if (dryRun) {
  143. log.info('[DRY RUN] 将更新 package.json、tauri.conf.json 和 Cargo.toml 版本号');
  144. return;
  145. }
  146. // 调用 bump-version.mjs 脚本统一更新所有版本号
  147. exec(`node scripts/bump-version.mjs ${newVersion}`);
  148. }
  149. // 构建所有平台
  150. function buildAllPlatforms(platforms, dryRun) {
  151. log.step('构建应用...');
  152. for (const platform of platforms) {
  153. log.info(`构建 ${platform} 平台...`);
  154. if (dryRun) {
  155. log.info(`[DRY RUN] 将构建 ${platform} 平台`);
  156. continue;
  157. }
  158. // Tauri 构建(build.js 不支持 -p 参数,直接调用)
  159. exec(`node scripts/build.js`);
  160. }
  161. // 复制构建产物到 release 目录
  162. if (!dryRun) {
  163. copyArtifactsToRelease();
  164. }
  165. log.success('所有平台构建完成');
  166. }
  167. // 复制 Tauri 构建产物到 release 目录
  168. function copyArtifactsToRelease() {
  169. log.step('复制构建产物到 release 目录...');
  170. const releaseDir = path.join(projectRoot, 'release');
  171. const tauriDir = path.join(projectRoot, 'src-tauri');
  172. const bundleDir = path.join(tauriDir, 'target', 'release', 'bundle');
  173. // 清空 release 目录(保留目录本身)
  174. if (fs.existsSync(releaseDir)) {
  175. const files = fs.readdirSync(releaseDir);
  176. for (const file of files) {
  177. const filePath = path.join(releaseDir, file);
  178. const stat = fs.statSync(filePath);
  179. if (stat.isFile()) {
  180. fs.unlinkSync(filePath);
  181. } else if (stat.isDirectory()) {
  182. fs.rmSync(filePath, { recursive: true });
  183. }
  184. }
  185. log.success('已清空 release 目录');
  186. } else {
  187. fs.mkdirSync(releaseDir, { recursive: true });
  188. }
  189. // 复制 NSIS 安装程序
  190. const nsisDir = path.join(bundleDir, 'nsis');
  191. if (fs.existsSync(nsisDir)) {
  192. const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe'));
  193. for (const file of files) {
  194. const src = path.join(nsisDir, file);
  195. const dest = path.join(releaseDir, file);
  196. fs.copyFileSync(src, dest);
  197. log.success(`复制: ${file}`);
  198. }
  199. }
  200. // 复制 MSI 安装程序
  201. const msiDir = path.join(bundleDir, 'msi');
  202. if (fs.existsSync(msiDir)) {
  203. const files = fs.readdirSync(msiDir).filter(f => f.endsWith('.msi'));
  204. for (const file of files) {
  205. const src = path.join(msiDir, file);
  206. const dest = path.join(releaseDir, file);
  207. fs.copyFileSync(src, dest);
  208. log.success(`复制: ${file}`);
  209. }
  210. }
  211. // 复制更新签名文件(如果存在)
  212. if (fs.existsSync(nsisDir)) {
  213. const sigFiles = fs.readdirSync(nsisDir).filter(f => f.endsWith('.sig'));
  214. for (const file of sigFiles) {
  215. const src = path.join(nsisDir, file);
  216. const dest = path.join(releaseDir, file);
  217. fs.copyFileSync(src, dest);
  218. log.success(`复制: ${file}`);
  219. }
  220. }
  221. // 复制 Portable exe(从 bundle 目录)
  222. const pkg = readPackageJson();
  223. const version = pkg.version;
  224. const portableExeName = `Claude-AI-Installer-${version}-win-x64-portable.exe`;
  225. const portableExePath = path.join(bundleDir, portableExeName);
  226. if (fs.existsSync(portableExePath)) {
  227. const dest = path.join(releaseDir, portableExeName);
  228. fs.copyFileSync(portableExePath, dest);
  229. log.success(`复制: ${portableExeName}`);
  230. }
  231. }
  232. // 提交版本更新
  233. function commitVersionUpdate(version) {
  234. log.step(`提交版本更新 v${version}...`);
  235. // 添加所有版本相关文件
  236. exec('git add package.json', { ignoreError: true });
  237. exec('git add src-tauri/tauri.conf.json', { ignoreError: true });
  238. exec('git add src-tauri/Cargo.toml', { ignoreError: true });
  239. // 提交
  240. exec(`git commit -m "本地发布版本: ${version}"`, { ignoreError: true });
  241. log.success(`版本更新已提交: v${version}`);
  242. }
  243. // 生成发布说明
  244. function generateReleaseNotes(version) {
  245. log.step('生成发布说明...');
  246. const releaseDir = path.join(projectRoot, 'release');
  247. const notesPath = path.join(releaseDir, `RELEASE_NOTES_v${version}.md`);
  248. // 获取最近的 commits
  249. let commits = '';
  250. try {
  251. commits = execSync('git log --oneline -20', {
  252. cwd: projectRoot,
  253. encoding: 'utf-8',
  254. });
  255. } catch (e) {
  256. commits = '无法获取提交历史';
  257. }
  258. // 获取构建产物列表
  259. let artifacts = [];
  260. if (fs.existsSync(releaseDir)) {
  261. artifacts = fs.readdirSync(releaseDir).filter(f => {
  262. const ext = path.extname(f).toLowerCase();
  263. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  264. });
  265. }
  266. const notes = `# Claude AI Installer v${version}
  267. ## 发布日期
  268. ${new Date().toISOString().split('T')[0]}
  269. ## 下载
  270. ${artifacts.map(a => `- ${a}`).join('\n') || '暂无构建产物'}
  271. ## 平台支持
  272. | 平台 | 文件格式 | 架构 |
  273. |------|----------|------|
  274. | Windows | NSIS 安装包 (.exe) | x64 |
  275. | macOS | DMG 镜像 (.dmg) | x64, arm64 |
  276. | Linux | AppImage (.AppImage) | x64 |
  277. ## 更新内容
  278. 请查看提交历史了解详细更新内容。
  279. ## 最近提交
  280. \`\`\`
  281. ${commits}
  282. \`\`\`
  283. ## 安装说明
  284. ### Windows
  285. 1. 下载 \`.exe\` 安装包
  286. 2. 双击运行安装程序
  287. 3. 按照向导完成安装
  288. ### macOS
  289. 1. 下载 \`.dmg\` 文件
  290. 2. 双击打开 DMG 镜像
  291. 3. 将应用拖拽到 Applications 文件夹
  292. ### Linux
  293. 1. 下载 \`.AppImage\` 文件
  294. 2. 添加执行权限: \`chmod +x *.AppImage\`
  295. 3. 双击运行或在终端执行
  296. ---
  297. 🤖 Generated with Claude AI Installer Build System
  298. `;
  299. fs.writeFileSync(notesPath, notes);
  300. log.success(`发布说明已生成: ${notesPath}`);
  301. }
  302. // 显示发布摘要
  303. function showSummary(version, platforms) {
  304. const releaseDir = path.join(projectRoot, 'release');
  305. console.log(`
  306. ${colors.bright}╔════════════════════════════════════════════╗
  307. ║ 发布摘要 - v${version.padEnd(20)}║
  308. ╚════════════════════════════════════════════╝${colors.reset}
  309. `);
  310. if (fs.existsSync(releaseDir)) {
  311. const files = fs.readdirSync(releaseDir).filter(f => {
  312. const ext = path.extname(f).toLowerCase();
  313. return ['.exe', '.msi', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  314. });
  315. if (files.length > 0) {
  316. console.log(`${colors.cyan}构建产物:${colors.reset}`);
  317. for (const file of files) {
  318. const filePath = path.join(releaseDir, file);
  319. const stats = fs.statSync(filePath);
  320. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  321. // 根据文件名添加描述
  322. let desc = '';
  323. if (file.includes('-setup.exe')) {
  324. desc = ' - NSIS 安装程序';
  325. } else if (file.endsWith('.msi')) {
  326. desc = ' - MSI 安装程序';
  327. } else if (file.includes('-portable.exe')) {
  328. desc = ' - 便携版';
  329. } else if (file.endsWith('.dmg')) {
  330. desc = ' - macOS 安装镜像';
  331. } else if (file.endsWith('.AppImage')) {
  332. desc = ' - Linux 可执行程序';
  333. }
  334. console.log(` ${colors.green}•${colors.reset} ${file} (${sizeMB} MB)${desc}`);
  335. }
  336. }
  337. }
  338. console.log(`
  339. ${colors.cyan}输出目录:${colors.reset} ${releaseDir}
  340. ${colors.cyan}版本号:${colors.reset} v${version}
  341. ${colors.cyan}构建平台:${colors.reset} ${platforms.join(', ')}
  342. `);
  343. }
  344. // 主函数
  345. async function main() {
  346. const options = parseArgs();
  347. if (options.help) {
  348. showHelp();
  349. process.exit(0);
  350. }
  351. console.log(`
  352. ${colors.bright}${colors.magenta}╔════════════════════════════════════════════╗
  353. ║ Claude AI Installer 发布脚本 ║
  354. ╚════════════════════════════════════════════╝${colors.reset}
  355. `);
  356. if (options.dryRun) {
  357. log.warn('DRY RUN 模式 - 不会执行实际操作');
  358. }
  359. const startTime = Date.now();
  360. try {
  361. // 读取当前版本
  362. const pkg = readPackageJson();
  363. const currentVersion = pkg.version;
  364. log.info(`当前版本: ${currentVersion}`);
  365. // 计算新版本
  366. const newVersion = calculateNewVersion(currentVersion, options.version);
  367. log.info(`目标版本: ${newVersion}`);
  368. // 更新版本号
  369. if (options.version) {
  370. updateVersion(newVersion, options.dryRun);
  371. }
  372. // 构建所有平台
  373. buildAllPlatforms(options.platforms, options.dryRun);
  374. // 生成发布说明
  375. if (!options.dryRun) {
  376. generateReleaseNotes(newVersion);
  377. }
  378. // 提交版本更新
  379. if (options.version && !options.dryRun) {
  380. commitVersionUpdate(newVersion);
  381. }
  382. // 显示摘要
  383. if (!options.dryRun) {
  384. showSummary(newVersion, options.platforms);
  385. }
  386. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  387. log.success(`发布完成! 耗时: ${duration}s`);
  388. } catch (error) {
  389. log.error(error.message);
  390. process.exit(1);
  391. }
  392. }
  393. main();