Aplikacja iOS / od pomysłu do telefonuPrompt dla asystenta AI ↓

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.

Część 2 z 3 · 9 września 2026 · macOS / Windows / Linux

Gotowy materiał do przejścia samodzielnie lub z AI.

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

ElementPo co jest potrzebny
Klucz prywatny + CSRNa 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łoPrzenosi 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.

Przykłady poniżej są fikcyjne: Jan Kowalski, Testowa Apka, repozytorium 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

  1. Otwórz Dostęp do pęku kluczy / Keychain Access przez Spotlight. Wybierz pęk Logowanie.
  2. W menu aplikacji wybierz Asystent certyfikatów → Poproś o certyfikat z urzędu certyfikacji (Certificate Assistant → Request a Certificate From a Certificate Authority).
  3. 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.
  4. 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

  1. Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
  2. Kliknij +, zaznacz Apple Distribution i kliknij Continue.
  3. Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
  4. Kliknij Download. Zapisz distribution.cer w katalogu z kluczem/CSR.

Do Apple przesyłasz CSR. Prywatny klucz zostaje u Ciebie. Instrukcja CSR Apple.

Import i eksport .p12

  1. Otwórz pobrany distribution.cer dwuklikiem na tym samym Macu, na którym powstał CSR.
  2. W Logowanie → Moje certyfikaty wyszukaj Apple Distribution. Rozwiń strzałkę przy certyfikacie. Pod nim musi być klucz prywatny.
  3. Zaznacz certyfikat z powiązanym kluczem, wybierz eksport z menu kontekstowego lub Plik → Eksportuj rzeczy.
  4. Wybierz format Wymiana danych osobistych (.p12), nazwę AppleDistribution.p12 i zapisz na Biurku.
  5. 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.

Apple Distribution z rozwiniętym kluczem prywatnym. Dane przykładowe.
Apple Distribution z rozwiniętym kluczem prywatnym. Dane przykładowe.
Eksport certyfikatu i klucza do AppleDistribution.p12.
Eksport certyfikatu i klucza do AppleDistribution.p12.
Hasło chroniące eksport .p12. Zapisz je do późniejszego użycia w CI.
Hasło chroniące eksport .p12. Zapisz je do późniejszego użycia w CI.
Czy ustawiać „Zawsze ufaj”? Podczas przygotowania materiału lokalny certyfikat pokazywał „nie jest zaufany”. Pozostawiliśmy domyślne ustawienia zaufania. Wysyłka przez CI i instalacja z TestFlight udały się bez ręcznego „Zawsze ufaj”. Runner importuje certyfikat pośredni Apple WWDR G3 i sprawdza poprawną tożsamość podpisującą. To wynik tego scenariusza, a nie zalecenie ignorowania dowolnych błędów zaufania.
Ścieżka Windows została opisana na podstawie dokumentacji OpenSSL. Nie wykonywaliśmy jej na Windowsie podczas tego tutorialu. Udany test całego procesu wykonano z .p12 wyeksportowanym na macOS.

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 version

Jeż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=1

Ostatnia 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

  1. Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
  2. Kliknij +, zaznacz Apple Distribution i kliknij Continue.
  3. Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
  4. Kliknij Download. Zapisz distribution.cer w 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.p12

Przy 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.p12

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

Ścieżka Linux została opisana na podstawie dokumentacji OpenSSL i wymaga sprawdzenia w Twoim środowisku. Nie zastępuje runnera macOS potrzebnego do kompilacji aplikacji iOS.

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 077

W 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

  1. Otwórz Apple Developer → Certificates i wybierz właściwy zespół.
  2. Kliknij +, zaznacz Apple Distribution i kliknij Continue.
  3. Wybierz Choose File i wskaż utworzony plik CSR. Wyślij go przez Continue.
  4. Kliknij Download. Zapisz distribution.cer w 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.p12

Przy 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.p12

Wynik 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

  1. Otwórz Apple Developer → Profiles i kliknij +.
  2. W sekcji Distribution zaznacz App Store Connect i kliknij Continue. Nie wybieraj iOS App Development ani Ad Hoc.
  3. Wybierz App ID swojej aplikacji. Bundle ID musi być identyczny jak w kodzie i rekordzie App Store Connect.
  4. Wybierz utworzony certyfikat Apple Distribution, którego klucz masz w .p12. Kliknij Continue.
  5. Podaj nazwę, np. TestowaApkaAppStore, kliknij Generate, a potem Download.
  6. Zapisz plik jako TestowaApkaAppStore.mobileprovision, np. na Biurku na Macu albo w katalogu testflight-signing na 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.

