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: truewajib 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 berkasAuthKey_XXXXXX.p8yang 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
endPipeline 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 internalMitigasi 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_keychainbawaan Fastlane. - Set
unlock: truedan tentukantimeout: 3600agar keychain tidak terkunci otomatis saat kompilasi Xcode yang lama. - Pastikan opsi
lock_when_sleeps: falsedisetel 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!