リンクをコピーしました

FlutterでPopCatみたいなミーム系のカウント&シェアゲームアプリを作ってみたところ、iOSのビルドからTestFlightまでのアップロードを試してみたので記録します。
iOSのみで試したため、Androidは未確認です。

前提

この記事の手順は MacBook(Apple Silicon) 上で実際に動かして確認した内容です。

事前に必要なもの

  • Apple Developer Program への登録(年 $99)
  • App Store Connect にアプリを登録済み(Bundle ID が確定していること)
  • GitHub アカウント(証明書用のプライベートリポジトリを作るため)
  • Flutter プロジェクトがローカルで flutter build ios まで通ること

全体の流れ

  1. Homebrew と fastlane をインストール
  2. ios/ 下に Gemfile を置いて bundle で fastlane をセットアップ
  3. GitHub に証明書用のプライベートリポジトリを作る
  4. fastlane match で証明書・プロビジョニングプロファイルを Git リポジトリで管理
  5. App Store Connect API キーを用意する
  6. Fastfilebeta レーンを書く
  7. 環境変数を設定してローカルから実行

Homebrew と fastlane のインストール

fastlane は Ruby gem ですが、まず Homebrew 版 を入れておくと fastlane init などの CLI が使いやすくなります。
プロジェクト内では Gemfile 経由の bundle 実行がメインになります。

Homebrew のインストール

未インストールの場合は Homebrew 公式 のコマンドを実行します。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

fastlane のインストール

brew install fastlane

バージョン確認:

fastlane --version

fastlane のプロジェクトセットアップ

init

ios/ ディレクトリで fastlane init を実行します。

cd ios
bundle exec fastlane init

次のような選択肢が表示されます。

1. 📸 Automate screenshots
2. 👩‍✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks

ここでは 2. Automate beta distribution to TestFlight を選べば OK です。
Apple アカウントへのログインが求められたら、そのまま進めます。

Appfile

ios/fastlane/Appfile にアプリの識別情報を書きます。
初期化でうまくいっていれば、自動で入っているはずです。

app_identifier("com.example.yourapp")
team_id("XXXXXXXXXX")

apple_id(ENV["APP_STORE_CONNECT_APPLE_ID"]) if ENV["APP_STORE_CONNECT_APPLE_ID"]
itc_team_id(ENV["APP_STORE_CONNECT_TEAM_ID"]) if ENV["APP_STORE_CONNECT_TEAM_ID"]

GitHub に証明書用リポジトリを作る

match は証明書とプロビジョニングプロファイルを 暗号化したうえで Git リポジトリ に保存します。
アプリのソースコードとは別の プライベートリポジトリ を 1 つ用意します。

1. プライベートリポジトリを作成

GitHub で新規リポジトリを作成します。

  1. GitHub → New repository
  2. 名前例: ios-certificates(任意)
  3. Private を選択

