CI/CD dla aplikacji mobilnej: testy, podpisywanie i publikacja releasów

0
50
3/5 - (3 votes)

Nawigacja:

Cel wdrożenia CI/CD dla aplikacji mobilnej

Intencją jest zbudowanie stabilnego, powtarzalnego pipeline’u CI/CD dla aplikacji mobilnej (Android i iOS), który obejmuje pełen przepływ: build, testy, podpisywanie, dystrybucję i publikację releasów. Chodzi o ograniczenie pracy ręcznej, błędów przy wydaniach i skrócenie czasu między zmianą w kodzie a nową wersją w sklepie.

Słowa kluczowe pomocnicze: pipeline CI/CD dla aplikacji mobilnej, automatyzacja buildów Android i iOS, testy jednostkowe i UI w mobile, podpisywanie aplikacji kluczami iOS Android, zarządzanie certyfikatami i provisioning profiles, automatyczna publikacja w Google Play i App Store, Fastlane Bitrise GitHub Actions, strategie wersjonowania i tagowania releasów, bezpieczeństwo sekretów w CI/CD, rollout staged release i beta testing

Dlaczego CI/CD w aplikacjach mobilnych wygląda inaczej niż w webie

Cykl publikacji i zależność od sklepów

W aplikacjach webowych wdrożenie to często proste wypchnięcie artefaktów na serwer lub do chmury. Zmiany są widoczne po kilku minutach, a rollback można wykonać niemal natychmiast. W mobile cały proces zależy od sklepów: Google Play i App Store.

Każda wersja aplikacji musi zostać zbudowana jako binarka (APK/AAB dla Androida, IPA dla iOS) i umieszczona w sklepie. W iOS dochodzi do tego review przez Apple, które potrafi potrwać od kilkudziesięciu minut do kilku dni. Zmienia to sposób myślenia o CI/CD: pipeline kończy się nie tylko deployem, ale także submission do sklepu i monitorowaniem statusu.

Google Play jest bardziej elastyczny, ale również wymaga odpowiedniego podpisania, konfiguracji tracków (internal, alpha, beta, production) i polityk. Automatyzacja tych kroków to jeden z głównych zysków z dobrze ustawionego pipeline’u CI/CD dla aplikacji mobilnej.

Buildy zależne od środowiska i kanałów dystrybucji

Aplikacje mobilne rzadko mają jeden prosty wariant builda. Częściej spotykane są różne konfiguracje:

  • środowiska: dev, stage, prod,
  • flavors / product flavors / build variants (Android),
  • schemes / configurations (iOS),
  • osobne pakiety dla klientów B2B, white-label, brandowane wersje.

Do tego dochodzą różne kanały dystrybucji: wewnętrzne (Enterprise, MDM), beta (TestFlight, Google Play internal testing), produkcja. Każdy kanał wymaga innych ustawień podpisywania, innych identyfikatorów pakietu, a czasem innych uprawnień.

Bez CI/CD większość zespołów utrzymuje to ręcznie w Android Studio i Xcode, co jest podatne na błędy. Pipeline CI/CD dla aplikacji mobilnej musi umieć zbudować i podpisać właściwy wariant bez udziału człowieka, na podstawie gałęzi, taga lub parametru joba.

Konsekwencje błędów wydań w mobile

Błąd przy wydaniu aplikacji webowej można naprawić szybko, wdrażając kolejną wersję. W mobile rollback jest dużo droższy. Jeśli wypuszczona wersja iOS zawiesza się przy starcie, a review kolejnego builda potrwa dzień lub dwa, użytkownicy zostają z popsutą aplikacją.

Nawet w Google Play, gdzie można stosować phased rollout, wypuszczenie krytycznie błędnej wersji na duży odsetek użytkowników oznacza słabe oceny, zwiększony churn i dodatkową presję na zespół. Co gorsza, jeśli błędnie podpiszesz aplikację lub zmienisz klucz, nie da się już zrobić update’u tej samej aplikacji – trzeba wypuszczać nową pozycję w sklepie.

To wszystko zwiększa wagę każdego releasu i uzasadnia inwestycję w solidny pipeline: z rozbudowanymi testami, automatycznym wersjonowaniem, kontrolą jakości i precyzyjną obsługą podpisywania.

Automatyzacja zamiast ręcznego „klikania” w IDE

Typowy ręczny proces wydania mobilnego release’u wygląda tak:

  • odpal Xcode/Android Studio,
  • przełącz scheme/flavor,
  • zaktualizuj numer wersji i build number,
  • zadbaj, by wybrać właściwy certyfikat/profil/keystore,
  • zbuduj archiwum,
  • wyeksportuj IPA/APK/AAB,
  • zaloguj się do konta dewelopera,
  • wgraj plik, wypełnij metadane, zapisz zmiany.

Każdy z tych kroków da się zautomatyzować. Pipeline CI/CD dla aplikacji mobilnej powinien wymagać jednego działania: push do odpowiedniej gałęzi, utworzenie taga albo kliknięcie „Run workflow”. Reszta dzieje się sama, w sposób powtarzalny, z logami i historią każdego releasu.

Telefon z ekranem instalacji aplikacji WHO Info na drewnianym stole
Źródło: Pexels | Autor: Markus Winkler

