Kod w GitHub. Aplikacja na iPhonie.
TestFlight
krok po kroku
Przygotuj podpisywanie raz. Buduj w GitHub Actions. Instaluj i aktualizuj własną aplikację na telefonie.
Ścieżka macOS oraz upload i instalacja zostały sprawdzone w praktyce. Zakładki Windows/Linux opisują przygotowanie plików przez OpenSSL. Wszystkie nazwy i identyfikatory w przykładach zastąp własnymi.
1. Co przygotowujemy i dlaczego
Na koniec zainstalujesz własną aplikację na iPhonie z TestFlight i dostarczysz jej kolejną aktualizację. Zakładamy, że masz już App ID i rekord aplikacji z tutorialu 1 oraz kod aplikacji w GitHub.
Potrzebujesz aktywnego Apple Developer Program, dostępu do App Store Connect, repozytorium z włączonym GitHub Actions i iPhone'a. TestFlight jest bezpłatną aplikacją Apple: pobierz TestFlight z App Store. Sam tester nie potrzebuje Maca ani własnego płatnego konta deweloperskiego.
Możesz pisać kod i przygotować certyfikat na Windowsie lub Linuksie. Kompilację i podpisywanie iOS wykona Mac w GitHub Actions (macos-latest). W tym wariancie nie musisz mieć własnego Maca. GitHub rozlicza użycie runnerów według planu i ustawień konta; darmowy kod tutorialu nie oznacza nieograniczonego darmowego CI.
| Element | Po co jest potrzebny |
|---|---|
| Klucz prywatny + CSR | Na komputerze tworzysz klucz i wniosek o certyfikat. Apple na podstawie CSR wystawia certyfikat. |
| Apple Distribution (.cer) | Potwierdza tożsamość zespołu, który podpisuje aplikację. Nie jest przypisany do modelu komputera ani jednego iPhone’a. |
| Plik .p12 i jego hasło | Przenosi certyfikat razem z kluczem prywatnym do runnera CI. Sam .cer nie wystarczy. |
| Profil App Store (.mobileprovision) | Łączy konkretny Bundle ID, zespół, uprawnienia aplikacji i certyfikat dystrybucyjny. |
| Klucz API App Store Connect (.p8) | Uwierzytelnia wysłanie podpisanego buildu do Apple. To inny klucz niż prywatny klucz z .p12. |
Certyfikat dystrybucyjny można ponownie wykorzystywać w aplikacjach tego samego zespołu. Profil jest przygotowany dla konkretnej aplikacji. Przekazanie własnego certyfikatu komuś z innego zespołu nie zmieni jego właściciela. Osoba publikująca na swoim koncie tworzy zasoby we własnym zespole.
Nasz workflow importuje istniejący certyfikat przy każdym uruchomieniu. Nie tworzy nowych certyfikatów w Apple przy każdym buildzie, więc nie zużywa w ten sposób limitu aktywnych certyfikatów.
jankowalski/testowa-apka, projekt i schemat TestowaApka, Bundle ID pl.jankowalski.testowaapka. Wstaw własne wartości. Screeny z danymi zostały przetworzone w celu anonimizacji i mają charakter poglądowy.2. Certyfikat: wybierz swój system
Wykonaj tylko jedną zakładkę. Każda prowadzi od CSR do gotowego AppleDistribution.p12. Potem wróć do wspólnego kroku 3. Podstawą są dokumentacja CSR oraz dokumentacja PKCS#12.
macOS: CSR w Dostępie do pęku kluczy
- Otwórz Dostęp do pęku kluczy / Keychain Access przez Spotlight. Wybierz pęk Logowanie.
- W menu aplikacji wybierz Asystent certyfikatów → Poproś o certyfikat z urzędu certyfikacji (Certificate Assistant → Request a Certificate From a Certificate Authority).
- Wpisz swój e-mail i nazwę rozpoznawczą, np.
Jan Kowalski Distribution. Pole e-mail urzędu certyfikacji pozostaw puste. Zaznacz Zapisany na dysku / Saved to disk. - Kliknij Dalej i zapisz CSR, np. na Biurku. Jeżeli pojawi się wybór parametrów klucza, użyj RSA 2048 bitów.
Wystawienie certyfikatu przez Apple
- Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
- Kliknij +, zaznacz Apple Distribution i kliknij Continue.
- Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
- Kliknij Download. Zapisz
distribution.cerw katalogu z kluczem/CSR.
Do Apple przesyłasz CSR. Prywatny klucz zostaje u Ciebie. Instrukcja CSR Apple.
Import i eksport .p12
- Otwórz pobrany
distribution.cerdwuklikiem na tym samym Macu, na którym powstał CSR. - W Logowanie → Moje certyfikaty wyszukaj
Apple Distribution. Rozwiń strzałkę przy certyfikacie. Pod nim musi być klucz prywatny. - Zaznacz certyfikat z powiązanym kluczem, wybierz eksport z menu kontekstowego lub Plik → Eksportuj rzeczy.
- Wybierz format Wymiana danych osobistych (.p12), nazwę
AppleDistribution.p12i zapisz na Biurku. - Ustaw hasło eksportu, powtórz je i zachowaj. Jeżeli macOS poprosi dodatkowo o hasło pęku kluczy, jest to osobne potwierdzenie systemowe.
Jeżeli .p12 nie jest dostępny, sprawdź, czy eksportujesz także pasujący klucz prywatny. Import samego .cer na innym komputerze nie odtworzy klucza.



