1. はじめに
Flutter は単一コードベースから iOS/Android(さらに Web/デスクトップ)を出力できますが、リリース工程は依然としてプラットフォームごとに固有の作業(署名、ストア提出、ビルド番号管理)が残ります。本記事では、Flutter プロジェクトに適した CI/CD ツールの選び方と、現場で扱いやすい GitHub Actions ベースの実装例を整理します。
2. CI/CD パイプラインの全体像
Flutter アプリで標準的に必要となるステージは次の 4 つです。
| ステージ | 主なコマンド/作業 | トリガ例 |
|---|---|---|
| 検証 | flutter analyze / flutter test / dart format --set-exit-if-changed |
PR |
| ビルド | flutter build appbundle / flutter build ipa |
main push、tag |
| 署名 | Android: keystore で署名 / iOS: fastlane match で証明書取得 | ビルド時 |
| 配布 | Firebase App Distribution / TestFlight / App Store / Google Play Console | tag、手動 |
3. ツール比較
| 観点 | GitHub Actions | Codemagic | Bitrise | Xcode Cloud | Fastlane 単体 |
|---|---|---|---|---|---|
| Flutter ネイティブ対応 | プラグインで対応 | 公式テンプレート | 公式ステップ | 非対応(スクリプト必要) | — (補助ツール) |
| iOS 署名の容易さ | 中(fastlane match 推奨) | 高(GUI で完結) | 高 | 高(Apple ID 連携) | 高(match) |
| 無料枠 | パブリック無制限 / プライベート月 2,000 分 | 月 500 分 | 制限あり | Apple Developer Program に月 25 時間含む(超過分は有償) | 無料 OSS |
| GitHub との統合 | ◎(同居) | ○ | ○ | △ | — |
| 向いているケース | GitHub 中心の開発 | iOS 署名を任せたい | 既存 Bitrise 資産あり | iOS のみ・Apple 完結 | 他 CI と組合せ |
判断基準:
- iOS の証明書運用に時間を取られている → Codemagic / Xcode Cloud。
- モノレポ・GitHub 中心・コストを抑えたい → GitHub Actions + fastlane。
- Android 専用 or 社内配布が中心 → GitHub Actions のみで十分(Linux ランナーは macOS の約 1/10 課金で、Android ビルドはコスト的に圧倒的に有利)。
3.1 Codemagic / Bitrise / Xcode Cloud の補足
- Codemagic: Flutter 専用のテンプレートを公式提供しており、iOS の署名・プロファイル管理を GUI から完結できます。
codemagic.yamlで宣言的にパイプラインを記述でき、App Store / Google Play Console への提出ステップも組み込み済みです。Flutter 専業チームで「CI/CD 工数を最小化したい」場合に有力です。 - Bitrise: モバイル CI として歴史が長く、ステップマーケットプレイスが充実しています。既に iOS/Android のネイティブアプリで Bitrise を運用しているチームが Flutter を後から追加するケースに向きます。ワークフローエディタが GUI 中心で、シェルに馴染みのないメンバーにも扱いやすい点が特徴です。
- Xcode Cloud: Apple 公式の CI で、Apple Developer Program に月 25 時間まで含まれます。Flutter は公式サポート外のため
ci_post_clone.sh等でflutter buildを呼ぶ構成になりますが、TestFlight 連携・証明書管理は最もスムーズです。iOS のみリリースする小規模プロジェクトでは検討価値があります。
4. GitHub Actions による実装例
PR では検証のみ、main への push でベータ配布、v* タグで本番ストア提出、という運用を想定します。
# .github/workflows/flutter-ci.yml
name: flutter-ci
on:
pull_request:
push:
branches: [main]
tags: ["v*"]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version-file: pubspec.yaml # `environment.flutter` を参照(FVM の `.flutter-version` でも可)。pubspec.yaml の `flutter:` は範囲指定不可、厳密な固定バージョンで指定する必要がある
channel: stable
cache: true
- run: flutter pub get
- run: dart format --set-exit-if-changed .
- run: flutter analyze
- run: flutter test --coverage
build-android:
needs: verify
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "17" }
- uses: subosito/flutter-action@v2
with: { flutter-version-file: pubspec.yaml, channel: stable, cache: true }
- name: Restore keystore
run: echo "$KEYSTORE_BASE64" | base64 -d > android/app/upload.jks
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
- name: Generate key.properties
# Gradle は env 変数を直接読まないため、key.properties を生成して `signingConfigs` から参照させる
run: |
cat > android/key.properties <<EOF
storeFile=upload.jks
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
EOF
- run: flutter pub get
- run: flutter build appbundle --release
- uses: actions/upload-artifact@v4
with:
name: appbundle
path: build/app/outputs/bundle/**/*.aab
build-ios:
needs: verify
if: github.event_name == 'push'
runs-on: macos-15 # Xcode のメジャー更新で挙動が変わるため、ランナー世代は明示固定するのが安全
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with: { flutter-version-file: pubspec.yaml, channel: stable, cache: true }
- run: flutter pub get
- run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT_BASE64: ${{ secrets.ASC_KEY_CONTENT_BASE64 }}
- uses: actions/upload-artifact@v4
with: { name: ipa, path: build/ios/ipa/*.ipa }
ポイント:
subosito/flutter-actionはflutter-version-file: pubspec.yamlでenvironment.flutterの制約を参照できます。FVM 利用時は.flutter-versionを指定し、バージョン情報をコード側で一元管理します。固定文字列での指定は CI と手元の乖離を生むため避けます。ただしflutter-version-fileが機能するには、pubspec.yamlのenvironment.flutterを">=3.19.0 <4.0.0"のような範囲指定にせず、3.24.0のような厳密な固定バージョンで記述する必要があります(subosito/flutter-action公式 README に明記された制約)。- Android 署名キーは base64 化して Secrets に保存し、ジョブ内で復元 します(リポジトリへの混入を避けるため)。さらに
android/key.propertiesを生成して Gradle のsigningConfigsから読ませる必要があります。これを忘れるとデバッグ署名のままビルドされ、ストアに弾かれる事故につながります(代替としてbuild.gradle側でSystem.getenv("ANDROID_KEY_ALIAS")等を読む構成にしても可)。 - iOS は fastlane match で証明書/プロビジョニングプロファイルを別 Git に格納し、
MATCH_PASSWORDで復号する運用が一般的です。 - App Store Connect API Key (
.p8+ Key ID + Issuer ID) を使うと 2FA 影響を受けず非対話で TestFlight アップロードできます。
4.1 必要 Secrets チェックリスト
| Secret 名 | 用途 | 取得元 |
|---|---|---|
ANDROID_KEYSTORE_BASE64 |
upload keystore (.jks) を base64 化したもの |
base64 -i upload.jks |
ANDROID_KEY_ALIAS |
keystore 内のキーエイリアス | keystore 生成時に指定 |
ANDROID_KEY_PASSWORD |
キーパスワード | keystore 生成時に指定 |
ANDROID_STORE_PASSWORD |
keystore パスワード | keystore 生成時に指定 |
PLAY_SERVICE_ACCOUNT_JSON |
Google Play Console 提出用サービスアカウント JSON | Google Cloud Console → IAM → サービスアカウント |
MATCH_PASSWORD |
fastlane match の暗号化パスフレーズ | fastlane match init 時に設定 |
MATCH_GIT_BASIC_AUTHORIZATION |
match リポジトリ取得用 Basic 認証文字列 | echo -n "user:token" | base64 |
ASC_KEY_ID |
App Store Connect API Key の Key ID | App Store Connect → Users and Access → Integrations |
ASC_ISSUER_ID |
App Store Connect の Issuer ID | 同上ページ上部 |
ASC_KEY_CONTENT_BASE64 |
.p8 ファイルを base64 化したもの |
base64 -i AuthKey_XXXX.p8 |
4.2 App Store Connect API Key の取得と base64 化
- App Store Connect の「Users and Access」→「Integrations」→「App Store Connect API」で 新しいキーを発行する(権限は通常 App Manager 以上)。
- 表示される Issuer ID、生成された Key ID、ダウンロードできる
AuthKey_<KeyID>.p8の 3 点を保管する(.p8は再ダウンロード不可)。 - base64 化して GitHub Secrets に登録します。
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy # クリップボードへ
# GitHub → Settings → Secrets → ASC_KEY_CONTENT_BASE64 に貼り付け
5. 配布の自動化(fastlane の例)
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
before_all do
setup_ci if is_ci? # CI 上の一時 keychain を自動構成(ローカル実行時は無効化)
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_CONTENT_BASE64"],
is_key_content_base64: true,
in_house: false
)
end
lane :beta do
match(type: "appstore", readonly: true)
upload_to_testflight(
ipa: "../build/ios/ipa/Runner.ipa",
skip_waiting_for_build_processing: true
)
end
end
before_all で app_store_connect_api_key を呼んでおくと、以降の upload_to_testflight / deliver / pilot は API Key を自動利用するため、各アクションに api_key_path: を渡す必要がなくなります。.p8 を base64 のまま Secrets で扱えるので、CI 上に一時ファイルを書き出す必要もありません(JSON スキーマで渡す場合は api_key_path: "fastlane/asc_api_key.json" に置き換える形になります)。
Android 側は upload_to_play_store(track: 'internal', aab: '...') を使うか、r0adkll/upload-google-play のような Action で PLAY_SERVICE_ACCOUNT_JSON を渡して提出します。
6. 現場で効くベストプラクティス
- ビルド番号は CI 側で採番:
--build-number=${{ github.run_number }}のように渡し、ストア側の重複を防ぎます。 - flavor を使い環境を分離:
--flavor dev/stg/prodと--dart-defineで API エンドポイントを切替え、同じパイプラインで複数環境に配布します。 - 依存キャッシュ:
~/.pub-cache、Gradle、CocoaPods (ios/Pods) をキャッシュすると iOS ビルド時間が体感で半減します。 - PR では署名しない: フォークからの PR に Secrets は渡らない仕様(GitHub の保護)を理解し、署名・配布は push/tag に限定します。
- テストはレイヤ別に: ユニット (
flutter test) → ウィジェット → 統合 (flutter test integration_test/) の順で並列化。Firebase Test Lab やpatrolを組み合わせ、実機でのスモーク確認を CD 前に挟むのが有効です。 - リリースノート自動化: タグ push をトリガに
softprops/action-gh-releaseで変更履歴を生成し、ストア説明文にはwhatsnew/ディレクトリで多言語管理します。
7. まとめ
- Flutter の CI/CD は 「analyze/test → build → 署名 → 配布」 という共通骨格を押さえれば、ツールに依らず移行可能な設計になります。
- まずは GitHub Actions + fastlane で最小構成を作り、iOS 署名運用が辛くなった段階で Codemagic / Xcode Cloud への部分移行を検討するのが、コストと自由度のバランスが良い選択です。
- Secrets と署名キーの取り扱いは最初に固めること。後からの作り直しがもっとも工数を食う領域です。
8. 参考リンク
- Continuous delivery with Flutter (公式)
- subosito/flutter-action
- fastlane match
- App Store Connect API Key
- Codemagic Flutter ガイド
この記事の執筆にあたり、AI の支援を受けています。