作成後の URL(例: https://github.com/yourname/ios-certificates.git)を控えておきます。
これが MATCH_GIT_URL になります。

2. GitHub PAT を発行する

match がプライベートリポジトリに push / pull するために、Personal Access Token(PAT) が必要です。

  1. GitHub → Settings → Developer settings → Personal access tokens
  2. Fine-grained または Classic のどちらでも可
  3. リポジトリへの Contents 読み書きを許可しておきます。
  4. 発行されたトークンをコピー(再表示できないので注意)

シェル用に Base64 エンコードしておきます。
ユーザー名は GitHub のユーザー名、トークンは先ほど発行した PAT です。

echo -n "yourname:ghp_xxxxxxxxxxxx" | base64 | pbcopy

出力が MATCH_GIT_BASIC_AUTHORIZATION の値になります。
上記コマンドが正常に動けばペーストできるようになっているはずです。

3. match init で Matchfile を生成

ios/ ディレクトリで次を実行すると、対話形式で Matchfile が作れます。

cd ios
bundle exec fastlane match init

以下のようにストレージ種別を聞かれます。

[13:00:02]: fastlane match supports multiple storage modes, please select the one you want to use:
1. git
2. google_cloud
3. s3
4. gitlab_secure_files

こんな風に聞かれるので今回は「1」を選びます。
Git URL を聞かれるので、先ほど作ったリポジトリの URL を入力します。

また、Matchfileapp_identifier を設定しておきます。

app_identifier(["com.example.appname"])

fastlane match でコード署名を管理する

手動でプロビジョニングプロファイルを管理するのは面倒なので、match を使います。
証明書とプロファイルをプライベート Git リポジトリに暗号化して保存し、ローカルや CI から同じ状態で使えるようにします。

初回セットアップ(appstore 用)

証明書リポジトリが空の状態で、初回だけ 次を実行します。

cd ios
bundle exec fastlane match appstore

実行の流れはおおよそ次のとおりです。

  1. 暗号化パスフレーズ を入力(新規作成)。
    これが MATCH_PASSWORD です。必ず保存してください
  2. Apple ID のログイン、または App Store Connect API キーによる認証
  3. Apple Developer Portal 上に Distribution 証明書App Store 用プロビジョニングプロファイル を作成
  4. 暗号化して GitHub の証明書リポジトリへ push

初回は Apple 側の処理を含むため、数分かかることがあります。

Apple Developer Portal での確認

match が成功すると、Certificates, Identifiers & Profiles に次が追加されています。

  • Certificates: Apple Distribution(名前に match やチーム名が含まれる)
  • Profiles: match AppStore com.example.yourapp のような App Store プロファイル

GitHub の証明書リポジトリ側には、certs/profiles/ などのディレクトリと暗号化されたファイルが commit されているはずです。

うまくいかないとき

  • 「Could not create another Distribution certificate」
    既存の Distribution 証明書が上限(通常 2 本)に達していることがあります。
    使っていない古い証明書を Developer Portal から失効させてから再実行します。
  • GitHub への push が 403
    PAT のスコープ不足、または MATCH_GIT_BASIC_AUTHORIZATION の Base64 形式(user:token)を確認してください

App Store Connect API キーを用意する

毎回 Apple ID でログインするのは手間なので、API キーを使います。

  1. App Store Connect → ユーザーとアクセス → キー
  2. 「+」から新規キーを作成(役割: App Manager)
  3. .p8 ファイルをダウンロード(一度しかダウンロードできないので注意)
api key

ダウンロードした .p8 は Base64 エンコードしておきます。

base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy

これが APP_STORE_CONNECT_API_KEY_CONTENT になります。

Fastfile を書く

ios/fastlane/Fastfile です。必要最小限で動いた構成を載せています。

require "shellwords"

default_platform(:ios)

REPO_ROOT = File.expand_path("../..", __dir__)

def app_store_connect_key
  app_store_connect_api_key(
    key_id: ENV.fetch("APP_STORE_CONNECT_API_KEY_ID"),
    issuer_id: ENV.fetch("APP_STORE_CONNECT_ISSUER_ID"),
    key_content: ENV.fetch("APP_STORE_CONNECT_API_KEY_CONTENT"),
    is_key_content_base64: true,
    in_house: false
  )
end

def repo_sh(*command)
  escaped_command = command.map { |part| Shellwords.escape(part) }.join(" ")
  sh("cd #{Shellwords.escape(REPO_ROOT)} && #{escaped_command}")
end

platform :ios do
  desc "Flutter のフォーマット・解析・テスト・unsigned ビルドを実行"
  lane :ci_checks do
    repo_sh("flutter", "pub", "get")
    repo_sh("dart", "format", "--output=none", "--set-exit-if-changed", "lib", "test")
    repo_sh("flutter", "analyze", "lib", "test")
    repo_sh("flutter", "test")
    repo_sh("flutter", "build", "ios", "--release", "--no-codesign")
  end

  desc "署名済み IPA をビルドして TestFlight にアップロード"
  lane :beta do
    setup_ci if ENV["CI"]

    api_key = app_store_connect_key

    match(
      type: "appstore",
      readonly: ENV["CI"] == "true",
      team_id: ENV.fetch("APP_STORE_CONNECT_TEAM_ID"),
      git_url: ENV.fetch("MATCH_GIT_URL"),
      api_key: api_key
    )

    update_code_signing_settings(
      use_automatic_signing: false,
      path: "Runner.xcodeproj",
      team_id: ENV.fetch("APP_STORE_CONNECT_TEAM_ID"),
      bundle_identifier: "com.example.yourapp",
      code_sign_identity: "Apple Distribution",
      profile_name: "match AppStore com.example.yourapp"
    )

    build_number = latest_testflight_build_number(api_key: api_key) + 1
    increment_build_number(
      build_number: build_number,
      xcodeproj: "Runner.xcodeproj"
    )

    gym(
      export_method: "app-store",
      clean: true,
      skip_package_dependencies_resolution: true,
    )

    upload_to_testflight(
      api_key: api_key,
      skip_waiting_for_build_processing: true
    )
  end
end

ポイントをいくつか。

  • latest_testflight_build_number + 1 でビルド番号を自動インクリメントしているので、手動で管理しなくて済みます
  • skip_package_dependencies_resolution: true は Flutter の SPM 解決を fastlane に任せないための設定です。flutter build ios --no-codesign で事前に解決済みにしてから gym を呼ぶ流れが安定していました

環境変数を設定する

.env ファイルを ios/fastlane/ に置くか、シェルの設定に書きます。
.env を使う場合は .gitignore に追加するのを忘れずに

export APP_STORE_CONNECT_APPLE_ID="your@email.com"
export APP_STORE_CONNECT_TEAM_ID="XXXXXXXXXX"
export APP_STORE_CONNECT_API_KEY_ID="XXXXXXXXXX"
export APP_STORE_CONNECT_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export APP_STORE_CONNECT_API_KEY_CONTENT="LS0tLS1CRUd..."  # base64 エンコード済み
export MATCH_GIT_URL="https://github.com/yourname/ios-certificates.git"
export MATCH_PASSWORD="your-match-passphrase"
export MATCH_GIT_BASIC_AUTHORIZATION="base64(user:token)"  # GitHub PAT

MATCH_GIT_BASIC_AUTHORIZATION は GitHub の PAT を user:token の形で Base64 エンコードしたものです。

echo -n "yourname:ghp_xxxxxxxxxxxx" | base64

ローカルから実行する

ios/ ディレクトリに移動して、

cd ios

# 事前確認(unsigned ビルドまで)
fastlane ios ci_checks

# TestFlight にアップ
fastlane ios beta

初回は match がプロファイルをダウンロードするので少し時間がかかります。
2回目以降は証明書がローカルにキャッシュされるので速いです。

よくあったトラブル

ビルド番号が重複してリジェクトされる

latest_testflight_build_number が TestFlightの最新番号を取得してくれるので、+ 1 しておけばほぼ重複しません。
ただしAPIの反映に少しラグがあるので、連続して複数回アップするときは注意が必要です。

match のパスフレーズを忘れた

MATCH_PASSWORD を忘れると証明書リポジトリが復号できなくなります。
パスワードマネージャーに保存しておくことを強くおすすめします。

まとめ

  • MATCH_PASSWORD は忘れないように必ず保管しておく
  • 初回セットアップは少し複雑だが、テンプレート化すれば再利用しやすい
  • ローカル実行が安定したら、次のステップとして GitHub Actions に載せやすい

次は同じ Fastfile を使って GitHub Actions から自動デプロイする構成です。

Flutter × GitHub Actions で TestFlight に自動デプロイする

関連リンク