Flutter CI/CD 配置指南:使用 GitHub Actions 实现自动化部署

引言

如今移动应用迭代速度越来越快,持续集成与持续部署(CI/CD)早就不再是“加分项”,而是保障开发效率和代码质量的标准实践。对于 Flutter 开发者来说,如果还停留在手动打包、测试、发布的老路上,不仅浪费时间,也容易出错。

好在 GitHub 官方提供的自动化工具 GitHub Actions,为我们搭建 Flutter 项目的 CI/CD 流水线提供了强大又灵活的支持。今天,我们就来一起完成从零开始配置一条完整的自动化部署流程,让你的代码在提交后自动完成检查、测试、构建,甚至发布。

理解核心概念

GitHub Actions 是如何工作的?

简单来说,GitHub Actions 采用事件驱动模式:当特定事件(比如推送代码、创建 PR)发生时,它会执行你在仓库中预设的自动化流程。这些流程通过 YAML 文件定义,主要包含几个核心概念:

  1. Workflow(工作流程):一个完整的自动化流程,对应 .github/workflows/ 目录下的一个 YAML 文件。
  2. Event(事件):触发工作流程运行的动作,例如 pushpull_requestrelease 等。
  3. Job(作业):工作流程中的一个执行单元,包含一系列步骤,在同一个 Runner 上运行。
  4. Step(步骤):作业内的具体任务,可以是执行一条 shell 命令,或调用一个预定义的动作。
  5. Runner(运行器):执行作业的服务器,可以使用 GitHub 托管的,也可以自己搭建。

设计 Flutter 的 CI/CD 流水线

一条健壮的 Flutter CI/CD 流水线通常会包含以下几个阶段,我们可以根据项目需求进行增减:

  1. 代码质量检查:运行静态分析和代码格式化检查。
  2. 依赖管理:获取项目依赖,并利用缓存加速后续流程。
  3. 构建验证:尝试编译不同平台(Android/iOS/Web)的产物,确保代码可构建。
  4. 自动化测试:执行单元测试和集成测试,生成测试覆盖率报告。
  5. 产物构建:生成用于发布的应用包(APK/IPA/Web 资源)。
  6. 部署发布:将构建产物自动上传到应用商店、测试平台或静态网站。

实战:编写 CI/CD 工作流

基础工作流配置文件

让我们先来看一个完整的 flutter-ci-cd.yml 示例,你可以将它保存到项目的 .github/workflows/ 目录下:

name: Flutter CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

env:
  FLUTTER_VERSION: '3.19.0'
  JAVA_VERSION: '17'

