Przejdź do głównej zawartości

Wydanie aplikacji iOS (TestFlight)

Aplikacja iOS AcePark trafia do testerów przez TestFlight. Do sierpnia 2026 robił to workflow GitHub Actions deploy-ios-pipeline.yml na runnerze macos-26. Został usunięty — buildy iOS na hostowanych runnerach macOS liczą się z mnożnikiem 10× minut, a każdy push do ios/ uruchamiał pełny deploy niezależnie od tego, czy było co wypuszczać.

Zastąpił go skrypt scripts/ios-release.sh, uruchamiany ręcznie na Macu dewelopera. Wykonuje dokładnie te same kroki co pipeline, na tym samym Xcode 26, i wypuszcza tę samą paczkę.

👤 Instrukcja

Jednorazowa konfiguracja

  1. Xcode 26 zainstalowany i wybrany (xcodebuild -version musi pokazać 26.x). Jeżeli masz kilka wersji, wskaż właściwą zmienną DEVELOPER_DIR:

    export DEVELOPER_DIR=/Applications/Xcode_26.app/Contents/Developer
  2. Certyfikat i profil. Skrypt korzysta z tego, co masz już w pęku kluczy: certyfikatu Apple Distribution (Xcode ➔ Settings ➔ Accounts ➔ Manage Certificates) i profilu AcePark App Store (pobierz z App Store Connect i otwórz dwuklikiem). Nie musisz nic eksportować.

  3. Fastlanebrew install fastlane. Systemowy Ruby (2.6) nie uciągnie wersji z ios/App/Gemfile, więc skrypt korzysta z instalacji Homebrew.

  4. Klucz App Store Connect — plik .p8 w ~/.apple-keys/, tak samo jak przy logowaniu przez Apple. Klucz pobiera się z App Store Connect ➔ Users and Access ➔ Integrations ➔ App Store Connect API, tylko raz — jeżeli plik przepadł, trzeba wygenerować nowy klucz.

  5. Plik z danymi dostępowymi ~/.acepark/ios-release.env (poza repozytorium, więc nie ma jak go przypadkiem zacommitować):

    APP_STORE_CONNECT_API_KEY_ID=XXXXXXXXXX
    APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

    Ścieżka do klucza domyślnie to ~/.apple-keys/AuthKey_<KEY_ID>.p8; inną ustawisz przez APP_STORE_CONNECT_API_KEY_PATH.

  6. Sprawdzenie, że wszystko gra — bez budowania czegokolwiek:

    yarn ios:release --check

    Skrypt weryfikuje klucz, wersję Xcode, certyfikat, profil i Fastlane, a przy każdym braku mówi wprost, czego brakuje i jak to uzupełnić.

Wypuszczenie wersji

yarn ios:release

Skrypt przechodzi kolejno przez instalację zależności, cap sync ios, build Release przez Fastlane i wysyłkę do TestFlight. Trwa to kilkanaście minut, a przetwarzanie po stronie Apple kolejne kilka — postęp widać w App Store Connect ➔ TestFlight.

Numeru wersji ani numeru buildu nie podajesz — skrypt odczytuje je z App Store Connect i sam wybiera następne. Szczegóły w sekcji technicznej niżej.

Przydatne warianty

PolecenieKiedy
yarn ios:release --checkWeryfikacja konfiguracji, bez budowania
yarn ios:release --skip-installSzybki ponowny build, gdy node_modules są aktualne
yarn ios:release --skip-syncZmiana tylko w natywnym kodzie ios/, bez ruszania Capacitora
yarn ios:release --build-onlyChcesz samą paczkę .ipa, bez wysyłki
yarn ios:release --upload-onlyBuild się udał, wysyłka padła — powtórz samą wysyłkę
yarn ios:release --build-number 1234Numer buildu narzucony ręcznie, zamiast z App Store Connect

yarn ios:release --help wypisuje pełną listę opcji.

🔧 Dokumentacja techniczna

Przebieg

Kroki D–J to jeden do jednego kroki starego workflow. Różnice wynikają wyłącznie z tego, że skrypt działa na stałej maszynie, a nie na wyrzucanej VM:

