1. Mengapa CI/CD untuk Mobile?
CI/CD (Continuous Integration / Continuous Delivery) mengotomatisasi proses build, test, dan deployment aplikasi mobile. Tanpa CI/CD, proses rilis manual memakan waktu berjam-jam dan rentan human error.
Pull Request
Unit & UI Tests
Play Store
Mulai dengan CI yang menjalankan lint dan unit test. Tambahkan deployment otomatis setelah pipeline stabil. Jangan langsung setup full pipeline.
2. Fastlane
Fastlane adalah tool open-source yang mengotomatisasi build, signing, dan deployment untuk iOS dan Android.
# Install via RubyGems gem install fastlane # Atau via Homebrew (macOS) brew install fastlane # Inisialisasi di proyek cd ios && fastlane init cd android && fastlane init
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Run tests"
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 15"],
code_coverage: true,
)
end
desc "Build & upload to TestFlight"
lane :beta do
# Increment build number
increment_build_number(
build_number: latest_testflight_build_number + 1
)
# Build
build_app(
scheme: "MyApp",
export_method: "app-store",
clean: true,
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
)
# Notify team
slack(
message: "New iOS build uploaded to TestFlight!",
channel: "#releases",
)
end
desc "Upload to App Store"
lane :release do
build_app(scheme: "MyApp", export_method: "app-store")
upload_to_app_store(
skip_metadata: false,
skip_screenshots: true,
submit_for_review: true,
automatic_release: true,
)
end
end
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Run tests"
lane :test do
gradle(task: "test")
end
desc "Build & upload to Play Store (internal track)"
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
)
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_screenshots: true,
)
slack(message: "New Android build uploaded to Play Store!")
end
desc "Promote internal to production"
lane :release do
upload_to_play_store(
track: "internal",
track_promote_to: "production",
skip_upload_aab: true,
skip_upload_metadata: false,
)
end
end
3. GitHub Actions
# .github/workflows/ci.yml
name: Mobile CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
flutter-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze --fatal-infos
- name: Run tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: coverage/lcov.info
build-android:
needs: flutter-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Build APK
run: flutter build apk --release
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: android-release
path: build/app/outputs/flutter-apk/app-release.apk
build-ios:
needs: flutter-test
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- name: Build IPA
run: flutter build ipa --release --no-codesign
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ios-release
path: build/ios/ipa/*.ipa
4. Code Signing
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install Fastlane
run: bundle install
- name: Setup signing
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
run: |
cd ios
bundle exec fastlane match appstore --readonly
- name: Build & Upload
env:
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}
run: |
cd ios
bundle exec fastlane beta
Gunakan GitHub Secrets untuk menyimpan API keys, passwords, dan certificates. JANGAN pernah commit credentials ke repository.
5. TestFlight & Play Store
| Platform | Track | Audience | Review Time |
|---|---|---|---|
| TestFlight Internal | Internal testing | Tim internal (100 orang) | Tidak perlu review |
| TestFlight External | Beta testing | Publik (10.000 orang) | 1-3 hari |
| Play Internal | internal | 100 tester | Tidak perlu review |
| Play Closed | closed (alpha/beta) | Undangan | Beberapa jam |
| Play Open | open (beta) | Publik (link) | Beberapa jam |
| Play Production | production | Semua user | 1-7 hari |
6. Beta Testing Strategy
# .github/workflows/beta.yml
name: Deploy Beta
on:
workflow_dispatch:
inputs:
platform:
description: 'Platform'
required: true
type: choice
options: ['ios', 'android', 'both']
track:
description: 'Track'
required: true
type: choice
options: ['internal', 'closed', 'open']
jobs:
beta-android:
if: inputs.platform == 'android' || inputs.platform == 'both'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- run: flutter build appbundle --release
- uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
packageName: com.myapp
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: ${{ inputs.track }}
beta-ios:
if: inputs.platform == 'ios' || inputs.platform == 'both'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- name: Deploy to TestFlight
run: cd ios && bundle exec fastlane beta
6.1 Testing Ring Strategy
| Ring | Siapa | Ukuran | Tujuan |
|---|---|---|---|
| Ring 0 | Developer | 1-5 | Smoke test |
| Ring 1 | QA Team | 5-20 | Regression test |
| Ring 2 | Internal team | 20-100 | Dogfooding |
| Ring 3 | Beta users | 100-1000 | Real-world testing |
| Ring 4 | Production | Semua | Full release |
# Android staged rollout
- uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SA }}
packageName: com.myapp
releaseFiles: app-release.aab
track: production
# Rollout ke 20% user dulu
status: inProgress
userFraction: 0.2
# Setelah yakin stabil, promote ke 100%
# Bisa dilakukan manual di Play Console
7. Team Notifications
# Tambahkan di akhir workflow
notify:
needs: [deploy-ios, deploy-android]
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "🚀 Release ${{ github.ref_name }} deployed!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Release ${{ github.ref_name }}*\nPlatform: ${{ github.event.inputs.platform }}\nTrack: ${{ github.event.inputs.track }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Setelah deploy: (1) Monitor crash reports 24 jam, (2) Cek analytics & user feedback, (3) Siapkan rollback plan, (4) Update release notes.
8. Tips Pipeline yang Efisien
| Tips | Manfaat |
|---|---|
| Cache dependencies (pub, gradle, cocoapods) | Build 2-5x lebih cepat |
| Run tests secara paralel | Kurangi total pipeline time |
| Conditional build (changed files only) | Skip build jika tidak ada perubahan |
| Separate CI & CD pipeline | CI di setiap push, CD manual/tag only |
| Artifact retention policy | Hapus build lama untuk hemat storage |
| Build matrix (debug + release) | Test kedua variant sekaligus |