| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- // .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 (errorCount > 0) {
- throw new Error(`部分文件上传失败,成功: ${uploadedCount}, 失败: ${errorCount}`);
- }
-
- // node-ssh 的 successful 标志有时会误报,以实际错误计数为准
- if (!uploadResult.successful && errorCount === 0) {
- console.warn('⚠️ node-ssh 报告 successful=false,但所有文件都成功上传');
- }
-
- 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('🔍 验证符号链接...');
- const readlinkResult = await ssh.execCommand(`readlink ${remotePath}/latest`);
- const currentTarget = readlinkResult.stdout.trim();
-
- if (currentTarget === version) {
- console.log(`✅ latest 符号链接已正确更新 -> ${version}`);
- } else {
- console.warn(`⚠️ 符号链接验证异常:`);
- console.warn(` 期望: ${version}`);
- console.warn(` 实际: ${currentTarget}`);
- }
-
- // 验证部署文件
- console.log('🔍 验证部署文件...');
- const lsResult = await ssh.execCommand(`ls -la ${remotePath}/latest`);
- console.log(` ${lsResult.stdout}`);
-
- // 统计远程文件数量
- const countResult = await ssh.execCommand(`find ${remoteDir} -type f | wc -l`);
- const remoteFileCount = parseInt(countResult.stdout.trim());
-
- console.log('📊 部署统计:');
- console.log(` 上传文件: ${uploadedCount} 个`);
- console.log(` 远程文件: ${remoteFileCount} 个`);
- console.log(` 失败文件: ${errorCount} 个`);
-
- if (remoteFileCount >= uploadedCount - errorCount) {
- console.log(' ✅ 文件数量验证通过');
- } else {
- console.warn(' ⚠️ 远程文件数量少于预期');
- }
-
- 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 };
|