返回網誌
DEVOPS2025年9月25日安安科技團隊閱讀時間 14 分鐘

FLUTTER CI/CD
流水線設置

為 Flutter 應用程式建立 CI/CD 流水線的完整指南。學習 GitHub Actions、自動化測試、程式碼質素檢查,以及部署至應用程式商店。

Flutter CI/CD 流水線設置
// 簡報

現代應用程式開發離不開自動化測試與部署流水線。人手測試和部署費時、容易出錯,而且隨著團隊擴大難以應付。

本指南將示範如何使用 GitHub Actions 為 Flutter 應用程式建立穩健的 CI/CD 流水線。你將學會自動化測試、程式碼質素檢查、構建,以及部署至 Google Play Store 和 Apple App Store。

GITHUB ACTIONS 基礎

GitHub Actions 為公開儲存庫提供免費 CI/CD,私有儲存庫亦有充裕的免費額度,並能與你的 Flutter 開發流程無縫整合。

.github/workflows/flutter_ci.yml
# .github/workflows/flutter_ci.yml
name: Flutter CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Verify formatting
      run: dart format --output=none --set-exit-if-changed .
      
    - name: Analyze project source
      run: flutter analyze --fatal-infos
      
    - name: Run tests
      run: flutter test --coverage
      
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage/lcov.info
        fail_ci_if_error: true

  build_android:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Build Android APK
      run: flutter build apk --release
      
    - name: Build Android App Bundle
      run: flutter build appbundle --release
      
    - name: Upload APK artifact
      uses: actions/upload-artifact@v3
      with:
        name: android-apk
        path: build/app/outputs/flutter-apk/app-release.apk
        
    - name: Upload AAB artifact
      uses: actions/upload-artifact@v3
      with:
        name: android-aab
        path: build/app/outputs/bundle/release/app-release.aab

  build_ios:
    needs: test
    runs-on: macos-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Build iOS (no signing)
      run: flutter build ios --release --no-codesign
      
    - name: Upload iOS artifact
      uses: actions/upload-artifact@v3
      with:
        name: ios-build
        path: build/ios/iphoneos/Runner.app

工作流程優勢

  • 每次 push 和 PR 均會執行
  • 並行構建 Android 和 iOS
  • 自動化程式碼質素檢查
  • 覆蓋率報告

主要功能

  • Flutter 版本快取
  • 依賴快取
  • 構建產物上傳
  • 多平台支援

全面的測試流水線

穩健的測試流水線涵蓋單元測試、Widget 測試、整合測試及程式碼質素檢查,確保應用程式在部署前穩定可靠。

.github/workflows/comprehensive_testing.yml
# .github/workflows/comprehensive_testing.yml
name: Comprehensive Testing

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  quality_checks:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    # Code formatting check
    - name: Check code formatting
      run: dart format --output=none --set-exit-if-changed .
      
    # Static analysis
    - name: Run static analysis
      run: flutter analyze --fatal-infos --fatal-warnings
      
    # Import sorting check
    - name: Check import sorting
      run: |
        flutter pub global activate import_sorter
        import_sorter --check-only
        
    # Unused dependencies check
    - name: Check for unused dependencies
      run: |
        flutter pub global activate dependency_validator
        dependency_validator
        
    # Security audit
    - name: Security audit
      run: |
        flutter pub deps --json > deps.json
        # Add security scanning tool here
        
  unit_tests:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Run unit tests
      run: |
        flutter test \
          --coverage \
          --test-randomize-ordering-seed random \
          --reporter expanded
          
    - name: Generate coverage report
      run: |
        sudo apt-get update
        sudo apt-get install lcov
        genhtml coverage/lcov.info -o coverage/html
        
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage/lcov.info
        fail_ci_if_error: true
        verbose: true

  widget_tests:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Run widget tests
      run: flutter test test/widget_test/
      
  integration_tests:
    runs-on: macos-latest
    strategy:
      matrix:
        device:
          - "iPhone 14 Pro Simulator (16.0)"
          - "iPad Pro (11-inch) (4th generation) Simulator (16.0)"
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Start iOS Simulator
      run: |
        xcrun simctl list devices
        xcrun simctl boot "${{ matrix.device }}" || true
        
    - name: Run integration tests
      run: |
        flutter test integration_test/ \
          --device-id="${{ matrix.device }}"
          
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: integration-test-results
        path: test_results/

