|
@@ -0,0 +1,219 @@
|
|
|
|
|
+// .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 };
|