1. はじめに

Flutter は単一コードベースから iOS/Android(さらに Web/デスクトップ)を出力できますが、リリース工程は依然としてプラットフォームごとに固有の作業(署名、ストア提出、ビルド番号管理)が残ります。本記事では、Flutter プロジェクトに適した CI/CD ツールの選び方と、現場で扱いやすい GitHub Actions ベースの実装例を整理します。

2. CI/CD パイプラインの全体像

Flutter アプリで標準的に必要となるステージは次の 4 つです。

Flutter CI/CDパイプラインの全体像(検証・ビルド・署名・配布の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-actionflutter-version-file: pubspec.yamlenvironment.flutter の制約を参照できます。FVM 利用時は .flutter-version を指定し、バージョン情報をコード側で一元管理します。固定文字列での指定は CI と手元の乖離を生むため避けます。ただし flutter-version-file が機能するには、pubspec.yamlenvironment.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 化

  1. App Store Connect の「Users and Access」→「Integrations」→「App Store Connect API」で 新しいキーを発行する(権限は通常 App Manager 以上)。
  2. 表示される Issuer ID、生成された Key ID、ダウンロードできる AuthKey_<KeyID>.p8 の 3 点を保管する(.p8 は再ダウンロード不可)。
  3. 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_allapp_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. 現場で効くベストプラクティス

  1. ビルド番号は CI 側で採番: --build-number=${{ github.run_number }} のように渡し、ストア側の重複を防ぎます。
  2. flavor を使い環境を分離: --flavor dev/stg/prod--dart-define で API エンドポイントを切替え、同じパイプラインで複数環境に配布します。
  3. 依存キャッシュ: ~/.pub-cache、Gradle、CocoaPods (ios/Pods) をキャッシュすると iOS ビルド時間が体感で半減します。
  4. PR では署名しない: フォークからの PR に Secrets は渡らない仕様(GitHub の保護)を理解し、署名・配布は push/tag に限定します。
  5. テストはレイヤ別に: ユニット (flutter test) → ウィジェット → 統合 (flutter test integration_test/) の順で並列化。Firebase Test Lab や patrol を組み合わせ、実機でのスモーク確認を CD 前に挟むのが有効です。
  6. リリースノート自動化: タグ push をトリガに softprops/action-gh-release で変更履歴を生成し、ストア説明文には whatsnew/ ディレクトリで多言語管理します。

7. まとめ

  • Flutter の CI/CD は 「analyze/test → build → 署名 → 配布」 という共通骨格を押さえれば、ツールに依らず移行可能な設計になります。
  • まずは GitHub Actions + fastlane で最小構成を作り、iOS 署名運用が辛くなった段階で Codemagic / Xcode Cloud への部分移行を検討するのが、コストと自由度のバランスが良い選択です。
  • Secrets と署名キーの取り扱いは最初に固めること。後からの作り直しがもっとも工数を食う領域です。

8. 参考リンク


この記事の執筆にあたり、AI の支援を受けています。