Code signing iOS pada project React Native sering menjadi sumber kegagalan build saat berpindah dari mesin lokal developer ke server CI/CD. Masalah ini berakar pada model desentralisasi sertifikat: setiap developer membuat sertifikat distribusi dan provisioning profile mandiri melalui Xcode. Artikel ini membahas implementasi Fastlane Match untuk menstandarkan proses code signing menggunakan repository Git terenkripsi, konfigurasi non-interaktif pada CI/CD, dan penyesuaian project Xcode.

Akar Masalah: Certificate Sprawl dan Profile Mismatch

Secara default, opsi Automatically manage signing pada Xcode menginstruksikan Xcode mengunduh atau men-generate sertifikat baru langsung dari Apple Developer Portal. Pola ini memicu sejumlah kendala:

  • Limit Sertifikat Distribusi: Akun Apple Developer Program membatasi jumlah maksimum sertifikat Apple Distribution aktif (biasanya 2-3 sertifikat). Ketika developer baru bergabung dan men-generate sertifikat baru, sertifikat lama sering kali ter-revoke. Akibatnya, runner CI/CD yang menyimpan sertifikat lama langsung gagal saat build.
  • Ketiadaan Private Key di CI: Runner CI (seperti GitHub Actions macOS runner) adalah mesin ephemeral (dibuat dan dihapus secara dinamis). Runner tidak memiliki private key yang tersimpan di Keychain lokal developer.
  • Provisioning Profile Mismatch: Profil provisi terikat pada UDID perangkat dan sertifikat spesifik. Jika profil di portal Apple tidak sinkron dengan sertifikat di Keychain CI, xcodebuild akan melempar error: No matching provisioning profiles found.

Solusi: Pendekatan Centralized Signing via Fastlane Match

Fastlane Match mengadopsi konsep Git as a Single Source of Truth. Seluruh sertifikat (development dan distribution) serta provisioning profile di-generate satu kali oleh Match, kemudian dienkripsi menggunakan OpenSSL (AES-256) dan disimpan di repository Git privat terpisah. Seluruh developer dan mesin CI/CD hanya perlu mengakses repo tersebut dan mendekripsinya menggunakan passphrase yang sama (MATCH_PASSWORD).

1. Menyiapkan Repository Git Privat

Buat sebuah repository Git privat kosong (misalnya di GitHub/GitLab: org-certs-repo). Repository ini hanya akan menyimpan file sertifikat dan profile terenkripsi. Berikan akses baca (read-only deploy key) ke runner CI.

2. Inisialisasi dan Konfigurasi Matchfile

Jalankan inisialisasi di dalam folder root React Native:

cd ios
bundle exec fastlane match init

Pilih storage mode git dan masukkan URL SSH repository privat Anda. Fastlane akan membuat file ios/fastlane/Matchfile. Konfigurasikan file tersebut sebagai berikut:

# ios/fastlane/Matchfile
git_url("[email protected]:organisasi-anda/certificates-repo.git")
storage_mode("git")
type("appstore")
app_identifier(["com.perusahaan.appname"])
username("[email protected]") # Opsional jika menggunakan API Key

Otentikasi Non-Interaktif dengan App Store Connect API Key

Apple mewajibkan Two-Factor Authentication (2FA) untuk Apple ID biasa, yang menyebabkan sesi login pada runner CI kadaluarsa dan membutuhkan input OTP. Gunakan App Store Connect API Key untuk otentikasi headless.

Generate API Key di App Store Connect (Menu: Users and Access > Integrations > App Store Connect API). Unduh file private key .p8, lalu catat Key ID dan Issuer ID.

Konfigurasi Fastfile: Lane Development dan App Store

