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-latest runner — tidak bisa di-build di Linux atau Windows. Android bisa di ubuntu-latest yang 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.

← Sebelumnya: Build & Release   Berikutnya: Best Practice →

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