Fundamenty CI/CD dla mobile – kluczowe pojęcia i architektura

Rozdzielenie Continuous Integration i Continuous Delivery

W kontekście aplikacji mobilnych dobrze jest jasno oddzielić dwa etapy:

  • CI (Continuous Integration) – reaguje na każdy commit lub pull request,
  • CD (Continuous Delivery/Deployment) – uruchamia się rzadziej, np. na merge do main, na utworzenie taga lub ręczne wyzwolenie releasu.

Pipeline CI obejmuje:

  • checkout kodu,
  • instalację zależności,
  • kompilację w trybie debug lub „test release”,
  • testy jednostkowe i wybrane testy integracyjne/UITest,
  • statyczną analizę (lint, detekt, SwiftLint, etc.),
  • publikację raportów (testy, coverage, lint).

Pipeline CD skupia się na:

  • budowie releasowego artefaktu (release, beta, internal),
  • podpisaniu aplikacji właściwymi kluczami,
  • uploadzie do sklepu lub narzędzia dystrybucyjnego,
  • ewentualnej automatycznej publikacji/rolloucie.

To rozdzielenie pozwala szybko zweryfikować jakość każdej zmiany bez marnowania zasobów na pełne release buildy za każdym razem.

Typowe etapy pipeline’u CI/CD dla aplikacji mobilnej

Niezależnie od narzędzia (Jenkins, GitHub Actions, GitLab CI, Bitrise, CircleCI, Azure DevOps) pipeline zwykle składa się z tych samych kroków:

  1. Checkout – pobranie kodu z repozytorium, czasem z submodułami.
  2. Przygotowanie środowiska – instalacja SDK, narzędzi linii komend, konfiguracja Javy, Xcode, Ruby/Node do narzędzi pomocniczych.
  3. Instalacja zależności – Gradle/Maven dla Androida, CocoaPods/Swift Package Manager/Carthage dla iOS, narzędzia do testów.
  4. Build – kompilacja aplikacji w wybranej konfiguracji.
  5. Testy – jednostkowe, UI, integracyjne, urządzenia w chmurze.
  6. Podpisywanie – użycie keystore (Android) lub certyfikatów/provisioning profiles (iOS).
  7. Publikacja artefaktów – paczki binarne, raporty, logi, mapy symboli (dSYM/proguard mapping).
  8. Dystrybucja – wysłanie builda do sklepu, na TestFlight, Firebase App Distribution, wewnętrznego portalu czy MDM.

Ułożenie tych etapów i warunków ich wywołania zależy od strategii releasowej i wielkości zespołu, ale architektura jest podobna.

Typy buildów w aplikacjach mobilnych

W praktyce używa się kilku kategorii buildów:

  • Debug – dla deweloperów, z pełnym loggingiem, dodatkowymi narzędziami, często z wyłączonym obfuscation.
  • Internal – build testowy dla zespołu i QA, zbliżony do produkcji, ale z dodatkowymi funkcjami debugowania (np. menu developerskie, możliwość zmiany backendu).
  • Beta – build dla beta testerów, zwykle podpisany już produkcyjnymi kluczami, ale dystrybuowany na ograniczoną skalę (TestFlight, Google Play beta).
  • Production release – finalny build idący do użytkowników końcowych.

Pipeline CI/CD powinien mieć jasno zdefiniowane, które joby tworzą które buildy, jak są one nazywane, wersjonowane i w jakie kanały dystrybucji trafiają.

Przegląd narzędzi CI/CD dla mobile

Najczęściej używane platformy do CI/CD w projektach mobilnych:

  • Jenkins – elastyczny, self-hosted, wymaga własnej infrastruktury; dobra opcja przy dużych wymaganiach i istniejącej platformie.
  • GitHub Actions – blisko repozytorium, dobre integrowanie z GitHubem, wsparcie macOS (ważne dla iOS), bogaty marketplace akcji.
  • GitLab CI – wbudowany w GitLaba, obsługa self-hosted runnerów, pipeline jako kod w .gitlab-ci.yml.
  • Bitrise – platforma wyspecjalizowana w mobile, gotowe kroki do Androida i iOS, prosty onboarding.
  • CircleCI – popularny SaaS z obsługą macOS, elastyczną konfiguracją pipeline’ów.
  • Azure DevOps – dobre przy integracji z ekosystemem Microsoftu, ma wsparcie dla macOS agentów.

Przy wyborze narzędzia pod CI/CD dla aplikacji mobilnej kluczowe są:

  • dostępność macOS runnerów (iOS),
  • łatwość integracji ze sklepami (API, gotowe taski/akcje),
  • koszty i limity minut buildowych,
  • obługa sekretów (certyfikaty, klucze, hasła),
  • łatwość skalowania i utrzymania.

Przygotowanie repozytorium i projektu pod CI/CD mobilne

Struktura repozytorium: mono vs multi repo

Przy projektach z wersjami na Androida i iOS pojawia się pytanie: jedno repo czy dwa?

  • Monorepo – jedno repo zawiera /android i /ios, czasem także wspólny kod (np. w projektach multiplatformowych, React Native, Flutter). Ułatwia zarządzanie issue i releasami, ale komplikuje pipeline’y (trzeba wykrywać, który projekt się zmienił).
  • Multirepo – osobne repo dla Androida i iOS. Czystszy podział, prostsze pipeline’y, ale trudniejsze koordynowanie releasów cross-platformowych.

