// .build/deploy-to-server.js - 跨平台部署脚本 const { NodeSSH } = require('node-ssh'); const path = require('path'); const fs = require('fs'); /** * 部署到远程服务器 * @param {Object} options 部署选项 * @param {string} options.privateKey SSH 私钥内容 * @param {string} options.host 服务器地址 * @param {string} options.username 用户名 * @param {string} options.remotePath 远程部署路径 * @param {string} options.localPath 本地构建产物路径 * @param {boolean} options.testOnly 是否仅测试连接 */ async function deployToServer(options) { const { privateKey, host, username, remotePath, localPath, testOnly = false } = options; const ssh = new NodeSSH(); try { console.log('🔐 正在连接到服务器...'); console.log(` 主机: ${username}@${host}`); // 连接到服务器 await ssh.connect({ host, username, privateKey, // 兼容多种 SSH 密钥算法 algorithms: { key: [ 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ], serverHostKey: [ 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ] } }); console.log('✅ SSH 连接成功!'); // 如果是测试模式,执行测试命令后返回 if (testOnly) { console.log('🧪 测试模式:执行测试命令...'); const result = await ssh.execCommand('echo "SSH key works!"'); console.log(` 输出: ${result.stdout}`); ssh.dispose(); console.log('✅ SSH 连接测试完成'); return; } // 检查本地构建产物是否存在 if (!fs.existsSync(localPath)) { throw new Error(`本地构建产物不存在: ${localPath}`); } // 生成版本号(格式: 20250129-143005) const version = new Date().toISOString() .replace(/[-:]/g, '') .replace('T', '-') .split('.')[0]; const remoteDir = `${remotePath}/${version}`; console.log(`📦 部署版本: ${version}`); console.log(` 本地路径: ${localPath}`); console.log(` 远程路径: ${remoteDir}`); // 创建远程目录 console.log('📁 创建远程目录...'); const mkdirResult = await ssh.execCommand(`mkdir -p ${remoteDir}`); if (mkdirResult.code !== 0) { throw new Error(`创建远程目录失败: ${mkdirResult.stderr}`); } console.log('✅ 远程目录已创建'); // 上传文件 console.log('📤 开始上传文件...'); let uploadedCount = 0; let errorCount = 0; const uploadResult = await ssh.putDirectory(localPath, remoteDir, { recursive: true, concurrency: 10, // 并发上传 10 个文件 tick: (localFilePath, remoteFilePath, error) => { const fileName = path.basename(localFilePath); if (error) { errorCount++; console.error(` ❌ ${fileName}: ${error.message}`); } else { uploadedCount++; // 每上传 10 个文件显示一次进度 if (uploadedCount % 10 === 0) { console.log(` ✓ 已上传 ${uploadedCount} 个文件...`); } } } }); if (!uploadResult.successful) { throw new Error(`文件上传失败,成功: ${uploadedCount}, 失败: ${errorCount}`); } console.log(`✅ 文件上传完成!总计: ${uploadedCount} 个文件`); // 更新 latest 符号链接 console.log('🔗 更新 latest 符号链接...'); const symlinkCmd = `cd ${remotePath} && ln -sfn ${version} latest`; const symlinkResult = await ssh.execCommand(symlinkCmd); if (symlinkResult.code !== 0) { throw new Error(`更新符号链接失败: ${symlinkResult.stderr}`); } console.log(`✅ latest 符号链接已更新 -> ${version}`); // 验证部署 console.log('🔍 验证部署...'); const verifyResult = await ssh.execCommand(`ls -la ${remotePath}/latest`); console.log(` ${verifyResult.stdout}`); console.log('\n🎉 部署成功!'); console.log(` 版本: ${version}`); console.log(` 访问: ${remotePath}/latest/`); ssh.dispose(); return version; } catch (error) { console.error('\n❌ 部署失败:', error.message); // 提供更详细的错误信息 if (error.level === 'client-authentication') { console.error(' → 认证失败,请检查 SSH 私钥是否正确'); } else if (error.level === 'client-timeout') { console.error(' → 连接超时,请检查网络和服务器地址'); } else if (error.code === 'ENOENT') { console.error(' → 文件或目录不存在'); } ssh.dispose(); throw error; } } // CLI 入口点 if (require.main === module) { const args = process.argv.slice(2); const testOnly = args.includes('--test'); // 从环境变量读取配置 const privateKey = process.env.DEPLOY_KEY; const host = process.env.DEPLOY_HOST; const username = process.env.DEPLOY_USER; const remotePath = process.env.DEPLOY_PATH; // 本地构建产物路径 const localPath = path.join(__dirname, '..', 'dist', 'h5'); // 验证必需的环境变量 const missingVars = []; if (!privateKey) missingVars.push('DEPLOY_KEY'); if (!host) missingVars.push('DEPLOY_HOST'); if (!username) missingVars.push('DEPLOY_USER'); if (!remotePath) missingVars.push('DEPLOY_PATH'); if (missingVars.length > 0) { console.error('❌ 缺少必需的环境变量:'); missingVars.forEach(varName => { console.error(` - ${varName}`); }); console.error('\n请设置这些环境变量后重试。'); process.exit(1); } // 执行部署 console.log('🚀 部署系统启动\n'); if (testOnly) { console.log('🧪 模式: 连接测试'); } else { console.log('📦 模式: 完整部署'); } console.log('='.repeat(50) + '\n'); deployToServer({ privateKey, host, username, remotePath, localPath, testOnly }) .then(() => { console.log('\n' + '='.repeat(50)); process.exit(0); }) .catch(error => { console.error('\n' + '='.repeat(50)); process.exit(1); }); } module.exports = { deployToServer };