فهرست منبع

refactor: 重构CI/CD部署流程,使用跨平台Node.js脚本替代Shell命令

- 添加 node-ssh 依赖实现纯JavaScript SSH部署
- 创建 .build/deploy-to-server.js 统一部署脚本,支持连接测试和完整部署
- 简化 build-win-h5-only.yml 工作流,移除PowerShell SSH步骤
- 简化 build-linux-arm-appimage.yml 工作流,移除Bash SSH步骤
- 实现跨平台兼容(Windows/Linux),无需系统SSH工具依赖
- 支持本地测试部署流程,提高可维护性

改动文件:
- package.json (添加 node-ssh 依赖)
- .build/deploy-to-server.js (新增)
- .github/workflows/build-win-h5-only.yml
- .github/workflows/build-linux-arm-appimage.yml
dengdx 4 روز پیش
والد
کامیت
43f0e8d2b9
4فایلهای تغییر یافته به همراه250 افزوده شده و 83 حذف شده
  1. 219 0
      .build/deploy-to-server.js
  2. 15 39
      .github/workflows/build-linux-arm-appimage.yml
  3. 15 44
      .github/workflows/build-win-h5-only.yml
  4. 1 0
      package.json

+ 219 - 0
.build/deploy-to-server.js

@@ -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 };

+ 15 - 39
.github/workflows/build-linux-arm-appimage.yml

@@ -17,13 +17,6 @@ jobs:
   build-h5-production:
     runs-on: [self-hosted,ARM64]
     steps:
-      - name: Test SSH key
-        run: |
-          echo "${{ secrets.DEPLOY_KEY }}" > /tmp/test_key
-          chmod 600 /tmp/test_key
-          ssh -i /tmp/test_key -o StrictHostKeyChecking=no deploy@${{ secrets.DEPLOY_HOST }} "echo 'SSH key works!'"
-          rm /tmp/test_key
-          
       - name: 检出代码
         uses: actions/checkout@v4
 
@@ -35,6 +28,14 @@ jobs:
       - name: 安装依赖
         run: npm install --force
 
+      - name: 测试 SSH 连接
+        env:
+          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+        run: node .build/deploy-to-server.js --test
+
       - name: 构建 H5 (生产环境)
         env:
           GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -57,38 +58,13 @@ jobs:
           rsync -av --delete dist/h5/ /home/ccos/dros/linux-arm64-unpacked/h5/
           echo "✅ 文件已成功复制到 /home/ccos/dros/linux-arm64-unpacked/h5"
 
-      - name: Set timestamp version
-        run: echo "VERSION=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
-
-      - name: Upload dist/h5/ to server
-        run: |
-          # 1. 将私钥写入临时文件
-          echo "${{ secrets.DEPLOY_KEY }}" > /tmp/deploy_key
-          chmod 600 /tmp/deploy_key
-          
-          # 2. 创建远程目录
-          ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \
-            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
-            "mkdir -p ${{ secrets.DEPLOY_PATH }}/${{ env.VERSION }}/"
-          
-          # 3. 上传文件(不删除本地)
-          rsync -avz -e "ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no" \
-            dist/h5/ \
-            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/${{ env.VERSION }}/"
-          
-          # 4. 清理密钥文件
-          rm -f /tmp/deploy_key
-
-      - name: Update latest symlink
-        run: |
-          echo "${{ secrets.DEPLOY_KEY }}" > /tmp/deploy_key
-          chmod 600 /tmp/deploy_key
-          
-          ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \
-            "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
-            "cd ${{ secrets.DEPLOY_PATH }} && ln -sfn ${{ env.VERSION }} latest"
-          
-          rm -f /tmp/deploy_key
+      - name: 部署到服务器
+        env:
+          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+        run: node .build/deploy-to-server.js
 
   # build-pkg:
   #   needs: deploy-h5-production

+ 15 - 44
.github/workflows/build-win-h5-only.yml

@@ -17,19 +17,6 @@ jobs:
   build-h5-production:
     runs-on: [win-h5-only]
     steps:
-      - name: 检查 PowerShell 执行策略
-        shell: cmd
-        run: |
-          powershell -Command "$policy = Get-ExecutionPolicy; Write-Host '当前有效执行策略:' $policy; if ($policy -eq 'Restricted' -or $policy -eq 'Undefined') { Write-Error '执行策略未正确配置!请在 runner 机器上执行:Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force'; exit 1 } else { Write-Host '✓ 执行策略检查通过' }"
-
-      - name: Test SSH key
-        shell: powershell
-        run: |
-          $keyPath = Join-Path $env:TEMP "test_key"
-          "${{ secrets.DEPLOY_KEY }}" | Out-File -FilePath $keyPath -Encoding UTF8 -NoNewline
-          ssh -i $keyPath -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_HOST }} "echo 'SSH key works!'"
-          Remove-Item -Path $keyPath -Force -ErrorAction SilentlyContinue
-          
       - name: 检出代码
         uses: actions/checkout@v4
 
@@ -41,6 +28,14 @@ jobs:
       - name: 安装依赖
         run: npm install --force
 
+      - name: 测试 SSH 连接
+        env:
+          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+        run: node .build/deploy-to-server.js --test
+
       - name: 构建 H5 (生产环境)
         shell: powershell
         env:
@@ -53,34 +48,10 @@ jobs:
           # Windows 环境不需要 ARM 平台特殊处理
           node .build/h5_for_production.js
 
-      - name: Set timestamp version
-        shell: powershell
-        run: |
-          $version = Get-Date -Format "yyyyMMdd-HHmmss"
-          "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding UTF8
-
-      - name: Upload dist/h5/ to server
-        shell: powershell
-        run: |
-          # 1. 将私钥写入临时文件
-          $keyPath = Join-Path $env:TEMP "deploy_key"
-          "${{ secrets.DEPLOY_KEY }}" | Out-File -FilePath $keyPath -Encoding UTF8 -NoNewline
-          
-          # 2. 创建远程目录
-          ssh -i $keyPath -o StrictHostKeyChecking=no "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" "mkdir -p ${{ secrets.DEPLOY_PATH }}/$env:VERSION/"
-          
-          # 3. 上传文件(使用 scp 替代 rsync)
-          scp -r -i $keyPath -o StrictHostKeyChecking=no dist/h5/* "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/$env:VERSION/"
-          
-          # 4. 清理密钥文件
-          Remove-Item -Path $keyPath -Force -ErrorAction SilentlyContinue
-
-      - name: Update latest symlink
-        shell: powershell
-        run: |
-          $keyPath = Join-Path $env:TEMP "deploy_key"
-          "${{ secrets.DEPLOY_KEY }}" | Out-File -FilePath $keyPath -Encoding UTF8 -NoNewline
-          
-          ssh -i $keyPath -o StrictHostKeyChecking=no "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" "cd ${{ secrets.DEPLOY_PATH }} && ln -sfn $env:VERSION latest"
-          
-          Remove-Item -Path $keyPath -Force -ErrorAction SilentlyContinue
+      - name: 部署到服务器
+        env:
+          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+        run: node .build/deploy-to-server.js

+ 1 - 0
package.json

@@ -119,6 +119,7 @@
     "husky": "^9.1.7",
     "lint-staged": "^16.1.0",
     "msw": "^2.10.2",
+    "node-ssh": "^13.2.0",
     "path-browserify": "^1.0.1",
     "postcss": "^8.5.4",
     "postcss-loader": "^8.1.1",