Distribution → App Store Connect.
Distribution → App Store Connect.
Wybór App ID aplikacji.
Wybór App ID aplikacji.
Wybór certyfikatu Distribution odpowiadającego plikowi .p12.
Wybór certyfikatu Distribution odpowiadającego plikowi .p12.
Nadaj własną nazwę i kliknij Generate. Dane na ilustracji ukryto.
Nadaj własną nazwę i kliknij Generate. Dane na ilustracji ukryto.
Gotowy profil App Store: kliknij Download.
Gotowy profil App Store: kliknij Download.

4. Klucz API do wysyłania buildów

  1. Otwórz App Store Connect → Users and Access → Integrations → App Store Connect API.
  2. 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.
  3. Wpisz nazwę GitHub TestFlight. W Access wybierz Developer i kliknij Generate.
  4. Kliknij Download API Key i potwierdź Download. Zachowaj plik AuthKey_XXXXXXXXXX.p8. Można pobrać go tylko raz.
  5. 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.

Generate API Key: nazwa GitHub TestFlight, następnie Access → Developer.
Generate API Key: nazwa GitHub TestFlight, następnie Access → Developer.
Team Keys: Key ID i Issuer ID to dwa różne identyfikatory.
Team Keys: Key ID i Issuer ID to dwa różne identyfikatory.
Plik .p8 można pobrać tylko raz.
Plik .p8 można pobrać tylko raz.

5. GitHub: cztery Variables

W repozytorium otwórz Settings → Secrets and variables → Actions → Variables → New repository variable. Nazwy przepisz dokładnie.

NazwaPrzykładowa wartośćSkąd ją wziąć
XCODE_PROJECTTestowaApkaWspólna nazwa projektu i schematu Xcode, bez .xcodeproj.
APPLE_TEAM_IDA1B2C3D4E5Team ID z konta Apple Developer, 10 znaków.
ASC_KEY_IDK8M2N4P6Q9Key ID klucza API, również występuje w nazwie .p8.
ASC_ISSUER_IDa7c29e41-6b83-4fd2-9a15-08e3b6c742d9Issuer 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.

Formularz New variable. Nazwy użytkownika i repozytorium są przykładowe.
Formularz New variable. Nazwy użytkownika i repozytorium są przykładowe.

Repository, Environment czy Organization?

ZakresZastosowanie
RepositoryUstawienia jednego repozytorium. Tego używa gotowy workflow.
EnvironmentOsobne ustawienia np. staging i production w obrębie jednego repozytorium. Job musi wskazywać np. environment: testflight.
OrganizationWspó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.

NazwaCo wkleić
DISTRIBUTION_CERTIFICATE_P12_BASE64Cały plik AppleDistribution.p12 zakodowany do Base64.
DISTRIBUTION_CERTIFICATE_PASSWORDHasło ustawione podczas eksportu .p12, zwykły tekst.
APP_STORE_PROVISIONING_PROFILE_BASE64Cały plik .mobileprovision zakodowany do Base64.
ASC_PRIVATE_KEYPełna treść .p8 jako tekst PEM, bez Base64.
New secret: wpisz dokładną nazwę i odpowiednią wartość, następnie Add secret.
New secret: wpisz dokładną nazwę i odpowiednią wartość, następnie Add secret.

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 | pbcopy
base64 -i ~/Desktop/TestowaApkaAppStore.mobileprovision | pbcopy
cat ~/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-Clipboard
Get-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.txt

Otwó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:

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"
          fi
Pokaż 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]

  1. 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.
  2. Przejdź do Actions → Build and upload to TestFlight → Run workflow.
  3. Wybierz gałąź z gotowym kodem i ustawieniami, np. main, a potem kliknij zielony Run workflow.
  4. 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.

Nie opieramy instrukcji na niepotwierdzonej liczbie „30 uploadów dziennie”. Jeśli Apple zwróci limit wysyłek, zastosuj wskazany w komunikacie okres oczekiwania. Nie uruchamiaj workflow wielokrotnie tylko dlatego, że build nadal ma status Processing.

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.

Nowy build 0.1 (74) jest Processing, poprzedni 0.1 (73) ma Complete.
Nowy build 0.1 (74) jest Processing, poprzedni 0.1 (73) ma Complete.

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.

TestFlight przed pierwszym zaproszeniem do testowania.
TestFlight przed pierwszym zaproszeniem do testowania.