jobs:
  quality-check:
    name: Code Quality Check
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: ${{ env.FLUTTER_VERSION }}
        channel: 'stable'
        cache: true

    - name: Show Flutter Environment
      run: flutter doctor -v

    - name: Get Dependencies
      run: flutter pub get

    - name: Analyze Code
      run: flutter analyze --no-fatal-infos

    - name: Check Format
      run: flutter format --set-exit-if-changed .

    - name: Run Tests
      run: flutter test --coverage

    - name: Upload Coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info
        fail_ci_if_error: false

  android-build:
    name: Android Build
    runs-on: ubuntu-latest
    needs: quality-check

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Setup Java
      uses: actions/setup-java@v3
      with:
        distribution: 'temurin'
        java-version: ${{ env.JAVA_VERSION }}

    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: ${{ env.FLUTTER_VERSION }}

    - name: Get Dependencies
      run: flutter pub get

    - name: Build APK
      run: flutter build apk --release --split-per-abi
      env:
        KEY_PROPERTIES: ${{ secrets.ANDROID_KEY_PROPERTIES }}
        KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}

    - name: Upload APK Artifacts
      uses: actions/upload-artifact@v4
      with:
        name: android-apks
        path: build/app/outputs/flutter-apk/*.apk
        retention-days: 7

  ios-build:
    name: iOS Build
    runs-on: macos-latest
    needs: quality-check
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: ${{ env.FLUTTER_VERSION }}

    - name: Install CocoaPods
      run: |
        cd ios
        pod install

    - name: Build iOS
      run: flutter build ios --release --no-codesign
      env:
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

    - name: Upload IPA Artifact
      uses: actions/upload-artifact@v4
      with:
        name: ios-ipa
        path: build/ios/ipa/*.ipa
        retention-days: 7

  web-build:
    name: Web Build
    runs-on: ubuntu-latest
    needs: quality-check

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: ${{ env.FLUTTER_VERSION }}

    - name: Get Dependencies
      run: flutter pub get

    - name: Build Web
      run: flutter build web --release

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      if: github.ref == 'refs/heads/main'
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./build/web
        keep_files: false

这个配置定义了一个多任务的工作流:代码质量检查通过后,才会并行触发 Android、iOS(仅针对版本标签)和 Web 的构建任务。

一些有用的辅助脚本

复杂的构建流程通常需要脚本辅助。这里有两个例子:

1. Android 构建环境配置脚本 (scripts/android_setup.sh)
这个脚本负责在 CI 环境中安全地设置签名密钥。

#!/bin/bash
set -e
echo "🔧 Setting up Android build environment..."
if [ -z "$KEY_PROPERTIES" ] || [ -z "$KEYSTORE" ]; then
    echo "❌ Missing required secrets: KEY_PROPERTIES or KEYSTORE"
    exit 1
fi
echo "$KEY_PROPERTIES" > android/key.properties
echo "$KEYSTORE" | base64 --decode > android/app/keystore.jks
chmod 600 android/key.properties
chmod 600 android/app/keystore.jks
echo "✅ Android build environment setup completed"

2. 版本管理工具类 (lib/utils/version_manager.dart)
在 CI 中自动递增版本号是个好习惯,这个 Dart 类可以帮你管理 pubspec.yaml 中的版本。

import 'dart:io';
import 'package:yaml/yaml.dart';

class VersionManager {
  final String pubspecPath = 'pubspec.yaml';

  Future<Map<String, dynamic>> getCurrentVersion() async {
    final file = File(pubspecPath);
    final content = await file.readAsString();
    final yaml = loadYaml(content);
    final version = yaml['version'] as String;
    final parts = version.split('+');
    return {
      'version': parts[0],
      'buildNumber': int.parse(parts[1]),
    };
  }

  Future<void> incrementVersion({String? newVersion, int? buildIncrement = 1}) async {
    final current = await getCurrentVersion();
    final currentVersion = current['version'] as String;
    final currentBuild = current['buildNumber'] as int;
    final versionParts = currentVersion.split('.');
    final major = int.parse(versionParts[0]);
    final minor = int.parse(versionParts[1]);
    final patch = int.parse(versionParts[2]);

    String updatedVersion;
    if (newVersion != null) {
      updatedVersion = newVersion;
    } else {
      updatedVersion = '$major.$minor.${patch + 1}';
    }
    final newBuildNumber = currentBuild + (buildIncrement ?? 1);

    final file = File(pubspecPath);
    List<String> lines = await file.readAsLines();
    for (int i = 0; i < lines.length; i++) {
      if (lines[i].contains('version:')) {
        lines[i] = 'version: $updatedVersion+$newBuildNumber';
        break;
      }
    }
    await file.writeAsString(lines.join('\n'));
    print('✅ Version updated: $updatedVersion+$newBuildNumber');
  }
}
// ... 更多方法详见原文

一步步集成到你的项目

第一步:项目初始化

  1. 启用 GitHub Actions:在你的项目根目录创建 .github/workflows/ 文件夹,并将上面的 YAML 配置文件放进去。
  2. 生成并配置密钥
    • 使用 keytool 生成 Android 签名密钥。
    • 将生成的 keystore.jks 文件用 Base64 编码(base64 -i keystore.jks)。
  3. 添加 GitHub Secrets:在仓库的 Settings -> Secrets and variables -> Actions 页面,添加 ANDROID_KEY_PROPERTIESANDROID_KEYSTORE 等密钥。这些信息会安全地注入到工作流环境中。

第二步:在本地进行测试

在推送到 GitHub 触发线上 Action 之前,最好先在本地模拟测试。可以创建一个简单的测试脚本:

#!/bin/bash
set -e
echo "🚀 Starting local CI test..."
export CI=true
echo "📋 Running quality checks..."
flutter analyze
flutter format --set-exit-if-changed .
flutter test --coverage
echo "🤖 Testing Android build..."
flutter build apk --release --split-per-abi
echo "🌐 Testing Web build..."
flutter build web --release
echo "✅ All local CI tests passed!"

第三步:遇到问题怎么办?

CI/CD 配置过程难免会遇到失败。建议创建一个问题排查文档,记录常见错误和解决方案。例如:

  • Flutter 版本不匹配:在 workflow 文件中明确指定稳定的 Flutter 版本号。
  • 依赖下载缓慢:为 Flutter SDK 和 Pub 依赖配置缓存。
  • Android 签名失败:仔细检查 key.properties 文件格式和 secrets 的配置是否正确。
  • iOS 证书问题:考虑使用 fastlane match 等工具在团队间同步证书和描述文件。

让流水线更快更智能

优化缓存策略

合理利用缓存可以极大缩短工作流运行时间。你可以基于 pubspec.lock 文件的哈希值来创建缓存键,确保依赖变更时缓存自动失效。

- name: Generate Cache Key
  id: cache-key
  run: |
    HASH=$(sha256sum pubspec.lock | cut -d' ' -f1)
    echo "key=flutter-deps-$HASH" >> $GITHUB_OUTPUT
- name: Cache Pub Dependencies
  uses: actions/cache@v3
  with:
    path: |
      ${{ github.workspace }}/.pub-cache
      ${{ github.workspace }}/build
    key: ${{ steps.cache-key.outputs.key }}

实现条件执行

不是每次提交都需要构建所有平台。使用 paths-filter 等 Action 可以智能地根据文件变更来决定执行哪些任务,比如只有 ios/ 目录下的文件变动了才触发 iOS 构建。

- name: Detect Changes
  id: changes
  uses: dorny/paths-filter@v2
  with:
    filters: |
      android:
        - 'android/**'
        - 'lib/**'
      ios:
        - 'ios/**'
        - 'lib/**'
- name: Build Android
  if: steps.changes.outputs.android == 'true'
  run: flutter build apk

值得遵循的最佳实践

  1. 最小权限原则:在 workflow 文件中显式定义所需的权限,只赋予必要的 readwrite 权限,避免安全风险。
  2. 建立监控通知:可以编写一个简单的 Python 脚本,调用 GitHub API 检查工作流状态,并在失败时通过 Slack、Teams 或邮件通知团队。
  3. 文档与代码同步:将 CI/CD 的配置、脚本和排查指南都纳入版本控制,并随着项目演进不断更新。

总结

走到这里,你已经掌握了使用 GitHub Actions 为 Flutter 项目搭建自动化部署管道的核心方法。从代码提交触发,到质量检查、多平台构建,再到最终部署,整个过程完全自动化,这能把你和团队从重复劳动中解放出来。

关键是,这套流程不是一成不变的。你可以从本文提供的配置出发,根据自己项目的实际需求进行调整:也许你需要接入 Firebase App Distribution,也许需要自动生成更新日志。随着项目的成长,持续优化你的 CI/CD 流水线,让它成为团队交付高质量应用的坚实后盾。


扩展资源与下一步

希望这篇指南能帮助你顺利启动 Flutter 项目的自动化之旅。在实践中如果遇到问题,多查阅日志、善用社区资源,通常都能找到解决方案。 Happy automating!

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