Windows: przygotuj OpenSSL
Zainstaluj Git for Windows z oficjalnej strony i otwórz Git Bash. Poniższe komendy wpisuj w Git Bash, nie w PowerShell. Sprawdź dostępność:
openssl versionJeżeli polecenie nie jest znalezione, sprawdź instalację Git Bash lub zainstaluj dystrybucję OpenSSL wskazaną na liście OpenSSL. Nie pobieraj kluczy ani gotowego .p12 z internetu.
mkdir -p ~/testflight-signing
cd ~/testflight-signing
export MSYS_NO_PATHCONV=1Ostatnia linia zapobiega zamianie parametru /CN=... na ścieżkę Windows przez Git Bash. Folder znajduje się zwykle w C:\Users\TwojLogin\testflight-signing; pwd -W pokaże jego dokładną ścieżkę.
openssl genpkey -algorithm RSA -aes-256-cbc -pkeyopt rsa_keygen_bits:2048 -out distribution.key
openssl req -new -sha256 -key distribution.key -out distribution.csr -subj "/CN=Jan Kowalski Distribution/emailAddress=jan@example.com"Pierwsza komenda pyta o hasło chroniące klucz prywatny, druga o to samo hasło. W polach CN oraz emailAddress wpisz własne dane. Nie nadpisuj istniejącego klucza, dla którego wystawiono już certyfikat.
Wystawienie certyfikatu przez Apple
- Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
- Kliknij +, zaznacz Apple Distribution i kliknij Continue.
- Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
- Kliknij Download. Zapisz
distribution.cerw katalogu z kluczem/CSR.
Do Apple przesyłasz CSR. Prywatny klucz zostaje u Ciebie. Instrukcja CSR Apple.
openssl x509 -inform DER -in distribution.cer -out distribution.pem
openssl pkcs12 -export -inkey distribution.key -in distribution.pem -name "Apple Distribution" -out AppleDistribution.p12Przy eksporcie podaj hasło prywatnego klucza, a następnie nowe hasło eksportu .p12 (Export Password). To drugie trafi do DISTRIBUTION_CERTIFICATE_PASSWORD. Certyfikat .cer musi pochodzić z CSR utworzonego z tego klucza.
Sprawdź zawartość bez wypisywania klucza:
openssl pkcs12 -info -noout -in AppleDistribution.p12Wynik powinien zawierać certyfikat oraz zaszyfrowany klucz, np. Certificate bag i Shrouded Keybag. Błąd niezgodności klucza i certyfikatu oznacza, że używasz innej pary plików.
Linux: przygotuj OpenSSL
W Ubuntu lub Debianie otwórz terminal:
sudo apt update
sudo apt install openssl
openssl version
mkdir -p ~/testflight-signing
cd ~/testflight-signing
umask 077W innych dystrybucjach użyj ich menedżera pakietów. umask 077 ogranicza dostęp do nowych plików do Twojego użytkownika. Folder znajdziesz jako /home/TwojLogin/testflight-signing.
openssl genpkey -algorithm RSA -aes-256-cbc -pkeyopt rsa_keygen_bits:2048 -out distribution.key
openssl req -new -sha256 -key distribution.key -out distribution.csr -subj "/CN=Jan Kowalski Distribution/emailAddress=jan@example.com"Pierwsza komenda pyta o hasło chroniące klucz prywatny, druga o to samo hasło. W polach CN oraz emailAddress wpisz własne dane. Nie nadpisuj istniejącego klucza, dla którego wystawiono już certyfikat.
Wystawienie certyfikatu przez Apple
- Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
- Kliknij +, zaznacz Apple Distribution i kliknij Continue.
- Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
- Kliknij Download. Zapisz
distribution.cerw katalogu z kluczem/CSR.
Do Apple przesyłasz CSR. Prywatny klucz zostaje u Ciebie. Instrukcja CSR Apple.
openssl x509 -inform DER -in distribution.cer -out distribution.pem
openssl pkcs12 -export -inkey distribution.key -in distribution.pem -name "Apple Distribution" -out AppleDistribution.p12Przy eksporcie podaj hasło prywatnego klucza, a następnie nowe hasło eksportu .p12 (Export Password). To drugie trafi do DISTRIBUTION_CERTIFICATE_PASSWORD. Certyfikat .cer musi pochodzić z CSR utworzonego z tego klucza.
Sprawdź zawartość bez wypisywania klucza:
openssl pkcs12 -info -noout -in AppleDistribution.p12Wynik powinien zawierać certyfikat oraz zaszyfrowany klucz, np. Certificate bag i Shrouded Keybag. Błąd niezgodności klucza i certyfikatu oznacza, że używasz innej pary plików.
3. Wspólny krok: profil App Store
- Otwórz Apple Developer → Profiles i kliknij +.
- W sekcji Distribution zaznacz App Store Connect i kliknij Continue. Nie wybieraj iOS App Development ani Ad Hoc.
- Wybierz App ID swojej aplikacji. Bundle ID musi być identyczny jak w kodzie i rekordzie App Store Connect.
- Wybierz utworzony certyfikat Apple Distribution, którego klucz masz w .p12. Kliknij Continue.
- Podaj nazwę, np.
TestowaApkaAppStore, kliknij Generate, a potem Download. - Zapisz plik jako
TestowaApkaAppStore.mobileprovision, np. na Biurku na Macu albo w katalogutestflight-signingna Windows/Linux.
To profil dystrybucji App Store używany także przez TestFlight. Nie wpisujesz UDID iPhone’a. Dla kompilacji w GitHub Actions nie musisz instalować tego profilu na swoim komputerze.





4. Klucz API do wysyłania buildów
- Otwórz App Store Connect → Users and Access → Integrations → App Store Connect API.
- Wybierz Team Keys, a następnie Generate API Key lub +. Pierwsze użycie API może wymagać wcześniejszego uzyskania dostępu przez Account Holder.
- Wpisz nazwę
GitHub TestFlight. W Access wybierz Developer i kliknij Generate. - Kliknij Download API Key i potwierdź Download. Zachowaj plik
AuthKey_XXXXXXXXXX.p8. Można pobrać go tylko raz. - Na tej samej stronie skopiuj Key ID i Issuer ID. Za chwilę zapiszesz je w GitHub jako Variables.
Rola Developer wystarczyła w sprawdzonym tutaj workflow: gotowy .p12, profil i ręczne podpisywanie w runnerze. Nie jest to konfiguracja automatycznego wystawiania certyfikatów ani cloud signing. Uprawnienia do tworzenia klucza przez użytkownika i rola przyznana samemu kluczowi to osobne kwestie. Apple: role do uploadu.



5. GitHub: cztery Variables
W repozytorium otwórz Settings → Secrets and variables → Actions → Variables → New repository variable. Nazwy przepisz dokładnie.
| Nazwa | Przykładowa wartość | Skąd ją wziąć |
|---|---|---|
XCODE_PROJECT | TestowaApka | Wspólna nazwa projektu i schematu Xcode, bez .xcodeproj. |
APPLE_TEAM_ID | A1B2C3D4E5 | Team ID z konta Apple Developer, 10 znaków. |
ASC_KEY_ID | K8M2N4P6Q9 | Key ID klucza API, również występuje w nazwie .p8. |
ASC_ISSUER_ID | a7c29e41-6b83-4fd2-9a15-08e3b6c742d9 | Issuer ID nad listą Team Keys. To UUID, nie hash. |
Własna nazwa projektu może być inna od nazwy widocznej pod ikoną aplikacji. Workflow buduje ścieżkę ${XCODE_PROJECT}.xcodeproj, nazwę archiwum ${XCODE_PROJECT}.xcarchive i używa tej samej wartości jako schematu. Dlatego jedna zmienna wystarcza, gdy projekt i schemat nazywają się tak samo.
Przykładowy link: github.com/jankowalski/testowa-apka/settings/variables/actions. Zastąp właściciela i repozytorium swoimi danymi. To przykład adresu, nie istniejący zasób tutorialu.

Repository, Environment czy Organization?
| Zakres | Zastosowanie |
|---|---|
| Repository | Ustawienia jednego repozytorium. Tego używa gotowy workflow. |
| Environment | Osobne ustawienia np. staging i production w obrębie jednego repozytorium. Job musi wskazywać np. environment: testflight. |
| Organization | Wspólne Variables/Secrets dla wybranych repozytoriów należących do organizacji, zależnie od planu i zasad dostępu. |
Environment nie współdzieli ustawień pomiędzy repozytoriami na prywatnym profilu. Do tego służy zakres Organization. Variables zawierają zwykły tekst i nie są automatycznie maskowane jak Secrets. Dokumentacja zmiennych GitHub.
6. GitHub: cztery Secrets
Otwórz Settings → Secrets and variables → Actions → Secrets → New repository secret. Przykładowy adres: github.com/jankowalski/testowa-apka/settings/secrets/actions/new.
| Nazwa | Co wkleić |
|---|---|
DISTRIBUTION_CERTIFICATE_P12_BASE64 | Cały plik AppleDistribution.p12 zakodowany do Base64. |
DISTRIBUTION_CERTIFICATE_PASSWORD | Hasło ustawione podczas eksportu .p12, zwykły tekst. |
APP_STORE_PROVISIONING_PROFILE_BASE64 | Cały plik .mobileprovision zakodowany do Base64. |
ASC_PRIVATE_KEY | Pełna treść .p8 jako tekst PEM, bez Base64. |

