From 96adc5cd91dfc0b32cd0f00667f78fc5dab67655 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Mar 2026 09:48:29 +0800 Subject: [PATCH] build(jenkins): add Jenkins pipeline for Android and dynamic app update script --- Jenkinsfile | 207 ++++++++++++++++++++++++++++++++++ tool/update_jenkins_apps.dart | 47 ++++++++ 2 files changed, 254 insertions(+) create mode 100644 Jenkinsfile create mode 100644 tool/update_jenkins_apps.dart diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..57d473c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,207 @@ +// 统一Jenkins构建配置 (单仓多App定制版) +// 专注 Android APK 构建,支持多应用与版本动态传参 + +pipeline { + agent any + + options { + // 超时时间 + timeout(time: 45, unit: 'MINUTES') + // 保留构建历史 + buildDiscarder(logRotator(numToKeepStr: '20')) + // 时间戳显示 + timestamps() + // 彩色输出 + ansiColor('xterm') + } + + environment { + // 基础配置 + PROJECT_NAME = 'Web Android Shell' + + // 此项目不使用 Melos,但通过 dart run tool/generate_app.dart 生成 App 工程 + FVM_VERSION = '3.41.2' + + // 动态环境配置 (根据参数设置) + ENVIRONMENT = "${params.BUILD_ENVIRONMENT}" + + // Fastlane和Flutter配置 + LC_ALL = 'en_US.UTF-8' + LANG = 'en_US.UTF-8' + CI = 'true' + FLUTTER_SUPPRESS_ANALYTICS = 'true' + FLUTTER_NO_ANALYTICS = 'true' + + // PATH配置(使用环境变量,避免硬编码用户路径) + PATH = "${env.HOME ?: '/Users/yuanxuan'}/.pub-cache/bin:/opt/homebrew/bin:/usr/local/bin:${env.PATH}" + } + + parameters { + // 🎯 目标应用 (可由 tool/update_jenkins_apps.dart 动态更新内容) + choice( + name: 'APP_NAME', + choices: ['aixue', 'test', 'yunxiao', 'all'], + description: '📱 选择要打包的应用 (从 flavors 目录生成)' + ) + + // 🎯 环境选择 + choice( + name: 'BUILD_ENVIRONMENT', + choices: ['development', 'preview', 'production'], + description: '🏗️ 构建环境: development(开发测试) | preview(预发布) | production(正式发布)' + ) + + // 🎯 版本相关参数 + string( + name: 'VERSION_NAME', + defaultValue: '', + description: '🏷️ 覆盖版本名称 (例如 1.0.0)。留空则使用默认配置中的版本名。' + ) + + string( + name: 'BUILD_NUMBER', + defaultValue: '', + description: '🔢 覆盖构建编号 (例如 1)。留空则使用默认配置中的构建版本号。' + ) + + // 🧹 清理构建 + booleanParam( + name: 'CLEAN_BUILD', + defaultValue: true, + description: '🧹 是否执行清理构建 (生产环境推荐开启)' + ) + } + + stages { + stage('🔧 环境初始化') { + steps { + script { + echo "🚀 开始构建: ${PROJECT_NAME} [${params.BUILD_ENVIRONMENT}]" + } + + sh ''' + #!/bin/zsh + set +x + # 静默加载环境 + [ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc" > /dev/null 2>&1 + + if command -v fvm &> /dev/null; then + echo " - Flutter: $(fvm flutter --version | head -1)" + else + echo "❌ 错误: 未找到FVM环境" + exit 1 + fi + ''' + } + } + + stage('🏗️ 生成与构建') { + steps { + script { + def appsToBuild = [] + if (params.APP_NAME == 'all') { + // 动态读取 flavors 目录下的所有 yaml 文件 + def flavorFiles = sh(script: "ls flavors/*.yaml | awk -F'/' '{print \$2}' | awk -F'.' '{print \$1}'", returnStdout: true).trim().split('\n') + appsToBuild = flavorFiles.toList() + } else { + appsToBuild = [params.APP_NAME] + } + + for (int i = 0; i < appsToBuild.size(); i++) { + def currentApp = appsToBuild[i] + if (!currentApp) continue + + echo "🤖 开始处理应用: ${currentApp}..." + + // 步骤1:生成 App 工程 (如果不存在) + sh """ + #!/bin/zsh + set +x + [ -f "\\$HOME/.zshrc" ] && source "\\$HOME/.zshrc" > /dev/null 2>&1 + + if [ ! -d "apps/${currentApp}" ]; then + echo " - 应用目录 apps/${currentApp} 不存在,正在执行 generate_app 脚本生成项目..." + dart run tool/generate_app.dart ${currentApp} + else + echo " - 应用目录 apps/${currentApp} 已存在,跳过生成工程阶段。" + fi + """ + + // 步骤2:执行 Flutter 构建 + echo " - 正在为 ${currentApp} 构建 Android APK..." + sh """ + #!/bin/zsh + set +x + [ -f "\\$HOME/.zshrc" ] && source "\\$HOME/.zshrc" > /dev/null 2>&1 + cd apps/${currentApp} + + if [ "${params.CLEAN_BUILD}" = "true" ]; then + echo " - 执行 flutter clean..." + fvm flutter clean + fi + + fvm flutter pub get + + # 拼接附加构建参数 + EXTRA_ARGS="" + if [ ! -z "${params.VERSION_NAME}" ]; then + EXTRA_ARGS="\\$EXTRA_ARGS --build-name=${params.VERSION_NAME}" + fi + if [ ! -z "${params.BUILD_NUMBER}" ]; then + EXTRA_ARGS="\\$EXTRA_ARGS --build-number=${params.BUILD_NUMBER}" + fi + + # 注入环境变量参数用于代码判断 (如需要) + EXTRA_ARGS="\\$EXTRA_ARGS --dart-define=APP_ENV=${params.BUILD_ENVIRONMENT}" + + echo " - 开始构建 APK 参数: \\$EXTRA_ARGS" + fvm flutter build apk --release \\$EXTRA_ARGS + """ + echo " ✅ ${currentApp} Android APK 构建完成" + } + } + } + } + } + + post { + always { + script { + echo "🧹 处理构建产物" + // 仅归档 APK 产物 + def findCount = sh( + script: "find apps/*/build/app/outputs/flutter-apk -name 'app-release.apk' 2>/dev/null | wc -l | tr -d ' '", + returnStdout: true + ).trim() + + if (findCount && findCount != '0') { + echo " - 归档Android Release APK..." + archiveArtifacts artifacts: 'apps/*/build/app/outputs/flutter-apk/app-release.apk', fingerprint: true + echo " ✅ 归档完成 (找到 ${findCount} 个 APK 文件)" + } else { + echo " ⚠️ 未找到任何构建的 APK" + } + + // 归档FVM配置便于追踪环境记录 + if (fileExists('.fvmrc')) { + archiveArtifacts artifacts: '.fvmrc', fingerprint: true, allowEmptyArchive: true + } + } + } + + success { + script { + echo "🎉 构建流程成功完成!" + echo "✅ 详情:" + echo " - 目标: ${params.APP_NAME}" + echo " - 环境: ${params.BUILD_ENVIRONMENT}" + if (params.VERSION_NAME) echo " - 版本名: ${params.VERSION_NAME}" + if (params.BUILD_NUMBER) echo " - 构建号: ${params.BUILD_NUMBER}" + } + } + + failure { + echo "❌ 构建流程遇到错误并终止。请检查上方日志找出失败原由。" + } + } +} diff --git a/tool/update_jenkins_apps.dart b/tool/update_jenkins_apps.dart new file mode 100644 index 0000000..3921041 --- /dev/null +++ b/tool/update_jenkins_apps.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +void main() { + final file = File('Jenkinsfile'); + if (!file.existsSync()) { + print('错误: 在项目根目录中找不到 Jenkinsfile。请确保在此目录下执行此脚本。'); + exit(1); + } + + final flavorsDir = Directory('flavors'); + if (!flavorsDir.existsSync()) { + print('错误: 在项目根目录中找不到 flavors 文件夹。'); + exit(1); + } + + final apps = flavorsDir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.yaml')) + .map((f) => f.path.split(Platform.pathSeparator).last.replaceAll('.yaml', '')) + .toList(); + + apps.sort(); + // 固定加上 'all' 的选项 + apps.add('all'); + + final choicesString = apps.map((a) => "'$a'").join(', '); + + var content = file.readAsStringSync(); + + // 匹配形如: + // name: 'APP_NAME', + // choices: ['aixue', 'test', 'yunxiao', 'all'], + final regex = RegExp(r"name:\s*'APP_NAME',\s*\n\s*choices:\s*\[.*\],"); + + if (regex.hasMatch(content)) { + content = content.replaceFirst( + regex, + "name: 'APP_NAME',\n choices: [$choicesString],", + ); + file.writeAsStringSync(content); + print("✅ 已自动更新 Jenkinsfile 中的可选 Apps: ${apps.join(', ')}"); + } else { + print('❌ 更换失败: 未能匹配到 Jenkinsfile 中的 APP_NAME choices 定位。如果更改过文件结构,请更新此脚本里的正则表达式。'); + exit(1); + } +}