Filmtone / Release Engineering
fastlane で iOS リリース工程を 6 lane に分ける — Filmtone の archive / screenshots / metadata / beta / release / submit_review 設計
fastlane を 6 lane に責務分割する: `archive` (IPA ビルド)、`screenshots` (UI test snapshots)、`metadata` (ASC localized metadata + review info)、`beta` (TestFlight upload)、`release` (binary + metadata + screenshots upload)、`submit_review` (既存 build だけで審査提出)。実運用で踏んだ罠 2 つの修正も含む: gym 2.233 が認証引数を `xcargs` と `export_xcargs` の両方に渡し IPA export が `Exit status: 64`、`deliver` が `release_notes` を upload options に明示せず ASC が `whatsNew` 不在で review を弾く。それぞれの最小修正と再提出フローの記録。
- Published
- 2026年4月29日
- Topics
- Filmtone · iOS · App Store · Fastlane · Release Engineering
fastlane を 6 lane に責務分割する
Filmtone iOS の `fastlane/Fastfile` は 6 つの lane に分かれている。lane を「IPA を作る / 審査用素材を上げる / レビュー提出する」という工程ごとに完全分離してあり、binary 再生成・スクリーンショット再生成・metadata 上書きが副作用として走らない。CI と手元の運用を同じ口で回すための前提だ。
- `archive` — Xcode automatic signing で IPA を `build/fastlane/Filmtone.ipa` に書き出す
- `screenshots` — iPhone 17 Pro Max (UDID 決め打ち) で UI test を走らせ、`ja` / `en-US` 両ロケールに stage する
- `metadata` — App Store Connect にローカライズ済み metadata、review info、release notes を upload。binary とスクリーンショットには触らない
- `beta` — 既存 IPA を TestFlight に upload。`IPA_PATH` を必ず明示する
- `release` — IPA + metadata + screenshots を ASC に upload。`SUBMIT_FOR_REVIEW=1` で審査提出、`AUTOMATIC_RELEASE=1` で承認後自動公開
- `submit_review` — binary も screenshots も触らず、既存 build を `APP_VERSION` / `BUILD_NUMBER` 指定で審査提出のみ
IPA がない時に勝手に archive しない fail-fast
`beta` / `release` / `submit_review` はいずれも `explicit_ipa_path!(options, lane:)` を最初に通す。`ipa:` 引数が無ければ `UI.user_error!` で即落ちる。これがないと、CI 上で過去の archive を意図せず再利用したり、archive を持たない手元から release を踏んで突然 30 分の archive が走ったりする。
def explicit_ipa_path!(options, lane:)
explicit_path = options[:ipa] || options["ipa"]
UI.user_error!(
"Pass ipa:... to `fastlane ios #{lane}`. Implicit archive fallback is disabled."
) if explicit_path.nil?
ipa_path = File.expand_path(explicit_path, APP_ROOT)
UI.user_error!("IPA not found at #{ipa_path}") unless File.exist?(ipa_path)
ipa_path
end踏んだ罠 1 — gym 2.233 が認証引数を二重に渡す
Fastlane 2.233.0 の gym (`build_app`) は、export 時に `export_xcargs` と `xcargs` の両方を `xcodebuild -exportArchive` に渡す。Fastfile が同じ ASC 認証引数 (`-authenticationKeyIssuerID` / `-authenticationKeyID` / `-authenticationKeyPath`) を `xcargs` と `export_xcargs` の両方に入れていると、`xcodebuild -exportArchive` が同じ argument を二重に受け取り、IPA export が `Exit status: 64` で死ぬ。Xcode archive 自体は通り、IPA だけが出ない症状になる。
修正は最小に留める。`xcargs` に認証引数を渡し、`export_xcargs` を明示しない。fastlane が export 時に内部で `xcargs` を再利用するので、結果として 1 回だけ渡る。
lane :archive do
xcargs_parts = ["-allowProvisioningUpdates"]
if asc_api_key_configured?
key_file_path = temp_key_path || File.expand_path(env!("ASC_KEY_PATH"), APP_ROOT)
xcargs_parts.concat([
"-authenticationKeyIssuerID", env!("ASC_ISSUER_ID"),
"-authenticationKeyID", env!("ASC_KEY_ID"),
"-authenticationKeyPath", key_file_path,
])
end
xcargs = xcargs_parts.map(&:shellescape).join(" ")
build_app(
workspace: IOS_WORKSPACE,
scheme: "App",
clean: true,
export_method: "app-store",
output_directory: BUILD_OUTPUT_DIR,
output_name: "Filmtone",
build_path: BUILD_OUTPUT_DIR,
xcargs: xcargs,
export_options: {
method: "app-store",
signingStyle: "automatic",
teamID: DEVELOPMENT_TEAM_ID,
}
)
end踏んだ罠 2 — release_notes を明示しないと `whatsNew` 不在で審査が弾かれる
`fastlane/metadata/{ja,en-US}/release_notes.txt` がディスク上に存在していても、`upload_to_app_store` の呼び出し options に `release_notes:` を明示しないと、ASC API は `whatsNew` フィールド未設定として扱う。binary / スクリーンショット upload は通り、precheck も通り、build selection まで終わるのに、`submit_for_review: true` の段階で次のエラーで弾かれる。
appStoreVersions ... is not in valid state.
The provided entity is missing a required attribute - You must provide a value for the attribute 'whatsNew' with this request解は `release_notes:` を upload options に明示すること。`metadata` / `release` / `submit_review` の 3 lane 全部で必要なので、ヘルパーに切り出して各 lane で参照する。TestFlight (`beta` lane) は `localized_build_info` に `{ whats_new: ... }` の hash を渡す形なので、別ヘルパーで詰め直す。
def localized_release_note_texts
{
"ja" => read_text_if_present(File.join(METADATA_TEMPLATE_PATH, "ja", "release_notes.txt")),
"en-US" => read_text_if_present(File.join(METADATA_TEMPLATE_PATH, "en-US", "release_notes.txt")),
}.compact
end
def localized_build_release_notes
localized_release_note_texts.each_with_object({}) do |(locale, notes), memo|
memo[locale] = { whats_new: notes }
end
endsubmit_review lane — binary を触らずに再提出する
release 中に `whatsNew` 不在で弾かれた時、binary は既に ASC に上がっている。再 archive する必要はなく、metadata と submit_for_review だけ正しく送り直したい。それを担うのが `submit_review` lane。`APP_VERSION` と `BUILD_NUMBER` を env または lane option から受け取り、`skip_binary_upload: true` / `skip_screenshots: true` / `skip_metadata: false` で metadata だけ上書きしてから審査提出する。
lane :submit_review do |options|
app_version = normalized_env("APP_VERSION") || options[:app_version]
build_number = normalized_env("BUILD_NUMBER") || options[:build_number]
UI.user_error!("Set APP_VERSION or pass app_version:") unless app_version
UI.user_error!("Set BUILD_NUMBER or pass build_number:") unless build_number
upload_to_app_store(
api_key: load_asc_api_key!,
app_identifier: APP_IDENTIFIER,
app_version: app_version,
build_number: build_number,
platform: "ios",
metadata_path: stage_metadata!,
skip_binary_upload: true,
skip_screenshots: true,
skip_metadata: false,
submit_for_review: true,
automatic_release: truthy_env?("AUTOMATIC_RELEASE"),
release_notes: localized_release_note_texts,
# ...
)
endscreenshots の locale 対称性を入口で潰す
`release` lane は screenshots を上書きしうるので、locale ごとに枚数や filename がズレた状態で走らせると ASC 側のスクリーンショット欄が壊れる。Fastfile に `validate_release_screenshots!` を入れ、`ja` と `en-US` の両方で 1〜10 枚、かつ filename 列が完全一致することを upload 前に強制する。一致しなければ `UI.user_error!` で落ちる。
def validate_release_screenshots!
screenshot_paths = staged_release_screenshot_paths
missing_locales = screenshot_paths.select { |_, paths| paths.empty? }.keys
unless missing_locales.empty?
UI.user_error!("Missing staged screenshots for locales: #{missing_locales.join(', ')}")
end
counts = screenshot_paths.transform_values(&:count)
unless counts.values.all? { |count| count.between?(1, 10) }
UI.user_error!("Each locale must have between 1 and 10 screenshots.")
end
unless counts.values.uniq.one?
UI.user_error!("Screenshot counts differ by locale.")
end
expected_basenames = nil
screenshot_paths.each do |locale, paths|
basenames = paths.map { |path| File.basename(path) }
expected_basenames ||= basenames
next if basenames == expected_basenames
UI.user_error!("Screenshot filenames must match across locales.")
end
endv1.2 リリース時の最終提出フロー
# 1. archive (1 回だけ)
bun run release:archive # → build/fastlane/Filmtone.ipa
# 2. metadata + binary を上げる (review submit までは行かない)
IPA_PATH=build/fastlane/Filmtone.ipa \
bun run release:appstore
# 3. whatsNew 不在で弾かれたので、binary を触らず metadata だけ送り直して審査提出
set -a; . ./.env.local; set +a
APP_VERSION=1.2 BUILD_NUMBER=1 AUTOMATIC_RELEASE=1 \
./scripts/bundle.sh exec fastlane ios submit_review