# CI/CD Pipeline ## GitHub Actions Setup ### Basic Workflow **.github/workflows/ci.yml:** ```yaml name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile - name: Run linter run: yarn lint - name: Run tests run: yarn test --coverage - name: Type check run: yarn type-check - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage/lcov.info build-android: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Setup Java uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '11' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Install dependencies run: yarn install --frozen-lockfile - name: Cache Gradle uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - name: Build Android APK run: | cd android ./gradlew assembleRelease - name: Upload APK uses: actions/upload-artifact@v3 with: name: android-apk path: android/app/build/outputs/apk/release/app-release.apk build-ios: needs: test runs-on: macos-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile - name: Install CocoaPods run: | cd ios pod install - name: Build iOS run: | cd ios xcodebuild -workspace SaayamApp.xcworkspace \ -scheme SaayamApp \ -configuration Release \ -destination generic/platform=iOS \ -archivePath $PWD/build/SaayamApp.xcarchive \ archive ``` ### Advanced Workflow with Fastlane **.github/workflows/deploy.yml:** ```yaml name: Deploy to Stores on: push: tags: - 'v*' jobs: deploy-android: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Setup Java uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '11' - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' bundler-cache: true - name: Install dependencies run: yarn install --frozen-lockfile - name: Decode Keystore env: ENCODED_STRING: ${{ secrets.KEYSTORE_BASE64 }} run: | echo $ENCODED_STRING | base64 -d > android/app/keystore.jks - name: Deploy to Play Store env: SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | cd android bundle exec fastlane deploy deploy-ios: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' bundler-cache: true - name: Install dependencies run: yarn install --frozen-lockfile - name: Install CocoaPods run: | cd ios pod install - name: Deploy to App Store env: MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} run: | cd ios bundle exec fastlane deploy ``` ## Fastlane Setup ### Installation ```bash # Install Fastlane sudo gem install fastlane # Initialize Fastlane for iOS cd ios && fastlane init # Initialize Fastlane for Android cd android && fastlane init ``` ### iOS Fastfile **ios/fastlane/Fastfile:** ```ruby default_platform(:ios) platform :ios do desc "Build and upload to TestFlight" lane :beta do setup_ci if ENV['CI'] match( type: "appstore", readonly: true ) increment_build_number( xcodeproj: "SaayamApp.xcodeproj" ) build_app( scheme: "SaayamApp", export_method: "app-store", export_options: { provisioningProfiles: { "com.saayam.app" => "match AppStore com.saayam.app" } } ) upload_to_testflight( skip_waiting_for_build_processing: true ) slack( message: "Successfully uploaded a new build to TestFlight! 🚀", channel: "#releases" ) end desc "Deploy to App Store" lane :deploy do setup_ci if ENV['CI'] match( type: "appstore", readonly: true ) build_app( scheme: "SaayamApp", export_method: "app-store" ) upload_to_app_store( force: true, reject_if_possible: true, skip_metadata: false, skip_screenshots: false, submit_for_review: true, automatic_release: false, submission_information: { add_id_info_limits_tracking: true, add_id_info_serves_ads: false, add_id_info_tracks_action: true, add_id_info_tracks_install: true, add_id_info_uses_idfa: true, content_rights_has_rights: true, content_rights_contains_third_party_content: true, export_compliance_platform: 'ios', export_compliance_compliance_required: false, export_compliance_encryption_updated: false, export_compliance_app_type: nil, export_compliance_uses_encryption: false, export_compliance_is_exempt: false, export_compliance_contains_third_party_cryptography: false, export_compliance_contains_proprietary_cryptography: false, export_compliance_available_on_french_store: false } ) slack( message: "Successfully deployed to App Store! 🎉", channel: "#releases" ) end desc "Create screenshots" lane :screenshots do capture_screenshots upload_to_app_store(skip_binary_upload: true) end error do |lane, exception| slack( message: "Error in #{lane}: #{exception.message}", success: false, channel: "#releases" ) end end ``` ### Android Fastfile **android/fastlane/Fastfile:** ```ruby default_platform(:android) platform :android do desc "Build and upload to Play Store Internal Testing" lane :beta do gradle( task: "clean bundleRelease", project_dir: "." ) upload_to_play_store( track: 'internal', release_status: 'draft', skip_upload_metadata: true, skip_upload_changelogs: true, skip_upload_images: true, skip_upload_screenshots: true ) slack( message: "Successfully uploaded a new build to Play Store Internal Testing! 🚀", channel: "#releases" ) end desc "Deploy to Play Store" lane :deploy do gradle( task: "clean bundleRelease", project_dir: "." ) upload_to_play_store( track: 'production', release_status: 'completed', skip_upload_metadata: false, skip_upload_changelogs: false, skip_upload_images: false, skip_upload_screenshots: false ) slack( message: "Successfully deployed to Play Store! 🎉", channel: "#releases" ) end desc "Create screenshots" lane :screenshots do capture_android_screenshots upload_to_play_store(skip_upload_apk: true) end error do |lane, exception| slack( message: "Error in #{lane}: #{exception.message}", success: false, channel: "#releases" ) end end ``` ## Environment Configuration ### Secrets Management **GitHub Secrets to configure:** - `KEYSTORE_BASE64`: Base64 encoded Android keystore - `KEYSTORE_PASSWORD`: Android keystore password - `KEY_ALIAS`: Android key alias - `KEY_PASSWORD`: Android key password - `GOOGLE_PLAY_SERVICE_ACCOUNT`: Google Play service account JSON - `MATCH_PASSWORD`: iOS certificates password - `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`: Apple app-specific password - `FASTLANE_SESSION`: Fastlane session for Apple ID - `SLACK_URL`: Slack webhook URL for notifications ### Match Setup (iOS) ```bash # Initialize match fastlane match init # Generate certificates fastlane match development fastlane match appstore ``` **Matchfile:** ```ruby git_url("https://github.com/your-org/certificates") storage_mode("git") type("development") app_identifier(["com.saayam.app"]) username("your-apple-id@example.com") ``` ## Branch Protection ### GitHub Branch Protection Rules ```yaml # .github/branch-protection.yml protection_rules: main: required_status_checks: strict: true contexts: - "test" - "build-android" - "build-ios" enforce_admins: true required_pull_request_reviews: required_approving_review_count: 2 dismiss_stale_reviews: true require_code_owner_reviews: true restrictions: users: [] teams: ["core-team"] ``` ## Automated Testing in CI ### E2E Testing in CI **.github/workflows/e2e.yml:** ```yaml name: E2E Tests on: pull_request: branches: [main] jobs: e2e-ios: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile - name: Install CocoaPods run: | cd ios pod install - name: Build for Detox run: detox build --configuration ios.sim.release - name: Run Detox tests run: detox test --configuration ios.sim.release --cleanup e2e-android: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'yarn' - name: Setup Java uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '11' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: AVD cache uses: actions/cache@v3 id: avd-cache with: path: | ~/.android/avd/* ~/.android/adb* key: avd-29 - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: false script: echo "Generated AVD snapshot for caching." - name: Install dependencies run: yarn install --frozen-lockfile - name: Run Detox tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 script: | detox build --configuration android.emu.release detox test --configuration android.emu.release --cleanup ```