Yunus Emre Alpak

Dördüncüsü Bir Komuttu — fastlane ile App Store Otomasyonu

2026 · 07 · 128 dk okuma

İ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.

fastlane ile tek komutta App Store'a — Vird'in yayın otomasyonu

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.

.p8 özel anahtar her istekte kısa ömürlü, imzalı bir token üretir — parola yok

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üş:

lanene yapar
metadatasadece metin metadata
screenshotssadece ekran görüntüleri (üstüne yazar)
storemetadata + screenshots (binary yok, submit yok)
build_uploadflutter build ipa + binary'yi ASC'ye yükler
releaseincelemeye 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

Otomasyonun yolundaki dört duvar: isim çakışması, ekran boyutu, gem çatışması, Hash vs Token

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_uploadstorerelease. 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.

Tek zincir: build_upload, store, release — elle tıklama yok

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.