CI/CD adalah robot yang memeriksa setiap perubahan kode secara otomatis (CI, continuous integration), lalu membuat dan mengirim aplikasinya secara otomatis (CD, continuous delivery). Bayangin meja QC di laundry Bu Rini. Setiap keranjang dihitung dan dicek nodanya sebelum disetrika: itu CI. Cucian yang lolos dibungkus, notanya dicap dan diberi nomor urut, lalu kurir mengantarnya: itu CD. Di aplikasi Laundry Bu Rini, keranjangnya adalah pull request (PR), ceknya typecheck, lint, dan Jest, nomor urutnya versionCode, capnya signing, dan kurirnya EAS Submit.
🧪 Tebakan cepat: Dalam satu PR kamu menambah library native
expo-hapticsdan mengubah teks diOrderRow. Setelah merge, kamu menjalankaneas update --channel preview. HP tester yang memasang build preview minggu lalu menerima update itu? Jawabannya ada di bagian "EAS Update: channel dan runtimeVersion".
Kenapa CI/CD mobile lebih ribet dari web
Di web, deploy selesai begitu file baru ada di server. Di mobile, tiga hal membuat pipeline-nya sendiri lebih berat:
- Build lama. Kode native dikompilasi dulu (Gradle untuk Android, Xcode untuk iOS), hitungan menit, bukan detik.
- Build iOS butuh macOS. Xcode cuma jalan di Mac. Di GitHub Actions, runner macOS 0,062 dolar per menit dan runner Linux 0,006 dolar (repo private; repo publik gratis): sekitar 10 kali lipat.
- Signing dan provisioning. Build rilis ditandatangani keystore (Android) atau sertifikat plus provisioning profile (iOS), dan kuncinya gak boleh bocor.
Di luar pipeline masih ada review toko dan versi lama yang tetap terpasang di HP pelanggan, tapi tiga hal tadi yang membuat CI/CD mobile beda. Alat yang populer: EAS Build (build di server Expo, termasuk iOS tanpa Mac milikmu), fastlane (otomasi signing, build, dan upload), Bitrise (layanan CI khusus mobile), dan GitHub Actions (dengan runner macOS kalau mau build iOS sendiri). Di sini GitHub Actions dipakai untuk cek, dan EAS untuk build, update, dan submit.
Langkah 1: cek di setiap pull request
# .github/workflows/cek.yml
name: Cek
on:
pull_request:
push:
branches: [main]
jobs:
cek:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npx expo lint
- run: npm run test:ci
- Cek jalan di setiap PR dan setelah merge ke
main, di runner Linux (ubuntu-latest), karena gak butuh Mac. actions/checkout@v7(v7.0.1) danactions/setup-node@v7(v7.1.0) adalah versi terbaru per 11 Oktober 2026. Contoh di dokumentasi Expo masih@v5dan@v6.cache: npmmenyimpan unduhan npm berdasarkanpackage-lock.json.npx expo lintyang pertama memasangeslint9 daneslint-config-expo57.0.2 lalu menuliseslint.config.js: commit file itu.npm run test:cimenjalankanjest --ci. Gak ada rahasia di file ini: GitHub gak memberi secrets ke workflow dari PR fork, jadi job cek harus bisa jalan tanpanya.
Pertama kali dijalankan di project ini, Jest ikut mengambil test Detox di e2e/:
$ npx jest --ci
FAIL e2e/order.test.js
● Pesanan saya › membuka detail order #13
ReferenceError: device is not defined
Tests: 1 failed, 1 passed, 2 total
device cuma ada kalau Detox yang menjalankan. Tambahkan "testPathIgnorePatterns": ["/node_modules/", "/e2e/"] di bagian jest pada package.json, dan hasilnya Tests: 1 passed, 1 total dalam 1,98 detik.
Profil build di eas.json
{
"cli": { "version": ">= 24.12.1", "appVersionSource": "remote" },
"build": {
"development": { "developmentClient": true, "distribution": "internal",
"environment": "development", "env": { "APP_VARIANT": "development" } },
"preview": { "distribution": "internal", "channel": "preview",
"environment": "preview", "android": { "buildType": "apk" } },
"production": { "channel": "production", "environment": "production", "autoIncrement": true }
},
"submit": { "production": { "android": { "track": "internal" } } }
}
development: development build denganexpo-dev-clientdan ID.devdariAPP_VARIANT(pelajaran Expo).preview:distribution: "internal", dipasang tester lewat link, Android sebagai APK.production:.aabdan.ipauntuk toko.autoIncrementdenganappVersionSource: "remote"menaikkanversionCodedanbuildNumberdi server EAS (pelajaran publishing).channeladalah saluran update yang ditanam di build,environmentkumpulan variabel EAS untuk build itu, daneas buildtanpa--profilememakaiproduction.
EAS Update: channel dan runtimeVersion
Setiap build membawa tiga label: platform, channel, dan runtimeVersion (identitas lapisan native-nya). Update cuma dipasang di build yang platform dan runtimeVersion-nya sama persis, lewat channel build itu: eas update --channel preview --environment preview (--environment wajib untuk project SDK 55 ke atas). runtimeVersion diisi policy di app.config.ts, setelah npx expo install expo-updates:
runtimeVersion: { policy: 'fingerprint' },
appVersion: sama denganversion("1.0.0"). Lupa menaikkannya saat native berubah, update yang rusak ikut masuk.nativeVersion:versionplus nomor build, misalnya"1.0.0(1)".fingerprint: hash dari semua yang memengaruhi lapisan native, dihitung@expo/fingerprint. Ini yang dipakai di sini.
Diukur di project ini dengan npx expo-updates fingerprint:generate --platform android (jalan tanpa login; eas fingerprint:generate minta login):
| Perubahan | Fingerprint Android | Yang dibutuhkan |
|---|---|---|
| build preview minggu lalu | af774548… |
|
status di OrderRow jadi huruf besar |
af774548… (sama) |
EAS Update |
npx expo install expo-haptics |
563c34c6… |
build baru |
teks cameraPermission di app.json |
dfd1c3a1… |
build baru |
track submit di eas.json |
4be0bcb4… |
build baru |
Update dari tebakan tadi dibuat untuk runtime 563c34c6…, sedangkan HP tester menjalankan af774548…, jadi HP itu gak memasangnya. Itu jawaban tebakannya. Justru ini melindungi: dengan runtimeVersion tetap "1.0.0", HP tester akan memuat JavaScript yang memanggil kode native yang gak ada, dan expo-updates harus mundur ke update sebelumnya. Fingerprint iOS dihitung terpisah (85371baa…), dan eas.json serta script di package.json ikut dihitung.
Langkah 2: build atau update, dipilih otomatis
EAS Workflows adalah CI milik Expo, jalan di server Expo setelah repo GitHub dihubungkan ke project EAS. Job fingerprint dan get-build-nya memilih jalannya:
# .eas/workflows/kirim-preview.yml
on:
push:
branches: ['main']
jobs:
fingerprint:
type: fingerprint
environment: preview
cari_build_android:
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
profile: preview
build_android:
needs: [cari_build_android]
if: ${{ !needs.cari_build_android.outputs.build_id }}
type: build
params: { platform: android, profile: preview }
update_android:
needs: [cari_build_android]
if: ${{ needs.cari_build_android.outputs.build_id }}
type: update
environment: preview
params: { channel: preview, platform: android }
# iOS: empat job yang sama dengan ios_fingerprint_hash dan platform: ios
fingerprintmenghitung hash dengan environment yang sama dengan profilpreview.get-buildmencari build preview dengan hash itu;build_idkosong berarti belum ada.- Belum ada:
build_androidmembuat build baru. Sudah ada:update_androidmengirim JavaScript ke channelpreview. - EAS Workflows gak menunggu hasil
cek.yml. Di GitHub, nyalakan branch protection "Require status checks to pass before merging" untukmain, supaya PR yang merah gak bisa di-merge. - Polanya dari contoh "Deploy to production" di dokumentasi Expo. Versi GitHub Actions-nya (
continuous-deploy-fingerprint) masih bertulisan "not yet ready for use".
Langkah 3: rilis ke TestFlight dan Play internal
# .github/workflows/rilis.yml
name: Rilis
on:
push:
tags: ['v*']
jobs:
rilis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 24, cache: npm }
- uses: expo/expo-github-action@v9
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: npm ci
- run: eas build --platform all --profile production --auto-submit --non-interactive --no-wait
- Jalan saat kamu push tag, misalnya
git tag v1.1.0lalugit push --tags.expo/expo-github-action@v9(9.0.0, Juni 2026) memasang EAS CLI dan login denganEXPO_TOKEN. --auto-submitmengirim hasilnya lewat profil submitproduction: Android ke trackinternal, iOS ke TestFlight.--no-waitmenyelesaikan job begitu build dimulai, jadi menit GitHub gak terpakai selama EAS bekerja.- Submit pertama tetap dijalankan sekali dengan tangan (
eas submit), supaya kunci Google Service Account dan App Store Connect API tersimpan di EAS.
Tanpa Expo (React Native bare atau aplikasi native), jalur yang umum adalah fastlane (2.241.0): lane berisi match untuk sertifikat, gradle atau build_app untuk build, lalu upload_to_play_store dan upload_to_testflight, dengan runner macOS untuk iOS.
Rahasia: token, keystore, dan variabel
EXPO_TOKEN: buat di halaman Access tokens di expo.dev, simpan di GitHub lewat Settings, Secrets and variables, Actions. Jangan ditulis di YAML. Tanpa token,eas buildberhenti denganAn Expo user account is required to proceed.- Keystore dan sertifikat disimpan di EAS credentials (pelajaran publishing), bukan di repo.
.gitignoreproject ini sudah mengabaikan*.jks,*.p8,*.p12,*.key, dan*.mobileprovision. - Variabel build:
eas env:set --name SENTRY_AUTH_TOKEN --value ... --environment production --visibility secret.eas secret:createdari materi lama sudah deprecated. Semua yang masuk ke kode aplikasi, termasukEXPO_PUBLIC_API_URL, bisa dibaca orang yang membongkar aplikasinya.
Maestro di CI: berapa biayanya
Flow buka-order-13.yaml dari pelajaran E2E butuh aplikasi yang sudah di-build dan emulator yang menyala. Harga per 11 Oktober 2026:
| Pilihan | Caranya | Biaya |
|---|---|---|
EAS Workflows, job maestro (masih alpha) |
build profil E2E (APK, simulator iOS), lalu Maestro di server EAS | build dihitung kuota, lalu menit workflow: Linux 0,018 dolar, macOS 0,075 dolar; paket Free sampai 60 menit |
| GitHub Actions dengan emulator Android | reactivecircus/android-emulator-runner@v2 di ubuntu-latest |
repo publik gratis, private 0,006 dolar per menit (simulator iOS di runner macOS: 0,062) |
| Maestro Cloud | perangkat di server Maestro, paralel | 250 dolar per perangkat per bulan |
Misalnya 40 PR sebulan dan 15 menit per run Android (angka contoh, ukur run-mu sendiri): 600 menit × 0,006 = 3,6 dolar di GitHub, 600 × 0,018 = 10,8 dolar di EAS Workflows, dan 250 dolar tetap di Maestro Cloud. Untuk Bu Rini: flow Android di setiap PR yang menyentuh layar order, iOS sekali sehari.
⚠️ Jebakan yang sering ketemu
- Jest ikut menjalankan test Detox. CI merah dengan
ReferenceError: device is not defined. Tambah"/e2e/"ketestPathIgnorePatterns. eas updatesetelah mengubah bagian native. Pakai policyfingerprintdan biarkan workflow memilih build atau update.- Token atau keystore di repo. Token di GitHub Secrets, keystore di EAS credentials.
- Build iOS di runner macOS untuk setiap PR. Sekitar 10 kali harga Linux. Cek di Linux, build di EAS, E2E iOS sehari sekali.
TL;DR: CI/CD mobile lebih ribet dari web karena build native lama, build iOS butuh macOS, dan signing serta provisioning harus dijaga; alat yang populer: EAS Build, fastlane, Bitrise, dan GitHub Actions. Untuk Laundry Bu Rini: GitHub Actions (actions/checkout@v7, actions/setup-node@v7, Node 24) menjalankan tsc --noEmit, expo lint, dan jest --ci di setiap PR tanpa rahasia, dengan e2e/ dikeluarkan dari Jest. eas.json punya profil development, preview (internal, APK, channel preview), dan production (autoIncrement, appVersionSource: "remote"). Update cuma dipasang di build dengan platform dan runtimeVersion yang sama; policy fingerprint mengubah runtime begitu bagian native berubah (library native, app.json, bahkan eas.json), dan EAS Workflows (fingerprint, get-build) memilih build baru atau update. Tag v* menjalankan eas build --profile production --auto-submit lewat expo/expo-github-action@v9 ke TestFlight dan track internal. EXPO_TOKEN di GitHub Secrets, keystore di EAS credentials, variabel dengan eas env:set. Maestro di CI: emulator Android di GitHub paling murah, Maestro Cloud 250 dolar per perangkat per bulan; tanpa Expo, fastlane.