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
-
Xcode 26 zainstalowany i wybrany (
xcodebuild -versionmusi pokazać 26.x). Jeżeli masz kilka wersji, wskaż właściwą zmiennąDEVELOPER_DIR:export DEVELOPER_DIR=/Applications/Xcode_26.app/Contents/Developer -
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ć.
-
Fastlane —
brew install fastlane. Systemowy Ruby (2.6) nie uciągnie wersji zios/App/Gemfile, więc skrypt korzysta z instalacji Homebrew. -
Klucz App Store Connect — plik
.p8w~/.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. -
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=XXXXXXXXXXAPP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxŚcieżka do klucza domyślnie to
~/.apple-keys/AuthKey_<KEY_ID>.p8; inną ustawisz przezAPP_STORE_CONNECT_API_KEY_PATH. -
Sprawdzenie, że wszystko gra — bez budowania czegokolwiek:
yarn ios:release --checkSkrypt 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
| Polecenie | Kiedy |
|---|---|
yarn ios:release --check | Weryfikacja konfiguracji, bez budowania |
yarn ios:release --skip-install | Szybki ponowny build, gdy node_modules są aktualne |
yarn ios:release --skip-sync | Zmiana tylko w natywnym kodzie ios/, bez ruszania Capacitora |
yarn ios:release --build-only | Chcesz samą paczkę .ipa, bez wysyłki |
yarn ios:release --upload-only | Build się udał, wysyłka padła — powtórz samą wysyłkę |
yarn ios:release --build-number 1234 | Numer 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 26 | Tylko weryfikacja wersji; wybór przez DEVELOPER_DIR, bez sudo |
Import .p12 do tymczasowego pęku kluczy i security list-keychain -s | Domyślnie certyfikat z login.keychain; import tylko awaryjnie i z dopisaniem do listy pęków, nie nadpisaniem |
Profil kopiowany do ~/Library/MobileDevice/Provisioning Profiles | Używany profil już zainstalowany; wgrany awaryjnie jest kasowany na wyjściu |
ruby/setup-ruby@v1 + bundle install | Fastlane z bundlera, jeśli bundle check przechodzi; w przeciwnym razie ten z Homebrew |
Numer buildu z GITHUB_RUN_NUMBER | Numer 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_numberdaje 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_numberdaje wersję zapisaną wproject.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 iosregenerujeios/App/CapApp-SPM/Package.swift, po czymsedpodbijacapacitor-swift-pmz 8.0.0 na 8.0.2,increment_build_numberwe Fastfile zapisuje numer buildu wios/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.