CI/CD #
CI/CD (Continuous Integration/Continuous Deployment) mengotomasi proses yang tadinya dilakukan manual: menjalankan test, build app, dan mendistribusikannya ke tester atau store. Dengan CI/CD yang baik, setiap push ke branch utama otomatis divalidasi dan distribusikan — kamu bisa fokus coding, bukan mengurusi build manual.
Strategi Branching #
main → production (protected, hanya via PR)
develop → staging / integration branch
feature/* → fitur baru (merge ke develop)
fix/* → bug fix
release/* → persiapan release (dari develop)
CI Triggers:
Push ke feature/* → run test + analyze
PR ke develop → run test + build staging
Push ke develop → build + distribusi ke tester (Firebase App Distribution)
Push ke main → build + distribusi ke Play Store internal track
Tag v*.*.* → upload ke Play Store production + App Store
Workflow Dasar — Test dan Analyze #
# .github/workflows/test.yml
name: Test & Analyze
on:
push:
branches: ['**'] # semua branch
pull_request:
branches: [main, develop]
jobs:
test:
name: Test & Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: stable
cache: true # cache Flutter SDK antar run
- name: Setup Java (untuk Android tools)
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle' # cache Gradle dependencies
- name: Install dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze --no-fatal-infos
- name: Run tests with coverage
run: flutter test --coverage
- name: Upload coverage (opsional)
uses: codecov/codecov-action@v4
with:
file: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
Workflow Build Android #
# .github/workflows/build-android.yml
name: Build Android
on:
push:
branches: [develop, main]
jobs:
build-android:
name: Build Android App Bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: stable
cache: true
- name: Install dependencies
run: flutter pub get
- name: Create dart-define file dari Secrets
run: |
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
- name: Decode Keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
- 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
- name: Build App Bundle
run: |
flutter build appbundle --release \
--dart-define-from-file=.dart_define/production.json
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: android-aab
path: build/app/outputs/bundle/release/app-release.aab
retention-days: 7
- name: Distribute 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 }}
groups: internal-testers
releaseNotes: "Build from commit: ${{ github.sha }}"
file: build/app/outputs/flutter-apk/app-release.apk
Workflow Build iOS #
# .github/workflows/build-ios.yml
name: Build iOS
on:
push:
branches: [develop, main]
jobs:
build-ios:
name: Build iOS IPA
runs-on: macos-latest # WAJIB macOS untuk build iOS
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: stable
cache: true
- name: Install CocoaPods dependencies
run: |
cd ios
pod install --repo-update
- name: Install Apple Certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ci-keychain-password
run: |
# Buat keychain sementara
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Import certificate
echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain \
-P "$P12_PASSWORD" -T /usr/bin/codesign
security list-keychain -d user -s build.keychain
- name: Install Provisioning Profile
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
PP_PATH=$RUNNER_TEMP/profile.mobileprovision
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Install dependencies
run: flutter pub get
- name: Build IPA
run: |
flutter build ipa --release \
--dart-define-from-file=.dart_define/production.json \
--export-options-plist=ios/ExportOptions.plist
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: build/ios/ipa/*.ipa
retention-days: 7
- name: Distribute to TestFlight
if: github.ref == 'refs/heads/main'
run: |
xcrun altool --upload-app \
--type ios \
--file build/ios/ipa/toko_saya.ipa \
--apiKey ${{ secrets.APP_STORE_API_KEY }} \
--apiIssuer ${{ secrets.APP_STORE_ISSUER_ID }}
- name: Cleanup keychain
if: always() # selalu cleanup meski ada error
run: security delete-keychain build.keychain
Setup GitHub Secrets #
Di GitHub Repository → Settings → Secrets and variables → Actions
Secrets yang dibutuhkan:
ANDROID:
ANDROID_KEYSTORE_BASE64 # keystore di-encode ke Base64
ANDROID_STORE_PASSWORD # password keystore
ANDROID_KEY_PASSWORD # password key alias
ANDROID_KEY_ALIAS # nama key alias
iOS:
IOS_CERTIFICATE_BASE64 # certificate .p12 di-encode ke Base64
IOS_CERTIFICATE_PASSWORD # password certificate .p12
IOS_PROVISIONING_PROFILE_BASE64 # provisioning profile di-encode ke Base64
APP STORES:
FIREBASE_ANDROID_APP_ID # dari Firebase Console
FIREBASE_SERVICE_ACCOUNT # JSON service account Google
APP_STORE_API_KEY # API key dari App Store Connect
APP_STORE_ISSUER_ID # Issuer ID dari App Store Connect
APP CONFIG:
PROD_API_URL # URL API production
PROD_API_KEY # API key production
CODECOV_TOKEN # opsional: untuk upload coverage
# Encode file ke Base64 untuk disimpan sebagai Secret:
base64 -i upload-keystore.jks | pbcopy # macOS -- copy ke clipboard
base64 -w 0 upload-keystore.jks | xclip # Linux
Caching untuk Build Lebih Cepat #
# Tambahkan caching untuk dependencies yang jarang berubah
- name: Cache pub packages
uses: actions/cache@v4
with:
path: |
~/.pub-cache
.dart_tool/
key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
restore-keys: |
pub-${{ runner.os }}-
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache CocoaPods (iOS)
uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
# Dengan caching, waktu build bisa berkurang dari 15 menit → 5 menit
Workflow Lengkap dengan Kondisi #
# .github/workflows/main.yml -- workflow utama
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
tags: ['v*.*.*']
pull_request:
branches: [main, develop]
jobs:
test:
uses: ./.github/workflows/test.yml # reuse workflow
build-android:
needs: test
if: github.event_name == 'push' # hanya saat push, bukan PR
uses: ./.github/workflows/build-android.yml
secrets: inherit
build-ios:
needs: test
if: github.event_name == 'push'
uses: ./.github/workflows/build-ios.yml
secrets: inherit
deploy-production:
needs: [build-android, build-ios]
if: startsWith(github.ref, 'refs/tags/v') # hanya saat ada tag v*.*.*
runs-on: ubuntu-latest
steps:
- name: Upload ke Play Store Production
# ... langkah upload ke Play Store
- name: Upload ke App Store
# ... langkah upload ke App Store
Ringkasan #
- Gunakan GitHub Actions dengan workflow yang terpisah per tugas: test, build Android, build iOS — ini memudahkan debugging dan reuse.
- Semua secret (keystore, certificate, API key) disimpan di GitHub Secrets dan di-inject via environment variable — tidak pernah ada di kode sumber.
- Encode file binary (keystore, certificate, provisioning profile) ke Base64 sebelum disimpan sebagai Secret, lalu decode kembali saat workflow berjalan.
- iOS build wajib di
macos-latestrunner — tidak bisa di-build di Linux atau Windows. Android bisa diubuntu-latestyang lebih murah.- Caching pub packages, Gradle, dan CocoaPods bisa mengurangi waktu build dari 15 menit menjadi 5 menit — selalu aktifkan untuk repository yang aktif.
- Gunakan workflow conditions (
if:) untuk menentukan kapan step dijalankan: hanya saat push (bukan PR), hanya di branch tertentu, atau hanya saat ada tag versi.- Distribusikan ke Firebase App Distribution untuk staging (tester internal) dan ke Play Store Internal Track / TestFlight untuk production candidate.