Jako właściciel konta możesz testować własną aplikację przez grupę wewnętrzną. Nie potrzebujesz publicznego linku ani zewnętrznej grupy.

  1. W App Store Connect otwórz swoją aplikację i zakładkę TestFlight.
  2. Kliknij + obok Internal Testing.
  3. Wpisz nazwę grupy Internal Testers.
  4. Zaznacz Enable automatic distribution i kliknij Create. Kolejne kwalifikujące się buildy z naszego workflow będą automatycznie trafiały do tej grupy.
TestFlight: przycisk + obok Internal Testing.
TestFlight: przycisk + obok Internal Testing.
Utworzenie grupy Internal Testers z automatyczną dystrybucją.
Utworzenie grupy Internal Testers z automatyczną dystrybucją.

Nie myl automatycznej dystrybucji do grupy z automatycznym instalowaniem aktualizacji na iPhonie. To dwa osobne ustawienia.

11. Dodaj siebie i przyjmij zaproszenie

  1. Otwórz Internal Testers → Testers i kliknij + lub Invite Testers.
  2. Zaznacz swoje konto App Store Connect i kliknij Add.
  3. 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.
  4. Na iPhonie otwórz e-mail z zaproszeniem i kliknij View in TestFlight.
  5. Zaakceptuj zaproszenie w aplikacji TestFlight.
Grupa ma 0 testerów i 1 build. Kliknij + przy Testers.
Grupa ma 0 testerów i 1 build. Kliknij + przy Testers.
Zaznacz własne konto i wybierz Add.
Zaznacz własne konto i wybierz Add.
Przykładowy e-mail zapraszający do Testowa Apka. Kliknij View in TestFlight.
Przykładowy e-mail zapraszający do Testowa Apka. Kliknij View in 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

  1. Zapisz kolejną zmianę w kodzie.
  2. Uruchom nowy workflow ręcznie albo wykonaj push commita ze znacznikiem [testflight].
  3. Poczekaj na zielony workflow, następnie na przetworzenie nowego buildu przez Apple.
  4. Po udostępnieniu buildu grupie otwórz TestFlight na iPhonie. Przy aplikacji zobaczysz Uaktualnij. Naciśnij go.
Nowy build 0.1 (74): Uaktualnij zamiast Otwórz.
Nowy build 0.1 (74): Uaktualnij zamiast Otwórz.

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.

Powiadomienia e-mail wyłączone; push i automatyczne aktualizacje włączone.
Powiadomienia e-mail wyłączone; push i automatyczne aktualizacje włączone.
Gotowe. Masz działającą instalację i aktualizacje przez TestFlight. Certyfikat i profil są ponownie używane przez CI, a klucz API ma rolę Developer. Kolejny niezależny temat to instalacja przez kabel.

14. Gdy coś się zatrzyma

ObjawCo sprawdzić
Brak klucza prywatnego pod certyfikatemCzy CSR i klucz powstały na tym komputerze? Wyeksportuj .p12 z maszyny, która ma klucz.
Błąd hasła .p12Użyj hasła eksportu, nie hasła Apple ID ani pęku Logowanie.
No valid Apple Distribution identityPara certyfikat-klucz, ważność certyfikatu, zgodność z profilem i łańcuch WWDR.
Profile does not match Bundle IDPorównaj Bundle ID w kodzie, profilu i App Store Connect.
Brak projektu lub schematuXCODE_PROJECT musi odpowiadać projektowi i schematowi wygenerowanemu przez project.yml.
Niepełny ASC_PRIVATE_KEYPełna treść .p8, ze znacznikami BEGIN/END PRIVATE KEY, bez Base64.
Błąd uwierzytelniania APIKey ID musi odpowiadać .p8; sprawdź Issuer ID, aktywność klucza i zespół.
Build Complete, brak aktualizacjiSprawdź grupę Builds, testerów, przyjęte zaproszenie i wymagania compliance.
Numer buildu był już użytyUruchom nowy workflow; Re-run zachowuje GITHUB_RUN_NUMBER.
Długi ProcessingSprawdź 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.

Pobierz prompt.txt
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

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

  1. 1. Nowa aplikacja w App Store Connect
  2. 2. TestFlight
  3. 3. Instalacja przez kabel z Maca

Zrzuty ekranu zostały zanonimizowane. Przykładowe nazwy i identyfikatory zastępują dane autora; niektóre fragmenty ilustracji odtworzono podczas anonimizacji.