Przeglądaj źródła

feat: 实现Android APK自动部署到服务器功能

- 创建deploy-android-to-server.js实现APK上传、版本管理和符号链接
- 修改build-android.js添加CI环境判断和自动部署调用
- 更新CI workflow添加SSH测试和部署环境变量配置
- 支持版本化管理、latest.apk符号链接和versions.json清单
- CI环境自动跳过ADB安装,本地开发保持原有行为
- 新增deploy-android-README.md详细使用文档

改动文件:
- .build/build-android.js (修改)
- .github/workflows/build-linux-arm-appimage.yml (修改)
- .build/deploy-android-to-server.js (新增)
- .build/deploy-android-README.md (新增)
dengdx 3 dni temu
rodzic
commit
c63e91c7a4

+ 41 - 3
.build/build-android.js

@@ -55,6 +55,44 @@ const dst = path.join(__dirname, `dros/platforms/android/app/build/outputs/apk/d
 fs.copyFileSync(src, dst);
 console.log(`✅ 已生成 ${dst}`);
 
-// 安装到连接的安卓设备
-execSync(`adb install "${dst}"`, { stdio: 'inherit' });
-console.log(`✅ 已安装到安卓设备 ${dst}`);
+// 安装到连接的安卓设备(CI 环境跳过)
+if (process.env.CI !== 'true') {
+  try {
+    execSync(`adb install "${dst}"`, { stdio: 'inherit' });
+    console.log(`✅ 已安装到安卓设备 ${dst}`);
+  } catch (error) {
+    console.warn('⚠️  ADB 安装失败,可能没有连接设备:', error.message);
+  }
+} else {
+  console.log('ℹ️  CI 环境,跳过 ADB 安装步骤');
+}
+
+// 8. 部署到服务器(可选)
+(async () => {
+  if (process.env.DEPLOY_ANDROID === 'true') {
+    console.log('\n📤 开始部署到服务器...');
+    console.log('='.repeat(50));
+    
+    try {
+      const { deployAndroidToServer } = require('./deploy-android-to-server');
+      
+      await deployAndroidToServer({
+        privateKey: process.env.DEPLOY_KEY,
+        host: process.env.DEPLOY_HOST,
+        username: process.env.DEPLOY_USER,
+        remotePath: process.env.DEPLOY_PATH,
+        apkPath: dst,
+        appVersion: version
+      });
+      
+      console.log('='.repeat(50));
+      console.log('✅ 部署完成!');
+    } catch (error) {
+      console.error('='.repeat(50));
+      console.error('❌ 部署失败:', error.message);
+      process.exit(1);
+    }
+  } else {
+    console.log('\n💡 提示: 设置 DEPLOY_ANDROID=true 可自动部署到服务器');
+  }
+})();

+ 245 - 0
.build/deploy-android-README.md

@@ -0,0 +1,245 @@
+# Android APK 自动部署到服务器
+
+## 概述
+
+本文档说明如何使用自动部署功能将构建好的 Android APK 文件上传到远程服务器。
+
+## 功能特性
+
+- ✅ 自动上传 APK 到服务器的 `android-releases` 目录
+- ✅ 版本化管理(格式:`v{版本号}-{时间戳}`)
+- ✅ 自动创建 `latest.apk` 符号链接指向最新版本
+- ✅ 生成版本清单文件 `versions.json` 供客户端查询
+- ✅ 保留所有历史版本
+
+## 目录结构
+
+```
+服务器端目录结构:
+/var/www/html/
+├── releases/              # H5 部署目录
+│   ├── v1.0.0-20250130-120000/
+│   └── latest -> v1.0.0-20250130-120000
+│
+└── android-releases/      # Android APK 部署目录
+    ├── v1.0.0-20250130-120000/
+    │   └── dros-v1.0.0.apk
+    ├── v1.0.1-20250131-150000/
+    │   └── dros-v1.0.1.apk
+    ├── latest.apk -> v1.0.1-20250131-150000/dros-v1.0.1.apk
+    └── versions.json
+```
+
+## 环境变量配置
+
+需要设置以下环境变量(与 H5 部署共用):
+
+```bash
+DEPLOY_KEY=<SSH 私钥内容>
+DEPLOY_HOST=<服务器地址>
+DEPLOY_USER=<SSH 用户名>
+DEPLOY_PATH=<H5 部署路径,如 /var/www/html/releases>
+```
+
+**新增**:启用 Android 自动部署
+
+```bash
+DEPLOY_ANDROID=true
+```
+
+## 使用方法
+
+### 方式 1:构建时自动部署(推荐)
+
+在构建 Android APK 时启用自动部署:
+
+```bash
+# Windows PowerShell
+$env:DEPLOY_ANDROID="true"
+npm run build:android
+
+# Linux/macOS
+DEPLOY_ANDROID=true npm run build:android
+```
+
+### 方式 2:仅构建不部署
+
+```bash
+npm run build:android
+```
+
+构建完成后会提示:
+```
+💡 提示: 设置 DEPLOY_ANDROID=true 可自动部署到服务器
+```
+
+### 方式 3:单独部署已有 APK
+
+如果已经有构建好的 APK,可以单独运行部署脚本:
+
+```bash
+# 方式 A:通过命令行参数
+node .build/deploy-android-to-server.js path/to/your-app.apk
+
+# 方式 B:通过环境变量
+APK_PATH=path/to/your-app.apk APP_VERSION=1.0.0 node .build/deploy-android-to-server.js
+```
+
+### 测试 SSH 连接
+
+在部署前可以先测试 SSH 连接是否正常:
+
+```bash
+node .build/deploy-android-to-server.js --test
+```
+
+## 版本清单文件 (versions.json)
+
+部署后会在服务器生成 `versions.json` 文件,格式如下:
+
+```json
+{
+  "latest": "v1.0.1-20250131-150000",
+  "versions": [
+    {
+      "version": "v1.0.1-20250131-150000",
+      "appVersion": "1.0.1",
+      "timestamp": "2025-01-31T15:00:00.000Z",
+      "filename": "dros-v1.0.1.apk",
+      "size": 15728640,
+      "downloadUrl": "/android-releases/v1.0.1-20250131-150000/dros-v1.0.1.apk"
+    },
+    {
+      "version": "v1.0.0-20250130-120000",
+      "appVersion": "1.0.0",
+      "timestamp": "2025-01-30T12:00:00.000Z",
+      "filename": "dros-v1.0.0.apk",
+      "size": 15200000,
+      "downloadUrl": "/android-releases/v1.0.0-20250130-120000/dros-v1.0.0.apk"
+    }
+  ]
+}
+```
+
+### 客户端使用
+
+客户端可以通过以下 URL 访问:
+
+- **最新版本下载**:`https://your-domain.com/android-releases/latest.apk`
+- **版本清单**:`https://your-domain.com/android-releases/versions.json`
+- **特定版本下载**:`https://your-domain.com/android-releases/v1.0.0-20250130-120000/dros-v1.0.0.apk`
+
+## 工作流程
+
+1. **构建 APK**
+   ```bash
+   npm run build:android
+   ```
+
+2. **生成版本化 APK**
+   - 文件名:`dros-v{版本号}.apk`
+   - 位置:`.build/dros/platforms/android/app/build/outputs/apk/debug/`
+
+3. **安装到设备**(如果连接了 Android 设备)
+   ```bash
+   adb install ...
+   ```
+
+4. **部署到服务器**(如果 `DEPLOY_ANDROID=true`)
+   - 连接 SSH
+   - 创建版本目录
+   - 上传 APK 文件
+   - 更新 `latest.apk` 符号链接
+   - 更新 `versions.json` 版本清单
+
+## 完整示例
+
+### 开发环境(本地构建)
+
+```bash
+npm run build:android
+```
+
+### 生产环境(自动部署)
+
+```bash
+# 1. 设置环境变量(仅首次需要)
+export DEPLOY_KEY="$(cat ~/.ssh/id_rsa)"
+export DEPLOY_HOST="your-server.com"
+export DEPLOY_USER="deploy"
+export DEPLOY_PATH="/var/www/html/releases"
+
+# 2. 构建并部署
+DEPLOY_ANDROID=true npm run build:android
+```
+
+### CI/CD 集成
+
+在 GitHub Actions 或其他 CI/CD 工具中:
+
+```yaml
+- name: Build and Deploy Android APK
+  env:
+    DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+    DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+    DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+    DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+    DEPLOY_ANDROID: true
+  run: |
+    npm run build:android
+```
+
+## 故障排除
+
+### 部署失败:认证错误
+
+```
+❌ 部署失败: All configured authentication methods failed
+   → 认证失败,请检查 SSH 私钥是否正确
+```
+
+**解决方法**:
+- 检查 `DEPLOY_KEY` 环境变量是否包含完整的私钥内容
+- 确认私钥格式正确(包括 `-----BEGIN ... KEY-----` 头尾)
+- 验证服务器是否允许该密钥登录
+
+### 部署失败:连接超时
+
+```
+❌ 部署失败: Timed out while waiting for handshake
+   → 连接超时,请检查网络和服务器地址
+```
+
+**解决方法**:
+- 检查 `DEPLOY_HOST` 是否正确
+- 确认网络连接正常
+- 检查防火墙设置
+
+### 路径转换问题
+
+如果 `DEPLOY_PATH` 不是以 `/releases` 结尾,路径转换可能失败。
+
+**当前路径转换逻辑**:
+```javascript
+/var/www/html/releases -> /var/www/html/android-releases
+```
+
+**解决方法**:
+确保 `DEPLOY_PATH` 以 `/releases` 结尾。
+
+## 注意事项
+
+1. **版本号管理**:版本号来自 `package.json` 的 `version` 字段,确保每次发布前更新版本号
+2. **磁盘空间**:历史版本会一直保留,需定期清理旧版本释放空间
+3. **下载链接**:确保 Nginx 配置允许访问 `android-releases` 目录
+4. **符号链接**:如果服务器不支持符号链接,需要修改脚本改用复制方式
+
+## 相关文件
+
+- `.build/deploy-android-to-server.js` - Android 部署脚本
+- `.build/build-android.js` - Android 构建脚本
+- `.build/deploy-to-server.js` - H5 部署脚本(参考)
+
+## 更新日志
+
+- 2025-01-30:初始版本,支持自动部署和版本管理

+ 315 - 0
.build/deploy-android-to-server.js

@@ -0,0 +1,315 @@
+// .build/deploy-android-to-server.js - Android APK 部署脚本
+const { NodeSSH } = require('node-ssh');
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * 部署 Android APK 到远程服务器
+ * @param {Object} options 部署选项
+ * @param {string} options.privateKey SSH 私钥内容
+ * @param {string} options.host 服务器地址
+ * @param {string} options.username 用户名
+ * @param {string} options.remotePath H5 部署路径(会自动转换为 android-releases)
+ * @param {string} options.apkPath 本地 APK 文件路径
+ * @param {string} options.appVersion 应用版本号(如 1.0.0)
+ * @param {boolean} options.testOnly 是否仅测试连接
+ */
+async function deployAndroidToServer(options) {
+  const {
+    privateKey,
+    host,
+    username,
+    remotePath,
+    apkPath,
+    appVersion,
+    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;
+    }
+    
+    // 检查本地 APK 文件是否存在
+    if (!fs.existsSync(apkPath)) {
+      throw new Error(`本地 APK 文件不存在: ${apkPath}`);
+    }
+    
+    // 路径转换:/var/www/html/releases -> /var/www/html/android-releases
+    const androidRemotePath = remotePath.replace(/\/releases$/, '/android-releases');
+    
+    console.log(`📱 Android APK 部署`);
+    console.log(`   H5 路径: ${remotePath}`);
+    console.log(`   Android 路径: ${androidRemotePath}`);
+    
+    // 生成时间戳
+    const timestamp = new Date().toISOString()
+      .replace(/[-:]/g, '')
+      .replace('T', '-')
+      .split('.')[0];
+    
+    // 生成版本号(格式: v1.1.5-20250129-143005)
+    const version = `v${appVersion}-${timestamp}`;
+    
+    const remoteVersionDir = `${androidRemotePath}/${version}`;
+    const apkFileName = path.basename(apkPath);
+    const remoteApkPath = `${remoteVersionDir}/${apkFileName}`;
+    
+    console.log(`📦 部署版本: ${version}`);
+    console.log(`   本地 APK: ${apkPath}`);
+    console.log(`   远程路径: ${remoteApkPath}`);
+    
+    // 创建远程 android-releases 根目录
+    console.log('📁 创建远程目录结构...');
+    const mkdirResult = await ssh.execCommand(`mkdir -p ${remoteVersionDir}`);
+    if (mkdirResult.code !== 0) {
+      throw new Error(`创建远程目录失败: ${mkdirResult.stderr}`);
+    }
+    console.log('✅ 远程目录已创建');
+    
+    // 上传 APK 文件
+    console.log('📤 开始上传 APK 文件...');
+    const fileStats = fs.statSync(apkPath);
+    const fileSizeMB = (fileStats.size / 1024 / 1024).toFixed(2);
+    console.log(`   文件大小: ${fileSizeMB} MB`);
+    
+    await ssh.putFile(apkPath, remoteApkPath);
+    console.log('✅ APK 文件上传完成!');
+    
+    // 验证上传的文件
+    console.log('🔍 验证上传的文件...');
+    const lsResult = await ssh.execCommand(`ls -lh ${remoteApkPath}`);
+    if (lsResult.code !== 0) {
+      throw new Error(`文件验证失败: ${lsResult.stderr}`);
+    }
+    console.log(`   ${lsResult.stdout}`);
+    
+    // 更新 latest.apk 符号链接
+    console.log('🔗 更新 latest.apk 符号链接...');
+    const latestLinkPath = `${androidRemotePath}/latest.apk`;
+    const symlinkCmd = `cd ${androidRemotePath} && ln -sfn ${version}/${apkFileName} latest.apk`;
+    const symlinkResult = await ssh.execCommand(symlinkCmd);
+    
+    if (symlinkResult.code !== 0) {
+      throw new Error(`更新符号链接失败: ${symlinkResult.stderr}`);
+    }
+    
+    // 验证符号链接
+    console.log('🔍 验证符号链接...');
+    const readlinkResult = await ssh.execCommand(`readlink ${latestLinkPath}`);
+    const currentTarget = readlinkResult.stdout.trim();
+    const expectedTarget = `${version}/${apkFileName}`;
+    
+    if (currentTarget === expectedTarget) {
+      console.log(`✅ latest.apk 符号链接已正确更新 -> ${expectedTarget}`);
+    } else {
+      console.warn(`⚠️  符号链接验证异常:`);
+      console.warn(`   期望: ${expectedTarget}`);
+      console.warn(`   实际: ${currentTarget}`);
+    }
+    
+    // 生成/更新版本清单文件
+    console.log('📋 更新版本清单文件...');
+    await updateVersionsManifest(ssh, androidRemotePath, {
+      version,
+      appVersion,
+      timestamp: new Date().toISOString(),
+      filename: apkFileName,
+      size: fileStats.size,
+      downloadUrl: `/android-releases/${version}/${apkFileName}`
+    });
+    
+    // 列出所有版本
+    console.log('📊 版本列表:');
+    const versionsResult = await ssh.execCommand(`ls -1 ${androidRemotePath} | grep "^v"`);
+    const versions = versionsResult.stdout.split('\n').filter(v => v.trim());
+    console.log(`   总计 ${versions.length} 个版本:`);
+    versions.forEach((v, i) => {
+      const marker = v === version ? '← 最新' : '';
+      console.log(`   ${i + 1}. ${v} ${marker}`);
+    });
+    
+    console.log('\n🎉 Android APK 部署成功!');
+    console.log(`   版本: ${version}`);
+    console.log(`   最新版本下载: ${androidRemotePath}/latest.apk`);
+    console.log(`   版本清单: ${androidRemotePath}/versions.json`);
+    
+    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;
+  }
+}
+
+/**
+ * 更新版本清单文件
+ * @param {NodeSSH} ssh SSH 连接实例
+ * @param {string} remotePath 远程 android-releases 路径
+ * @param {Object} versionInfo 版本信息
+ */
+async function updateVersionsManifest(ssh, remotePath, versionInfo) {
+  const manifestPath = `${remotePath}/versions.json`;
+  const tempLocalPath = path.join(__dirname, '.versions-temp.json');
+  
+  let manifest = {
+    latest: versionInfo.version,
+    versions: []
+  };
+  
+  try {
+    // 尝试下载现有的版本清单文件
+    console.log('   下载现有版本清单...');
+    await ssh.getFile(tempLocalPath, manifestPath);
+    
+    // 读取并解析
+    const content = fs.readFileSync(tempLocalPath, 'utf8');
+    manifest = JSON.parse(content);
+    console.log(`   已加载现有清单,包含 ${manifest.versions.length} 个版本`);
+  } catch (error) {
+    // 文件不存在,使用默认清单
+    console.log('   版本清单文件不存在,创建新清单');
+  }
+  
+  // 更新清单
+  manifest.latest = versionInfo.version;
+  
+  // 检查版本是否已存在
+  const existingIndex = manifest.versions.findIndex(v => v.version === versionInfo.version);
+  if (existingIndex !== -1) {
+    // 更新现有版本信息
+    manifest.versions[existingIndex] = versionInfo;
+    console.log('   更新现有版本信息');
+  } else {
+    // 添加新版本到数组开头
+    manifest.versions.unshift(versionInfo);
+    console.log(`   添加新版本,当前总计 ${manifest.versions.length} 个版本`);
+  }
+  
+  // 写入本地临时文件
+  fs.writeFileSync(tempLocalPath, JSON.stringify(manifest, null, 2), 'utf8');
+  
+  // 上传到服务器
+  console.log('   上传版本清单到服务器...');
+  await ssh.putFile(tempLocalPath, manifestPath);
+  
+  // 清理临时文件
+  fs.unlinkSync(tempLocalPath);
+  
+  console.log('✅ 版本清单已更新');
+}
+
+// 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;
+  
+  // APK 文件路径(需要从命令行参数或环境变量获取)
+  const apkPath = process.env.APK_PATH || args.find(arg => arg.endsWith('.apk'));
+  const appVersion = process.env.APP_VERSION || '0.0.0';
+  
+  // 验证必需的环境变量
+  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 (!testOnly && !apkPath) missingVars.push('APK_PATH 或命令行参数');
+  
+  if (missingVars.length > 0) {
+    console.error('❌ 缺少必需的环境变量/参数:');
+    missingVars.forEach(varName => {
+      console.error(`   - ${varName}`);
+    });
+    console.error('\n请设置这些环境变量后重试。');
+    console.error('\n使用示例:');
+    console.error('  node deploy-android-to-server.js /path/to/app.apk');
+    console.error('  或');
+    console.error('  APK_PATH=/path/to/app.apk APP_VERSION=1.0.0 node deploy-android-to-server.js');
+    process.exit(1);
+  }
+  
+  // 执行部署
+  console.log('🚀 Android APK 部署系统启动\n');
+  if (testOnly) {
+    console.log('🧪 模式: 连接测试');
+  } else {
+    console.log('📦 模式: 完整部署');
+  }
+  console.log('='.repeat(50) + '\n');
+  
+  deployAndroidToServer({
+    privateKey,
+    host,
+    username,
+    remotePath,
+    apkPath,
+    appVersion,
+    testOnly
+  })
+    .then(() => {
+      console.log('\n' + '='.repeat(50));
+      process.exit(0);
+    })
+    .catch(error => {
+      console.error('\n' + '='.repeat(50));
+      process.exit(1);
+    });
+}
+
+module.exports = { deployAndroidToServer };

+ 16 - 1
.github/workflows/build-linux-arm-appimage.yml

@@ -125,5 +125,20 @@ jobs:
           java-version: '17'
           distribution: 'temurin'
 
-      - name: 构建 Android APK
+      - 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-android-to-server.js --test
+
+      - name: 构建 Android APK 并自动部署
+        env:
+          DEPLOY_ANDROID: 'true'
+          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
+          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+          CI: 'true'
         run: npm run build:android