macOS: kopiowanie plików do schowka
Uruchamiaj pojedynczo i po każdej komendzie wklej zawartość schowka do właściwego sekretu:
base64 -i ~/Desktop/AppleDistribution.p12 | pbcopybase64 -i ~/Desktop/TestowaApkaAppStore.mobileprovision | pbcopycat ~/Downloads/AuthKey_K8M2N4P6Q9.p8 | pbcopy
Dostosuj nazwę pliku .p8 i jego katalog. Jeśli leży na Biurku, użyj ~/Desktop/. Polecenie cat AuthKey_K8M2N4P6Q9.p8 | pbcopy zadziała, jeśli terminal jest już w folderze tego pliku.
Windows: PowerShell i schowek
Po przygotowaniu plików w Git Bash możesz otworzyć PowerShell. Poniższe polecenia są dla PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("$HOME\testflight-signing\AppleDistribution.p12")) | Set-Clipboard[Convert]::ToBase64String([IO.File]::ReadAllBytes("$HOME\testflight-signing\TestowaApkaAppStore.mobileprovision")) | Set-ClipboardGet-Content -Raw "$HOME\Downloads\AuthKey_K8M2N4P6Q9.p8" | Set-Clipboard
Linux: Base64 do pliku tekstowego
base64 -w 0 ~/testflight-signing/AppleDistribution.p12 > ~/testflight-signing/certificate-base64.txt
base64 -w 0 ~/testflight-signing/TestowaApkaAppStore.mobileprovision > ~/testflight-signing/profile-base64.txtOtwórz każdy plik tekstowy, zaznacz całość i skopiuj do właściwego sekretu. .p8 otwórz bez konwersji. Nie dodawaj plików z kluczami ani ich zakodowanych kopii do repozytorium. Base64 to kodowanie, nie szyfrowanie.
Alternatywa dla .p8: zwykły edytor tekstu
Na każdym systemie możesz otworzyć .p8 w edytorze, wybrać Zaznacz wszystko i Kopiuj. Wklej również pierwszą i ostatnią linię:
-----BEGIN PRIVATE KEY-----
...cała rzeczywista zawartość Twojego klucza...
-----END PRIVATE KEY-----Te linie nie są opcjonalnymi komentarzami. To znaczniki formatu PEM; nasz workflow odrzuci niepełny plik. Nie kopiuj przykładu z wielokropkiem i nie koduj .p8 drugi raz do Base64.
Sekrety DEVICE_CERTIFICATE_P12_BASE64, DEVICE_CERTIFICATE_PASSWORD i DEVICE_PROVISIONING_PROFILE_BASE64 dotyczą osobnego tutorialu o kablu. Nie są potrzebne temu workflow i nie trzeba ich tu tworzyć.
7. Dodaj gotowy kod workflow
W ZIP-ie są dwa pliki. Skopiuj je do repozytorium z zachowaniem ścieżek:
- code/.github/workflows/testflight.yml →
.github/workflows/testflight.yml - code/scripts/prepare-testflight-export.py →
scripts/prepare-testflight-export.py
Ten wariant jest dla projektu używającego XcodeGen, z project.yml w katalogu głównym. Projekt i schemat muszą odpowiadać XCODE_PROJECT. Przykładowo name: TestowaApka oraz target/scheme TestowaApka. Ustaw PRODUCT_BUNDLE_IDENTIFIER na Bundle ID z kroku 3. Samo dodanie zmiennej w GitHub nie zmienia nazw w kodzie ani nie tworzy aplikacji.
Jeżeli masz gotowy projekt Xcode bez XcodeGen, usuń kroki instalowania XcodeGen i generowania projektu. Dla workspace, wielu aplikacji, rozszerzeń lub Watch app potrzebna jest adaptacja. Dołączony skrypt celowo zatrzyma się przy rozszerzeniach, które wymagają osobnych profili.
Workflow tworzy tymczasowy pęk kluczy, importuje .p12 i pośredni certyfikat WWDR G3, archiwizuje aplikację, a potem podpisuje ją podczas eksportu. Skrypt porównuje profil z Bundle ID, zespołem i certyfikatem oraz sprawdza datę ważności. Na końcu wysyła build i usuwa lokalne zasoby podpisywania z runnera.
W kodzie uwzględniono przypadek, kiedy WWDR G3 jest już dołączony do .p12: ignorujemy tylko konkretny komunikat o duplikacie. Nie pomijamy wszystkich błędów importu. Nie używamy -allowProvisioningUpdates do tworzenia nowych zasobów podpisywania.
Pokaż pełny testflight.yml
name: Build and upload to TestFlight
on:
push:
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
XCODE_PROJECT: ${{ vars.XCODE_PROJECT }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
ASC_KEY_ID: ${{ vars.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ vars.ASC_ISSUER_ID }}
jobs:
testflight:
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[testflight]')
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Validate repository variables
run: |
python3 - <<'PY'
import os
import re
required = ("XCODE_PROJECT", "APPLE_TEAM_ID",
"ASC_KEY_ID", "ASC_ISSUER_ID")
missing = [name for name in required if not os.environ.get(name, "").strip()]
if missing:
raise SystemExit("Missing GitHub Actions Variables: " + ", ".join(missing))
if not re.fullmatch(r"[A-Z0-9]{10}", os.environ["APPLE_TEAM_ID"]):
raise SystemExit("APPLE_TEAM_ID must contain 10 uppercase letters or digits.")
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", os.environ["XCODE_PROJECT"]):
raise SystemExit("XCODE_PROJECT must be the project and scheme name without an extension.")
PY
- name: Install XcodeGen
run: |
# GitHub's macOS runner currently includes an untrusted aws/tap.
# It is unrelated to this project and causes Homebrew to emit a warning.
brew untap aws/tap >/dev/null 2>&1 || true
brew install xcodegen
- name: Generate Xcode project
run: xcodegen generate
- name: Prepare App Store Connect API key
env:
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
run: |
python3 - <<'PY'
import os
from pathlib import Path
key_id = os.environ["ASC_KEY_ID"].strip()
key = os.environ["ASC_PRIVATE_KEY"].strip()
# Accept either a normal multiline GitHub secret or a value pasted
# with literal \n escape sequences, and normalize line endings.
if "\\n" in key and "\n" not in key:
key = key.replace("\\n", "\n")
key = key.replace("\r\n", "\n").replace("\r", "\n").strip()
begin = "-----BEGIN PRIVATE KEY-----"
end = "-----END PRIVATE KEY-----"
if not key.startswith(begin) or not key.endswith(end):
raise SystemExit(
"ASC_PRIVATE_KEY is not a valid .p8 PEM value: "
"it must include BEGIN PRIVATE KEY and END PRIVATE KEY lines."
)
path = Path(os.environ["RUNNER_TEMP"]) / f"AuthKey_{key_id}.p8"
path.write_text(key + "\n", encoding="utf-8")
path.chmod(0o600)
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file:
env_file.write(f"ASC_KEY_PATH={path}\n")
PY
- name: Import reusable distribution signing assets
env:
DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.DISTRIBUTION_CERTIFICATE_PASSWORD }}
APP_STORE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APP_STORE_PROVISIONING_PROFILE_BASE64 }}
run: |
umask 077
: "${DISTRIBUTION_CERTIFICATE_P12_BASE64:?Missing distribution certificate secret}"
: "${DISTRIBUTION_CERTIFICATE_PASSWORD:?Missing distribution certificate password}"
: "${APP_STORE_PROVISIONING_PROFILE_BASE64:?Missing App Store profile secret}"
printf '%s' "$DISTRIBUTION_CERTIFICATE_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/distribution.p12"
printf '%s' "$APP_STORE_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$RUNNER_TEMP/app-store.mobileprovision"
openssl smime -inform der -verify -noverify \
-in "$RUNNER_TEMP/app-store.mobileprovision" \
-out "$RUNNER_TEMP/app-store-profile.plist"
KEYCHAIN_PATH="$RUNNER_TEMP/testflight.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$RUNNER_TEMP/distribution.p12" \
-P "$DISTRIBUTION_CERTIFICATE_PASSWORD" -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
# Install Apple's intermediate certificate without overriding system trust.
curl --fail --silent --show-error --location \
https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer \
-o "$RUNNER_TEMP/AppleWWDRCAG3.cer"
# A Keychain-exported .p12 may already contain this intermediate.
# Ignore only the duplicate-item error; propagate every other failure.
if import_output=$(security import "$RUNNER_TEMP/AppleWWDRCAG3.cer" -k "$KEYCHAIN_PATH" 2>&1); then
printf '%s\n' "$import_output"
else
import_status=$?
if [[ "$import_output" == *"SecKeychainItemImport: The specified item already exists in the keychain."* ]]; then
echo "Apple WWDR G3 intermediate is already installed."
else
printf '%s\n' "$import_output" >&2
exit "$import_status"
fi
fi
# Reuse the imported certificate at export; never request new signing assets.
- name: Archive without local signing
run: |
xcodebuild \
-project "${XCODE_PROJECT}.xcodeproj" \
-scheme "$XCODE_PROJECT" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/${XCODE_PROJECT}.xcarchive" \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
clean archive
- name: Validate signing profile and create manual export options
run: python3 scripts/prepare-testflight-export.py
- name: Sign with imported certificate and upload to App Store Connect
run: |
xcodebuild \
-exportArchive \
-archivePath "$RUNNER_TEMP/${XCODE_PROJECT}.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-authenticationKeyPath "$ASC_KEY_PATH" \
-authenticationKeyID "$ASC_KEY_ID" \
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
- name: Remove temporary signing assets
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/testflight.keychain-db" 2>/dev/null || true
rm -f "$RUNNER_TEMP/distribution.p12" "$RUNNER_TEMP/app-store.mobileprovision" \
"$RUNNER_TEMP/app-store-profile.plist" "$RUNNER_TEMP/AppleWWDRCAG3.cer" \
"$RUNNER_TEMP/ExportOptions.plist"
if [[ -n "$ASC_KEY_PATH" ]]; then rm -f "$ASC_KEY_PATH"; fi
if [[ -f "$RUNNER_TEMP/testflight-profile-paths.txt" ]]; then
while IFS= read -r profile_path; do rm -f "$profile_path"; done < "$RUNNER_TEMP/testflight-profile-paths.txt"
rm -f "$RUNNER_TEMP/testflight-profile-paths.txt"
fiPokaż pełny prepare-testflight-export.py
"""Validate one app's App Store profile and select an imported signing identity."""
import datetime
import hashlib
import os
from pathlib import Path
import plistlib
import re
import shutil
import subprocess
def export_options(profile, bundle_id, team_id, identities):
entitlements = profile.get("Entitlements", {})
if profile.get("TeamIdentifier") != [team_id]:
raise ValueError("App Store profile belongs to a different Apple team.")
prefixes = profile.get("ApplicationIdentifierPrefix", [])
app_id = entitlements.get("application-identifier")
if not any(app_id == prefix + "." + bundle_id for prefix in prefixes):
raise ValueError("App Store profile does not match the archived Bundle ID.")
if entitlements.get("get-task-allow", False):
raise ValueError("Use an App Store profile, not a Development profile.")
if "ProvisionedDevices" in profile or profile.get("ProvisionsAllDevices"):
raise ValueError("Use an App Store profile, not Ad Hoc or Enterprise.")
expiry = profile.get("ExpirationDate")
if not expiry or expiry.replace(tzinfo=datetime.timezone.utc) <= datetime.datetime.now(datetime.timezone.utc):
raise ValueError("App Store profile is expired or has no expiration date.")
uuid = profile.get("UUID", "")
if not re.fullmatch(r"[A-Fa-f0-9-]{36}", uuid):
raise ValueError("App Store profile has an invalid UUID.")
# find-identity only lists valid identities whose private keys are available.
valid = set(re.findall(r'([A-Fa-f0-9]{40})\s+"Apple Distribution:', identities))
matching = [hashlib.sha1(cert).hexdigest().upper()
for cert in profile.get("DeveloperCertificates", [])]
fingerprint = next((sha for sha in matching if sha in {item.upper() for item in valid}), None)
if not fingerprint:
raise ValueError("No valid Apple Distribution identity matches the profile. "
"Check the .p12, its private key, certificate expiry and WWDR trust chain.")
return {
"method": "app-store-connect",
"destination": "upload",
"signingStyle": "manual",
"signingCertificate": fingerprint,
"teamID": team_id,
"provisioningProfiles": {bundle_id: uuid},
"manageAppVersionAndBuildNumber": False,
"uploadSymbols": True,
}
def main():
temp = Path(os.environ["RUNNER_TEMP"])
archive = temp / (os.environ["XCODE_PROJECT"] + ".xcarchive")
apps = list((archive / "Products/Applications").glob("*.app"))
if len(apps) != 1:
raise ValueError("Expected one application in the Xcode archive.")
if list(apps[0].rglob("*.appex")) or (apps[0] / "Watch").exists():
raise ValueError("Extensions and Watch apps need their own provisioning profile mappings.")
with (apps[0] / "Info.plist").open("rb") as stream:
bundle_id = plistlib.load(stream)["CFBundleIdentifier"]
with (temp / "app-store-profile.plist").open("rb") as stream:
profile = plistlib.load(stream)
identities = subprocess.check_output(
["security", "find-identity", "-v", "-p", "codesigning",
str(temp / "testflight.keychain-db")], text=True)
options = export_options(profile, bundle_id, os.environ["APPLE_TEAM_ID"], identities)
# Support both older Xcode and current Xcode provisioning profile locations.
installed = []
for directory in ("Library/MobileDevice/Provisioning Profiles",
"Library/Developer/Xcode/UserData/Provisioning Profiles"):
target = Path.home() / directory / (profile["UUID"] + ".mobileprovision")
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(temp / "app-store.mobileprovision", target)
installed.append(str(target))
(temp / "testflight-profile-paths.txt").write_text("\n".join(installed) + "\n")
with (temp / "ExportOptions.plist").open("wb") as stream:
plistlib.dump(options, stream)
print("Validated App Store profile and reusable distribution identity.")
if __name__ == "__main__":
main()Kod jest udostępniony do skopiowania bez opłat. Pliki są zgodne z wariantem, na którym wykonano udany upload w tym tutorialu.
8. Uruchomienie ręczne lub przez [testflight]
- Zapisz oba pliki w repozytorium i wypchnij commit. Workflow musi być na domyślnej gałęzi, aby można było uruchomić go ręcznie w interfejsie GitHub.
- Przejdź do Actions → Build and upload to TestFlight → Run workflow.
- Wybierz gałąź z gotowym kodem i ustawieniami, np.
main, a potem kliknij zielony Run workflow. - Otwórz uruchomienie i sprawdź wynik. Zielony job oznacza zakończenie kompilacji, podpisywania i uploadu.
Automatycznie po wybranym commicie
Wpisz znacznik w wiadomości commita:
git commit -m "Popraw wygląd przycisku [testflight]"Następnie wykonaj push. Możesz też poprosić agenta AI: „Wprowadź poprawkę i dodaj [testflight] do wiadomości commita, żeby uruchomić build.” Przy kilku commitach w jednym pushu znacznik musi być w ostatnim, ponieważ warunek sprawdza head_commit.message. Przy squash merge umieść go w końcowej wiadomości squasha.
Push bez znacznika może pokazać workflow z pominiętym jobem, ale nie wykonuje kompilacji i uploadu. Domyślnie uruchamiaj je ręcznie; znacznik stosuj do zmian, które chcesz testować na telefonie. Oszczędza to minuty CI i niepotrzebne wysyłki.
Numer kompilacji pochodzi z GITHUB_RUN_NUMBER. Nowe uruchomienie workflow dostaje nowy numer; Re-run jobs tego samego uruchomienia zachowuje numer. Po udanym uploadzie uruchom nowy workflow, aby uniknąć próby ponownego wysłania tego samego numeru.
9. Poczekaj na Processing → Complete
Po zielonym workflow otwórz App Store Connect → Apps → Twoja aplikacja → TestFlight → iOS → Build Uploads. Apple przetwarza plik niezależnie od GitHub Actions.