Tambahkan lane di ios/fastlane/Fastfile. Gunakan action setup_ci untuk membuat Keychain sementara yang terisolasi pada runner CI, mencegah prompt interaktif dari macOS Keychain.

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Load App Store Connect API Key"
  lane :load_api_key do
    app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
      key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
      is_key_content_base64: true,
      in_house: false
    )
  end

  desc "Sync certificates for local development"
  lane :sync_dev do
    api_key = load_api_key
    match(
      type: "development",
      readonly: true,
      api_key: api_key
    )
  end

  desc "Build and Sign App for App Store / TestFlight"
  lane :build_release do
    api_key = load_api_key

    # Inisialisasi keychain temporer khusus CI
    setup_ci if is_ci

    match(
      type: "appstore",
      readonly: is_ci,
      api_key: api_key
    )

    # Update project signing settings secara programatis sebelum build
    update_code_signing_settings(
      use_automatic_signing: false,
      path: "MyApp.xcodeproj"
    )

    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          "com.perusahaan.appname" => "match AppStore com.perusahaan.appname"
        }
      }
    )
  end
end

Penyesuaian Konfigurasi Signing di Xcode (project.pbxproj)

Agar Match bekerja deterministik tanpa intervensi Xcode, ubah pengaturan signing target aplikasi utama:

  1. Buka ios/MyApp.xcworkspace di Xcode.
  2. Pilih target project, buka tab Signing & Capabilities.
  3. Untuk build configuration Release: Hilangkan centang pada Automatically manage signing.
  4. Pilih Provisioning Profile: gunakan profile yang di-download oleh Match, dengan format nama: match AppStore com.perusahaan.appname.
  5. Pilih Signing Certificate: Apple Distribution.
  6. Untuk konfigurasi Debug, Anda dapat mempertahankan Automatically manage signing untuk kenyamanan simulator, atau mengaturnya ke Manual menggunakan match Development com.perusahaan.appname jika perlu running langsung di physical device.

Penting: Menggunakan update_code_signing_settings di Fastfile mengamankan build CI agar tidak tertimpa setting lokal Xcode developer.

Setup Workflow GitHub Actions

Runner GitHub Actions membutuhkan SSH key privat untuk mengakses repository certificates Match, serta passphrase dekripsi.

Simpan variabel berikut di GitHub Secrets:

  • MATCH_PASSWORD: Kata sandi enkripsi repository sertifikat.
  • MATCH_GIT_PRIVATE_KEY: SSH Private Key yang memiliki akses read ke repository certificates.
  • APP_STORE_CONNECT_KEY_ID: Key ID dari Apple Developer Console.
  • APP_STORE_CONNECT_ISSUER_ID: Issuer ID (UUID).
  • APP_STORE_CONNECT_API_KEY_CONTENT: Konten file .p8 dalam format Base64.

Snippet pipeline .github/workflows/ios-build.yml:

name: iOS Release Build

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: macos-14
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

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

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

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

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

      - name: Install CocoaPods
        run: |
          cd ios
          bundle exec pod install

      - name: Run Fastlane Release Build
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
          APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
        run: |
          cd ios
          bundle exec fastlane build_release

Troubleshooting Masalah Umum

1. Error: "User interaction is not allowed"

Penyebab: macOS Keychain terkunci atau sistem meminta konfirmasi dialog GUI untuk mengakses private key. Runner CI tidak memiliki interface visual.

Solusi: Pastikan action setup_ci dijalankan di awal lane Fastlane. Action ini membuat keychain sementara dengan password acak, membukanya (unlock), dan menonaktifkan timeout penguncian keychain selama proses build berlangsung.

2. Sertifikat Revoked atau Profil Tidak Valid

Jika sertifikat distribution tidak sengaja ter-revoke melalui web Apple Developer:

  1. Hapus profil dan sertifikat lama yang rusak menggunakan Fastlane nuke:
    bundle exec fastlane match nuke appstore
  2. Generate sertifikat dan profil baru yang fresh:
    bundle exec fastlane match appstore
  3. Commit perubahan yang dihasilkan Match ke repo privat sertifikat Anda. CI/CD akan otomatis membaca sertifikat baru pada eksekusi berikutnya tanpa perlu setup manual di Xcode.