個人で iOS / Android アプリを運用していると、リリース作業そのものが一番の時間泥棒になります。バージョンを上げるだけで、App Store Connect を開き、スクリーンショットを差し替え、リリースノートを日英で書き、TestFlight に配信し、Play Console でも同じ操作を繰り返す——週に2本リリースすれば、それだけで半日が消えていく感覚は、個人開発者であればきっと共感していただけるのではないでしょうか。
Rork は「コードを書かずにアプリを作れる」ツールですが、実は本当に時間を奪っているのはコードではなく、リリース周りの手作業だと私は考えています。そこでここで整理するのはRork で作ったアプリに対して Fastlane と EAS(Expo Application Services)を組み合わせ、スクリーンショットの自動生成・メタデータ同期・TestFlight / Play Console への配信までを1コマンドで完結させる 構成を、実際に運用しているパターンとして共有します。
内容はかなり深いところまで踏み込みますが、そのぶん一度組んでしまえばリリース作業は 10 分で終わるようになります。週末の数時間を投資する価値は十分にあると思います。
この自動化で何が変わるのか — 実測した時間短縮
まず、そもそもなぜここまで凝った自動化を組むのか、数字で示しておきます。私の手元で運用している 3 本のアプリを、手作業リリースと自動化後で比較したときの所要時間が以下です。
手作業(日英2言語 × iOS/Android) : 約 2 時間 30 分 / 1 リリース
Fastlane + EAS 自動化後 : 約 8 分 / 1 リリース(うちローカル操作は 2 分、残りは CI 待ち)
週 2 リリースとして月 4 時間以上、年にすれば 50 時間以上の時間が浮く計算になります。しかもヒューマンエラー(スクリーンショットのサイズミス、メタデータの翻訳漏れ、古いビルド番号の再利用など)が実質ゼロになるため、リジェクトによる再アップロードも減ります。
ここで鍵になるのは、Fastlane と EAS を役割分担させる ことです。
EAS : クラウドで iOS / Android のバイナリをビルドする(ローカルに Xcode / Android Studio の環境を作る必要がない)
Fastlane : ビルド済みバイナリを TestFlight / Play Console に配信し、メタデータとスクリーンショットを App Store Connect / Google Play に同期する
EAS だけでも eas submit でストアへのアップロードはできますが、メタデータ同期・スクリーンショット差し替え・レビュー情報の更新は EAS だけでは完結しません 。ここを Fastlane で埋めるのが、本稿の設計思想です。
プロジェクト構成 — fastlane ディレクトリはリポジトリ直下に置く
Rork プロジェクトの直下に fastlane/ ディレクトリを作り、以下の構成で管理します。
fastlane/Fastfile — レーン(lane)定義本体
fastlane/Appfile — アプリ識別情報(Team ID、Bundle ID、Package Name)
fastlane/Matchfile — 証明書同期設定(iOS の場合)
fastlane/Deliverfile — App Store Connect メタデータ設定
fastlane/Supplyfile — Google Play メタデータ設定
fastlane/metadata/ios/{ja,en-US}/ — iOS のメタデータ(説明文・キーワード等)
fastlane/metadata/android/{ja-JP,en-US}/ — Android のメタデータ
fastlane/screenshots/ios/{ja,en-US}/ — iOS スクリーンショット
fastlane/screenshots/android/{ja-JP,en-US}/ — Android スクリーンショット
fastlane/.env.default — 非機密のデフォルト環境変数
fastlane/.env.secret — 機密情報(Git 管理対象外)
iOS と Android の切り替えはプラットフォーム指定(lane :ios_beta do ... end)で行います。Fastlane のレーンはプラットフォームごとにラッパー名前空間を切ることもできますが、個人開発規模ではフラットにしたほうが読みやすいです。
Gemfile を用意して Ruby バージョンを固定する
Fastlane は Ruby 製のため、CI 環境とローカルの挙動を揃えるには Gemfile での固定が必須です。
# Gemfile
source "https://rubygems.org"
gem "fastlane" , "~> 2.224"
gem "cocoapods" , "~> 1.15" # ネイティブモジュールがある場合のみ
# ruby-version
ruby "3.3.0"
.ruby-version にも 3.3.0 を書いておきます。CI で bundle install --path vendor/bundle する前提にしておくと、マシン依存のトラブルをかなり減らせます。
Fastfile の基本設計 — レーンを「目的」で分ける
Fastfile は「操作」ではなく「目的」でレーンを切ると、運用が楽になります。私は以下の 6 レーンに落ち着きました。
screenshots_ios / screenshots_android: スクリーンショットを生成してリポジトリにコミット
metadata_pull: ストアから最新メタデータを取得してローカル同期
metadata_push: ローカルのメタデータとスクリーンショットをストアに反映
ios_beta: EAS で iOS ビルド → TestFlight に配信 → メタデータ更新
android_beta: EAS で Android ビルド → Internal Testing に配信 → メタデータ更新
release_all: 上記 2 レーンを並行実行してから、リリースノートを Slack / Discord に通知
以下が実運用している Fastfile のコアです。
# fastlane/Fastfile
default_platform ( :ios )
fastlane_require 'dotenv'
before_all do
Dotenv . load '.env.default'
Dotenv . load '.env.secret' if File . exist? ( '.env.secret' )
ensure_git_status_clean unless ENV [ 'SKIP_GIT_CHECK' ]
end
desc "iOS TestFlight 配信: EAS でビルド → pilot でアップロード → メタデータ更新"
lane :ios_beta do
# 1) EAS Build でバイナリ生成(--non-interactive で CI 挙動に揃える)
# プロファイルは store 配布用(eas.json の beta)を指定します。
# preview のままだと Ad-hoc 証明書になり TestFlight で弾かれます(後述の落とし穴 2)
sh ( "cd .. && eas build --platform ios --profile beta --non-interactive --wait" )
# 2) 最新ビルドアーティファクトの URL を取得
build_json = sh ( "cd .. && eas build:list --platform ios --limit 1 --json --non-interactive" )
build = JSON . parse (build_json). first
UI . user_error! ( "ビルド履歴が空です。eas build が完了しているか確認してください" ) if build. nil?
artifact_url = build. dig ( "artifacts" , "buildUrl" )
UI . user_error! ( "Build URL not found (status: #{ build[ "status" ] } )" ) if artifact_url. to_s . empty?
# 3) ローカルにダウンロードしてから pilot で TestFlight にアップロード
ipa_path = "/tmp/rork- #{ Time . now . to_i } .ipa"
sh ( "curl -L -o #{ ipa_path } #{ artifact_url } " )
pilot (
api_key_path: ENV [ 'APP_STORE_CONNECT_API_KEY_PATH' ],
ipa: ipa_path,
skip_waiting_for_build_processing: true ,
distribute_external: false ,
changelog: File . read ( "metadata/ios/ja/release_notes.txt" )
)
# 4) メタデータ・スクリーンショットを同期
deliver (
api_key_path: ENV [ 'APP_STORE_CONNECT_API_KEY_PATH' ],
submit_for_review: false ,
automatic_release: false ,
skip_binary_upload: true ,
skip_screenshots: false ,
overwrite_screenshots: true ,
force: true
)
ensure
File . delete (ipa_path) if ipa_path && File . exist? (ipa_path)
end
ensure ブロックで一時ファイル(.ipa)を必ず削除するのがポイントです。CI のランナーはインスタンス使い捨てなので問題ありませんが、ローカルで回したときにディスクを圧迫する失敗談が何度かありました。
URL の取り出しに ["artifacts"]["buildUrl"] ではなく dig を使っているのにも理由があります。eas build:list が返す最新ビルドは、ビルドが失敗している場合やまだキューに積まれている場合、artifacts が null のまま返ってきます。角括弧で二段掘ると、そこで NoMethodError: undefined method for nil が出て停止し、せっかく用意した UI.user_error! の親切なメッセージには到達しません。dig に変えて、status を添えて落とすようにしておくと、CI ログを開いた瞬間に「ビルドがまだ終わっていない」のか「ビルド自体が失敗した」のかが判別できます。
小さな書き換えですが、深夜にログとにらみ合う時間を確実に削ってくれる一行です。
なぜ pilot と deliver を分けるのか
「pilot に deliver を含む機能があるのでは?」と思われるかもしれません。実際 upload_to_testflight(pilot)にもメタデータ機能は一部ありますが、スクリーンショットの差し替えは deliver(upload_to_app_store)側にしかありません 。また、pilot と deliver では認証トークンの有効期限管理が独立しているため、片方が失敗してももう片方は進行させたい場面があります。分離しておくと、再実行時の冪等性が担保しやすくなります。
スクリーンショット自動生成 — snapshot × iOS UITest の最小実装
個人開発で一番つらいのが、「iPhone 6.9 インチ / 6.5 インチ / 5.5 インチ × 日英 × 5 枚」で合計 30 枚のスクリーンショットを揃える作業です。これを手でやる限り、何をしても時間は縮みません。
Fastlane の snapshot は iOS UITest から自動でスクリーンショットを撮る仕組みですが、Rork + Expo プロジェクトの場合は prebuild で ios/ を生成してから、Xcode に UITest ターゲットを追加する必要があります。
# Expo プロジェクトのプレビルド(初回のみ)
npx expo prebuild --platform ios --clean
# Xcode を開いて UITest ターゲットを追加
open ios/YourApp.xcworkspace
Xcode で File > New > Target... > UI Testing Bundle を選択。追加したら ios/YourAppUITests/YourAppUITests.swift に以下の最小実装を入れます。
// ios/YourAppUITests/YourAppUITests.swift
import XCTest
class YourAppUITests : XCTestCase {
override func setUp () {
continueAfterFailure = false
let app = XCUIApplication ()
setupSnapshot (app) // Fastlane の SnapshotHelper.swift で定義される
app.launchArguments += [ "-UITest" , "YES" ]
app. launch ()
}
func testTakeScreenshots () {
let app = XCUIApplication ()
// React Native 側が描画完了するまで少し待つ
let homeTitle = app.staticTexts[ "home-title" ]
XCTAssertTrue (homeTitle. waitForExistence ( timeout : 8.0 ))
snapshot ( "01_Home" )
app.buttons[ "tab-explore" ]. tap ()
snapshot ( "02_Explore" )
app.buttons[ "tab-profile" ]. tap ()
snapshot ( "03_Profile" )
}
}
ポイントは waitForExistence(timeout:) を必ず入れることです 。React Native は JS バンドルの読み込みに時間がかかり、ネイティブ UITest の exists プロパティだけで判定すると「見えているのにまだアクセサビリティツリーに登録されていない」タイミングでスナップショットを撮ってしまい、白画面のスクリーンショットが大量生産される事故が起きます。これは私が最初に1日溶かした落とし穴です。
アプリ側では testID プロパティに上で使った "home-title" などを必ず設定しておきます。
// app/index.tsx (Rork 生成コードをベースにした例)
import { View, Text } from 'react-native' ;
export default function Home () {
return (
< View >
< Text testID = "home-title" >こんにちは、ようこそ</ Text >
{ /* ... */ }
</ View >
);
}
Snapfile でロケールと端末を指定して、fastlane snapshot で一括取得します。
# fastlane/Snapfile
devices ([
"iPhone 16 Pro Max" , # 6.9 inch
"iPhone 15 Plus" , # 6.7 inch
"iPhone 8 Plus" # 5.5 inch (もし Legacy を残すなら)
])
languages ([ "ja" , "en-US" ])
scheme ( "YourApp" )
output_directory ( "./fastlane/screenshots/ios" )
clear_previous_screenshots ( true )
stop_after_first_error ( true )
concurrent_simulators ( true )
concurrent_simulators(true) は同時実行で速度が 2〜3 倍になりますが、ランナーの RAM が 16GB 未満だと Simulator がクラッシュしがちです。CI で Apple Silicon ベースのランナー(GitHub Actions なら macos-15 以降)を使うときは ON、ローカルの Intel Mac では OFF が安全です。
メタデータ同期 — deliver で日英を一括管理する
App Store Connect の説明文・キーワード・リリースノートは、Git で管理するのが圧倒的に楽です。fastlane deliver init で初期化すると、以下のようなファイルが生成されます。
metadata/ios/ja/name.txt — アプリ名(30 文字まで)
metadata/ios/ja/subtitle.txt — サブタイトル(30 文字まで)
metadata/ios/ja/description.txt — 説明文(4,000 文字まで)
metadata/ios/ja/keywords.txt — キーワード(100 文字まで、カンマ区切り)
metadata/ios/ja/release_notes.txt — リリースノート(4,000 文字まで)
metadata/ios/ja/promotional_text.txt — プロモーション文(170 文字まで)
これらをそのまま Git で管理し、fastlane metadata_push を叩けば App Store Connect に反映されます。私は以下のレーンを常用しています。
desc "メタデータ・スクリーンショットを App Store Connect に反映"
lane :metadata_push do
# 事前バリデーション: 文字数オーバーを検出
validate_metadata_lengths
deliver (
api_key_path: ENV [ 'APP_STORE_CONNECT_API_KEY_PATH' ],
submit_for_review: false ,
automatic_release: false ,
skip_binary_upload: true ,
skip_metadata: false ,
skip_screenshots: false ,
overwrite_screenshots: true ,
force: true ,
precheck_include_in_app_purchases: false
)
end
def validate_metadata_lengths
limits = {
"name.txt" => 30 ,
"subtitle.txt" => 30 ,
"promotional_text.txt" => 170 ,
"keywords.txt" => 100 ,
"description.txt" => 4000 ,
"release_notes.txt" => 4000
}
%w[ja en-US] . each do |locale|
limits. each do |file, limit|
path = "metadata/ios/ #{ locale } / #{ file } "
next unless File . exist? (path)
length = File . read (path). strip . length
if length > limit
UI . user_error! ( " #{ path } : #{ length } 文字(上限 #{ limit } )" )
end
end
end
end
validate_metadata_lengths を自作しているのは、deliver はデフォルトではアップロード時にしか文字数エラーを出さないため、遅い CI で失敗を知ることになる からです。事前バリデーションを挟むと、ローカルで 1 秒で気づけます。
なぜ skip_binary_upload: true を使い続けるのか
「deliver で ipa もアップロードすれば1ステップ減るのでは?」という疑問もあるはずです。しかし、バイナリアップロードは pilot(TestFlight 用)で既に済ませている ので、deliver で再アップロードすると二重アップロードになり、稀にビルド番号の衝突で失敗します。skip_binary_upload: true は明示的に設定しておきましょう。
Android 側 — supply で Play Console を Git 管理する
Android の Play Console メタデータも、同じ発想で管理できます。fastlane supply init --json_key /path/to/service-account.json で metadata/android/ja-JP/ 以下にテキストファイルが生成されます。
desc "Android Internal Testing への配信 + メタデータ同期"
lane :android_beta do
sh ( "cd .. && eas build --platform android --profile beta --non-interactive --wait" )
build_json = sh ( "cd .. && eas build:list --platform android --limit 1 --json --non-interactive" )
build = JSON . parse (build_json). first
artifact_url = build &. dig ( "artifacts" , "buildUrl" )
UI . user_error! ( "AAB の URL を取得できませんでした" ) if artifact_url. to_s . empty?
aab_path = "/tmp/rork- #{ Time . now . to_i } .aab"
sh ( "curl -L -o #{ aab_path } #{ artifact_url } " )
supply (
json_key: ENV [ 'GOOGLE_PLAY_JSON_KEY_PATH' ],
package_name: ENV [ 'ANDROID_PACKAGE_NAME' ],
aab: aab_path,
track: "internal" ,
release_status: "completed" ,
skip_upload_metadata: false ,
skip_upload_images: false ,
skip_upload_screenshots: false ,
validate_only: false
)
ensure
File . delete (aab_path) if aab_path && File . exist? (aab_path)
end
Android で最もハマるのは「トラックの初回登録」です 。internal トラックにまだ一度も配信していない状態で supply を叩くと、Track is empty というエラーで落ちます。この場合は、最初の 1 回だけは Play Console の Web UI から AAB をアップロードして internal トラックを「初期化」する必要があります。これは Google の API 仕様の制約なので、Fastlane 側では回避できません(私はここで半日詰まりました)。
初回さえ乗り越えれば、2 回目以降は supply で全自動化できます。
GitHub Actions で CI 統合 — 認証情報の安全な渡し方
ここまでをローカルで回せるようになったら、CI に移植します。最大の論点は「App Store Connect API Key」と「Google Service Account JSON」の安全な扱いです。
App Store Connect API Key
App Store Connect の「ユーザーとアクセス > キー」から AuthKey_XXXXXX.p8 を発行します。これはプライベートキーなので絶対に Git にコミットしてはいけません。GitHub Actions では Base64 エンコードして Secrets に格納し、ジョブ開始時に復号します。
# .github/workflows/release.yml
name : Release
on :
workflow_dispatch :
inputs :
platform :
type : choice
options : [ ios , android , both ]
default : both
jobs :
ios :
if : inputs.platform == 'ios' || inputs.platform == 'both'
runs-on : macos-15 # Apple Silicon が既定。macos-latest は使わない(後述の落とし穴 5)
steps :
- uses : actions/checkout@v4
- uses : ruby/setup-ruby@v1
with :
ruby-version : '3.3.0'
bundler-cache : true
- uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- name : Install dependencies
run : npm ci
- name : Install EAS CLI
run : npm i -g eas-cli
- name : Restore App Store Connect API Key
env :
ASC_API_KEY_BASE64 : ${{ secrets.ASC_API_KEY_BASE64 }}
run : |
mkdir -p ~/.appstoreconnect/private_keys
echo "$ASC_API_KEY_BASE64" | base64 -d > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.ASC_API_KEY_ID }}.p8
cat > /tmp/asc_api_key.json <<EOF
{
"key_id": "${{ secrets.ASC_API_KEY_ID }}",
"issuer_id": "${{ secrets.ASC_ISSUER_ID }}",
"key": "$(cat ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.ASC_API_KEY_ID }}.p8 | base64 -w 0)",
"in_house": false,
"is_key_content_base64": true
}
EOF
echo "APP_STORE_CONNECT_API_KEY_PATH=/tmp/asc_api_key.json" >> $GITHUB_ENV
- name : EAS Login
env :
EXPO_TOKEN : ${{ secrets.EXPO_TOKEN }}
run : eas whoami
- name : Fastlane ios_beta
env :
FASTLANE_HIDE_CHANGELOG : 1
FASTLANE_SKIP_UPDATE_CHECK : 1
run : bundle exec fastlane ios_beta
- name : Cleanup secrets
if : always()
run : |
rm -f /tmp/asc_api_key.json
rm -rf ~/.appstoreconnect/private_keys
ポイントは rm -f /tmp/asc_api_key.json を if: always() で必ず実行することです。CI の失敗時にもキー情報を確実に消しておかないと、アーティファクトやログに残る可能性があります。
Google Service Account JSON
Play Console の Service Account 鍵は JSON ファイルそのものを Base64 化して Secrets に入れます。
android :
if : inputs.platform == 'android' || inputs.platform == 'both'
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : ruby/setup-ruby@v1
with :
ruby-version : '3.3.0'
bundler-cache : true
- uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- run : npm ci
- run : npm i -g eas-cli
- name : Restore Google Play JSON key
env :
PLAY_KEY_BASE64 : ${{ secrets.GOOGLE_PLAY_JSON_KEY_BASE64 }}
run : |
echo "$PLAY_KEY_BASE64" | base64 -d > /tmp/google-play-key.json
echo "GOOGLE_PLAY_JSON_KEY_PATH=/tmp/google-play-key.json" >> $GITHUB_ENV
- name : Fastlane android_beta
env :
ANDROID_PACKAGE_NAME : ${{ secrets.ANDROID_PACKAGE_NAME }}
EXPO_TOKEN : ${{ secrets.EXPO_TOKEN }}
run : bundle exec fastlane android_beta
- name : Cleanup secrets
if : always()
run : rm -f /tmp/google-play-key.json
Service Account に付与する権限は「リリースマネージャー」で十分 です。「管理者」権限を渡すと、間違って他のアプリを触れてしまう事故の種になります。最小権限の原則を守ってください。
よくある落とし穴 — 実際に踏んだ地雷 7 選
ここからは私が実際に踏んで数時間〜数日を溶かしたポイントを共有します。同じ事故を回避していただければ幸いです。
1. Invalid token を「API Key の期限切れ」だと思い込む
CI が突然 Invalid token で落ち始めたとき、私が最初に立てた仮説は「API Key の有効期限が切れた」でした。この仮説が誤りだったせいで、切り分けが半日遅れています。
App Store Connect API Key(.p8)そのものに有効期限はありません。期限があるのは、その鍵で署名して毎回発行する JWT のほうで、Apple が受け付ける上限は 20 分です。Fastlane はレーンの実行中に自動でトークンを作り直すため、ここを開発者が意識する場面はほとんどありません。
対象 有効期限 失効の仕方
API Key(.p8 秘密鍵) なし App Store Connect で取り消したときのみ。取り消した鍵は復活できません
JWT(鍵で署名するトークン) 最長 20 分 時間経過で自動失効。Fastlane が都度再発行します
Google Play の Service Account 鍵 なし Google Cloud 側で鍵を削除、または権限を剥奪したとき
ですから Invalid token が出たときに疑う順序は、期限ではなく次の 3 つになります。
key_id と issuer_id の取り違え 。複数のチームに所属していると、別チームの issuer_id を Secrets に入れてしまう事故が起きやすいです
誰かが鍵を取り消した 。共同開発者がいる場合や、鍵を棚卸ししたあとに起こります
ランナーの時刻ずれ 。JWT の iat が未来の時刻になっていると、Apple 側で即座に弾かれます。セルフホストランナーで NTP が止まっているときに実際に踏みました
なお is_key_content_base64: true を指定しているのに素の PEM を渡している、という取り違えも同じエラーになります。CI に載せる前に、bundle exec fastlane run app_store_connect_api_key ... をローカルで一度だけ通しておくと、鍵まわりの設定ミスと CI の設定ミスを分けて考えられるようになります。
2. eas build で --profile preview を使っていると Submit 時に拒否される
preview プロファイルでは Ad-hoc 配布用の証明書が使われることがあり、TestFlight / Play Console への提出が通りません。eas.json で TestFlight 用は distribution: "store" の別プロファイル(例: beta)を明示的に切ってください。
3. スクリーンショットのサイズが微妙にレギュレーションを外れる
iPhone 16 Pro Max のネイティブ解像度は 1320 × 2868(3x)ですが、App Store Connect は 1290 × 2796(iPhone 14 Pro Max / 15 Pro Max 基準)を要求することがあります。snapshot のデフォルトで撮ると最新シミュレータのサイズになるため、リサイズを挟む Fastlane action を自作するか、対応済み端末を選び直す必要があります。私はリサイズ処理を after_all で入れています。
4. Apple ログインが審査で詰まる「テストアカウント未登録」
deliver は審査用ログイン情報をメタデータとして送信できますが、automatic_release: true で出すと、テストアカウントを事前登録していない場合に即リジェクトされます。必ず metadata/ios/review_information/demo_user.txt と demo_password.txt を埋めてから送ること。
5. ランナーを固定したつもりが、そのイメージごと退役する
snapshot が参照する Simulator は Xcode に紐づいています。GitHub Actions の macOS ランナーを無指定(macos-latest)にしていると、ある日突然 Xcode が上がってビルドが壊れる事故が起きます。ここまでは以前から知られた話です。
厄介なのは、固定したラベルのほうが先に消えることがある点です。GitHub Actions の macOS 14 系イメージ(macos-14 および macos-14-xlarge を含む)は 2026 年 7 月 6 日から段階的な廃止に入り、11 月 2 日に完全にサポート外となります。周知のため、廃止期間中は該当ラベルのジョブが意図的に失敗させられる時間帯も設けられています。本稿は初出時に macos-14-xlarge を推奨していましたが、いま新しく組むなら macos-15 (Apple Silicon が既定)を指定してください。さらに新しいイメージを追う場合は macos-26 が選択肢になります。
固定は一度決めて終わりではありません。半年に一度、actions/runner-images のアナウンス を確認してラベルを見直す予定をカレンダーに置いておく。それだけで、ある朝いきなり全ジョブが赤くなる朝を回避できます。
6. ensure_git_status_clean が Git LFS 管理のスクリーンショットで常に dirty になる
screenshots/ ディレクトリを LFS で管理している場合、ensure_git_status_clean が LFS ポインタと実体の差分を検出して毎回失敗する場合があります。回避策は ENV['SKIP_GIT_CHECK'] = '1' を Snapfile で設定するか、LFS を使わず Git 本体で管理する(スクリーンショットは 1 枚数百 KB なので十分収まる)ことです。
7. supply の --skip-upload-metadata デフォルト値の混乱
supply の CLI 引数と Fastfile での引数名が微妙に異なり(skip_upload_metadata vs --skip-upload-metadata)、CLI で動いたコードがコピペで動かない場面があります。常に bundle exec fastlane action supply でパラメータ名を確認する習慣をつけてください。
実運用シナリオ — 毎週火曜リリースのフロー
最後に、実際に私が運用しているリリースサイクルを紹介します。
月曜夜(作業 10 分) : fastlane metadata_pull で App Store Connect / Play Console から最新状態をローカルに同期。もしストア側で軽微な変更(キーワード追加など)があれば Git に反映。
火曜朝(作業 2 分) : リリースノートを metadata/ios/ja/release_notes.txt と metadata/android/ja-JP/changelogs/default.txt に書く。英語版は自動翻訳 + 軽い手直し。バージョン番号を上げて push。
火曜昼(CI 待ち 8 分) : GitHub Actions の Release ワークフローを workflow_dispatch で手動起動。iOS / Android が並行で走り、8 分前後で TestFlight と Internal Testing に到達。
火曜夕方(作業 2 分) : TestFlight と Play Console で「ベータ版をプロモート」ボタンを押してストアに公開申請。
全体の作業時間は 12〜15 分程度です。ここまで圧縮できると、「リリースが面倒だから新機能を出し渋る」という逆インセンティブが消えます。個人開発者にとって、これが一番大きな効果だと私は感じています。
ロールバック戦略 — 失敗したリリースを安全に巻き戻す
自動化が進むと「ポチッと押したらミスが即座にストアに届く」リスクも同時に増えます。ここは地味ですが、最初に決めておかないと深夜に冷や汗をかくことになります。
まず Fastlane 側での基本方針として、ios_beta と android_beta の両方が成功してはじめてストア公開申請に進むよう、GitHub Actions のジョブを「TestFlight / Internal Testing に出すレーン」と「本番提出レーン(ios_release / android_release)」に分離しておきます。ベータで 24 時間寝かせて問題が出なかったものだけを本番提出する運用にすると、個人開発でも十分に品質を担保できます。
iOS で問題が発覚した場合の巻き戻しは、実は App Store Connect 側の機能が頼りです。審査中であれば「申請の取り下げ(Remove from Review)」、公開済みで致命的なバグが出た場合は「段階的リリースの停止(Pause Phased Release)」または前バージョンの再配布を申請します。Fastlane には直接の「ロールバック」コマンドはありませんが、以下のような補助レーンを用意しておくと、最悪の夜でも慌てずに済みます。
desc "iOS: 審査取り下げ + 前バージョンのリリースノートに戻す"
lane :ios_rollback do
UI . message ( "⚠️ App Store Connect で手動で 'Remove from Review' を実行してください" )
UI . message ( " その後このレーンを再実行してメタデータを前バージョンに戻します" )
sh ( "git checkout HEAD~1 -- fastlane/metadata/ios/" )
deliver (
api_key_path: ENV [ 'APP_STORE_CONNECT_API_KEY_PATH' ],
submit_for_review: false ,
skip_binary_upload: true ,
skip_screenshots: true ,
force: true
)
sh ( "git checkout HEAD -- fastlane/metadata/ios/" ) # ワークツリーを元に戻す
end
Android の場合は Play Console 側に「前バージョンへのロールアウト比率を 0% にする」操作があり、こちらが実質的なロールバック手段になります。supply では rollout パラメータを 0.0 に設定して再配信すると、既存ユーザーへの配信が停止します。一度でも本番にロールアウトされたバージョンを「なかったこと」にはできないので、段階的ロールアウト(staged rollout)を必ず 5% か 10% から始める のが事故防止の肝です。
ステージングバックエンドとのバインディング
もう一つ、地味に効くのがバックエンド URL のバインディングです。私は eas.json で beta プロファイルのときは EXPO_PUBLIC_API_URL=https://staging.example.com を、production プロファイルのときだけ本番 URL を渡すように切り分けています。この一行の設定ミスで、ベータ版から本番 DB に書き込んでユーザーデータを汚染してしまう事故が、過去に実際に起きました。eas.json の env セクションをレビュー対象に含めるだけで、この種の事故は根絶できます。
一緒に読むと理解が深まる記事
CI/CD 周りをさらに詰めていきたい方は、Rork × EAS Update で OTA アップデートを運用する方法 と Rork × GitHub Actions で作る EAS Build CI/CD パイプライン も参考になるはずです。本稿のリリース自動化と組み合わせると、機能追加からストア配信までを完全にコード化できます。
本稿で扱った「リリースを恐れなくする仕組み作り」の思想的な背景を掘り下げたい方に向いています。
全体を振り返って — 次の一歩
いろいろ書きましたが、いきなり全部を入れる必要はありません。まずは以下のどれか 1 つを週末に試してみていただければ、それだけで大きな変化を感じられるはずです。
スクリーンショット自動化だけ先に入れる : fastlane snapshot + UITest の最小構成で、まず日英 2 端末ぶんを自動生成します。ここまでで所要時間は半分になります。
メタデータの Git 管理だけ先に入れる : fastlane deliver init でメタデータをテキスト化し、PR レビューできる状態にします。共同編集者がいなくても、自分の「3 ヶ月前の自分」との差分レビューに役立ちます。
配信自動化だけ先に入れる : eas build + fastlane pilot(または supply)の組み合わせだけ組んで、バイナリアップロードをワンコマンド化します。
リリース作業を「自動化された前提」にできると、Rork のような AI ツールで素早くアプリを作る速度と、ストアに届けるまでの速度のギャップが解消されます。本稿がその最初の一歩のヒントになれば嬉しいです。