Processing oznacza, że upload jest jeszcze przetwarzany. Po Complete build może zostać udostępniony testerom. Przy poprawnej konfiguracji grupy i spełnieniu wymagań aplikacji pojawi się w TestFlight do instalacji lub aktualizacji.
Podczas tego testu trwało to około 2-3 minut. Czas jest orientacyjny i może być dłuższy. Complete dotyczy przetworzenia uploadu, nie jest zatwierdzeniem publicznej publikacji aplikacji.
Jeśli pojawi się Missing Compliance, uzupełnij pytania dotyczące szyfrowania zgodnie z rzeczywistym działaniem aplikacji. Jeżeli build jest Failed, sprawdź szczegóły w App Store Connect i wiadomość od Apple. Jeśli jest Complete, ale nie ma go w telefonie, sprawdź grupę, dodany build i zaakceptowanie zaproszenia.
10. Zainstaluj TestFlight i utwórz grupę
Na iPhonie zainstaluj TestFlight od Apple. Przed pierwszym zaproszeniem zobaczysz pusty ekran „Wszystko gotowe”. To normalne.

Jako właściciel konta możesz testować własną aplikację przez grupę wewnętrzną. Nie potrzebujesz publicznego linku ani zewnętrznej grupy.
- W App Store Connect otwórz swoją aplikację i zakładkę TestFlight.
- Kliknij + obok Internal Testing.
- Wpisz nazwę grupy
Internal Testers. - Zaznacz Enable automatic distribution i kliknij Create. Kolejne kwalifikujące się buildy z naszego workflow będą automatycznie trafiały do tej grupy.


