Dördüncüsü Bir Komuttu — fastlane ile App Store Otomasyonu
İlk submission'ı elle yaptım. İkincisinde bir string'i yanlış yazdım, üçüncüsünde ekran görüntülerini yanlış boyutta yükledim. O noktada durdum: yayın süreci bir tıklama dizisi değil, versiyonlanan bir dosya olmalı. Bu yazı, Vird'in App Store yayınını fastlane'e devrederken öğrendiklerim — asıl kırılma noktası olan .p8 auth'tan, deliver'ın beni reddettiği o dört ana kadar.

Bittiği sanılan iş
Vird'i bitirdim. Widget'lar çalışıyordu, paywall satın alma yapıyordu, tema geçişleri yerindeydi. "Tamam" dedim, "yayına gönderelim." Sonra App Store Connect'in submission ekranıyla yüz yüze geldim: birkaç dilde metadata, her cihaz boyutu için ekran görüntüleri, binary yükleme, export compliance soruları, IDFA beyanı, içerik hakları... Her biri elle, her biri bir web formunda, her biri bir sonraki build'de baştan.
İlk submission'ı elle yaptım. İkincisinde bir string'i yanlış yazdım. Üçüncüsünde ekran görüntülerini yanlış boyutta yükledim. O noktada durdum: bu iş tekrarlanabilir olmalı. fastlane'i kurdum.
fastlane bir cümlede
fastlane, iOS/Android yayın adımlarını Ruby'de lane'lere (şerit — sıralı adımlar dizisi) toplayan bir otomasyon aracı. Her lane bir dizi action çağırır: upload_to_app_store, build_app, increment_build_number. Sen fastlane release yazarsın; o metadata'yı, ekran görüntülerini ve binary'yi App Store Connect'e gönderip incelemeye sokar. Kritik olan şu: yayın süreci artık bir tıklama dizisi değil, versiyonlanan bir dosya.
Asıl kırılma: şifre değil, .p8 anahtarı
fastlane hikâyelerinin çoğu Apple ID + 2FA cehenneminde geçer: CI'da her koşuda gelen doğrulama kodu, süresi biten session token'lar. Bunların hiçbirini yaşamadım, çünkü baştan App Store Connect API key kullandım.
ASC'den bir .p8 özel anahtar indiriyorsun (bir Key ID ve Issuer ID ile birlikte). fastlane bununla her istek için kısa ömürlü, ES256 imzalı bir JWT üretiyor. Apple ID yok, parola yok, 2FA yok.
def asc_key
app_store_connect_api_key(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
key_filepath: ENV.fetch("ASC_KEY_PATH"),
duration: 1200,
in_house: false
)
end
Anahtarın kendisi repoya girmiyor: değerler git-ignore'lu bir .env'de, .p8 dosyası repo dışında. Bu ayrım önemli — anahtar sızarsa hesabına yazma erişimi sızar.

Metadata artık dosya
deliver'ın (yani upload_to_app_store) en güzel yanı: metadata bir web formu değil, bir klasör ağacı.
fastlane/
metadata/
tr/
description.txt
keywords.txt
...
en-US/
description.txt
...
screenshots/
tr/
01-today.png
...
Açıklamayı bir metin editöründe yazıyorsun, git'e commit'liyorsun, fastlane metadata diyorsun. Diff'lenebilir, geri alınabilir, PR'da review edilebilir. Bir daha web formuna metin yapıştırmıyorsun.
Lane'ler ihtiyaca göre bölünmüş:
| lane | ne yapar |
|---|---|
metadata | sadece metin metadata |
screenshots | sadece ekran görüntüleri (üstüne yazar) |
store | metadata + screenshots (binary yok, submit yok) |
build_upload | flutter build ipa + binary'yi ASC'ye yükler |
release | incelemeye gönderir |
Binary: Flutter'dan IPA'ya
Vird SwiftPM-only (CocoaPods yok). fastlane bunu hiç umursamadı — çünkü binary'yi Flutter üretiyor, fastlane sadece yüklüyor:
lane :build_upload do
Dir.chdir("../..") do
sh("flutter", "build", "ipa", "--release", "--export-method", "app-store")
end
upload_to_app_store(
api_key: asc_key,
ipa: "../../build/ios/ipa/vird.ipa",
skip_metadata: true,
skip_screenshots: true
)
end
İncelemeye göndermek
Asıl "gönder" lane'i, submit'in yanında Apple'ın sorduğu üç beyanı da programatik dolduruyor — o web formundaki radio button'ları:
lane :release do
upload_to_app_store(
api_key: asc_key,
app_version: "1.0.0",
build_number: "3",
skip_binary_upload: true,
skip_metadata: true,
skip_screenshots: true,
submit_for_review: true,
automatic_release: false, # onaydan sonra elle yayınla
submission_information: {
add_id_info_uses_idfa: false,
export_compliance_uses_encryption: false,
content_rights_contains_third_party_content: false
}
)
end
automatic_release: false bilinçli: Apple onaylasın, yayın anını ben seçeyim.
Spaceship: ASC'ye soru sormak
deliver yazma tarafı; okuma tarafı Spaceship (fastlane'in ASC API sarmalayıcısı). Hangi build hangi durumda, versiyon "Waiting for Review" mı — bunu bir status lane'iyle sorguladım:
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
filepath: ENV.fetch("ASC_KEY_PATH")
)
app = Spaceship::ConnectAPI::App.find("com.yemrealpak.vird")
Yol boyunca çarptığım duvarlar

Kurulum "temiz" görünür ama gerçek her zaman biraz kanar. Not düştüklerim:
1. Uygulama adı çakışması. İlk metadata koşumda deliver patladı: "The app name you entered is already being used." "Vird" ismi alınmıştı. Çözüm: metadata/'dan name.txt'yi tamamen çıkardım — adı ASC'de elle yönetiyorum, deliver artık ona dokunmuyor. Metadata'da olmayan alan, metadata'nın ezemeyeceği alandır.
2. Ekran görüntüsü boyutu. deliver 1320×2868'i reddetti: "Invalid screen size." deliver'ın validator'ı yalnızca eski 6.9" boyutunu (1290×2796) tanıyordu. Görselleri width→1290'a küçültüp 2796'ya ortadan kırptım, geçti.
3. rexml/gem çakışması. ASC'yi bağımsız bir script'ten sorgulamak istedim; require 'spaceship' bir gem sürüm çatışmasıyla (rexml) patladı. Çözüm: script'i bağımsız çalıştırmayı bıraktım, onu fastlane'in kendi gem ortamında koşan bir status lane'ine taşıdım. Aracın ortamının içinde kal, dışında değil.
4. Token mı Hash mi? En sinsi olanı. app_store_connect_api_key bir Hash döndürür — deliver'ın api_key: parametresi tam da bunu ister. Ama doğrudan Spaceship çağrısı bir Token nesnesi ister. İkisini karıştırınca undefined method 'expired?' for Hash aldım. Aynı .p8, iki farklı sarmalama: deliver için Hash, Spaceship için Token.create.
Geriye kalan
Şimdi Vird'in bir sonraki sürümü tek zincir: build_upload → store → release. Metadata git'te, ekran görüntüleri git'te, beyanlar kodda. Bir string'i yanlış yazma ihtimalim yok, çünkü artık string'i bir kez yazıp commit'liyorum.

fastlane'in bana öğrettiği şey aslında fastlane'le ilgili değil: elle yapılan her tekrarlanan iş, sessizce bir hata bekler. Üçüncü submission'da bunu gördüm. Dördüncüsü bir komuttu.