Mengelola sertifikat signing dan profil provisioning pada tim engineering React Native sering kali menimbulkan konflik sertifikat (certificate thrashing) dan kegagalan build di mesin CI. Pendekatan manual mengharuskan setiap pengembang mengimpor file .p12 dan provisioning profile secara lokal, yang sering berujung pada pencabutan sertifikat aktif oleh rekan satu tim. Fastlane Match menyelesaikan masalah ini dengan memperlakukan code signing sebagai kode (code signing as code).

Artikel ini membahas arsitektur integrasi Fastlane Match dengan GitHub Actions untuk membangun pipeline rilis iOS (TestFlight) dan Android (Google Play Store Internal Sharing) yang sepenuhnya headless dan terprediksi.

Arsitektur Code Signing Terpusat dengan Fastlane Match

Fastlane Match mengimplementasikan filosofi deterministik: seluruh sertifikat dan profile disimpan dalam repositori Git terpisah, dienkripsi menggunakan OpenSSL (AES-256-CBC). Seluruh mesin pengembang dan runner CI mengakses sumber kebenaran tunggal yang sama secara read-only.

Konfigurasi Matchfile

Letakkan file Matchfile di direktori ios/fastlane/ untuk mengarahkan konfigurasi penyimpanan sertifikat:

git_url("[email protected]:organisasi-anda/ios-certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.organisasi.app"])
username("[email protected]")

Catatan: Di lingkungan CI, flag readonly: true wajib digunakan untuk mencegah runner menghapus atau memperbarui sertifikat yang sudah valid di repositori penyimpanan jika terjadi kesalahan resolusi konfigurasi.

Bypass Autentikasi 2FA: App Store Connect API Key

Penggunaan Apple ID standar dengan autentikasi dua faktor (2FA) tidak dapat diandalkan pada CI/CD non-interaktif karena sesi berbasis token fastlane spaceauth kadaluarsa setiap 30 hari. Solusi standar industri adalah menggunakan App Store Connect API Key (berbasis JWT, berkas .p8).

Diperlukan tiga variabel rahasia pada GitHub Secrets:

  • ASC_KEY_ID: ID Kunci 10 karakter dari App Store Connect.
  • ASC_ISSUER_ID: UUID Issuer ID dari App Store Connect.
  • ASC_KEY_CONTENT: Isi mentah berkas AuthKey_XXXXXX.p8 yang dienkode dalam Base64.

Konfigurasi Fastlane (Fastfile)

Berikut implementasi Fastfile untuk menangani isolasi keychain di macOS runner iOS dan penandatanganan Android Release AAB.

default_platform(:ios)

platform :ios do
  desc "Build dan distribusikan ke TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV["ASC_KEY_ID"],
      issuer_id: ENV["ASC_ISSUER_ID"],
      key_content: ENV["ASC_KEY_CONTENT"],
      is_key_content_base64: true,
      in_house: false
    )

    # Menghindari prompt interaktif OS dengan membuat keychain sementara
    keychain_name = "ci_keychain"
    keychain_password = ENV["KEYCHAIN_PASSWORD"]

    create_keychain(
      name: keychain_name,
      password: keychain_password,
      default_keychain: true,
      unlock: true,
      timeout: 3600,
      lock_when_sleeps: false
    )

    match(
      type: "appstore",
      readonly: true,
      keychain_name: keychain_name,
      keychain_password: keychain_password,
      api_key: api_key
    )

    update_project_provisioning(
      xcodeproj: "ios/MyApp.xcodeproj",
      profile: ENV["sigh_com.organisasi.app_appstore_profile-path"],
      target: "MyApp"
    )

    build_app(
      workspace: "ios/MyApp.xcworkspace",
      scheme: "MyApp",
      export_method: "app-store",
      output_directory: "./ios/build",
      output_name: "MyApp.ipa"
    )

    upload_to_testflight(
      api_key: api_key,
      skip_waiting_for_build_processing: true
    )

    delete_keychain(name: keychain_name)
  end
end

platform :android do
  desc "Build AAB dan upload ke Google Play Internal App Sharing"
  lane :internal do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/"
    )

    upload_to_play_store_internal_app_sharing(
      package_name: "com.organisasi.app",
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
      json_key_data: ENV["PLAY_STORE_JSON_KEY"]
    )
  end
end

Pipeline GitHub Actions

Simpan konfigurasi alur kerja berikut di .github/workflows/release.yml. Workflow memisahkan runner macOS untuk iOS dan runner Ubuntu untuk Android untuk efisiensi biaya dan kecepatan komputasi.

name: Release Pipeline

on:
  push:
    tags:
      - 'v*'

jobs:
  build-ios:
    name: iOS Build & TestFlight
    runs-on: macos-14
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true

      - name: Install JavaScript dependencies
        run: yarn install --frozen-lockfile

      - name: Cache CocoaPods
        uses: actions/cache@v4
        with:
          path: ios/Pods
          key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-pods-

      - name: Install Pods
        run: |
          cd ios
          pod install

      - name: Setup SSH for Fastlane Match
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.MATCH_DEPLOY_KEY }}

      - name: Deploy iOS to TestFlight
        env:
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: bundle exec fastlane ios beta

  build-android:
    name: Android Build & Play Store
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'

      - name: Setup Java JDK
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true

      - name: Install Dependencies
        run: yarn install --frozen-lockfile

      - name: Decode Android Keystore
        env:
          KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
        run: |
          echo "$KEYSTORE_BASE64" | base64 --decode > android/app/release.keystore

      - name: Deploy Android
        env:
          PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
        run: bundle exec fastlane android internal

Mitigasi Error Umum pada CI/CD

1. User Interaction Not Allowed (Keychain Lock)

Kesalahan umum saat build iOS di CI adalah munculnya dialog sistem operasi: SecKeychainSearchCopyNext: The specified keychain is not valid atau User interaction is not allowed. Hal ini terjadi karena runner macOS mengeksekusi proses lewat sesi shell non-GUI, sehingga dialog input kata sandi sistem macet hingga timeout.

Solusi:

  • Selalu buat custom keychain terisolasi menggunakan aksi create_keychain bawaan Fastlane.
  • Set unlock: true dan tentukan timeout: 3600 agar keychain tidak terkunci otomatis saat kompilasi Xcode yang lama.
  • Pastikan opsi lock_when_sleeps: false disetel aktif.

2. Build Timeout pada Metro Bundler

Ketika menjalankan bundleRelease di Android atau build_app di iOS, JavaScript bundling via Metro sering memakan alokasi RAM default Node.js (1.4 GB) dan menyebabkan runner crash diam-diam (OOM - Out of Memory).

Tambahkan variabel alokasi memori pada tahap build di runner environment:

NODE_OPTIONS: "--max-old-space-size=4096"

3. Desinkronisasi Versi Dependency Ruby

Hindari pemanggilan langsung fastlane [lane] pada runner. Selalu komit berkas Gemfile.lock ke repositori dan eksekusi Fastlane menggunakan bundle exec fastlane [lane]. Ini memastikan versi CocoaPods, Match, dan Fastlane identik antara lingkungan lokal developer dan mesin CI/CD.