Pipeline (runner jednorazowy)Skrypt lokalny
sudo xcode-select -s na znaleziony Xcode 26Tylko weryfikacja wersji; wybór przez DEVELOPER_DIR, bez sudo
Import .p12 do tymczasowego pęku kluczy i security list-keychain -sDomyślnie certyfikat z login.keychain; import tylko awaryjnie i z dopisaniem do listy pęków, nie nadpisaniem
Profil kopiowany do ~/Library/MobileDevice/Provisioning ProfilesUżywany profil już zainstalowany; wgrany awaryjnie jest kasowany na wyjściu
ruby/setup-ruby@v1 + bundle installFastlane z bundlera, jeśli bundle check przechodzi; w przeciwnym razie ten z Homebrew
Numer buildu z GITHUB_RUN_NUMBERNumer buildu i wersja odczytane z App Store Connect (Fastfile zmieniony)
Sekrety z GitHub Secrets~/.acepark/ios-release.env + ~/.apple-keys/*.p8

Adres, na który celuje aplikacja

capacitor.config.ts bierze APP_BASE_URL z .env.local, a na maszynie dewelopera jest tam jego własny host tunelu. Build zrobiony bez zabezpieczenia trafia więc do TestFlight wskazując na czyjś laptop — apka działa tylko wtedy, gdy tunel stoi, a logowanie przez Google odsyła na localhost, bo lokalny yarn dev:client ustawia BETTER_AUTH_URL=http://localhost:3002.

Skrypt uruchamia npx cap sync ios z jawnym APP_BASE_URL=$RELEASE_APP_BASE_URL (domyślnie https://klient.acepark.pl), co ma pierwszeństwo przed .env.local — dotenv nie nadpisuje zmiennych już obecnych w środowisku. Dodatkowo server.url jest sprawdzany dwa razy: w capacitor.config.json po syncu i w gotowym AcePark.ipa tuż przed wysyłką, więc --skip-sync i --upload-only też nie przepuszczą pliku celującego w zły host.

Build pod inny adres (np. własny tunel) wymaga świadomego nadpisania:

RELEASE_APP_BASE_URL=https://daniel.acepark.pl yarn ios:release --build-only

Wersja i numer buildu

Obie liczby ustala Fastfile, pytając App Store Connect przez ten sam klucz API, którym potem leci wysyłka:

  • latest_testflight_build_number daje ostatni train w TestFlight i jego numer buildu — numer buildu to zawsze ten plus jeden,
  • app_store_build_number(live: true) daje wersję będącą w sprzedaży,
  • get_version_number daje wersję zapisaną w project.pbxproj.

Wysyłana wersja to nowsza z dwóch: tej z projektu i tej z TestFlight. Jeśli wypadnie równa lub niższa od wersji w sprzedaży, ostatni człon rośnie o jeden — train raz zatwierdzony jest zamknięty i Apple odrzuca do niego kolejne buildy (błędy 90062 i 90186).

App Store Connect: TestFlight 1.1 (37), on sale 1.0
Project: 1.1
Shipping 1.1 (38)

Nowe wydanie robi się więc samo. Podbicie na wyższy train wystarczy zapisać w project.pbxproj — wersja z projektu wygrywa, gdy jest wyższa niż to, co widzi TestFlight. --build-number nadal wymusza konkretny numer buildu, gdy trzeba obejść stan po stronie Apple.

Zapis do project.pbxproj jest cofany na wyjściu razem z resztą plików dotkniętych przez build (patrz niżej) — i tak nie ma czego trzymać w repo, skoro każde uruchomienie odczytuje stan na nowo z App Store Connect.

Skąd bierze się Fastlane

Pipeline instalował Ruby 3.2 akcją ruby/setup-ruby i robił bundle install na ios/App/Gemfile. Lokalnie systemowy Ruby to 2.6.10 z Bundlerem 1.17 — bundle install na tym zestawie nie przejdzie. Skrypt najpierw sprawdza bundle check; jeśli bundler ma komplet gemów, używa bundle exec fastlane, a jeśli nie — sięga po fastlane z PATH (Homebrew, obecnie 2.230.0, spełnia ~> 2.226 z Gemfile). Gemfile zostaje w repo jako zapis wymaganej wersji.

Bezpieczeństwo pęku kluczy

Krok pipeline'u security list-keychain -d user -s $KEYCHAIN_PATH zastępował listę pęków użytkownika. Na maszynie jednorazowej jest to bez znaczenia, na lokalnym Macu wyrzuciłby login.keychain z wyszukiwania i odciął hasła w pozostałych aplikacjach. Skrypt w ścieżce awaryjnej dopisuje tymczasowy pęk przed istniejące (-s "$TEMP" $ORIGINAL), a trap EXIT przywraca oryginalną listę i kasuje tymczasowy pęk także po błędzie i po Ctrl-C.

Czystość drzewa roboczego

Dwa kroki buildu modyfikują pliki śledzone przez gita:

  • npx cap sync ios regeneruje ios/App/CapApp-SPM/Package.swift, po czym sed podbija capacitor-swift-pm z 8.0.0 na 8.0.2,
  • increment_build_number we Fastfile zapisuje numer buildu w ios/App/App.xcodeproj/project.pbxproj.

Skrypt zapamiętuje, czy przed startem te pliki były czyste, i jeśli tak — cofa je przez git checkout na wyjściu. Zmiany lokalne, które miałeś w nich wcześniej, nie są ruszane.

Klucz dla altool

xcrun altool --upload-app szuka klucza .p8 w ~/.appstoreconnect/private_keys/. Skrypt kopiuje go tam z ~/.apple-keys/ przy pierwszym uruchomieniu (uprawnienia 600) i przy kolejnych już go nie rusza. Sam Fastlane dostaje ten klucz osobno, jako base64 w APP_STORE_CONNECT_API_KEY_CONTENT — Fastfile deklaruje is_key_content_base64: true.

Gdy build musi wrócić na CI

Skrypt nie zależy od niczego lokalnego poza Xcode i pękiem kluczy. Przywrócenie pipeline'u to git revert commita usuwającego deploy-ios-pipeline.yml, plus podmiana BUILD_NUMBER z powrotem na GITHUB_RUN_NUMBER — albo ustawienie BUILD_NUMBER: ${{ github.run_number }} w kroku builda.