GitHub Actionsでfastlaneを動かすiOSリリース自動化|設定とCI固有エラー対処
ローカルで通る fastlane が、GitHub Actions に載せた途端に固まる。署名で落ちる。bundle install すら通らない。この落差はほぼ全部、CI 固有の事情から来ています。この記事の射程は「fastlane とは何か」ではなく、GitHub Actions の macOS ランナー上で fastlane を最後まで走らせること。公式ワークフローの各行が何をしているか、Appfile と Fastfile に何を書くか、詰まったときどこを見るかを扱います。fastlane そのものの導入手順や個別アクションの一覧はfastlaneとは?iOSアプリのビルド・配信を自動化する使い方を解説にまとめてあります。
まとめ:GitHub Actions × fastlane 構成の要点
- 最小構成は3ステップ。公式ガイドのワークフローは
actions/checkout、ruby/setup-ruby、fastlane <lane>の3つだけです。CI 側で複雑なことをせず、処理は Fastfile に寄せます。 - Fastfile の1行目は
setup_ci。一時キーチェーンを作るアクションで、公式ガイドは入れない場合を「ビルドが固まって終わらない可能性がある」と明記しています。 - 認証は App Store Connect API キー。Apple ID とパスワードでは2要素認証のコード入力で止まります。
app_store_connect_api_keyで .p8 キーを渡します。 - Ruby 3.0 未満はもう動きません。fastlane 2.235.0(2026年5月26日公開)から必要 Ruby が 3.0 以上に上がりました。macOS 26.6 同梱の
/usr/bin/rubyは 2.6.10 なので、システム Ruby のままでは入りません。 - Xcode は固定する。
macos-latestは現在 macOS 26 Arm64 で、Xcode 26.0.1〜26.6 が同居します。既定は2026年7月28日のイメージ更新で 26.5 から 26.6 へ入れ替わりました。ビルドの再現性が要るなら明示指定してください。 - macOS ランナーは分単価 $0.062。Linux 2-core の $0.006 に対して約10倍です。ビルド専用なら Xcode Cloud のほうが単価は安く、この構成を選ぶ理由は別のところにあります。
GitHub Actionsでfastlaneを動かす最小構成のワークフロー
公式サンプルYAMLと各ステップが担う役割
fastlane 公式の GitHub Actions 統合ガイドが示すワークフローは、次の内容です(.github/workflows/build-ios-app.yml)。
name: build-ios-app
on:
push:
branches:
- 'master'
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
- run: fastlane beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
実質的な処理は3ステップだけ。runs-on: macos-latest は Xcode が必要なので必須で、Linux ランナーでは iOS ビルドはできません。ruby/setup-ruby は Ruby のバージョンを固定する役割で、ランナーイメージの更新でプリインストール Ruby が変わっても影響を受けなくなります。bundler-cache: true を足せば bundle install の実行と gem キャッシュまで面倒を見てくれるため、リポジトリ直下に gem 'fastlane' を書いた Gemfile がある前提なら、この1行で依存解決が済みます。
認証情報はすべて env: 経由で GitHub Secrets から渡します。ワークフローに直接書かないのはもちろんですが、run のコマンドライン引数にも書かないでください。ジョブログにそのまま出ます。
macos-latestランナーの実体とXcodeバージョンの固定
macos-latest が実際にどのマシンなのかは、GitHub が公開しているランナーイメージの定義で確認できます。イメージバージョン 20260728.0273.1(2026年7月28日公開)時点の内容は次のとおりです。
| 項目 | 内容 |
|---|---|
| ラベル | macos-latest = macOS 26 Arm64(macos-26 と同一) |
| OS バージョン | macOS 26.5.2(25F84) |
| Ruby | 3.4.10(RubyGems 4.0.17) |
| Xcode 既定 | 26.6(build 17F113) |
| 同居する Xcode | 26.0.1 / 26.1.1 / 26.2 / 26.3 / 26.4.1 / 26.5 |
| x64 が要る場合 | macos-26-intel を指定(macos-14 は非推奨) |
厄介なのは既定 Xcode の扱いです。イメージには7つの Xcode が同居していて、/Applications/Xcode.app のシンボリックリンクがどれを指すかは GitHub 側の判断で変わります。直前の 20260720.0258.1 では既定が 26.5(build 17F42)でしたが、2026年7月21日に切替が予告され、7月28日のイメージ更新で 26.6 に入れ替わりました。-latest ラベル自体も、公式方針として1〜2か月かけて段階的に新 OS へ移行していきます。コードを1行も変えていないのにビルドだけが落ちる、という事故はここから起きます。
再現性が要るなら、ジョブの先頭で使う Xcode を明示してください。
- run: sudo xcode-select -s /Applications/Xcode_26.5.app
OS ラベルも macos-latest ではなく macos-26 のように固定したほうが安全です。GA イメージには macOS のメジャーバージョンごとに Xcode のメジャーバージョンが1つしか載りません。ただしプレビュー版は別ラベルで提供されていて、2026年7月16日から runs-on: xcode-27(または xcode-27-xlarge)で Xcode 27 のイメージを選べます。GitHub 自身が「一部のソフトウェアが不安定になる可能性がある」と断っているため、本番のリリースレーンではなく検証用ジョブで使う想定のラベルです。
CIでの認証と署名:setup_ciとApp Store Connect APIキー
setup_ciが作る一時キーチェーンとmatchのreadonly化
CI で最初に呼ぶべきアクションが setup_ci です。公式ドキュメントは役割を3つ挙げています。match 用の一時キーチェーンを作ること、match を readonly モードに切り替えて CI から新しい証明書やプロファイルを発行させないこと、ログとテスト結果の出力先を収集しやすい場所に設定することです。
一時キーチェーンが要る理由は、GitHub のランナーではログイン中のユーザーがおらず、通常のキーチェーンがロック解除できないからです。公式ガイドは setup_ci を入れない場合について「ビルドが固まって終わらない可能性がある」と書いています。署名の途中でキーチェーンのパスワード入力を待ち続け、ジョブのタイムアウトまで無言で止まる、という挙動になります。CI で原因不明のハングに当たったら、まずここを疑ってください。
| パラメータ | 既定値 | 用途 |
|---|---|---|
| force | false | CI 判定に関係なく強制実行する |
| provider | 自動検出 | CI プロバイダを明示する。指定できる値は travis と circleci のみ |
| timeout | 3600(秒) | キーチェーンのタイムアウト。0 で無期限 |
| keychain_name | fastlane_tmp_keychain | 作成する一時キーチェーン名 |
| set_default_keychain | true | 一時キーチェーンをシステム既定に設定する |
provider には注意してください。値の検証ブロックが travis と circleci 以外を弾くため、provider: "github" と書くと「サポートされていない CI プロバイダ」でエラーになります。GitHub Actions では指定を省き、自動検出に任せます。
ビルドとアップロードを合わせて60分を超えるパイプラインでは、既定の 3600 秒でキーチェーンがロックされ、署名を伴う後続処理が落ちます。長いジョブでは timeout: 0 を指定してください。
もうひとつ、setup_ci を書いたのに効かないケースがあります。ソース上、環境変数 MATCH_KEYCHAIN_NAME がすでに設定されていると「キーチェーンは指定済み」と判断して何もせずに戻ります。ワークフローやリポジトリ変数で MATCH_KEYCHAIN_NAME を渡している場合は、一時キーチェーンが作られていないことを疑ってください。
App Store Connect APIキーで2要素認証を回避する
Apple ID とパスワードによる認証は、CI ではまず成立しません。2要素認証の確認コードを入力する相手がいないためです。app_store_connect_api_key アクションで、App Store Connect で発行した .p8 形式のキーを使います。
| パラメータ | 既定値 | 内容 |
|---|---|---|
| key_id | 必須 | キー ID |
| issuer_id | 省略可 | Issuer ID。個人用 API キーでは省略する |
| key_content | — | .p8 の中身を直接渡す |
| key_filepath | — | .p8 ファイルのパスを渡す |
| is_key_content_base64 | false | key_content が Base64 かどうか |
| duration | 500 | トークンの有効期間 |
| in_house | false | Enterprise(社内配布)アカウントかどうか |
実行結果は SharedValues::APP_STORE_CONNECT_API_KEY に格納され、以降の build_app や upload_to_testflight が自動的に参照します。レーンの先頭で一度呼べば、各アクションに個別指定する必要はありません。
GitHub Secretsに登録する値と改行の扱い
.p8 キーは複数行のテキストです。Secrets は複数行の値も保持できますが、シェル経由の受け渡しで改行が壊れるトラブルが多いため、Base64 で1行にしてから登録するほうが確実です。
base64 -i AuthKey_D83848D23.p8 | pbcopy
この base64 -i は macOS 版の書式で、GNU coreutils では -i の意味が違います。Linux で作るなら base64 < AuthKey_D83848D23.p8 としてください。出力を ASC_KEY_CONTENT として登録し、Fastfile 側で is_key_content_base64: true を指定します。match を併用するなら、証明書リポジトリの復号パスフレーズを MATCH_PASSWORD に、プライベートリポジトリへの認証情報を MATCH_GIT_BASIC_AUTHORIZATION に登録します。後者は match のオプション定義が「通常は Base64 の文字列」と説明しているとおり、ユーザー名:トークン を Base64 化した値です。
default_platform(:ios)
platform :ios do
desc "TestFlightへ配信する"
lane :beta do
setup_ci if ENV['CI']
app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'],
issuer_id: ENV['ASC_ISSUER_ID'],
key_content: ENV['ASC_KEY_CONTENT'],
is_key_content_base64: true
)
match(type: "appstore")
build_app(scheme: "MyApp")
upload_to_testflight(skip_waiting_for_build_processing: true)
end
end
証明書とプロビジョニングプロファイルの仕組みそのものでつまずいている場合は、プロビジョニングプロファイルとは|iOS署名の仕組み・4種類・作成・更新・エラー対処で先に構造を押さえたほうが早く解決します。
Appfileに書く項目とチームIDの使い分け
app_identifier・apple_id・team_id・itc_team_idの役割
Appfile は ./fastlane ディレクトリに置く設定ファイルで、アプリと組織を識別する値だけを持ちます。ここでよく混乱するのが「Apple ID が2種類」「チーム ID が2種類」ある点です。Developer Portal と App Store Connect が別システムだった名残で、アカウントが分かれている組織ではそれぞれ違う値になります。
| キー | 指す値 |
|---|---|
| app_identifier | アプリのバンドル ID |
| apple_id | Apple アカウントのメールアドレス |
| apple_dev_portal_id | Developer Portal 用アカウント(apple_id と分ける場合) |
| itunes_connect_id | App Store Connect 用アカウント(同上) |
| team_id | Developer Portal の Team ID(英数10桁) |
| team_name | Developer Portal のチーム名 |
| itc_team_id | App Store Connect の Team ID(数値) |
| itc_team_name | App Store Connect のチーム名 |
itc_team_id の指定が要るのは、1つの Apple アカウントが複数のチームに所属していて、かつ Apple ID によるログイン(spaceship セッション)を使う場合です。未指定だと fastlane がどのチームで実行するかを対話的に尋ねてきますが、CI には答える相手がいません。後述の App Store Connect API キーだけで完結する構成ならトークンが発行元チームに紐づくため、この選択自体が発生しません。Apple ID 認証を残しているなら書いてください。値は App Store Connect の「ユーザーとアクセス」画面のほか、ローカルで一度対話実行したときに fastlane が表示するチーム一覧からも拾えます。
for_platform / for_laneで環境ごとに値を切り替える
開発用と本番用でバンドル ID を分けている場合、Appfile 側でレーンごとに値を切り替えられます。
app_identifier "com.example.myapp"
apple_id "[email protected]"
team_id "ABCDE12345"
itc_team_id "123456789"
for_platform :ios do
for_lane :test do
app_identifier "com.example.myapp.dev"
end
end
優先順位は公式ドキュメントが明記していて、レーン固有の値があればそれを使い、なければファイル冒頭の値にフォールバックします。Fastfile 側から値を読みたいときは CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) で取得できます。
Fastfileのlane設計:テスト・ビルド・配信
run_testsによるテスト実行と結果ファイルの回収
run_tests は xcodebuild test のラッパーです。CI で使うときは、対象シミュレータを固定することとテスト結果の出力先を指定することが要点になります。
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 17"],
result_bundle: true,
output_directory: "./fastlane/test_output"
)
end
devices を書かないとランナーイメージに入っているシミュレータの並び順に依存するため、イメージ更新でテスト対象の端末が変わります。result_bundle: true を付けておけば .xcresult が残り、actions/upload-artifact でそのまま成果物として回収できます。
output_directory の明示は省かないでください。setup_ci の説明には「ログとテスト結果のパスを収集しやすい場所に設定する」とありますが、ソースを読むとこの処理はプロバイダが CircleCI か CodeBuild のときにしか呼ばれず、さらに環境変数 FL_OUTPUT_DIR が設定されている場合に限られます。GitHub Actions では走りません。setup_ci を入れたのに .xcresult が想定の場所に出ず、アーティファクトのアップロードが空振りする原因がこれです。
build_appのIPA生成とexport_methodの整合
build_app が担うのはビルドとアーカイブ、エクスポートまで。以前の名前である gym は現在もエイリアスとして残っていて、ソース上では gym、build_ios_app、build_mac_app が別名として定義されています。古い記事のサンプルが gym と書かれていても、そのまま動きます。
build_app(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build",
clean: true
)
export_method がとれる値は8種類(app-store、validation、ad-hoc、package、enterprise、development、developer-id、mac-application)で、iOS の配布で使うのはこのうち app-store、ad-hoc、enterprise、development の4つです。match で取得したプロファイルの種別と一致していないとエクスポート段階で落ちるため、match(type: "appstore") なら export_method: "app-store" と揃えてください。
upload_to_testflight・upload_to_app_storeによる配信先の使い分け
配信先で使うアクションが変わります。TestFlight への内部配信は upload_to_testflight、App Store への申請は upload_to_app_store(エイリアスは deliver と appstore)です。
CI で upload_to_testflight を使うときは skip_waiting_for_build_processing: true を付けてください。付けないと Apple 側の処理完了までジョブが待機し続け、その待ち時間もそのまま macOS ランナーの課金対象になります。処理完了を待つ必要があるのは、アップロード直後にテスターへ自動配布したい場合だけです。
metadataとscreenshotsによるストア情報の自動反映
metadataディレクトリのファイル構成とリリースノートの置き場所
upload_to_app_store は、決められたディレクトリ構成のテキストファイルと画像を読んで App Store Connect に反映します。ソースコード上の実装は、ローカライズされる値を <metadata_path>/<言語>/<キー名>.txt から、言語に依存しない値を <metadata_path>/<キー名>.txt から読み込みます。
fastlane/
Appfile
Fastfile
metadata/
copyright.txt
primary_category.txt
ja/
description.txt
keywords.txt
release_notes.txt
en-US/
description.txt
keywords.txt
release_notes.txt
screenshots/
ja/
en-US/
言語ごとに置けるキーは description、keywords、release_notes、support_url、marketing_url、promotional_text、name、subtitle、privacy_url です。リリースノートを探しているなら、ファイル名は release_notes.txt です(App Store Connect の API 側では whats_new という名前で扱われますが、ファイル名は release_notes のままです)。copyright と primary_category は言語に依存しないため metadata 直下に置きます。
CIでforce: trueとskip_screenshotsを指定する理由
CI で必ず指定すべきなのが force: true です。このオプションの説明は「HTML プレビューファイルの検証をスキップする」で、指定しないと deliver は反映内容の HTML プレビューを生成して確認入力を待ちます。setup_ci 未指定のときと同じく、ジョブが無言で止まる原因になります。
upload_to_app_store(
force: true,
skip_screenshots: true,
submit_for_review: false
)
スクリーンショットのアップロードは端末サイズの数だけ画像が転送されるため、毎回のビルドで走らせると時間と課金の両方を消費します。スクリーンショットを更新する専用レーンを分け、通常のリリースレーンでは skip_screenshots: true にするのが実用的です。
CI固有のエラーと切り分け手順
multi_jsonのGem::LoadError(Ruby 3.3以降 × fastlane 2.236.0未満)
2026年半ばに増えたのが、次のエラーで fastlane が起動時に落ちるパターンです。
multi_json is not part of the bundle. Add it to your Gemfile. (Gem::LoadError)
紛らわしいのは、bundle install 自体は成功する点です。落ちるのは require の瞬間、つまり fastlane コマンドを叩いたときになります。原因は fastlane 本体ではなく依存先で、googleauth の v1.17.0 が multi_json を依存から外したことが起点でした。fastlane リポジトリの PR #30062 は問題を「Ruby 3.3 以降のモジュールテストで全ビルドが失敗している。原因は googleapis/google-api-ruby-client の issue #26611」と説明し、対処として multi_json を fastlane 自身の直接依存に追加しています。
この修正が入った版は RubyGems の依存情報で特定できます。2.235.0(2026年5月26日)の runtime dependencies に multi_json はなく、2.236.0(2026年6月8日)から multi_json ~> 1.12 が入りました。切り分けは版番号だけで済みます。Ruby 3.3 以降でのみ発生するため、rbenv で 3.2 系を使っている手元では再現せず、Ruby 3.4 を入れたランナー上だけで落ちる、という形で表面化しがちです。
bundle update fastlane
bundle exec fastlane --version
2.236.0 以上に上げれば解消します。上げられない事情があるなら、Gemfile に gem 'multi_json' を明示するか、上流側の googleauth を更新するのが暫定策です(issue #26611 は 2026年6月8日にクローズ済み)。版ごとの変更点は fastlane リポジトリの Releases ページで確認できます。
bundle installのRuby要件エラー(2.235.0での引き上げ)
fastlane は 2.235.0(2026年5月26日)から必要 Ruby を 3.0 以上に引き上げました。直前の 2.234.0(2026年5月10日)までは 2.7 以上だったので、この2版の間に境界があります。
ここで問題になるのが macOS 同梱の Ruby です。macOS 26.6 の /usr/bin/ruby は 2.6.10p210 で、要件を満たしません。ローカル環境でシステム Ruby を使い続けていると、CI では ruby/setup-ruby が 3.4 を入れるので通り、手元だけ落ちる、という逆パターンになります。rbenv や Homebrew で 3.0 以上を用意してください。バージョン管理ツールの選定はrbenvとは何か?RVMとの違いや特徴を徹底解説する基本ガイドが参考になります。
ジョブが無言で止まるときの対話待ち5パターン
ジョブが進まずタイムアウトまで無言で待つ症状は、ほぼ「fastlane が対話入力を待っている」ことが原因です。CI には入力する相手がいないので、エラーも出ないまま止まります。しかも待っている間も macOS ランナーの $0.062/分は課金され続ける。GitHub ホストランナーのジョブ上限は6時間なので、放置すれば1回あたり最大で約22ドルの空費です。timeout-minutes を短く切っておいてください。原因は次の5つに集約できます(各項目の詳細は上の各章)。
| 止まる場所 | 待っているもの | 対処 |
|---|---|---|
| match / 署名 | キーチェーンのロック解除 | レーン先頭で setup_ci を呼ぶ |
| upload_to_app_store | HTML プレビューの確認 | force: true を指定する |
| Apple へのログイン | 2要素認証のコード | app_store_connect_api_key に切り替える |
| チーム選択 | 所属チームの選択 | Appfile に team_id と itc_team_id を書く |
| ジョブ後半のアップロード | —(キーチェーンが期限切れ) | setup_ci の timeout を 0 にする |
切り分けの順番は、ジョブログの最終行が何のアクションかを見て、この表の行に当てはめるだけです。ログが途中で終わっているのにエラーが無い、という状態自体が対話待ちのサインです。
macOSランナーの費用と、この構成を選ぶべきでない場面
macOSランナーの分単価とXcode Cloudとの時間単価比較
GitHub Actions の標準ランナーは、OS で単価がまるで違います。macOS の 3-core または 4-core が1分あたり $0.062、Linux 2-core (x64) が $0.006、Windows 2-core が $0.010。macOS は Linux の約10倍で、しかもジョブごとに分未満は切り上げられます。なお標準ランナーはパブリックリポジトリでは無料で、プランごとの含有分数(GitHub Free 2,000分、Pro と Team が3,000分、Enterprise Cloud が50,000分)はプライベートリポジトリ向けです。含有分数は larger runners には使えません。
金額感を出すと、iOS のビルドとテストで1回15分かかるパイプラインは1回あたり約 $0.93 です。1日5回、月20営業日で100回なら約 $93 になります。ここに match のクローンや TestFlight のアップロード待ちが乗ると、さらに伸びます。
この単価を Xcode Cloud と並べると、判断が変わってきます。Xcode Cloud は Apple Developer Program の年会費に月25コンピュートアワーが含まれ、超過分は100時間で US$49.99 です。時間換算すると $0.50 で、GitHub Actions の macOS ランナー($0.062 × 60 = $3.72)の約7分の1です。
GitHub Actions + fastlaneを選ぶべきでない条件と、選ぶ理由
ビルドと TestFlight 配信しかしないなら、GitHub Actions + fastlane は選ばないほうがいい。単価で7倍負けているうえ、証明書を match で自前管理する手間まで増えます。Xcode Cloud なら署名は Apple 側が持つので、この記事の前半で扱ったキーチェーンと API キーの設定がまるごと不要です。
逆に、次のどれかに当てはまるならこの構成に投資する価値があります。
- Android と同じパイプラインに載せたい。fastlane は
upload_to_play_storeを持つので、iOS と Android を1つの Fastfile で扱えます。Xcode Cloud は iOS 側しか担当しません。 - ビルド以外のジョブを同居させたい。Lint、依存の脆弱性チェック、リリースノートの自動生成、Slack 通知などを同じワークフローに書けます。
- リポジトリのイベントを起点にしたい。特定ラベルの付いた PR だけ配信する、タグ push で本番申請する、といった条件分岐は GitHub Actions のほうが自由に書けます。
判断基準は「iOS のビルドが目的か、リリース工程全体の自動化が目的か」です。前者なら Xcode Cloud、後者ならこの構成です。テストとビルドを GitHub Actions 側でどう組み立てるかは、GitHub Actionsでビルド・自動テストを設定する方法|CI/CDワークフローの作り方でワークフロー全体の作り方を扱っています。
よくある質問
fastlaneとは何をするツールですか?
iOS と Android アプリのビルド、テスト、署名、ストアへの配信を自動化する Ruby 製のオープンソースツールです。実体は多数の「アクション」の集合で、Fastfile に処理の並びを書いて実行します。導入手順や各アクションの一覧はfastlaneとは?iOSアプリのビルド・配信を自動化する使い方を解説にまとめています。
Fastlane.swift(fastlane init swift)で書いたほうがよいですか?
CI で使うなら Ruby の Fastfile を勧めます。Fastlane.swift は公式ドキュメントが現在も「ベータ版です」と明記している状態で、正式版になっていません。構成上も fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj という Xcode プロジェクトを介する分、Ruby 版に無い工程が挟まります。分単価の高い macOS ランナーではその差がそのまま課金に効いてくる形。Swift で書けること自体の利点より、情報の少なさと実行時間の不利が上回ります。
fastlaneからGitのタグ付けやコミットはできますか?
できます。add_git_tag がタグを作成するアクションで、tag で文字列を直接指定するほか、grouping、prefix、build_number、postfix を組み合わせた自動生成にも対応します。リモートへの反映は push_to_git_remote です。CI から push する場合は、ワークフロー側で permissions: contents: write を与える必要があります。
fastlane supply はiOSでも使いますか?
使いません。supply は upload_to_play_store のエイリアスで、Google Play 向けのアクションです。iOS の App Store に送るのは upload_to_app_store(エイリアスは deliver)のほう。どちらもメタデータとスクリーンショットを扱うため、取り違えやすい組み合わせになっています。
build_app と gym は違うものですか?
同じものです。fastlane のソース上で gym は build_app のエイリアスとして定義されています。同じ関係が scan と run_tests、sigh と get_provisioning_profile にもあり、いずれも旧名が別名として残っている状態。古い記事のサンプルが gym や scan で書かれていても、そのまま動きます。