Na starcie mniejsze zespoły często wybierają monorepo, bo łatwiej utrzymać wspólny workflow. Istotne jest wyraźne rozdzielenie konfiguracji w drzewie katalogów i stosowanie osobnych plików konfiguracyjnych CI dla Androida i iOS (osobne joby, osobne workflow).

Pliki konfiguracyjne i skrypty pomocnicze

Dobrze przygotowane repozytorium ułatwia przeniesienie procesu buildowania z lokalnych maszyn do CI. W praktyce powoduje to kilka decyzji:

  • Android:
    • utrzymanie konfiguracji w build.gradle (lub build.gradle.kts),
    • skrypty shellowe/Gradle taski do local buildów (np. ./gradlew assembleDevDebug),
    • plik gradle.properties z rozdzieleniem lokalnych i CI-sekretnych ustawień.
  • iOS:
    • ustrukturyzowany .xcodeproj / .xcworkspace,
    • schemes skonfigurowane jako „Shared” (udostępnione w repo),
    • skrypty do buildów w CLI (xcodebuild lub Fastlane).

Dodatkowo opłaca się dodać pliki:

  • Fastfile (Fastlane) – definiuje lane’y „build_beta”, „build_release”, „upload_to_play_store” itd.,
  • konfiguracje CI: .github/workflows/*.yml, .gitlab-ci.yml, konfiguracje Jenkinsfile lub pliki Bitrise.

Kluczowe komendy do lokalnego builda powinny być opisane w README, tak by każdy deweloper i CI wykonywali ten sam zestaw komend.

Rozdzielenie konfiguracji środowisk

Rozdzielenie dev/stage/prod jest w mobile często realizowane inaczej niż w backendzie. Typowe techniki to:

  • Android: productFlavors, buildTypes, pliki google-services.json, BuildConfig, różne applicationId per flavor.
  • iOS: różne schemy, konfiguracje Debug/Release, różne pliki .xcconfig, inne bundle identifier per środowisko.

W pipeline CI/CD dla aplikacji mobilnej każda konfiguracja powinna być możliwa do zbudowania jednym poleceniem, np.:

  • ./gradlew assembleStageRelease,
  • xcodebuild -scheme MyAppStage -configuration Release ….

Dobre praktyki:

  • nie trzymać sekretów (API key, client secret) w repozytorium, nawet w plikach konfiguracyjnych,
  • używać placeholderów i wstrzykiwać wartości przez zmienne środowiskowe w CI,
  • trzymać wspólną logikę w jednym miejscu i różnicować tylko to, co konieczne.

Wersjonowanie kodu i releasów

Strategia wersjonowania i numeracji buildów

Mobile wymusza konkretną politykę wersjonowania, bo sklepy walidują numery wersji i buildów. Trzeba spiąć to z pipeline’em CI.

  • Android:
    • versionName – wersja widoczna dla użytkownika (np. 2.3.0).
    • versionCode – rosnąca liczba całkowita, unikalna dla każdego builda w sklepie.
  • iOS:
    • CFBundleShortVersionString – wersja marketingowa (np. 2.3.0).
    • CFBundleVersion – build number, musi rosnąć w obrębie tej samej wersji.

Praktyczny schemat:

  • semver (MAJOR.MINOR.PATCH) dla wersji użytkowej, ustalanej ręcznie przy release,
  • build number generowany automatycznie w CI na bazie numeru builda, daty lub liczby commitów (np. BUILD_ID z CI).

Dla Androida versionCode można wyliczać w Gradle, np. na podstawie MAJOR * 10000 + MINOR * 100 + PATCH albo wprost z build number. Na iOS build number jest zwykle wstrzykiwany przez Fastlane lub skrypt agvtool.

Ważne, aby proces ustawiania wersji był powtarzalny:

  • branch release: wersja ustalana ręcznie, build number automatycznie,
  • branch develop/feature: dopisek -SNAPSHOT lub -beta w wersji użytkowej, build number także z CI.

Numer wersji powinien być też odzwierciedlony w tagach w repozytorium, np. v2.3.0. To bardzo ułatwia odtwarzanie konkretnych releasów i rollback.

Zbliżenie ekranu smartfona z aplikacją Reddit i ikoną powiadomienia
Źródło: Pexels | Autor: Brett Jordan

Pipeline CI – build i testy dla Androida

Minimalny pipeline dla Androida

Najprostszy, ale użyteczny pipeline Androida na każdy pull request obejmuje:

  • ./gradlew clean,
  • ./gradlew assembleDebug lub konkretne flavor,
  • ./gradlew testDebugUnitTest,
  • ./gradlew lint oraz np. ./gradlew detekt,
  • upload raportów testów i lint do CI (JUnit XML, HTML).

Build release w CI opłaca się wykonywać dopiero na merge do main lub przy tagu. Wtedy pipeline może dodać kroki podpisywania i uploadu.

Konfiguracja Gradle pod CI

Gradle wspiera rozdział konfiguracji lokalnej i CI poprzez gradle.properties i zmienne środowiskowe. Typowy zestaw:

  • org.gradle.daemon=false w CI (często wstępnie wyłączone),
  • org.gradle.configureondemand=true,
  • org.gradle.parallel=true,
  • cache ~/.gradle/caches między buildami (jeśli runner na to pozwala).

Sekrety, jak hasła keystore, klucze do API, nie powinny trafić do gradle.properties w repo. Zamiast tego:

  • definiowanie System.getenv("KEYSTORE_PASSWORD") w build.gradle,
  • ustawianie zmiennych w panelu CI (sekrety),
  • opcjonalnie użycie local.properties tylko lokalnie (ignorowany w git).

Przykładowy workflow Androida (GitHub Actions)

Konfiguracja minimalnego workflow dla Androida może wyglądać tak (skrótowo):

name: Android CI

on:
  pull_request:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

      - name: Build Debug
        run: ./gradlew assembleDebug --stacktrace

      - name: Unit tests
        run: ./gradlew testDebugUnitTest

      - name: Lint
        run: ./gradlew lintDebug

Na etapie releasu wystarczy dodać kolejne joby: assembleRelease, podpisywanie i upload.

Pipeline CI – build i testy dla iOS

Specyfika buildów iOS w CI

iOS wymusza wykorzystanie macOS i Xcode, a to zwykle najdroższy element infrastruktury CI. Trzeba unikać marnowania czasu buildowego.

Dla każdego PR typowy zestaw kroków to:

  • xcodebuild -scheme MyApp -configuration Debug -sdk iphonesimulator – build na symulator,
  • uruchomienie testów xcodebuild test z -destination 'platform=iOS Simulator,...',
  • analiza statyczna (SwiftLint, SonarQube, inne).

Buildy na urządzenia fizyczne i podpisywanie distribution/AdHoc zostają na pipeline release’owy.

Wymagane elementy projektu pod CI

Bez kilku rzeczy Xcode nie zbuduje projektu na CI:

  • schemy oznaczone jako Shared (zapisane w repo),
  • stabilne użycie menedżera zależności (CocoaPods/SPM/Carthage),
  • brak odwołań do lokalnych plików spoza projektu (np. obrazków z pulpitu dewelopera).

Dla CocoaPods typowy krok w CI to:

bundle exec pod install --repo-update

Warto spinąć wersje gemów (Fastlane, CocoaPods) przez Gemfile, aby CI używał tej samej wersji narzędzi co lokalnie.

Przykładowy job iOS (GitLab CI)

stages:
  - test

ios_tests:
  stage: test
  tags:
    - macos
  script:
    - bundle install
    - bundle exec pod install --repo-update
    - xcodebuild 
        -workspace MyApp.xcworkspace 
        -scheme MyApp 
        -sdk iphonesimulator 
        -destination 'platform=iOS Simulator,name=iPhone 14' 
        clean test
  artifacts:
    when: always
    paths:
      - build/reports

W praktyce taki job można rozbić na osobne kroki: instalacja zależności, build, testy, raporty.

Smartfon z ikonami aplikacji społecznościowych w zbliżeniu
Źródło: Pexels | Autor: Sanket Mishra

Testy w CI/CD dla aplikacji mobilnych – strategia i praktyka

Warstwy testów w mobile

Żeby pipeline był szybki i dawał sensowny feedback, testy muszą być uporządkowane warstwowo:

  • testy jednostkowe – logika biznesowa, bez UI, kilkadziesiąt sekund,
  • testy integracyjne – współpraca modułów, prosty networking, bazy lokalne,
  • testy UI – end-to-end na emulatorach/symulatorach lub urządzeniach w chmurze.

Na każdy PR wystarczy pełny zestaw testów jednostkowych i ograniczony UI (np. smoke test kilku głównych ścieżek). Pełny pakiet regresyjny UI można odpalać cyklicznie (np. nocą) lub na release branch.

Narzędzia testowe – Android

Dla Androida zestaw jest w miarę standardowy:

  • JUnit / Kotest – testy jednostkowe,
  • Robolectric – testy zbliżone do urządzenia, ale w JVM,
  • Espresso / UI Automator – testy UI na emulatorze,
  • urządzenia w chmurze: Firebase Test Lab, BrowserStack, Saucelabs.

W CI można stosować prostą matrycę: kilka konfiguracji emulatora (wersje Androida, rozdzielczości). Testy UI warto dzielić na grupy (tagi, pakiety) i uruchamiać różne grupy w różnych jobach.

Narzędzia testowe – iOS

Na iOS podstawą jest XCTest i testy UI przez XCUITest. W projektach bardziej rozbudowanych:

  • Snapshot testing (np. iOSSnapshotTestCase),
  • urządzenia w chmurze (BrowserStack, Bitrise, inne).

Podział na zestawy testów można zrobić przez osobne schemy lub konfiguracje targets. Pipeline testowy wybiera wtedy odpowiedni scheme, np. MyAppUnitTests i MyAppUITests.

Urządzenia w chmurze i stabilność testów

Testy UI na urządzeniach zewnętrznych są podatne na flaky. Praktyka:

  • idempotentne scenariusze (brak zależności od danych, clock freeze),
  • retry wybranych testów (np. do 2 razy),
  • oznaczanie flaków i wyłączanie ich z krytycznego smoke seta, dopóki nie zostaną naprawione.

W pipeline często wydziela się osobny job „UI tests cloud”, który może failować nieblokująco dla zwykłych PR, ale jest obowiązkowy dla branchy releasowych.

Raporty i feedback dla zespołu

Sam fakt uruchamiania testów nie wystarczy. Potrzebny jest szybki wgląd w wyniki.

  • JUnit XML/HTML – automatyczne raportowanie w Jenkinsie, GitLabie czy Actions,
  • coverage (JaCoCo, Slather) – minimalne progi pokrycia,
  • screenshots i logi z testów UI jako artefakty (szczególnie dla flaków).

W wielu zespołach dopuszcza się merge tylko wtedy, gdy pipeline jest zielony. Dla długich testów UI można wprowadzić soft-fail, ale trzeba jasno określić zasady, żeby nie ignorować problemów.

Podpisywanie aplikacji Android – keystore, konfiguracja, automatyzacja

Podstawy podpisywania Androida

Android wymaga podpisania każdej APK/AAB kluczem prywatnym. Dla produkcji to musi być ten sam klucz przez całe życie aplikacji (inaczej nie będzie aktualizacji).

Wyróżnia się:

  • debug keystore – generowany automatycznie, używany lokalnie,
  • release keystore – trzymany w bezpiecznym miejscu, używany do releasów.

Keystore zawiera alias(y) i hasła. Po utracie klucza releasowego nie da się wypuścić aktualizacji istniejącej aplikacji.

Konfiguracja signingConfigs w Gradle

Konfiguracja podpisywania w build.gradle zwykle wygląda tak:

android {
    signingConfigs {
        release {
            storeFile file(System.getenv("ANDROID_KEYSTORE_PATH"))
            storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD")
            keyAlias System.getenv("ANDROID_KEY_ALIAS")
            keyPassword System.getenv("ANDROID_KEY_PASSWORD")
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

W CI plik keystore jest dostarczany jako artefakt lub tworzony z base64 ze zmiennej środowiskowej. Przykład kroku w GitHub Actions:

echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > app/keystore.jks

Path keystore (np. app/keystore.jks) jest następnie przekazywany przez ANDROID_KEYSTORE_PATH.

Google Play App Signing i klucz upload

Google Play wprowadził App Signing, gdzie Google przechowuje główny klucz, a deweloper używa tzw. publicznego upload key. To bezpieczniejszy wariant.

  • klucz uploadowy jest w keystore trzymanym po stronie zespołu,
  • Google wewnętrznie przepisuje podpis na klucz główny przy dystrybucji do użytkowników.

W pipeline nie zmienia to wiele – nadal trzeba podpisać AAB, ale ryzyko utraty „klucza życia” aplikacji przenosi się na Google. Konfiguracja w Gradle jest podobna, zmienia się tylko używany keystore.

Automatyczne podpisywanie dla różnych środowisk

Często istnieją osobne aplikacje (inne applicationId) dla dev/stage/prod, każda z własnym keystore. Wtedy:

  • dla każdego flavor osobne signingConfig,
  • osobne sekrety w CI: ANDROID_KEYSTORE_DEV_BASE64, ANDROID_KEYSTORE_PROD_BASE64 itd.,
  • branch protection: użycie produkcyjnych kluczy tylko na releasowych pipeline’ach.

Dla buildów internal/beta niekiedy używa się innego klucza niż produkcyjny. Wymaga to osobnej aplikacji w Google Play, ale separuje testowe dystrybucje od produkcji.

Podpisywanie aplikacji iOS – certyfikaty, provisioning profiles, Fastlane match

Elementy ekosystemu podpisywania iOS

iOS ma kilka elementów, które muszą ze sobą pasować:

  • certyfikat (Development, Distribution) – powiązany z kontem Apple Developer i prywatnym kluczem,
  • provisioning profile – łączy app id (bundle identifier), certyfikat i listę urządzeń (dla Development/AdHoc),
  • App ID – identyfikator aplikacji w Apple, np. com.example.myapp.

Typy certyfikatów i profili w praktyce CI

W środowisku CI zwykle używa się minimalnego zestawu elementów:

  • Apple Distribution – do buildów App Store / TestFlight,
  • Apple Development – jeśli pipeline buduje appki na fizyczne urządzenia testowe,
  • App Store profile – dla releasów,
  • Ad Hoc / Development profile – dla wewnętrznych buildów na konkretne urządzenia.

CI nie powinno generować nowych certyfikatów przy każdym jobie. Zespół techniczny przygotowuje jeden zestaw i automatyzuje jego dystrybucję.

Ręczne zarządzanie podpisywaniem a CI

Ręczne pobieranie profili z Apple Developer i klikanie w Xcode działa lokalnie, ale rozwala się na CI:

  • profile wygasają,
  • ktoś usunie certyfikat i trzeba generować nowy,
  • pipeline przestaje się budować w losowym momencie.

Docelowo wszystkie certyfikaty i profile powinny być odtwarzalne z repozytorium i skryptów, bez udziału Xcode UI.

Fastlane match – założenia

match rozwiązuje problem współdzielenia certyfikatów w zespole i na CI:

  • tworzy jeden zestaw certyfikatów i profili na konto,
  • przechowuje je zaszyfrowane w repo (git),
  • na każdym środowisku (dev, CI) pobiera i instaluje je automatycznie.

Zamiast „każdy deweloper ma swój certyfikat”, jest jeden wspólny, co ułatwia odtwarzanie środowiska i zmniejsza liczbę limitów Apple.

Konfiguracja match – repo i szyfrowanie

Na początku potrzebne jest osobne repozytorium dla profili, dostępne tylko dla zaufanych osób i CI.

# Fastfile
default_platform(:ios)

platform :ios do
  desc "Setup signing"
  lane :setup_signing do
    match(
      type: "appstore",         # lub "development", "adhoc", "enterprise"
      git_url: "git@github.com:org/certificates.git",
      git_branch: "main",
      storage_mode: "git"
    )
  end
end

match szyfruje pliki za pomocą hasła (MATCH_PASSWORD), które trafia do CI jako sekret. Bez tego hasła repo z certyfikatami jest bezużyteczne.

Integracja match z pipeline CI

Typowy fragment joba iOS w CI:

bundle exec fastlane match appstore 
  --readonly 
  --git_url "git@github.com:org/certificates.git"

Opcja --readonly blokuje przypadkowe tworzenie nowych certyfikatów z CI. Po instalacji profili standardowy krok builda wygląda jak wcześniej, xcodebuild korzysta z aktualnych profili obecnych w systemie.

Automatyczne odświeżanie profili

Profile wygasają, dodawane są nowe capabilities. Bez automatyzacji wszystko rozjeżdża się w czasie.

  • cykliczny lane w Fastlane (np. uruchamiany ręcznie raz na jakiś czas),
  • komenda match nuke w ostateczności – wtedy generowany jest świeży zestaw profili.
lane :renew_profiles do
  match(type: "appstore", force_for_new_devices: true)
end

W mniejszych zespołach pojedyncza osoba odpowiada za „higienę” profili i uruchamia takie lane przy zmianach w capabilities.

Różne konfiguracje podpisywania dla środowisk

Przy wielu bundle id (np. dev, stage, prod) match obsługuje wszystkie warianty:

  • osobne app_identifier w Matchfile lub lane,
  • różne typy profili (development/ad-hoc/appstore) dla tego samego bundle id,
  • powiązanie z konfiguracjami Xcode (Debug/Release, inne).
lane :sign_staging do
  match(
    type: "adhoc",
    app_identifier: ["com.example.myapp.staging"]
  )
end

Pipeline wybiera odpowiedni lane w zależności od brancha i celu (build na urządzenia QA vs build do TestFlight).

Bezpieczeństwo kluczy i dostęp do konta Apple

Przy większych zespołach dostęp do Apple Developer jest ograniczany do kilku osób, CI działa na tokenach lub hasłach „serwisowych”.

  • Apple ID z włączonym 2FA i app-specific password dla Fastlane,
  • sekrety przechowywane w bezpiecznym store CI (GitHub Secrets, GitLab Variables, Vault),
  • brak wrzucania .p12 i profili do głównego repo kodu.

Jeśli CI używa Xcode API (App Store Connect API key), jeszcze prościej: klucz API jako JSON/plik P8 jest szyfrowany i przechowywany w sekretnych zmiennych.

Typowe błędy podpisywania na CI

Problemy z podpisywaniem często objawiają się mało czytelnymi komunikatami xcodebuild.

  • „No provisioning profile found” – profil nie został zainstalowany lub nie pasuje do bundle id.
  • „Code signing is required for product type 'Application’” – konfiguracja Xcode ma włączone podpisywanie, ale nie wskazano poprawnego team/profilu.
  • „No signing certificate 'Apple Distribution’ found” – certyfikat nie jest zainstalowany w keychain CI lub nazwa teamu nie pasuje.

W praktyce przydaje się osobny lane „diagnostic”, który odpala samo match i listę profili, bez builda, żeby szybko zweryfikować konfigurację.

Podpisywanie i uploading IPA w jednym kroku

Najbardziej stabilne pipeline’y łączą podpisywanie i wysyłkę do App Store / TestFlight w jednym, powtarzalnym kroku.

lane :beta do
  match(type: "appstore")
  build_ios_app(
    scheme: "MyApp",
    export_method: "app-store"
  )
  upload_to_testflight
end

CI uruchamia tylko bundle exec fastlane beta, a wszystkie szczegóły podpisywania i wysyłki są ukryte w lane. To zmniejsza ryzyko, że ktoś zmieni pojedynczą flagę w komendzie xcodebuild i rozbije podpisywanie.

Release pipeline Android – artefakty i promocja

Po podpisaniu AAB trzeba go dostarczyć do Google Play i kontrolować jego cykl życia.

  • generowanie AAB/APK jako artefaktu pipeline’u,
  • przekazanie go do osobnego joba „deploy”,
  • wydawanie na odpowiedni track (internal, alpha, beta, production).
./gradlew :app:bundleRelease

bundle exec fastlane supply 
  --aab app/build/outputs/bundle/release/app-release.aab 
  --track internal 
  --skip_upload_metadata true 
  --skip_upload_images true

Metadane (opis, screenshoty) zwykle trzymane są osobno i zmieniane rzadziej, dlatego często pomija się ich automatyczny upload w pipeline’ach codziennych.

Release pipeline iOS – dystrybucja przez TestFlight i App Store

Na iOS release zwykle przechodzi przez TestFlight, a dopiero potem trafia do App Store Production.

  • lane typu beta dla TestFlight,
  • lane typu release dla App Store, z możliwością ręcznego zatwierdzenia.
lane :release do
  match(type: "appstore")
  build_ios_app(
    scheme: "MyApp",
    export_method: "app-store"
  )
  upload_to_app_store(
    submit_for_review: false,
    automatic_release: false
  )
end

Decyzja o wypuszczeniu wersji produkcyjnej może zostać spięta z manualnym approvalem w CI (np. „Deploy to production?” w GitLab/Jenkins).

Rozdzielenie pipeline’u build/test od pipeline’u release

Skuteczna praktyka to wydzielenie dwóch klas pipeline’ów:

  • CI – szybki feedback na PR: build + testy + proste artefakty, bez podpisywania release’owego,
  • CD – uruchamiany z tagów/branchy releasowych: podpisywanie, wysyłka do sklepów, zmiany wersji.

To ogranicza liczbę miejsc, w których używane są wrażliwe klucze, i przyspiesza zwykłe pipeline’y deweloperskie.

Automatyczne wersjonowanie buildu

Manualna zmiana numeru wersji w build.gradle czy Info.plist szybko prowadzi do chaosu.

  • inkrementacja numeru builda na podstawie numeru pipeline’u / commitu,
  • numer wersji aplikacji definiowany w tagu git (np. v1.2.3).
lane :set_build_number do
  build_number = ENV["CI_PIPELINE_IID"] || Time.now.to_i
  increment_build_number(build_number: build_number)
end

Na Androidzie podobny efekt da się uzyskać przez modyfikację versionCode w Gradle przed buildem, np. na podstawie zmiennej środowiskowej.

Obsługa wielu flavorów i targetów w jednym repo

Gdy aplikacja ma wiele brandów lub klientów, pipeline szybko się komplikuje. Da się to jednak opanować bez duplikacji konfiguracji.

  • flavors w Gradle (Android) i targets/scheme w Xcode (iOS),
  • matryca jobów w CI (np. brand=A, brand=B),
  • odpowiednie mapowanie na bundle id / applicationId i profile.

Przykład: parametryzowany job, w którym zmienna APP_FLAVOR decyduje, który scheme/flavor i jakie klucze są używane. Jeden plik CI, wiele wariantów aplikacji.

Rollout i kontrola jakości releasów mobilnych

Jedna z przewag sklepów mobilnych to możliwość stopniowego rollout’u nowej wersji.

  • wydanie na internal track / TestFlight dla zespołu,
  • potem rollout na wybrany procent użytkowników,
  • monitoring crashy (Crashlytics, Sentry) i logów.

Pipeline może automatyzować pierwsze dwa kroki, ale decyzję o zwiększeniu procentu rollout’u pozostawia zespołowi po analizie metryk.

Integracja CI/CD z narzędziami produktowymi

Releasy aplikacji mobilnych rzadko są jedynie technicznym wydarzeniem. Warto powiązać pipeline z narzędziami produktowymi.

  • automatyczne wpisy w kanale releasowym (Slack, Teams),
  • linki do changelogów generowanych na podstawie commitów / PR,
  • oznaczanie ticketów w Jirze / Linearze przy wydaniu.

Prosty webhook lub skrypt Fastlane generujący krótką notkę o releasie często wystarcza, żeby zespół produktowy wiedział, co faktycznie trafiło do sklepu.

Najczęściej zadawane pytania (FAQ)

Jak zaprojektować pipeline CI/CD dla aplikacji mobilnej (Android i iOS)?

Typowy pipeline dzieli się na dwa główne etapy: CI (dla każdego commitu/PR) oraz CD (dla releasów, tagów i wydań na sklepy). CI skupia się na szybkim buildzie, testach i analizie statycznej, CD na stabilnym buildzie releasowym, podpisaniu i dystrybucji.

Praktyczne minimum to: checkout kodu, instalacja SDK i zależności, build debug/test, testy jednostkowe i UI, lint, a potem osobny workflow dla buildów release/beta z podpisywaniem i publikacją do Google Play / App Store / TestFlight.

Jak zautomatyzować podpisywanie aplikacji Android i iOS w CI/CD?

Na Androidzie w pipeline używa się keystore, hasła i aliasu przechowywanych jako sekrety w systemie CI. Gradle można skonfigurować tak, by podpisywał build release na podstawie zmiennych środowiskowych ustawianych w jobie.

Na iOS potrzebne są certyfikaty oraz provisioning profiles. Najczęściej stosuje się Fastlane (match, cert, sigh) lub natywne mechanizmy systemu CI, które importują profile i certyfikaty przed buildem. Klucze i profile trzyma się zaszyfrowane (np. w storage Fastlane lub w secret store w CI).

Jakie typy buildów mobile warto mieć w pipeline CI/CD?

W praktyce używa się czterech głównych typów: debug (dla deweloperów), internal (dla zespołu i QA), beta (dla testerów zewnętrznych) oraz production release (dla użytkowników końcowych). Różnią się konfiguracją, poziomem logowania i kanałem dystrybucji.

Dobrze, gdy każdy typ jest jasno powiązany z konkretnym jobem/gałęzią: np. pull request → build debug + testy, merge do develop → internal, tag vX.Y.Z → beta/production w odpowiednim tracku sklepu.

Jak automatycznie publikować aplikację w Google Play i App Store?

Do Google Play najczęściej używa się Fastlane (supply) lub gotowych kroków w Bitrise/GitHub Actions, które wrzucają AAB/APK na wybrany track (internal, alpha, beta, production). Klucze serwisowe Google Play Console przechowuje się jako sekrety w CI.

W przypadku App Store proces obsługuje Fastlane (deliver, pilot) lub natywne akcje CI, które wysyłają IPA do TestFlight i dalej na produkcję. Trzeba skonfigurować App Store Connect API key i ustalić, czy publikacja ma być automatyczna, czy reviewer w App Store Connect ręcznie ją zatwierdza.

Jak ustawić wersjonowanie i tagowanie releasów w CI/CD dla mobile?

Najprostszy schemat to semver (major.minor.patch) jako numer wersji, a build number inkrementowany automatycznie przez pipeline. Tagi w repozytorium (np. v1.3.0) mogą wyzwalać job releasowy, który ustawia numer wersji w projektach Android/iOS.

Często stosuje się generowanie build number z numeru joba CI lub z timestampu, żeby było unikalne dla sklepów. Numer wersji z taga ułatwia śledzenie, który commit trafił do konkretnego releasu w Google Play / App Store.

Jak bezpiecznie przechowywać klucze i certyfikaty do mobile CI/CD?

Hasła do keystore, certyfikaty i profile provisioning nie powinny trafiać do repozytorium. Trzyma się je w secret store systemu CI (GitHub Secrets, GitLab CI variables, Bitrise secrets) lub w dedykowanym narzędziu typu HashiCorp Vault.

Dodatkowo można użyć Fastlane match, który przechowuje certyfikaty w zaszyfrowanym repozytorium. Dostęp do sekretów warto ograniczyć tylko do jobów releasowych i osób odpowiedzialnych za wydania.

Czym różni się CI/CD dla aplikacji mobilnych od webowych?

W mobile pipeline kończy się binarką (APK/AAB/IPA) oraz submission do sklepu, często z review po stronie Apple. Rollback jest wolny i kosztowny, więc nacisk na testy, podpisywanie i kontrolę jakości jest dużo większy niż w webie.

Dochodzi też zarządzanie wieloma wariantami (flavors, schemes), kanałami dystrybucji i kluczami. Bez automatyzacji każdy release to ręczne klikanie w Xcode/Android Studio, co szybko prowadzi do błędów i chaosu przy kilku środowiskach.

Najważniejsze wnioski

  • Pipeline CI/CD dla aplikacji mobilnych musi obejmować pełny przepływ: build, testy, podpisywanie, dystrybucję i submission do sklepów, aby ograniczyć ręczną pracę i ryzyko błędów przy releasach.
  • Publikacja w Google Play i App Store, z ich review, trackami (internal/beta/production) i wymaganiami dot. podpisywania, sprawia, że mobile CI/CD kończy się dopiero na poprawnym złożeniu i monitorowaniu wersji w sklepie, a nie tylko na deployu artefaktu.
  • Różne środowiska (dev/stage/prod), flavors/schemes i kanały dystrybucji (beta, produkcja, Enterprise) wymagają od pipeline’u automatycznego wyboru właściwej konfiguracji i kluczy na podstawie gałęzi, taga lub parametru joba.
  • Błędy releasów mobilnych są kosztowne – rollback trwa długo, a pomyłka w podpisywaniu może zablokować aktualizacje danej aplikacji, więc testy, kontrola jakości i poprawne zarządzanie certyfikatami są krytyczne.
  • Automatyzacja zastępuje ręczne „klikanie” w Xcode/Android Studio (zmiana wersji, wybór profili, eksport IPA/APK/AAB, upload do sklepu) jednym spójnym workflowem wyzwalanym pushem lub tagiem.
  • Rozdzielenie CI (szybka weryfikacja każdej zmiany: build, testy, lint, raporty) od CD (rzadsze buildy releasowe, podpisywanie, upload, rollout) pozwala oszczędzać zasoby i utrzymać wysoką jakość releasów.
  • Bibliografia

  • Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation. Addison-Wesley Professional (2010) – Podstawy CI/CD, praktyki automatyzacji buildów i releasów
  • Continuous Integration: Improving Software Quality and Reducing Risk. Addison-Wesley Professional (2007) – Definicje i wzorce CI, rozdzielenie CI od CD
  • Accelerate: The Science of Lean Software and DevOps. IT Revolution Press (2018) – Wpływ CI/CD i DevOps na jakość, stabilność i czas dostarczania
  • Android Developers – App Signing. Google – Zasady podpisywania APK/AAB, klucze, konsekwencje zmiany klucza
  • App Store Connect Help – Distribute apps. Apple – Proces uploadu IPA, TestFlight, review i publikacja w App Store
  • GitHub Actions Documentation – CI/CD for mobile apps. GitHub – Przykłady workflow CI/CD dla Androida i iOS w GitHub Actions
  • Bitrise Documentation – Android and iOS CI/CD pipelines. Bitrise – Gotowe kroki pipeline’u, build, testy, podpisywanie i dystrybucja