
現代應用程式開發離不開自動化測試與部署流水線。人手測試和部署費時、容易出錯,而且隨著團隊擴大難以應付。
本指南將示範如何使用 GitHub Actions 為 Flutter 應用程式建立穩健的 CI/CD 流水線。你將學會自動化測試、程式碼質素檢查、構建,以及部署至 Google Play Store 和 Apple App Store。
GITHUB ACTIONS 基礎
GitHub Actions 為公開儲存庫提供免費 CI/CD,私有儲存庫亦有充裕的免費額度,並能與你的 Flutter 開發流程無縫整合。
# .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
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
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
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 最佳實踐
快速反饋循環: 將構建時間控制在 10 分鐘以內。利用快取、並行任務和增量構建加快流水線速度。
快速失敗: 先執行靜態分析和單元測試。若基本質素檢查不通過,就不要在昂貴的整合測試上浪費時間。
分階段部署: 先部署至預發佈環境,再部署至正式環境。使用功能開關(feature flags)和漸進式發佈,將風險減至最低。
全面監控: 追蹤構建成功率、測試覆蓋率、部署頻率及變更前置時間。
回滾策略: 時刻準備好快速回滾部署的方案。保留舊版本隨時可用,並定期測試回滾程序。


