deploy-to-server.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. // .build/deploy-to-server.js - 跨平台部署脚本
  2. const { NodeSSH } = require('node-ssh');
  3. const path = require('path');
  4. const fs = require('fs');
  5. /**
  6. * 部署到远程服务器
  7. * @param {Object} options 部署选项
  8. * @param {string} options.privateKey SSH 私钥内容
  9. * @param {string} options.host 服务器地址
  10. * @param {string} options.username 用户名
  11. * @param {string} options.remotePath 远程部署路径
  12. * @param {string} options.localPath 本地构建产物路径
  13. * @param {boolean} options.testOnly 是否仅测试连接
  14. */
  15. async function deployToServer(options) {
  16. const {
  17. privateKey,
  18. host,
  19. username,
  20. remotePath,
  21. localPath,
  22. testOnly = false
  23. } = options;
  24. const ssh = new NodeSSH();
  25. try {
  26. console.log('🔐 正在连接到服务器...');
  27. console.log(` 主机: ${username}@${host}`);
  28. // 连接到服务器
  29. await ssh.connect({
  30. host,
  31. username,
  32. privateKey,
  33. // 兼容多种 SSH 密钥算法
  34. algorithms: {
  35. key: [
  36. 'ssh-rsa',
  37. 'ecdsa-sha2-nistp256',
  38. 'ecdsa-sha2-nistp384',
  39. 'ecdsa-sha2-nistp521',
  40. 'ssh-ed25519'
  41. ],
  42. serverHostKey: [
  43. 'ssh-rsa',
  44. 'ecdsa-sha2-nistp256',
  45. 'ecdsa-sha2-nistp384',
  46. 'ecdsa-sha2-nistp521',
  47. 'ssh-ed25519'
  48. ]
  49. }
  50. });
  51. console.log('✅ SSH 连接成功!');
  52. // 如果是测试模式,执行测试命令后返回
  53. if (testOnly) {
  54. console.log('🧪 测试模式:执行测试命令...');
  55. const result = await ssh.execCommand('echo "SSH key works!"');
  56. console.log(` 输出: ${result.stdout}`);
  57. ssh.dispose();
  58. console.log('✅ SSH 连接测试完成');
  59. return;
  60. }
  61. // 检查本地构建产物是否存在
  62. if (!fs.existsSync(localPath)) {
  63. throw new Error(`本地构建产物不存在: ${localPath}`);
  64. }
  65. // 生成版本号(格式: 20250129-143005)
  66. const version = new Date().toISOString()
  67. .replace(/[-:]/g, '')
  68. .replace('T', '-')
  69. .split('.')[0];
  70. const remoteDir = `${remotePath}/${version}`;
  71. console.log(`📦 部署版本: ${version}`);
  72. console.log(` 本地路径: ${localPath}`);
  73. console.log(` 远程路径: ${remoteDir}`);
  74. // 创建远程目录
  75. console.log('📁 创建远程目录...');
  76. const mkdirResult = await ssh.execCommand(`mkdir -p ${remoteDir}`);
  77. if (mkdirResult.code !== 0) {
  78. throw new Error(`创建远程目录失败: ${mkdirResult.stderr}`);
  79. }
  80. console.log('✅ 远程目录已创建');
  81. // 上传文件
  82. console.log('📤 开始上传文件...');
  83. let uploadedCount = 0;
  84. let errorCount = 0;
  85. const uploadResult = await ssh.putDirectory(localPath, remoteDir, {
  86. recursive: true,
  87. concurrency: 10, // 并发上传 10 个文件
  88. tick: (localFilePath, remoteFilePath, error) => {
  89. const fileName = path.basename(localFilePath);
  90. if (error) {
  91. errorCount++;
  92. console.error(` ❌ ${fileName}: ${error.message}`);
  93. } else {
  94. uploadedCount++;
  95. // 每上传 10 个文件显示一次进度
  96. if (uploadedCount % 10 === 0) {
  97. console.log(` ✓ 已上传 ${uploadedCount} 个文件...`);
  98. }
  99. }
  100. }
  101. });
  102. if (!uploadResult.successful) {
  103. throw new Error(`文件上传失败,成功: ${uploadedCount}, 失败: ${errorCount}`);
  104. }
  105. console.log(`✅ 文件上传完成!总计: ${uploadedCount} 个文件`);
  106. // 更新 latest 符号链接
  107. console.log('🔗 更新 latest 符号链接...');
  108. const symlinkCmd = `cd ${remotePath} && ln -sfn ${version} latest`;
  109. const symlinkResult = await ssh.execCommand(symlinkCmd);
  110. if (symlinkResult.code !== 0) {
  111. throw new Error(`更新符号链接失败: ${symlinkResult.stderr}`);
  112. }
  113. console.log(`✅ latest 符号链接已更新 -> ${version}`);
  114. // 验证部署
  115. console.log('🔍 验证部署...');
  116. const verifyResult = await ssh.execCommand(`ls -la ${remotePath}/latest`);
  117. console.log(` ${verifyResult.stdout}`);
  118. console.log('\n🎉 部署成功!');
  119. console.log(` 版本: ${version}`);
  120. console.log(` 访问: ${remotePath}/latest/`);
  121. ssh.dispose();
  122. return version;
  123. } catch (error) {
  124. console.error('\n❌ 部署失败:', error.message);
  125. // 提供更详细的错误信息
  126. if (error.level === 'client-authentication') {
  127. console.error(' → 认证失败,请检查 SSH 私钥是否正确');
  128. } else if (error.level === 'client-timeout') {
  129. console.error(' → 连接超时,请检查网络和服务器地址');
  130. } else if (error.code === 'ENOENT') {
  131. console.error(' → 文件或目录不存在');
  132. }
  133. ssh.dispose();
  134. throw error;
  135. }
  136. }
  137. // CLI 入口点
  138. if (require.main === module) {
  139. const args = process.argv.slice(2);
  140. const testOnly = args.includes('--test');
  141. // 从环境变量读取配置
  142. const privateKey = process.env.DEPLOY_KEY;
  143. const host = process.env.DEPLOY_HOST;
  144. const username = process.env.DEPLOY_USER;
  145. const remotePath = process.env.DEPLOY_PATH;
  146. // 本地构建产物路径
  147. const localPath = path.join(__dirname, '..', 'dist', 'h5');
  148. // 验证必需的环境变量
  149. const missingVars = [];
  150. if (!privateKey) missingVars.push('DEPLOY_KEY');
  151. if (!host) missingVars.push('DEPLOY_HOST');
  152. if (!username) missingVars.push('DEPLOY_USER');
  153. if (!remotePath) missingVars.push('DEPLOY_PATH');
  154. if (missingVars.length > 0) {
  155. console.error('❌ 缺少必需的环境变量:');
  156. missingVars.forEach(varName => {
  157. console.error(` - ${varName}`);
  158. });
  159. console.error('\n请设置这些环境变量后重试。');
  160. process.exit(1);
  161. }
  162. // 执行部署
  163. console.log('🚀 部署系统启动\n');
  164. if (testOnly) {
  165. console.log('🧪 模式: 连接测试');
  166. } else {
  167. console.log('📦 模式: 完整部署');
  168. }
  169. console.log('='.repeat(50) + '\n');
  170. deployToServer({
  171. privateKey,
  172. host,
  173. username,
  174. remotePath,
  175. localPath,
  176. testOnly
  177. })
  178. .then(() => {
  179. console.log('\n' + '='.repeat(50));
  180. process.exit(0);
  181. })
  182. .catch(error => {
  183. console.error('\n' + '='.repeat(50));
  184. process.exit(1);
  185. });
  186. }
  187. module.exports = { deployToServer };