Nie myl automatycznej dystrybucji do grupy z automatycznym instalowaniem aktualizacji na iPhonie. To dwa osobne ustawienia.
11. Dodaj siebie i przyjmij zaproszenie
- Otwórz Internal Testers → Testers i kliknij + lub Invite Testers.
- Zaznacz swoje konto App Store Connect i kliknij Add.
- Sprawdź zakładkę Builds. Na pokazanym ekranie grupa ma już 1 Build. Jeśli u Ciebie jest pusta, wybierz Add Builds, zaznacz gotowy build, kliknij Next, uzupełnij What to Test i dodaj.
- Na iPhonie otwórz e-mail z zaproszeniem i kliknij View in TestFlight.
- Zaakceptuj zaproszenie w aplikacji TestFlight.



Tester wewnętrzny jest użytkownikiem App Store Connect z dostępem do aplikacji. Dla znajomych spoza zespołu używa się testów zewnętrznych, które mogą wymagać Beta App Review. Ten tutorial prowadzi przez test wewnętrzny. Instrukcja Apple.
12. Zainstaluj i otwórz aplikację
Na ekranie aplikacji w TestFlight naciśnij Zainstaluj. Po zakończeniu pojawi się Otwórz oraz możliwość wysłania opinii. Uruchom aplikację i sprawdź jej działanie.



Buildy TestFlight są dostępne przez ograniczony czas, na screenie 90 dni. Testowa instalacja nie oznacza publikacji aplikacji w publicznym App Store.
13. Kolejny build i powiadomienia
- Zapisz kolejną zmianę w kodzie.
- Uruchom nowy workflow ręcznie albo wykonaj push commita ze znacznikiem
[testflight]. - Poczekaj na zielony workflow, następnie na przetworzenie nowego buildu przez Apple.
- Po udostępnieniu buildu grupie otwórz TestFlight na iPhonie. Przy aplikacji zobaczysz Uaktualnij. Naciśnij go.

