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 branch develop, pipeline akan melakukan build otomatis menggunakan konfigurasi Staging dan mendistribusikannya ke penguji internal via Firebase App Distribution.
  • main: Branch produksi utama. Setiap push ke branch main memicu 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 (misalnya v2.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 --> EndNotify

Continuous 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:

  1. Buka halaman repositori GitHub kita di web browser.
  2. 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.

← Sebelumnya: Build & Release   Berikutnya: Best Practice →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact