FlutterでPopCatみたいなミーム系のカウント&シェアゲームアプリを作ってみたところ、iOSのビルドからTestFlightまでのアップロードを試してみたので記録します。
iOSのみで試したため、Androidは未確認です。
前提
この記事の手順は MacBook(Apple Silicon) 上で実際に動かして確認した内容です。
事前に必要なもの
- Apple Developer Program への登録(年 $99)
- App Store Connect にアプリを登録済み(Bundle ID が確定していること)
- GitHub アカウント(証明書用のプライベートリポジトリを作るため)
- Flutter プロジェクトがローカルで
flutter build iosまで通ること
全体の流れ
- Homebrew と fastlane をインストール
ios/下に Gemfile を置いて bundle で fastlane をセットアップ- GitHub に証明書用のプライベートリポジトリを作る
fastlane matchで証明書・プロビジョニングプロファイルを Git リポジトリで管理- App Store Connect API キーを用意する
Fastfileにbetaレーンを書く- 環境変数を設定してローカルから実行
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 --versionfastlane のプロジェクトセットアップ
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 で新規リポジトリを作成します。
- GitHub → New repository
- 名前例:
ios-certificates(任意) - Private を選択
作成後の URL(例: https://github.com/yourname/ios-certificates.git)を控えておきます。
これが MATCH_GIT_URL になります。
2. GitHub PAT を発行する
match がプライベートリポジトリに push / pull するために、Personal Access Token(PAT) が必要です。
- GitHub → Settings → Developer settings → Personal access tokens
- Fine-grained または Classic のどちらでも可
- リポジトリへの Contents 読み書きを許可しておきます。
- 発行されたトークンをコピー(再表示できないので注意)
シェル用に 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 を入力します。
また、Matchfile に app_identifier を設定しておきます。
app_identifier(["com.example.appname"])fastlane match でコード署名を管理する
手動でプロビジョニングプロファイルを管理するのは面倒なので、match を使います。
証明書とプロファイルをプライベート Git リポジトリに暗号化して保存し、ローカルや CI から同じ状態で使えるようにします。
初回セットアップ(appstore 用)
証明書リポジトリが空の状態で、初回だけ 次を実行します。
cd ios
bundle exec fastlane match appstore実行の流れはおおよそ次のとおりです。
- 暗号化パスフレーズ を入力(新規作成)。
これがMATCH_PASSWORDです。必ず保存してください - Apple ID のログイン、または App Store Connect API キーによる認証
- Apple Developer Portal 上に Distribution 証明書 と App Store 用プロビジョニングプロファイル を作成
- 暗号化して 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 キーを使います。
- App Store Connect → ユーザーとアクセス → キー
- 「+」から新規キーを作成(役割: App Manager)
.p8ファイルをダウンロード(一度しかダウンロードできないので注意)

ダウンロードした .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 PATMATCH_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 に自動デプロイする