Wyłącz e-maile, jeśli przeszkadzają
W TestFlight otwórz szczegóły aplikacji i przewiń w dół. Wyłącz Powiadomienia e-mail. Możesz osobno pozostawić włączone powiadomienia push oraz Uaktualnienia automatyczne, tak jak na screenie. Wyłączenie e-maili nie kończy udziału w testach.

14. Gdy coś się zatrzyma
| Objaw | Co sprawdzić |
|---|---|
| Brak klucza prywatnego pod certyfikatem | Czy CSR i klucz powstały na tym komputerze? Wyeksportuj .p12 z maszyny, która ma klucz. |
| Błąd hasła .p12 | Użyj hasła eksportu, nie hasła Apple ID ani pęku Logowanie. |
| No valid Apple Distribution identity | Para certyfikat-klucz, ważność certyfikatu, zgodność z profilem i łańcuch WWDR. |
| Profile does not match Bundle ID | Porównaj Bundle ID w kodzie, profilu i App Store Connect. |
| Brak projektu lub schematu | XCODE_PROJECT musi odpowiadać projektowi i schematowi wygenerowanemu przez project.yml. |
| Niepełny ASC_PRIVATE_KEY | Pełna treść .p8, ze znacznikami BEGIN/END PRIVATE KEY, bez Base64. |
| Błąd uwierzytelniania API | Key ID musi odpowiadać .p8; sprawdź Issuer ID, aktywność klucza i zespół. |
| Build Complete, brak aktualizacji | Sprawdź grupę Builds, testerów, przyjęte zaproszenie i wymagania compliance. |
| Numer buildu był już użyty | Uruchom nowy workflow; Re-run zachowuje GITHUB_RUN_NUMBER. |
| Długi Processing | Sprawdź stan w Apple. Sam zielony workflow nie gwarantuje natychmiastowej gotowości instalacji. |
Asystent AI
Przejdź te kroki z pomocą czatu
Skopiuj prompt do nowej rozmowy i dołącz pliki kodu z paczki. Asystent będzie prowadził Cię po jednym kroku.
Pokaż pełny prompt
Materiały źródłowe do tej serii: 1. Nowa aplikacja: https://aleksanderfigiel.pl/ios-new-app-tutorial-1 2. TestFlight: https://aleksanderfigiel.pl/ios-testflight-tutorial-2 3. Instalacja przez kabel: https://aleksanderfigiel.pl/ios-macos-cable-tutorial-3 Przed rozpoczęciem otwórz i przeczytaj stronę części, której dotyczy ten prompt. Korzystaj z jej instrukcji, ilustracji i kodu. Pozostałe części traktuj jako kontekst i sięgaj do nich, gdy wymaga tego bieżący krok. Zachowaj zakres tej części i prowadź mnie po jednym kroku. Jeśli nie masz dostępu do strony lub nie została jeszcze opublikowana, powiedz o tym i poproś o jej treść albo pliki; nie udawaj, że ją przeczytałeś. Poprowadź mnie po polsku, po jednym małym kroku na wiadomość, przez TestFlight według dołączonego tutorialu 2. Celem jest pierwszy udany upload, zaproszenie mnie jako testera wewnętrznego, instalacja na iPhonie i kolejna aktualizacja. Nie publikuj aplikacji w publicznym App Store. Nie używaj znaku em dash. Zacznij od ustalenia systemu: macOS, Windows lub Linux, oraz adresu mojego repozytorium GitHub. Ustal już zarejestrowany Bundle ID i rekord aplikacji, nazwę projektu/schematu oraz używanie XcodeGen. Nie zakładaj, że przykłady TestowaApka, pl.jankowalski.testowaapka lub jankowalski/testowa-apka są moimi danymi. Zachowuj potwierdzone wartości i generuj konkretne linki https://github.com/OWNER/REPO/settings/variables/actions oraz https://github.com/OWNER/REPO/settings/secrets/actions. Nie powtarzaj pytań, gdy odpowiedź jest w kontekście. Wyjaśnij: tester potrzebuje TestFlight z https://apps.apple.com/pl/app/testflight/id899247664 i iPhone'a, nie Maca. Klucz i CSR można przygotować na każdym systemie; certyfikat wystawia Apple, a kompilację i podpisywanie wykonuje runner macOS w GitHub Actions. Wymagane są Apple Developer Program i gotowy kod aplikacji. Kieruj do tutorialu 1, jeśli brakuje App ID/rekordu. Nie twórz duplikatów zasobów. Korzystaj z odpowiedniej zakładki systemowej. macOS: Keychain Access, Certificate Assistant, CSR zapisany na dysku; Apple Distribution w portalu; pobranie .cer; import na komputerze z prywatnym kluczem; eksport certyfikatu z kluczem do .p12 z hasłem. Windows: Git Bash + OpenSSL, MSYS_NO_PATHCONV=1 dla -subj. Linux: OpenSSL. W obu utwórz zaszyfrowany klucz RSA 2048 i CSR, prześlij CSR do Apple, pobierz .cer, przekonwertuj DER do PEM i wyeksportuj .p12. Ścieżki Windows/Linux są opracowane z dokumentacji, nie deklaruj ich osobistego przetestowania. Nie zalecaj bezwarunkowego Always Trust. W naszym teście domyślne zaufanie na Macu wystarczyło, bo CI ma poprawny łańcuch WWDR. Wspólne kroki: Profiles +, Distribution > App Store Connect, właściwy App ID, właściwy Apple Distribution, nazwa i Generate, Download .mobileprovision. Nie wybieraj Development/Ad Hoc i nie żądaj UDID do TestFlight. Następnie Users and Access > Integrations > App Store Connect API > Team Keys > Generate API Key, nazwa GitHub TestFlight, Access Developer. W tym wariancie używamy ręcznego podpisywania gotowym .p12 i profilem. Pobierz .p8 tylko raz i zachowaj bezpiecznie; skopiuj Key ID i Issuer ID. Repository Variables: XCODE_PROJECT (bez rozszerzenia, wspólna nazwa projektu i schematu), APPLE_TEAM_ID, ASC_KEY_ID, ASC_ISSUER_ID. Repository Secrets: DISTRIBUTION_CERTIFICATE_P12_BASE64, DISTRIBUTION_CERTIFICATE_PASSWORD, APP_STORE_PROVISIONING_PROFILE_BASE64, ASC_PRIVATE_KEY. Nigdy nie proś mnie o przesłanie prywatnego klucza, .p12, hasła ani zawartości sekretów do czatu. Wyjaśnij różnicę Repository/Environment/Organization: Environment dotyczy jednego repozytorium i wymaga environment w jobie; współdzielenie pomiędzy repozytoriami to Organization, zależnie od planu. Na Macu pokazuj konkretne ścieżki, np. base64 -i ~/Desktop/AppleDistribution.p12 | pbcopy i base64 -i ~/Desktop/TestowaApkaAppStore.mobileprovision | pbcopy. Dopasuj nazwę do pliku użytkownika. .p8 kopiujemy jako zwykły tekst, np. cat ~/Downloads/AuthKey_KEYID.p8 | pbcopy. Alternatywa: edytor tekstu, zaznacz wszystko, kopiuj. Wyraźnie wymagaj obu linii -----BEGIN PRIVATE KEY----- i -----END PRIVATE KEY-----, to znaczniki PEM, nie opcjonalne komentarze. Dla Windows podaj PowerShell ReadAllBytes/ToBase64String/Set-Clipboard, a .p8 Get-Content -Raw; dla Linux base64 -w 0 do pliku. Base64 nie jest szyfrowaniem. Przed zmianami kodu przeczytaj AGENTS.md i istniejące workflow. Użyj obu dołączonych plików code/.github/workflows/testflight.yml oraz code/scripts/prepare-testflight-export.py. Nie pomiń helpera. Dostosuj do faktycznej struktury projektu; nie zmieniaj Bundle ID istniejącej aplikacji bez zgody. Ten wariant zakłada jedną aplikację, XcodeGen oraz wspólną nazwę projektu/schematu; rozszerzenia i Watch wymagają adaptacji. Nie twórz certyfikatów przy każdym buildzie. Kod używa manual export signing, bez -allowProvisioningUpdates. WWDR import obsługuje konkretny duplikat, inne błędy zatrzymują job. Nie wpisuj prywatnych danych w workflow i nie dodawaj plików kluczy do git. Uruchomienie: Actions > Build and upload to TestFlight > Run workflow, wybór gałęzi. Domyślnie ręcznie. Push z [testflight] w wiadomości ostatniego commita uruchamia job automatycznie, także po commicie przez agenta AI. Push bez znacznika pomija job. Nie podawaj niepotwierdzonego limitu 30 uploadów dziennie jako faktu. Nowe uruchomienie zwiększa GITHUB_RUN_NUMBER, Re-run zachowuje go. Nie uruchamiaj pętli CI i nie czekaj na statusy w pętli, jeśli AGENTS tego zabrania. Po zielonym workflow: App Store Connect > aplikacja > TestFlight > iOS > Build Uploads. Poczekaj na Processing > Complete; około 2-3 minut w naszym przykładzie, nie gwarancja. Uzupełnij compliance tylko zgodnie z prawdziwymi funkcjami aplikacji. Utwórz grupę Internal Testers z Enable automatic distribution. Dodaj własne konto przez Testers + / Invite Testers > Add; upewnij się, że grupa ma build, w razie potrzeby Add Builds. Nie każ wykonywać Beta App Review dla tego scenariusza testów wewnętrznych. Na telefonie otwórz mail > View in TestFlight > akceptacja > Zainstaluj. Po instalacji Otwórz. Przy kolejnym buildzie i Complete pojawia się Uaktualnij, o ile build jest udostępniony grupie. W szczegółach aplikacji można wyłączyć Powiadomienia e-mail, pozostawiając push i Uaktualnienia automatyczne. Grupa automatycznej dystrybucji i aktualizacje na telefonie to osobne opcje. Po każdym kroku czekaj na potwierdzenie lub screenshot; oceniaj realny stan, nie zakładaj sukcesu. Przy błędzie czytaj konkretny log, nie zmieniaj roli na Admin bez analizy. Jeżeli masz internet, weryfikuj aktualne wymagania Apple/GitHub. Na końcu podsumuj faktycznie zakończone kroki. Tutorial 3 o kablu jest osobnym tematem. Nie generuj nowego HTML podczas prowadzenia mnie, chyba że poproszę.
Źródła i zakres sprawdzenia
- Apple: CSR
- Apple: profil App Store
- Apple: klucze API
- Apple: wysyłanie buildów
- Apple: testerzy wewnętrzni
- Apple: TestFlight
- OpenSSL: req i pkcs12
- GitHub: Variables i Secrets
Przetwarzanie 2-3 minuty i udany upload z Developer to obserwacje z opisanej konfiguracji. Czas przetwarzania, interfejs i wymagania usług mogą się zmieniać. Anonimizowane ilustracje są poglądowe; nazwy przycisków i kolejność działań opisuje tekst.
Wszystkie części tutorialu iOS
Zrzuty ekranu zostały zanonimizowane. Przykładowe nazwy i identyfikatory zastępują dane autora; niektóre fragmenty ilustracji odtworzono podczas anonimizacji.