开篇:为什么 CI/CD 对 Flutter 至关重要
移动端应用的分发流程远比 Web 复杂:iOS 需要证书、Provisioning Profile、App Store Connect 审核;Android 需要密钥库、Play Store 签名(bundle signing)。手动执行这些步骤不仅耗时且容易出错,尤其在团队协作中,“我本地能跑但 CI 挂了"是最常见也最头痛的问题。
建立一条完整的 CI/CD 流水线,让每次代码提交自动触发代码检查、测试、构建和分发,是 Flutter 项目走向专业化的必经之路。一个成熟的 CI/CD 体系通常包含四个阶段:代码提交阶段的静态分析与格式化检查、构建阶段的单元测试与集成测试、发布阶段的打包与签名、交付阶段的多渠道分发与版本追踪。只有将这四个阶段全部自动化,团队才能从繁琐的手动操作中解放出来,专注于产品本身。
一、方案选型
| 方案 | 托管 | 价格 | 特点 |
|---|---|---|---|
| GitHub Actions | GitHub | 免费额度/按量 | 与代码仓库深度集成,社区生态丰富 |
| Codemagic | 独立 SaaS | 免费 500min/月 | Flutter/Dart 原生优化,M1 Mac 构建 |
| Bitrise | 独立 SaaS | 免费额度 | 移动优先,工作流可视化编辑 |
| CircleCI | 独立 SaaS | 按量 | 高度可配置,Docker 支持好 |
选型建议:
- 初创团队:直接从 GitHub Actions 起步,零额外成本,与 GitHub 仓库无缝集成,社区上的 Flutter Actions 模板可以直接复用
- 中型团队(构建频繁):考虑 Codemagic,M1 Mac 构建速度比 GitHub Actions 的 macOS runner 快 2-3 倍,且内置 Flutter 环境,无需每次安装
- 大型企业:Bitrise 的可视化编辑对非技术背景的运营人员更友好,同时支持 SAML SSO 等企业级安全要求
一句话总结:大多数团队从 GitHub Actions 起步(零额外成本),对构建速度要求高的团队可升级到 Codemagic 的 M1 构建。
二、Flutter 构建系统深度解析
要配置好 CI/CD,必须首先理解 Flutter 底层的构建系统。Android 侧依赖 Gradle 构建工具链,iOS 侧依赖 Xcode 的编译体系。
2.1 Gradle 配置与优化
Android 构建的核心在 android/app/build.gradle(Kotlin DSL 为 build.gradle.kts)。以下是经过生产验证的优化配置:
// android/app/build.gradle
android {
namespace = "com.example.flutter_app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.flutter_app"
minSdk = 21
targetSdk = 34
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// 签名配置:从环境变量读取,避免密钥入仓
signingConfigs {
release {
def keystoreFile = file(System.getenv("KEYSTORE_PATH") ?: "keystore.jks")
if (keystoreFile.exists()) {
storeFile keystoreFile
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// Bundle 格式支持(Play Store 要求)
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}
在 android/gradle.properties 中增加并行构建加速:
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
2.2 Xcode 构建设置
iOS 构建的核心在 ios/Runner.xcworkspace。CI 环境中需要预先配置好签名团队、Bundle ID 和编译模式。
# CI 中的 Xcode 配置脚本示例
flutter build ios --release --no-codesign
cd ios
xcodebuild -workspace Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
archivePath "$PWD/build/Runner.xcarchive" \
archive
xcodebuild -exportArchive \
-archivePath "$PWD/build/Runner.xcarchive" \
-exportPath "$PWD/build/Release" \
-exportOptionsPlist ExportOptions.plist
ExportOptions.plist 核心字段:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string> <!-- 或 ad-hoc / development -->
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.app</key>
<string>Your Provisioning Profile Name</string>
</dict>
</dict>
</plist>
2.3 Flavor 多环境管理
生产环境通常需要 dev、staging、prod 三套配置。Flutter 的 Flavor 机制支持在编译时切换环境:
# 构建不同 Flavor
flutter build apk --flavor dev -t lib/main_dev.dart
flutter build apk --flavor staging -t lib/main_staging.dart
flutter build apk --flavor prod -t lib/main_prod.dart --release
对应的 android/app/build.gradle 配置:
android {
flavorDimensions += "env"
productFlavors {
dev {
dimension = "env"
applicationIdSuffix = ".dev"
resValue "string", "app_name", "MyApp Dev"
}
staging {
dimension = "env"
applicationIdSuffix = ".staging"
resValue "string", "app_name", "MyApp Staging"
}
prod {
dimension = "env"
resValue "string", "app_name", "MyApp"
}
}
}
iOS 侧需在 Xcode 中为每个 Flavor 新建 Scheme,并配置对应的 Bundle ID 后缀。
2.4 Dart-define 编译时变量
相比配置文件,--dart-define 在编译时将常量注入代码,安全性更高(不落入反编译产物),且无需额外依赖包。
flutter build apk --release \
--dart-define=API_URL=https://api.production.com \
--dart-define=ANALYTICS_ENABLED=true
lib/config.dart 中的读取方式:
class Config {
static const String apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'https://api.dev.local',
);
static const bool analyticsEnabled = bool.fromEnvironment(
'ANALYTICS_ENABLED',
defaultValue: false,
);
}
在 CI 中结合 GitHub Actions secrets:
- run: flutter build apk --release --dart-define=API_URL=${{ secrets.API_URL }}
三、GitHub Actions 自动化实战
3.1 完整的 CI/CD 工作流
# .github/workflows/flutter_ci.yml
name: Flutter CI/CD
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main]
env:
FLUTTER_VERSION: '3.24.0'
JAVA_VERSION: '17'
jobs:
# ---------- 阶段 1: 代码分析与测试 ----------
analyze-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Get dependencies
run: flutter pub get
- name: Generate code
run: dart run build_runner build --delete-conflicting-outputs
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- name: Analyze project source
run: flutter analyze --fatal-infos --fatal-warnings
- name: Run tests with coverage
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage/lcov.info
fail_ci_if_error: false
# ---------- 阶段 2: 矩阵构建 ----------
build-android:
needs: analyze-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
flavor: [dev, staging, prod]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: 'gradle'
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Decode keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo "$KEYSTORE_BASE64" | base64 --decode > android/app/keystore.jks
- name: Build APK
run: flutter build apk --release --flavor ${{ matrix.flavor }} -t lib/main_${{ matrix.flavor }}.dart
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
- name: Build App Bundle
if: matrix.flavor == 'prod'
run: flutter build appbundle --release --flavor prod -t lib/main_prod.dart
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: android-${{ matrix.flavor }}
path: |
build/app/outputs/flutter-apk/*.apk
build/app/outputs/bundle/${{ matrix.flavor }}Release/*.aab
# ---------- 阶段 3: iOS 构建 ----------
build-ios:
needs: analyze-and-test
runs-on: macos-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Setup certificates and provisioning profile
env:
P12_CERTIFICATE_BASE64: ${{ secrets.P12_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
echo "$P12_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
security create-keychain -p "password" build.keychain
security import certificate.p12 -t agg -k build.keychain -P "$P12_PASSWORD" -A
security list-keychains -s $(security list-keychains | sed 's/"//g') build.keychain
security unlock-keychain -p "password" build.keychain
security set-keychain-settings -lut 3600 build.keychain
- name: Build iOS
run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist --flavor prod -t lib/main_prod.dart
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ios-build
path: build/ios/ipa/*.ipa
# ---------- 阶段 4: Web 构建 ----------
build-web:
needs: analyze-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- run: flutter pub get
- run: flutter build web --release --dart-define=API_URL=${{ secrets.API_URL }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: web-build
path: build/web
3.2 缓存策略
上述工作流中已经开启了三层缓存:
| 缓存层级 | 机制 | 效果 |
|---|---|---|
| Flutter SDK | subosito/flutter-action 的 cache: true | 跳过 SDK 下载,节省 1-2 min |
| Gradle 依赖 | actions/setup-java 的 cache: 'gradle' | 复用 wrapper 和第三方依赖 |
| Gradle 构建 | gradle.properties 中 org.gradle.caching=true | 增量编译,增量包构建 |
对于更激进的缓存方案,可使用 actions/cache 手动缓存 ~/.gradle/caches 和 ~/.pub-cache。
3.3 Artifact 上传与版本管理
建议为每个 tag 触发的发布构建打版本号,并附加到 GitHub Release 中:
release:
needs: [build-android, build-ios]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
android-prod/**/*.apk
android-prod/**/*.aab
ios-build/**/*.ipa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
一句话总结:GitHub Actions 通过 YAML 定义工作流,配合矩阵构建、多层缓存和 Artifact 管理,将代码检查、测试、多平台构建串联为自动化流水线,是 Flutter 项目 CI/CD 的入门首选。
四、代码签名与分发
4.1 Android Keystore 管理
Android 应用必须使用 Keystore 进行签名才能上架 Google Play。Aprk 使用 v1/v2/v3 签名方案,AAB 强制要求 Play App Signing。
生成 Keystore:
keytool -genkey -v \
-keystore release-keystore.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias release
转换为 Base64 存入 GitHub Secrets:
base64 -i release-keystore.jks | pbcopy # macOS
# 将输出粘贴到 GitHub Secrets 中的 KEYSTORE_BASE64
验证签名(CI 中可选步骤):
apksigner verify --verbose build/app/outputs/flutter-apk/app-release.apk
4.2 iOS 代码签名
iOS 签名比 Android 更复杂,涉及三个核心文件:
- Developer Certificate(.p12):开发者身份证书
- Provisioning Profile(.mobileprovision):描述哪些设备/平台可以安装此应用
- Team ID:Apple Developer 团队标识
从 macOS Keychain 导出 .p12 和 Profile:
# 导出证书(需要先通过 Xcode 登录 Apple ID 下载)
security find-identity -p codesigning
# 找到对应的 SHA-1 hash,然后导出
security export -k ~/Library/Keychains/login.keychain-db \
-t identities -f pkcs12 -P "export_password" > certificate.p12
# 转换为 Base64 存入 Secrets
base64 -i certificate.p12 | pbcopy
base64 -i ~/Library/MobileDevice/Provisioning\ Profiles/YourProfile.mobileprovision | pbcopy
4.3 签名安全最佳实践
| 场景 | 推荐方案 | 风险等级 |
|---|---|---|
| 个人项目 | GitHub Secrets | 低 |
| 小团队 | GitHub Secrets + 2FA | 中 |
| 企业级 | Apple Developer Portal API + HashiCorp Vault | 极低 |
切勿将 Keystore、.p12、Provisioning Profile 以任何明文形式提交到 Git。如果不小心提交了,应立即撤销证书并重新生成。
五、Fastlane 自动化分发
Fastlane 是一套用 Ruby 编写的移动端自动化工具链,能够将分发流程抽象为简洁的 Lane 命令。
5.1 安装与初始化
cd ios && fastlane init
cd android && fastlane init
5.2 Android Fastfile
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Beta 测试:Firebase App Distribution"
lane :beta do
sh("flutter build apk --release --flavor prod -t lib/main_prod.dart")
firebase_app_distribution(
app: ENV['FIREBASE_APP_ID_ANDROID'],
groups: "qa-team, beta-testers",
apk_path: "../build/app/outputs/flutter-apk/app-prod-release.apk",
service_credentials_file: "../firebase-service-account.json",
release_notes: last_git_commit[:message],
)
end
desc "内部测试:Play Store Internal Track"
lane :internal do
sh("flutter build appbundle --release --flavor prod -t lib/main_prod.dart")
upload_to_play_store(
track: 'internal',
aab: '../build/app/outputs/bundle/prodRelease/app-prod-release.aab',
json_key_data: ENV['PLAY_STORE_JSON_KEY'],
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
end
desc "正式上架:Play Store Production"
lane :release do
sh("flutter build appbundle --release --flavor prod -t lib/main_prod.dart")
upload_to_play_store(
track: 'production',
aab: '../build/app/outputs/bundle/prodRelease/app-prod-release.aab',
json_key_data: ENV['PLAY_STORE_JSON_KEY'],
)
end
end
5.3 iOS Fastfile
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Beta 测试:TestFlight + Firebase"
lane :beta do
# increment_build_number 可用以自动递增构建号
increment_build_number(
build_number: latest_testflight_build_number + 1,
xcodeproj: "Runner.xcodeproj"
)
sh("flutter build ipa --release --flavor prod -t lib/main_prod.dart --export-options-plist=ExportOptions.plist")
upload_to_testflight(
ipa: "../build/ios/ipa/Runner.ipa",
skip_waiting_for_build_processing: true,
)
firebase_app_distribution(
app: ENV['FIREBASE_APP_ID_IOS'],
groups: "qa-team",
ipa_path: "../build/ios/ipa/Runner.ipa",
service_credentials_file: "../firebase-service-account.json",
)
end
desc "正式上架:App Store"
lane :release do
increment_build_number(
build_number: latest_app_store_build_number + 1,
xcodeproj: "Runner.xcodeproj"
)
sh("flutter build ipa --release --flavor prod -t lib/main_prod.dart --export-options-plist=ExportOptions.plist")
upload_to_app_store(
ipa: "../build/ios/ipa/Runner.ipa",
force: true,
skip_screenshots: false,
skip_metadata: false,
submit_for_review: false, # 设为 true 可自动提交审核
precheck_include_in_app_purchases: false,
)
end
end
在 CI 中调用 Fastlane:
- run: cd android && fastlane beta
env:
FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
一句话总结:Fastlane 封装了各大平台的分发 API,将原本需要 Web 控制台操作的多步流程简化为一条命令,是移动端自动化分发的行业事实标准。
六、Firebase App Distribution 与 CD 流水线
Firebase App Distribution 允许将 APK/IPA 直接分发给测试人员,无需经过应用商店审核,是敏捷团队进行内部测试和 Beta 发布的首选方案。
6.1 Firebase CLI 配置
# 安装 Firebase CLI
npm install -g firebase-tools
# 登录并选择项目
firebase login
cd your-flutter-project
firebase appdistribution:distribute \
build/app/outputs/flutter-apk/app-release.apk \
--app YOUR_APP_ID \
--groups "qa-team,beta-testers" \
--release-notes "Bug fixes and performance improvements"
6.2 GitHub Actions 集成
将 Firebase 分发直接嵌入 CI 流水线:
firebase-distribute:
needs: build-android
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download Android artifacts
uses: actions/download-artifact@v4
with:
name: android-prod
path: ./artifacts
- name: Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
groups: qa-team, beta-testers
file: artifacts/app-prod-release.apk
releaseNotes: |
Commit: ${{ github.sha }}
Branch: ${{ github.ref_name }}
Trigger: ${{ github.event_name }}
6.3 测试组管理
Firebase Console 中可以对测试组进行权限管理:
- qa-team:内部 QA 工程师,接收每次 main 分支构建
- beta-testers:外部 Beta 用户,接收正式发布前的 RC 版本
- stakeholders:项目干系人,仅接收重大版本更新
测试人员会收到邮件和通知,点击链接即可安装,支持 iOS 的 UDID 自动收集(对于 Ad Hoc 分发非常重要)。
七、自动化测试集成
没有测试覆盖的 CI/CD 是不完整的。Flutter 提供了单元测试、Widget 测试和集成测试三层测试体系。
7.1 单元测试与代码覆盖率
// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/calculator.dart';
void main() {
group('Calculator', () {
test('addition', () {
final calc = Calculator();
expect(calc.add(2, 3), equals(5));
});
test('division by zero throws', () {
final calc = Calculator();
expect(() => calc.divide(1, 0), throwsArgumentError);
});
});
}
运行并生成覆盖率报告:
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
7.2 Widget 测试
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {
testWidgets('Login button triggers authentication', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
// 找到输入框并填入内容
await tester.enterText(find.byType(TextField).first, 'test@example.com');
await tester.enterText(find.byType(TextField).last, 'password123');
// 点击登录按钮
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// 验证登录后的界面出现
expect(find.text('Welcome back!'), findsOneWidget);
});
}
7.3 集成测试
集成测试需要部署到真实设备或模拟器:
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End-to-End Flow', () {
testWidgets('complete login to checkout flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// 登录
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password');
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// 浏览到商品页
await tester.tap(find.text('Products'));
await tester.pumpAndSettle();
// 添加到购物车
await tester.tap(find.byIcon(Icons.add_shopping_cart).first);
await tester.pumpAndSettle();
// 进入购物车并结算
await tester.tap(find.byIcon(Icons.shopping_cart));
await tester.pumpAndSettle();
await tester.tap(find.text('Checkout'));
await tester.pumpAndSettle();
expect(find.text('Order Confirmed'), findsOneWidget);
});
});
}
运行方式:
flutter test integration_test/app_test.dart
7.4 CI 中的测试流水线
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
cache: true
- run: flutter pub get
- run: flutter test --coverage
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
files: coverage/lcov.info
integration-test:
runs-on: macos-latest
strategy:
matrix:
api-level: [29, 33, 34]
target: [default, google_apis]
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: flutter test integration_test/
八、Codemagic 高级部署
Codemagic 是专为 Flutter 打造的 CI/CD 服务,提供比 GitHub Actions 更原生的 Flutter 支持。
8.1 工作流配置
# codemagic.yaml
workflows:
flutter-release-workflow:
name: Flutter Release
instance_type: mac_mini_m1 # M1 构建机,速度显著提升
max_build_duration: 60
environment:
flutter: stable
xcode: latest
cocoapods: default
vars:
APP_ID: "com.example.flutter_app"
secrets:
- keystore
- key_password
- google_play_json
- p12_certificate
- provisioning_profile
cache:
cache_paths:
- ~/.pub-cache
- $CM_BUILD_DIR/.gradle
triggering:
events:
- push
branch_patterns:
- pattern: main
include: true
source: true
- pattern: release/*
include: true
source: true
artifacts:
- build/app/outputs/**/*.apk
- build/app/outputs/**/*.aab
- build/ios/ipa/*.ipa
- build/web/**
publishing:
firebase:
firebase_service_account: $FIREBASE_SERVICE_ACCOUNT
android:
app_id: "1:123456789:android:abcdef"
groups:
- qa-team
ios:
app_id: "1:123456789:ios:abcdef"
groups:
- qa-team
app_store_connect:
api_key: $APP_STORE_CONNECT_KEY
key_id: $APP_STORE_KEY_ID
issuer_id: $APP_STORE_ISSUER_ID
submit_to_testflight: true
google_play:
credentials: $GOOGLE_PLAY_JSON
track: internal
email:
recipients:
- devops@example.com
notify:
success: true
failure: true
8.2 与 GitHub 的差异
| 维度 | GitHub Actions | Codemagic |
|---|---|---|
| Flutter 环境 | 每次安装 SDK | 预装,开箱即用 |
| macOS 构建速度 | Intel runner,较慢 | M1 Mini,快 2-3x |
| 配置复杂度 | YAML 完全自定义 | 预设模板 + YAML 微调 |
| 费用 | 免费额度有限 | 500 min/月免费,超出按量 |
| 多平台并发 | 需手动配置矩阵 | 自动并行构建 Android/iOS/Web |
8.3 何时选择 Codemagic
建议在以下场景考虑 Codemagic:
- 构建频率高,GitHub Actions 免费额度不够
- iOS 构建占比大,需要 M1 加速
- 团队希望减少 CI 配置维护成本
- 需要内置的设备测试实验室(Codemagic 支持 Firebase Test Lab 一键集成)
FAQ
Q1: 代码签名证书如何安全存储?
- GitHub Secrets:适合大多数场景,支持 Repository、Environment 和 Organization 三级作用域
- 1Password Service Account:通过
1password/load-secrets-action在 CI 中动态获取,支持轮换审计 - HashiCorp Vault:企业级方案,支持动态凭据和细粒度 ACL
- Apple Developer Portal API:彻底避免 .p12 文件,使用 API Key + JWT 调用 App Store Connect API
- 切勿将密钥提交到版本控制。如果不慎提交,应立即吊销证书并重新生成。
Q2: iOS 构建为什么比 Android 慢?
iOS 编译需要在 macOS 上运行 Xcode,GitHub Actions 的 macOS runner 为 Intel x86 架构,且资源配额比 Linux runner 更紧张。Codemagic 提供专用 M1 构建机,速度提升 2-3 倍。此外建议开启 Swift/Objective-C 编译缓存(ccache),并避免在 CI 中重复下载 CocoaPods 依赖。
Q3: Web 构建产物如何部署?
Flutter Web 输出为静态文件,可部署到任何静态托管:
- Vercel
:
vercel --prod - Netlify:
netlify deploy --prod - Firebase Hosting:
firebase deploy --only hosting - GitHub Pages:通过
peaceiris/actions-gh-pagesAction - AWS S3 + CloudFront:适合需要 CDN 和自定义域名的企业级部署
需要注意 Flutter Web 的路径策略(Hash 模式 vs Path 模式)与托管平台的 404 回退配置。
Q4: 构建失败如何排查?
- 在 CI 中开启 Flutter 详细日志:
flutter build <target> --verbose - 保留失败构建的 Artifact:
actions/upload-artifact@v4捕获build/目录 - 本地复现:确保 CI 使用的 Flutter 版本与本地一致(
flutter --version) - Gradle 清理:Android 构建失败时尝试
cd android && ./gradlew clean - iOS 签名问题:检查 Provisioning Profile 有效期、Bundle ID 匹配、证书是否被吊销
Q5: 如何管理多个环境的配置?
推荐组合:Flavor(Android/iOS 原生差异)+ Dart-define(编译时变量)+ --target(入口文件)。三者组合可以覆盖 90% 以上的多环境需求。对于更复杂的配置(如远程配置中心),可考虑 Firebase Remote Config 或 LaunchDarkly。
相关阅读
- https://plumephp.com/flutter-testing/ — 测试策略与自动化
- https://plumephp.com/flutter-web-desktop/ — Web 与桌面端构建配置
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。