測試策略:

  • 單元測試覆蓋業務邏輯(佔測試 70%)
  • Widget 測試覆蓋 UI 元件(佔測試 20%)
  • 整合測試覆蓋用戶流程(佔測試 10%)
  • 強制執行程式碼覆蓋率門檻

自動化部署流水線

測試通過後,自動將 Flutter 應用程式部署至 Google Play Store 和 Apple App Store,涵蓋應用程式簽署、版本管理及發佈說明。

.github/workflows/deploy.yml
# .github/workflows/deploy.yml
name: Deploy to App Stores

on:
  push:
    tags:
      - 'v*'  # Deploy when version tags are pushed

jobs:
  deploy_android:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    # Setup Android signing
    - name: Setup Android signing
      run: |
        echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
        echo "storeFile=keystore.jks" >> android/key.properties
        echo "keyAlias=$ANDROID_KEY_ALIAS" >> android/key.properties
        echo "storePassword=$ANDROID_STORE_PASSWORD" >> android/key.properties
        echo "keyPassword=$ANDROID_KEY_PASSWORD" >> android/key.properties
      env:
        ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
        ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
        ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
        ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
        
    - name: Build signed App Bundle
      run: flutter build appbundle --release
      
    - name: Deploy to Google Play Console
      uses: r0adkll/upload-google-play@v1
      with:
        serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
        packageName: com.onon.yourapp
        releaseFiles: build/app/outputs/bundle/release/app-release.aab
        track: production
        status: completed
        inAppUpdatePriority: 2
        userFraction: 0.33
        whatsNewDirectory: distribution/whatsnew
        mappingFile: build/app/outputs/mapping/release/mapping.txt

  deploy_ios:
    runs-on: macos-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Install dependencies
      run: flutter pub get
      
    # Setup iOS certificates and provisioning profiles
    - name: Install Apple Certificate
      uses: apple-actions/import-codesign-certs@v1
      with:
        p12-file-base64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
        p12-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
        
    - name: Install Provisioning Profile
      uses: apple-actions/download-provisioning-profiles@v1
      with:
        bundle-id: com.onon.yourapp
        issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
        api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
        api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
        
    - name: Build iOS app
      run: |
        flutter build ios --release --no-codesign
        cd ios
        xcodebuild -workspace Runner.xcworkspace \
          -scheme Runner \
          -configuration Release \
          -destination generic/platform=iOS \
          -archivePath $PWD/build/Runner.xcarchive \
          archive
        xcodebuild -exportArchive \
          -archivePath $PWD/build/Runner.xcarchive \
          -exportOptionsPlist ExportOptions.plist \
          -exportPath $PWD/build
          
    - name: Deploy to TestFlight
      uses: apple-actions/upload-testflight-build@v1
      with:
        app-path: ios/build/Runner.ipa
        issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
        api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
        api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

  create_release:
    needs: [deploy_android, deploy_ios]
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Create GitHub Release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        body: |
          🚀 New release deployed to app stores!
          
          ## What's New
          - Check CHANGELOG.md for detailed changes
          
          ## Deployment Status
          - ✅ Android: Google Play Store
          - ✅ iOS: TestFlight & App Store
        draft: false
        prerelease: false

部署功能:

  • 兩個平台的自動化應用程式簽署
  • 分階段發佈以降低風險
  • 從更新日誌自動生成發佈說明
  • 版本管理與標籤

環境與密鑰管理

妥善的密鑰管理和環境配置能確保部署安全,同時支援開發、預發佈及正式等不同環境。

.github/workflows/multi_environment.yml
# .github/workflows/multi_environment.yml
name: Multi-Environment Deployment

on:
  push:
    branches:
      - develop      # Deploy to staging
      - main         # Deploy to production
  pull_request:
    branches:
      - main         # Test on PR

