CI/CD #
Dalam rekayasa perangkat lunak modern, otomatisasi merupakan kunci utama untuk mempertahankan produktivitas tim dan kualitas produk. CI/CD (Continuous Integration / Continuous Delivery) menggantikan seluruh tugas manual rilis aplikasi yang melelahkan dan rentan kesalahan manusia (human error)—seperti pengujian kode, analisis gaya bahasa pemrograman (linting), pengaturan kunci API, pembuatan tanda tangan digital, hingga pengunggahan biner ke toko aplikasi—menjadi satu sistem pipa otomatisasi (automated pipeline).
Dengan pipeline CI/CD yang terkonfigurasi secara matang, setiap kali tim pengembang mendorong (push) kode ke repositori, server CI/CD akan memicu pengujian otomatis. Jika seluruh tes lolos, sistem akan membangun aplikasi secara otomatis dan mengirimkannya ke tim penjamin mutu (QA) atau langsung memublikasikannya ke pengguna akhir di App Store dan Google Play Store.
Konsep Dasar CI/CD dan Strategi Percabangan (Branching) #
Keberhasilan otomatisasi CI/CD bertumpu pada penerapan strategi percabangan kode (Git branching strategy) yang disiplin. Branching menentukan kapan sebuah pengujian dijalankan, jenis konfigurasi environment apa yang disuntikkan, dan ke mana biner aplikasi akan didistribusikan.
Berikut adalah strategi percabangan standar industri yang kita rekomendasikan untuk proyek Flutter:
feature/*: Digunakan oleh pengembang untuk menulis fitur baru. Setiap push ke branch ini hanya memicu pipeline Continuous Integration (CI) (tes unit dan analisis linter) untuk memastikan tidak ada kode rusak yang diunggah.develop: Berfungsi sebagai branch integrasi staging. Setiap kali terjadi merge ke branchdevelop, pipeline akan melakukan build otomatis menggunakan konfigurasi Staging dan mendistribusikannya ke penguji internal via Firebase App Distribution.main: Branch produksi utama. Setiap push ke branchmainmemicu build biner menggunakan konfigurasi Production dan mengunggahnya ke jalur pengujian internal (Internal Testing) Google Play Store dan Apple TestFlight.- Release Tag (
v*.*.*): Ketika kita membuat tag versi rilis resmi (misalnyav2.4.0), pipeline akan memproses build final rilis produksi secara otomatis dan meluncurkannya ke jalur publik Play Store dan App Store.
Berikut adalah diagram alur visual siklus hidup pipeline CI/CD dari proses push kode hingga pendistribusian di toko aplikasi:
flowchart TD
DevPush(["Developer Push / PR"]) --> TriggerCI["GitHub Actions Triggered"]
TriggerCI --> JobCI["Job 1: Linter & Unit Test"]
JobCI --> CheckStatus{"Apakah Test Lolos?"}
CheckStatus -- Tidak --> FailNotification["Kirim Notifikasi Gagal (Slack/Email)"]
CheckStatus -- Ya --> BranchBranch{"Deteksi Branch / Tag"}
BranchBranch -- "develop branch" --> StagingCD["Job 2: Staging Deployment<br/>1. Decode Keystore & API Keys<br/>2. Build APK/AAB<br/>3. Upload ke Firebase App Distribution"]
BranchBranch -- "main branch" --> TestFlightCD["Job 3: Release Candidate<br/>1. Setup iOS Certificate/Profile<br/>2. Build IPA Rilis<br/>3. Upload ke Apple TestFlight"]
BranchBranch -- "Tag v*.*.*" --> ProdCD["Job 4: Production Release<br/>1. Build AAB & IPA Final<br/>2. Publish ke Play Store Production & App Store"]
StagingCD --> EndNotify["Notifikasi Sukses Deployment"]
TestFlightCD --> EndNotify
ProdCD --> EndNotifyContinuous Integration (CI): Tes dan Analisis Otomatis #
Tahap pertama dari pipeline kita bertujuan memvalidasi kebersihan kode. Kita menggunakan GitHub Actions karena terintegrasi secara native dengan repositori GitHub kita dan menyediakan runner server yang andal.
Berikut adalah konfigurasi workflow GitHub Actions untuk melakukan linter dan pengujian otomatis:
# .github/workflows/ci_validation.yml
name: Continuous Integration
on:
push:
branches: [ '**' ] # Berjalan di seluruh branch untuk memvalidasi setiap push
pull_request:
branches: [ main, develop ]
jobs:
test_and_analyze:
name: Lint & Unit Testing
runs-on: ubuntu-latest
steps:
# 1. Unduh repositori kode kita ke runner
- name: Checkout Code
uses: actions/checkout@v4
# 2. Inisialisasi Java (Sangat penting untuk Gradle & modul Android)
- name: Setup Java JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# 3. Inisialisasi SDK Flutter dengan Caching Otomatis
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
cache: true # Mengaktifkan caching SDK Flutter untuk mempercepat build berikutnya
# 4. Unduh dependensi package Dart
- name: Install Dependencies
run: flutter pub get
# 5. Jalankan analisis linter
- name: Run Linter Analyze
run: flutter analyze --no-fatal-infos
# 6. Jalankan seluruh unit dan widget test disertai laporan coverage
- name: Run Tests
run: flutter test --coverage
# 7. Unggah laporan cakupan kode (opsional, jika menggunakan Codecov)
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
Continuous Delivery (CD) Android: Build dan Distribusi Otomatis #
Setelah proses validasi CI lolos, kita melangkah ke otomatisasi CD. Untuk Android, tantangannya adalah bagaimana menyuntikkan keystore fisik dan berkas properti penandatanganan secara dinamis tanpa menyimpannya secara terang-terangan di repositori git kita.
Kita memecahkan ini dengan melakukan enkripsi berkas keystore biner .jks menjadi teks Base64 dan menyimpannya di GitHub Secrets.
# .github/workflows/cd_android.yml
name: Android Continuous Delivery
on:
push:
branches: [ develop, main ]
jobs:
build_android:
name: Build & Distribute Android
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Java JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle' // Cache dependencies gradle otomatis
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
cache: true
- name: Install Dependencies
run: flutter pub get
# 1. Tulis berkas konfigurasi JSON compile-time dari GitHub Secrets
- name: Inject Dart Environment Defines
run: |
mkdir -p .dart_define
cat > .dart_define/production.json << EOF
{
"APP_ENV": "production",
"API_URL": "${{ secrets.PROD_API_URL }}",
"API_KEY": "${{ secrets.PROD_API_KEY }}",
"ENABLE_LOGS": "false"
}
EOF
# 2. Dekode keystore biner dari string Base64 yang disimpan di Secrets
- name: Decode Android Keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
# 3. Tulis berkas key.properties secara dinamis di runner
- name: Create key.properties
run: |
cat > android/key.properties << EOF
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
storeFile=upload-keystore.jks
EOF
# 4. Eksekusi kompilasi rilis Android App Bundle
- name: Build Android App Bundle
run: |
flutter build appbundle --release \
--dart-define-from-file=.dart_define/production.json
# 5. Unggah biner AAB sebagai artefak GitHub (dapat diunduh tim)
- name: Upload AAB Artifact
uses: actions/upload-artifact@v4
with:
name: android-release-bundle
path: build/app/outputs/bundle/release/app-release.aab
retention-days: 5
# 6. Kirim rilis ke tester internal via Firebase App Distribution (khusus branch develop)
- name: Upload to Firebase App Distribution
if: github.ref == 'refs/heads/develop'
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
groups: QA-Testers
file: build/app/outputs/bundle/release/app-release.aab
Continuous Delivery (CD) iOS: Konfigurasi Keychain & Build macOS #
Membangun aplikasi iOS memiliki kompleksitas tersendiri karena wajib dijalankan di atas server operasi macOS (runs-on: macos-latest) dan memerlukan pengaturan otorisasi gantungan kunci (Keychain) di sistem macOS runner untuk membuka kunci sertifikat digital .p12.
# .github/workflows/cd_ios.yml
name: iOS Continuous Delivery
on:
push:
branches: [ main ]
jobs:
build_ios:
name: Build & Distribute iOS
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
cache: true
- name: Install CocoaPods
run: |
cd ios
pod install --repo-update
# 1. Konfigurasi Gantungan Kunci Sementara (Keychain) di macOS Runner
- name: Initialize macOS Keychain
env:
CERTIFICATE_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASS: "temporary-keychain-pass-123"
run: |
# Buat file keychain baru
security create-keychain -p "$KEYCHAIN_PASS" temporary.keychain
security set-keychain-settings -lut 21600 temporary.keychain
security unlock-keychain -p "$KEYCHAIN_PASS" temporary.keychain
# Dekode sertifikat .p12 dan import ke dalam keychain
echo "$CERTIFICATE_BASE64" | base64 --decode > certificate.p12
security import certificate.p12 -k temporary.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security list-keychain -d user -s temporary.keychain
# 2. Dekode dan Pasang Provisioning Profile iOS
- name: Install iOS Provisioning Profile
env:
PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
echo "$PROFILE_BASE64" | base64 --decode > $PROFILE_PATH
# Salin profil ke direktori pencarian Xcode default
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PROFILE_PATH ~/Library/MobileDevice/Provisioning\ Profiles/
# 3. Suntikkan variabel JSON AppConfig
- name: Inject Environment Config
run: |
mkdir -p .dart_define
cat > .dart_define/production.json << EOF
{
"APP_ENV": "production",
"API_URL": "${{ secrets.PROD_API_URL }}",
"API_KEY": "${{ secrets.PROD_API_KEY }}",
"ENABLE_LOGS": "false"
}
EOF
# 4. Bangun Berkas IPA Rilis
- name: Compile iOS IPA
run: |
flutter build ipa --release \
--dart-define-from-file=.dart_define/production.json \
--export-options-plist=ios/ExportOptions.plist
# 5. Unggah biner ke Apple TestFlight (menggunakan App Store Connect API Key)
- name: Upload to Apple TestFlight
run: |
xcrun altool --upload-app \
--type ios \
--file build/ios/ipa/*.ipa \
--apiKey ${{ secrets.APP_STORE_API_KEY }} \
--apiIssuer ${{ secrets.APP_STORE_ISSUER_ID }}
# 6. Bersihkan gantungan kunci (selalu dieksekusi meskipun proses di atas error)
- name: Cleanup macOS Keychain
if: always()
run: |
security delete-keychain temporary.keychain
Keamanan Kredensial via GitHub Secrets #
Untuk mengonfigurasi rahasia di repositori kita:
- Buka halaman repositori GitHub kita di web browser.
- Masuk ke menu Settings $\rightarrow$ Secrets and variables $\rightarrow$ Actions $\rightarrow$ klik New repository secret.
Untuk berkas biner (seperti berkas Keystore .jks atau sertifikat Apple .p12), kita harus mengubahnya menjadi string teks Base64 terlebih dahulu di komputer lokal kita sebelum menempelkannya (paste) ke GitHub Secrets:
# Untuk pengguna macOS: Ubah file keystore menjadi string Base64 dan salin ke clipboard
base64 -i android/app/upload-keystore.jks | pbcopy
# Untuk pengguna Linux:
base64 -w 0 android/app/upload-keystore.jks | xclip -sel clip
Mempercepat Waktu Build dengan Caching #
Waktu eksekusi build di server cloud CI/CD dapat memakan waktu yang sangat lama (15 hingga 20 menit) karena server harus mengunduh ulang ribuan dependensi package dan library native Gradle/CocoaPods dari awal pada setiap proses run.
Kita dapat mereduksi waktu tunggu build hingga di bawah 5 menit dengan mengaktifkan fitur caching pada file konfigurasi GitHub Actions kita:
# 1. Caching Package Dependensi Dart (Pub Cache)
- name: Cache Pub Dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
.dart_tool/
key: pub-${{ runner.os }}-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
pub-${{ runner.os }}-
# 2. Caching Build Tooling Gradle Android
- name: Cache Gradle Data
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
# 3. Caching Berkas Instalasi CocoaPods iOS
- name: Cache CocoaPods Pods
uses: actions/cache@v4
with:
path: ios/Pods
key: cocoapods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
cocoapods-${{ runner.os }}-
Otomatisasi Tingkat Lanjut Menggunakan Fastlane #
Meskipun menulis skrip GitHub Actions mentah sangat efektif, standar industri pengembangan aplikasi mobile berskala besar biasanya memisahkan logika deployment menggunakan Fastlane. Fastlane adalah mesin otomatisasi berbasis bahasa Ruby yang berjalan di atas command line native Android dan iOS.
Keuntungan menggunakan Fastlane adalah skrip deployment kita (yang disebut Fastfile) dapat dijalankan dengan instruksi yang sama persis baik secara lokal di komputer lokal pengembang maupun di dalam server CI/CD apa pun (GitHub Actions, GitLab CI, Bitrise, Jenkins).
Struktur folder integrasi Fastlane:
android/
fastlane/
Appfile # Menyimpan konfigurasi ID paket Android
Fastfile # Berisi alur rilis (lanes) Android
ios/
fastlane/
Appfile # Menyimpan konfigurasi Apple ID
Fastfile # Berisi alur rilis (lanes) iOS
Berikut adalah contoh penulisan alur rilis (lanes) di dalam berkas android/fastlane/Fastfile:
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Menjalankan linter dan unit testing"
lane :test do
gradle(task: "test")
end
desc "Kirim build uji coba ke Firebase App Distribution"
lane :distribute_beta do
# Jalankan kompilasi apk rilis via gradle
gradle(task: "clean assembleRelease")
firebase_app_distribution(
app: ENV["FIREBASE_ANDROID_APP_ID"],
groups: "QA-Testers",
apk_path: "../build/app/outputs/apk/release/app-release.apk",
release_notes: "Build otomatis via Fastlane"
)
end
desc "Rilis build final ke Google Play Store (Internal Track)"
lane :publish_to_playstore do
gradle(task: "clean bundleRelease")
upload_to_play_store(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
end
end
Kini, di dalam alur kerja GitHub Actions kita, kita cukup memanggil perintah Fastlane yang bersih:
- name: Execute Fastlane Beta Lane
run: |
cd android
bundle exec fastlane distribute_beta
Ringkasan #
- Continuous Integration (CI): Fokus pada proses otomatisasi analisis kode (
flutter analyze) dan unit testing (flutter test) pada setiap proses push branch atau pengajuan Pull Request.- Continuous Delivery (CD): Mengotomatiskan proses build biner rilis. Manfaatkan Runner berbasis Linux (
ubuntu-latest) untuk Android, dan wajib menggunakan macOS (macos-latest) untuk iOS.- Enkripsi Base64: Amankan kredensial biner native (Android Keystore
.jks& Apple Certificate.p12) dengan mengonversinya menjadi string teks Base64 untuk disimpan secara terenkripsi di GitHub Secrets.- Optimasi Cache: Pasang caching untuk folder Pub cache, build Gradle, dan folder instalasi CocoaPods iOS guna memotong waktu build hingga $70%$.
- Otomatisasi Fastlane: Terapkan Fastlane sebagai pengelola alur rilis (lanes) independen yang dapat dieksekusi secara seragam baik di server cloud CI/CD maupun secara lokal di komputer pengembang.