build(jenkins): add Jenkins pipeline for Android and dynamic app update script

This commit is contained in:
Max 2026-03-24 09:48:29 +08:00
parent 318c829ced
commit 96adc5cd91
2 changed files with 254 additions and 0 deletions

207
Jenkinsfile vendored Normal file
View File

@ -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 "❌ 构建流程遇到错误并终止。请检查上方日志找出失败原由。"
}
}
}

View File

@ -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<File>()
.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);
}
}