CI/CD 与自动化发布

Flutter 自动化流水线:GitHub Actions、Codemagic、Fastlane 配置,代码签名、自动化测试、多渠道分发。

开篇:为什么 CI/CD 对 Flutter 至关重要

移动端应用的分发流程远比 Web 复杂:iOS 需要证书、Provisioning Profile、App Store Connect 审核;Android 需要密钥库、Play Store 签名(bundle signing)。手动执行这些步骤不仅耗时且容易出错,尤其在团队协作中,“我本地能跑但 CI 挂了"是最常见也最头痛的问题。

建立一条完整的 CI/CD 流水线,让每次代码提交自动触发代码检查、测试、构建和分发,是 Flutter 项目走向专业化的必经之路。一个成熟的 CI/CD 体系通常包含四个阶段:代码提交阶段的静态分析与格式化检查、构建阶段的单元测试与集成测试、发布阶段的打包与签名、交付阶段的多渠道分发与版本追踪。只有将这四个阶段全部自动化,团队才能从繁琐的手动操作中解放出来,专注于产品本身。


一、方案选型

方案托管价格特点
GitHub ActionsGitHub免费额度/按量与代码仓库深度集成,社区生态丰富
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 SDKsubosito/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 更复杂,涉及三个核心文件:

  1. Developer Certificate(.p12):开发者身份证书
  2. Provisioning Profile(.mobileprovision):描述哪些设备/平台可以安装此应用
  3. 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 ActionsCodemagic
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-pages Action
  • AWS S3 + CloudFront:适合需要 CDN 和自定义域名的企业级部署

需要注意 Flutter Web 的路径策略(Hash 模式 vs Path 模式)与托管平台的 404 回退配置。

Q4: 构建失败如何排查?

  1. 在 CI 中开启 Flutter 详细日志:flutter build <target> --verbose
  2. 保留失败构建的 Artifact:actions/upload-artifact@v4 捕获 build/ 目录
  3. 本地复现:确保 CI 使用的 Flutter 版本与本地一致(flutter --version)
  4. Gradle 清理:Android 构建失败时尝试 cd android && ./gradlew clean
  5. 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 与桌面端构建配置

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Flutter」更多文章

  1. 崩溃监控与线上可观测性
  2. 蓝牙 BLE 与外设集成
  3. 包体积与启动优化