jobs:
  determine_environment:
    runs-on: ubuntu-latest
    outputs:
      environment: ${{ steps.env.outputs.environment }}
      api_url: ${{ steps.env.outputs.api_url }}
    steps:
    - name: Determine environment
      id: env
      run: |
        if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
          echo "environment=production" >> $GITHUB_OUTPUT
          echo "api_url=https://api.yourapp.com" >> $GITHUB_OUTPUT
        elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
          echo "environment=staging" >> $GITHUB_OUTPUT
          echo "api_url=https://staging-api.yourapp.com" >> $GITHUB_OUTPUT
        else
          echo "environment=test" >> $GITHUB_OUTPUT
          echo "api_url=https://test-api.yourapp.com" >> $GITHUB_OUTPUT
        fi

  build_and_test:
    needs: determine_environment
    runs-on: ubuntu-latest
    environment: ${{ needs.determine_environment.outputs.environment }}
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.24.0'
        channel: 'stable'
        cache: true
        
    - name: Create environment config
      run: |
        cat > lib/config/environment.dart << EOF
        class Environment {
          static const String apiUrl = '${{ needs.determine_environment.outputs.api_url }}';
          static const String environment = '${{ needs.determine_environment.outputs.environment }}';
          static const String apiKey = '${{ secrets.API_KEY }}';
          static const bool isProduction = ${{ needs.determine_environment.outputs.environment == 'production' }};
        }
        EOF
        
    - name: Install dependencies
      run: flutter pub get
      
    - name: Run tests
      run: flutter test
      
    - name: Build for environment
      run: |
        if [[ "${{ needs.determine_environment.outputs.environment }}" == "production" ]]; then
          flutter build apk --release --flavor production -t lib/main_production.dart
        elif [[ "${{ needs.determine_environment.outputs.environment }}" == "staging" ]]; then
          flutter build apk --release --flavor staging -t lib/main_staging.dart
        else
          flutter build apk --debug --flavor development -t lib/main_development.dart
        fi

# GitHub Repository Secrets Setup:
# ANDROID_KEYSTORE_BASE64          - Base64 encoded Android keystore
# ANDROID_KEY_ALIAS                - Android key alias
# ANDROID_STORE_PASSWORD           - Android keystore password
# ANDROID_KEY_PASSWORD             - Android key password
# IOS_CERTIFICATE_BASE64           - Base64 encoded iOS certificate
# IOS_CERTIFICATE_PASSWORD         - iOS certificate password
# APPSTORE_ISSUER_ID               - App Store Connect issuer ID
# APPSTORE_KEY_ID                  - App Store Connect key ID
# APPSTORE_PRIVATE_KEY             - App Store Connect private key
# GOOGLE_PLAY_SERVICE_ACCOUNT_JSON - Google Play Console service account
# API_KEY                          - Your app's API key
# FIREBASE_CONFIG_ANDROID          - Firebase config for Android
# FIREBASE_CONFIG_IOS              - Firebase config for iOS

# Environment-specific secrets:
# For each environment (production, staging, test), create:
# - API_URL
# - DATABASE_URL
# - ANALYTICS_KEY
# - Feature flags configuration

安全最佳實踐:

  • 所有敏感資料均存放於 GitHub Secrets
  • 為每個環境使用獨立配置
  • 定期輪換密鑰
  • 只允許必要的工作流程存取密鑰

CI/CD 最佳實踐

1

快速反饋循環: 將構建時間控制在 10 分鐘以內。利用快取、並行任務和增量構建加快流水線速度。

2

快速失敗: 先執行靜態分析和單元測試。若基本質素檢查不通過,就不要在昂貴的整合測試上浪費時間。

3

分階段部署: 先部署至預發佈環境,再部署至正式環境。使用功能開關(feature flags)和漸進式發佈,將風險減至最低。

4

全面監控: 追蹤構建成功率、測試覆蓋率、部署頻率及變更前置時間。

5

回滾策略: 時刻準備好快速回滾部署的方案。保留舊版本隨時可用,並定期測試回滾程序。

// 自動化

準備好自動化你的 FLUTTER 部署了嗎?

我們的團隊可以協助你建立穩健的 CI/CD 流水線,節省時間、減少錯誤,並提升應用程式質素。一起把你的開發流程自動化!

聯絡我們
// 擴散訊號

分享本指南