SaFi Bank Space : Flutter Github Actions for CI/CD and Firbase App Distribution

Table of contents :

Overview

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production.


Firebase App Distribution makes distributing your apps to trusted testers painless. By getting your apps onto testers' devices quickly, you can get feedback early and often. And if you use Crashlytics in your apps, you’ll automatically get stability metrics for all your builds, so you know when you’re ready to ship.

Overall CI/CD Script

name: SaFi Mobile App Pipeline

on:
  push:
    paths:
      - app/**
      - .github/workflows/safi-mobile-app-ci.yml
      - .github/workflows/safi-mobile-analyze-test-ci.yml
      - .github/workflows/safi-mobile-app-build-android-ci.yml
      - .github/workflows/safi-mobile-app-build-ios-ci.yml
      - .github/workflows/safi-mobile-app-upload-android-ci.yml
      - .github/workflows/safi-mobile-app-upload-ios-ci.yml
      - .github/workflows/safi-mobile-app-manual-trigger-ci.yml
    #TODO: change with main branch
    branches:
      - main
    tags:
      - sma-[0-9]+.[0-9]+.[0-9]+\+[0-9]+
    # paths-ignore:
    #   - '**/README.md'

  pull_request:
    paths:
      - app/**
      - .github/workflows/safi-mobile-app-ci.yml
      - .github/workflows/safi-mobile-analyze-test-ci.yml
      - .github/workflows/safi-mobile-app-build-android-ci.yml
      - .github/workflows/safi-mobile-app-build-ios-ci.yml
      - .github/workflows/safi-mobile-app-upload-android-ci.yml
      - .github/workflows/safi-mobile-app-upload-ios-ci.yml
      - .github/workflows/safi-mobile-app-manual-trigger-ci.yml
    #TODO: change with main branch
    branches:
      - main

jobs:
  mobile_versioning:
    name: mobile versioning
    needs:
      - analyze_test_main_app
      - analyze_test_feature_account
      - analyze_test_feature_cards
      - analyze_test_feature_dashboard
      - analyze_test_feature_loans
      - analyze_test_feature_login
      - analyze_test_feature_mobile_data
      - analyze_test_feature_onboarding
      - analyze_test_feature_transactions
      - analyze_test_feature_transactions_hive_example
      - analyze_test_generic_analytics
      - analyze_test_generic_app_config
      - analyze_test_generic_module_common
      - analyze_test_generic_monitoring
      - analyze_test_generic_template
      - analyze_test_generic_ui
      - analyze_test_library_device_fingeprint_plugin
      - analyze_test_library_extensions
      - analyze_test_library_forgerock_plugin
      - analyze_test_library_injection
      - analyze_test_library_iqa_plugin
      - analyze_test_library_liveness_plugin
      - analyze_test_library_logger
      - analyze_test_library_performance_monitoring
      - analyze_test_library_video_kyc
      - analyze_test_library_vida
      - analyze_test_feature_subsctiption
    #TODO: change with main branch
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          token: "${{ secrets.SMA_GITHUB_TOKEN }}"
      - uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "11"
      # There is issue on this action,
      # so we need to hardcode the version to 2.4.0
      # https://github.com/subosito/flutter-action/issues/179#issuecomment-1195120616
      - uses: subosito/flutter-action@v2.4.0
        with:
          flutter-version: "3.3.0"
      - name: update patch version
        working-directory: ./app/app_safi
        env:
          SMA_GITHUB_TAG_PREFIX: "sma"
        run: |
          releaseNotes="${{ github.event.head_commit.message }}"
          git fetch --all --tags
          githubBumpedVersion=$(git tag --sort=creatordate -l "${SMA_GITHUB_TAG_PREFIX}*" | tail -1 | cut -d'-' -f2)
          echo githubBumpedVersion:$githubBumpedVersion
          version=".version=\"${githubBumpedVersion}\""
          echo version:$version
          yq -i "${version}" pubspec.yaml
          echo updated version in pubspec.yaml:$(yq eval '.version' pubspec.yaml)
          dart pub global activate cider
          export PATH="$PATH":"$HOME/.pub-cache/bin"
          cider bump patch --bump-build
          bumpedVersion="$(yq eval '.version' pubspec.yaml)"
          regex="^([0-9]{1,}\.[0-9]{1,}\.[0-9]{1,})\+([0-9]{1,})$"
          if [[ $bumpedVersion =~ $regex ]]; then
            VERSION=${BASH_REMATCH[1]}
            BUILD_NUMBER=${BASH_REMATCH[2]}
            nextTag="${SMA_GITHUB_TAG_PREFIX}-${VERSION}+${BUILD_NUMBER}"
            if [ $(git tag --list "${tagName}") ]; then
              echo "ERROR: The tag ${tagName} already exists..."
              exit 1
            fi
            git config --global user.email 'ropiudin@dkatalis.com'
            git config --global user.name 'ropiDK'
            NEXT_COMMIT_MESSAGE="Bump Version to ${VERSION}+${BUILD_NUMBER} : ${releaseNotes}"
            git add .
            git commit -m "${NEXT_COMMIT_MESSAGE}"
            git tag "${nextTag}"
            git commit --amend -m "${NEXT_COMMIT_MESSAGE}"
            git push origin ${nextTag}
          else
            echo "ERROR: The version in pubspec.yaml doesn't match the pattern X.Y.Z+W";
            exit 1;
          fi
  ### -- Analyze Test -- ###
  analyze_test_main_app:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/app_safi
  ### -- Feature Analyze Test -- ###
  analyze_test_feature_account:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/account
  analyze_test_feature_cards:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/cards
  analyze_test_feature_dashboard:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/dashboard
  analyze_test_feature_loans:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/loans
  analyze_test_feature_login:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/login
  analyze_test_feature_mobile_data:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/mobile_data
  analyze_test_feature_onboarding:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/onboarding
  analyze_test_feature_subsctiption:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/subscription
  analyze_test_feature_transactions:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/transactions
  analyze_test_feature_transactions_hive_example:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/feature/transactions_hive_example
  ### -- End of Feature Analyze Test -- ###
  ### -- Generic Analyze Test -- ###
  analyze_test_generic_analytics:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/analytics
  analyze_test_generic_app_config:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/app_config
  analyze_test_generic_module_common:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/module_common
  analyze_test_generic_monitoring:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/monitoring
  analyze_test_generic_template:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/template
  analyze_test_generic_ui:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/ui
  ### -- End of Generic Analyze Test -- ###
  ### -- Library Analyze Test -- ###
  analyze_test_library_device_fingeprint_plugin:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/device_fingeprint/device_fingeprint_plugin
  analyze_test_library_extensions:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/extensions
  analyze_test_library_forgerock_plugin:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/forgerock/forgerock_plugin
  analyze_test_library_injection:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/injection
  analyze_test_library_iqa_plugin:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/iqa/iqa_plugin
  analyze_test_library_liveness_plugin:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/liveness/liveness_plugin
  analyze_test_library_logger:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/logger
  analyze_test_library_performance_monitoring:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/performance_monitoring
  analyze_test_library_vida:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/vida/vida_plugin
  analyze_test_library_video_kyc:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/library/video_kyc/vkyc_plugin
  ### -- End of Library Analyze Test -- ###
  ### -- End of Analyze Test -- ###
  ## Dev
  build_ios_dev:
    name: Build Ios Dev
    needs:
      - analyze_test_main_app
      - analyze_test_feature_account
      - analyze_test_feature_cards
      - analyze_test_feature_dashboard
      - analyze_test_feature_loans
      - analyze_test_feature_login
      - analyze_test_feature_mobile_data
      - analyze_test_feature_onboarding
      - analyze_test_feature_transactions
      - analyze_test_feature_transactions_hive_example
      - analyze_test_generic_analytics
      - analyze_test_generic_app_config
      - analyze_test_generic_module_common
      - analyze_test_generic_monitoring
      - analyze_test_generic_template
      - analyze_test_generic_ui
      - analyze_test_library_device_fingeprint_plugin
      - analyze_test_library_extensions
      - analyze_test_library_forgerock_plugin
      - analyze_test_library_injection
      - analyze_test_library_iqa_plugin
      - analyze_test_library_liveness_plugin
      - analyze_test_library_logger
      - analyze_test_library_performance_monitoring
      - analyze_test_library_video_kyc
      - analyze_test_library_vida
      - analyze_test_feature_subsctiption
    if: startsWith(github.ref, 'refs/tags')
    uses: ./.github/workflows/safi-mobile-app-build-ios-ci.yml
    with:
      fastlane-env: dev
      environment-variable: nonprod
      p12-file: // base 64 p12-file generated
      p12-password: Lyra123 //just for example
      google-cloud-project-id: //google cloud project id
      firebase-token: // firebase token using firebase login
  build_apk_dev:
    name: Build Android Dev
    needs:
      - analyze_test_main_app
      - analyze_test_feature_account
      - analyze_test_feature_cards
      - analyze_test_feature_dashboard
      - analyze_test_feature_loans
      - analyze_test_feature_login
      - analyze_test_feature_mobile_data
      - analyze_test_feature_onboarding
      - analyze_test_feature_transactions
      - analyze_test_feature_transactions_hive_example
      - analyze_test_generic_analytics
      - analyze_test_generic_app_config
      - analyze_test_generic_module_common
      - analyze_test_generic_monitoring
      - analyze_test_generic_template
      - analyze_test_generic_ui
      - analyze_test_library_device_fingeprint_plugin
      - analyze_test_library_extensions
      - analyze_test_library_forgerock_plugin
      - analyze_test_library_injection
      - analyze_test_library_iqa_plugin
      - analyze_test_library_liveness_plugin
      - analyze_test_library_logger
      - analyze_test_library_performance_monitoring
      - analyze_test_library_video_kyc
      - analyze_test_library_vida
      - analyze_test_feature_subsctiption
    if: startsWith(github.ref, 'refs/tags')
    uses: ./.github/workflows/safi-mobile-app-build-android-ci.yml
    with:
      environment-variable: nonprod
      google-cloud-key: //base 64 google cloud key
      google-cloud-project-id: // project id
      firebase-token: // firebase token
  upload_apk_dev:
    name: Upload Android Dev to Firebase App Distribution
    needs: [build_apk_dev]
    uses: ./.github/workflows/safi-mobile-app-upload-android-ci.yml
    with:
      app-id: //app id in firebase
      environment-variable: nonprod
      firebase-token: // firebase token
      groups: "Dkatalis, Testers, Vacuumlabs"
  upload_ipa_dev:
    name: Upload Ios Dev to TestFlight
    needs: [build_ios_dev]
    uses: ./.github/workflows/safi-mobile-app-upload-ios-ci.yml
    with:
      key-id: // key id of apple dev
      issuer-id: // issuer id
      key-path: "ios.p8"
      api-key: //api-key
      environment-variable: nonprod

Mobile Versioning

mobile_versioning:
    name: mobile versioning
    needs: [analyze_test_main_app,analyze_test_analytics,analyze_test_dashboard,analyze_test_injection,analyze_test_iqa_plugin,analyze_test_liveness_plugin,analyze_test_login,analyze_test_onboarding,analyze_test_template,analyze_test_ui,analyze_test_video_kyc]
    #TODO: change with main branch
    if: github.ref == 'refs/heads/feature/application-setup'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
      with:
        token: 'YOUR_PERSONAL_TOKEN'
    - uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - uses: subosito/flutter-action@v1
      with:
        flutter-version: '2.10.5'
    - name: update patch version
      working-directory: ./app/app_safi
      run: |
        dart pub global activate cider
        export PATH="$PATH":"$HOME/.pub-cache/bin"
        cider bump patch --bump-build
        bumpedVersion="$(yq eval '.version' pubspec.yaml)"
        regex="^([0-9]{1,}.[0-9]{1,}.[0-9]{1,})\+([0-9]{1,})$"
        if [[ $bumpedVersion =~ $regex ]]; then
          VERSION=${BASH_REMATCH[1]}
          BUILD_NUMBER=${BASH_REMATCH[2]}
          nextTag="${VERSION}+${BUILD_NUMBER}"
          if [ $(git tag --list "${tagName}") ]; then
            echo "ERROR: The tag ${tagName} already exists..."
            exit 1
          fi
          git config --global user.email 'ropiudin@dkatalis.com'
          git config --global user.name 'ropiDK'
          NEXT_COMMIT_MESSAGE="Bump Version to ${VERSION}+${BUILD_NUMBER}"
          git add .
          git commit -m "${NEXT_COMMIT_MESSAGE}"
          git tag "${nextTag}"
          git commit --amend -m "${NEXT_COMMIT_MESSAGE} [skip ci]"
          git push
          git push origin ${nextTag}
        else
          echo "ERROR: The version in pubspec.yaml doesn't match the pattern X.Y.Z+W";
          exit 1;
        fi

This code to automatically increment patch and build version using cider.

Generate personal access token

To generetate personal acces token, just follow step bellow :

  • YOUR_PERSONAL_ACCOUNT for token =

https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token

Add CI/CD for Analyzer, Unit Test and Code Coverage

CI/CD Code : Reusable analyze and test workflow (safi-mobile-analyze-test-ci.yml)

name: Reusable test and analyze mobile app

on:
  workflow_call:
    inputs:
      working-directory:
        required: true
        type: string

jobs:
  analyze_test:
    name: Analyzer and Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "11"
      # There is issue on this action,
      # so we need to hardcode the version to 2.4.0
      # https://github.com/subosito/flutter-action/issues/179#issuecomment-1195120616
      - uses: subosito/flutter-action@v2.4.0
        with:
          flutter-version: "3.3.0"
      - name: analyze and test
        working-directory: .${{ inputs.working-directory }}
        run: |
          flutter pub get
          flutter analyze
          flutter test --coverage
          flutter pub run build_runner build --delete-conflicting-outputs || true
          [[ -z "$(git status --porcelain)" ]] || (echo "Please run scripts/tools/pub_all_modules.sh -gbda and commit\n$(git status --porcelain)" && exit 1)
      - name: Setup sonarqube
        uses: warchant/setup-sonar-scanner@v3
      - name: Run Sonarqube tests
        working-directory: .${{ inputs.working-directory }}
        env:
          SONARQUBE_URL: //sonar url
          SONARQUBE_TOKEN: //sonar token
        run: |
          SONARQUBE_PROJECT=$(yq '.name' pubspec.yaml)
          SONARQUBE_PROJECT_VERSION=$(yq '.version' pubspec.yaml)
          SONARQUBE_PROPERTIES_FILE="${GITHUB_WORKSPACE}/app/sonar-project.properties"
          echo "This is the path: ${SONARQUBE_PROPERTIES_FILE}"
          sonar-scanner \
          -Dsonar.projectKey=${SONARQUBE_PROJECT} \
          -Dsonar.projectVersion=${SONARQUBE_PROJECT_VERSION} \
          -Dproject.settings=${SONARQUBE_PROPERTIES_FILE} \
          -Dsonar.sources=.

we will reuse this code, in main workflow, like this :

 analyze_test_main_app:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/app_safi
  analyze_test_analytics:
    uses: ./.github/workflows/safi-mobile-analyze-test-ci.yml
    with:
      working-directory: /app/generic/analytics
 ...etc

Setup Github Actions (Android)

In this section, we will setup CI/CD for build APK and publish to Firebase App Distribution.

Add CI/CD for Build APK

Reusable workflow (safi-mobile-app-build-android-ci.yml)

name: Reusable android build

on:
  workflow_call:
    inputs:
      environment-variable:
        required: true
        type: string
      google-cloud-key:
        required: true
        type: string
      google-cloud-project-id:
        required: true
        type: string
      firebase-token:
        required: true
        type: string

jobs:
  build_apk:
    name: Build Flutter (Android)
    runs-on: ubuntu-latest
    env:
      FIREBASE_TOKEN: ${{ inputs.firebase-token }}
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "11"
      - uses: subosito/flutter-action@v2.4.0
        with:
          flutter-version: "3.3.0"
      - name: Build
        working-directory: ./app/app_safi
        run: |
          rm -f ${{ github.workspace }}/app/app_safi/lib/env.dart
          mv ${{ github.workspace }}/app/scripts/tools/env/env_${{ inputs.environment-variable }}.dart ${{ github.workspace }}/app/app_safi/lib/env.dart
          flutter clean
          flutter pub get
          npm install -g firebase-tools
          dart pub global activate flutterfire_cli
          APP_BUNDLE="ph.safibank.app.${{ inputs.environment-variable }}"
          flutterfire configure -y -t "${{env.FIREBASE_TOKEN}}" "--platforms=ios,android" -p "${{ inputs.google-cloud-project-id }}" -a "${APP_BUNDLE}" -i "${APP_BUNDLE}" -m "${APP_BUNDLE}"
          export environment=${{ inputs.environment-variable }}
          bumpedVersion="$(yq eval '.version' pubspec.yaml)"
          regex="^([0-9]{1,}\.[0-9]{1,}\.[0-9]{1,})\+([0-9]{1,})$"
          if [[ $bumpedVersion =~ $regex ]]; then
            BUILD_NUMBER=${BASH_REMATCH[2]}
          fi
          flutter build apk --release --build-name=$(echo $GITHUB_REF | sed -e "s#refs/tags/##g")  --build-number=$BUILD_NUMBER
      - name: Upload
        uses: actions/upload-artifact@v3
        with:
          name: apk-build-${{ inputs.environment-variable }}
          path: ${{ github.workspace }}/app/app_safi/build/app/outputs/apk/release
          if-no-files-found: error

This job is to build apk, and will upload to artifacts.

Setup Firebase App Distribution

App ID on Firebase App Distribution

  • First, To integrate Github Actions and Firebase App Distribution you need App ID of your project on firebase, you can find it on your firebase console on setting, see picture below:

  • After that, you need firebase token, you can run on your terminal :

firebase login:ci

Register Tester on App Distribution

To resgister tester on the app distribution you can goto firebase console > realease and monitor > app distribution, and you can register the tester on it :

after that, you will received email, and click get started to approve the invitation :

Integrate Firebase App Distribution to Github Actions

After we setup our firebase, we will integrate it to Github actions safi-mobile-app-upload-android-ci.yml :

name: Reusable upload android

on:
  workflow_call:
    inputs:
      app-id:
        required: true
        type: string
      environment-variable:
        required: true
        type: string
      firebase-token:
        required: true
        type: string
      groups:
        required: true
        type: string
      
jobs:
  publish_app_distribution:
      name: Upload Android to Firebase App Distribution
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v3
        - uses: actions/setup-java@v3
          with:
            distribution: "zulu"
            java-version: "11"
        - name: Download Artifact
          uses: actions/download-artifact@master
          with:
            name: apk-build-${{ inputs.environment-variable }}
        - name: Upload APK
          uses: wzieba/Firebase-Distribution-Github-Action@v1.3.3
          with:
            appId: ${{ inputs.app-id }}
            token: ${{ inputs.firebase-token }}
            groups: ${{ inputs.groups }}
            releaseNotes: ${{ github.event.head_commit.message }}
            file: app-release.apk

Build IPA for Ios and publish to TestFlight

Setting Fastfile for ios build

Before we continue to build the CI, we need Google Cloud Keys (you can get it from SRE team), copy the gc_keys.json to ios folder on app_safi, and setting Fastfile like this :

###
### Disclaimer: Fastlane won't work properly until we have apple account
###
default_platform(:ios)

platform :ios do
  groupBundleId = "ph.safibank.app"
  appDisplayName = "SaFi"

  desc "Create Dev Build"
  lane :dev do
    bundleId = groupBundleId + ".nonprod"
    appDisplayName += " - Dev"
    build(
      variant: "dev",
      appDisplayName: appDisplayName,
      baseBundleId: bundleId,
      appProfile: "match AppStore "+ bundleId,
      exportMethod: "app-store",
      teamId: "2368TD7QJ5"
  )
  end

  desc "Create Stage Build"
  lane :stage do
    bundleId = groupBundleId + ".stage"
    appDisplayName += " - Stage"
    build(
      variant: "stage",
      appDisplayName: appDisplayName,
      baseBundleId: bundleId,
      appProfile: "match AppStore "+ bundleId,
      exportMethod: "app-store",
      teamId: "2368TD7QJ5"
  )
  end

  desc "Common build commands for generating an iOS build"
  lane :build do |options|
    variant = options[:variant]
    appDisplayName = options[:appDisplayName]
    baseBundleId = options[:baseBundleId]
    appProfile = options[:appProfile]
    exportMethod = options[:exportMethod]
    teamId = options[:teamId]
    
    update_code_signing_settings(
      path: "Runner.xcodeproj",
      profile_name: "#{appProfile}",
      bundle_identifier: "#{baseBundleId}",
    )
    update_app_identifier(
      app_identifier: "#{baseBundleId}",
      plist_path: "Runner/Info.plist"
    )
    update_info_plist(
      app_identifier: "#{baseBundleId}",
      plist_path: "Runner/Info.plist",
      display_name: "#{appDisplayName}"
    )
    update_project_team(
      path: "Runner.xcodeproj/",
      teamid: "#{teamId}"
    )

    Dir.chdir("../..") do
      ENV["environment"]="#{variant}"
      sh "flutter clean;flutter pub get"
      Dir.chdir("ios") do
        if "#{variant}" == "prod"
          # Do Special treatment for prod here
        end
        sh "rm -rf ~/Library/Caches/CocoaPods"
        sh "rm -rf Pods || true"
        sh "rm -rf ~/Library/Developer/Xcode/DerivedData/*"
        sh "pod --version"
        sh "fastlane --version"
        sh "pod install"
        sh "echo '--- Print project.pbxproj for debugging'; cat Runner.xcodeproj/project.pbxproj"
        sh "echo '--- Print Podfile.lock for debugging'; cat Podfile.lock"
      end
    end
    
    install_certificates(
      variant: variant,
      baseBundleId: baseBundleId,
      teamId: teamId
    )

    gym(
      export_method: "#{exportMethod}",
      xcargs: "-allowProvisioningUpdates APP_PROFILE='#{appProfile}' CODE_SIGN_IDENTITY='iPhone Distribution' CODE_SIGN_STYLE='Manual' TARGETED_DEVICE_FAMILY=\"1\"",
      scheme: 'Runner',
      output_name: "SaFi.ipa",
      export_options: {
        provisioningProfiles: {
          "#{baseBundleId}": "#{appProfile}",
        }
      }
    )
  end

  desc "Download, install, and push profiles to repo (set 'readonly: false' to sync with AppStoreConnect)"
  lane :install_certificates do |options|
    variant = options[:variant]
    baseBundleId = options[:baseBundleId]
    teamId = options[:teamId]
    bucketName = nil
    gCloudKeyFile = ENV["GOOGLE_CLOUD_KEY_FILE"]
    gCloudProjectId = ENV["GOOGLE_CLOUD_PROJECT_ID"]

    appIdentifiers = [
      baseBundleId
    ]

    if variant == "prod"
      bucketName = "mobile_ios_certificates"
    elsif variant == "stage"
      bucketName = "safi-stage-mobile-ios-certs"
    else
      bucketName = "mobile_ios_certificates"
    end

    # if is_ci
      match(
        type: "appstore",
        readonly: true,
        team_id: teamId,
        # keychain_password: ENV["KEYCHAIN_PASSWORD"],
        storage_mode: "google_cloud",
        app_identifier: appIdentifiers,
        google_cloud_bucket_name: bucketName,
        google_cloud_keys_file: gCloudKeyFile,
        google_cloud_project_id: gCloudProjectId
      )
  end


  desc "Upload binary to TestFlight"
  lane :upload do
    iosKeyId = ENV["IOS_KEY_ID"]
    iosIssuerId = ENV["IOS_ISSUER_ID"]
    iosApiKey = ENV["IOS_API_KEY_PATH"]
    
    apiKeyDetails = app_store_connect_api_key(
      key_id: "#{iosKeyId}",
      issuer_id: "#{iosIssuerId}",
      key_filepath: "#{iosApiKey}"
    )

    changes = changelog_from_git_commits(commits_count: 1, merge_commit_filtering: "exclude_merges")
    upload_to_testflight(
      api_key: apiKeyDetails,
      changelog: changes,
      skip_submission: true,
      groups: ["developers"],
      skip_waiting_for_build_processing: true
    )
  end
end

safi-mobile-app-build-ios-ci.yml :

name: Reusable ios build

on:
  workflow_call:
    inputs:
      environment-variable:
        required: true
        type: string
      fastlane-env:
        required: true
        type: string
      p12-file:
        required: true
        type: string
      p12-password:
        required: true
        type: string
      google-cloud-project-id:
        required: true
        type: string
      google-cloud-key:
        required: true
        type: string
      firebase-token:
        required: true
        type: string

jobs:
  build_ios:
    runs-on: macos-latest
    env:
      P12_FILE: ${{ inputs.p12-file }}
      P12_PASSWORD: ${{ inputs.p12-password }}
      GOOGLE_CLOUD_PROJECT_ID: ${{ inputs.google-cloud-project-id }}
      GOOGLE_CLOUD_KEY: ${{ inputs.google-cloud-key }}
      GOOGLE_CLOUD_KEY_FILE: "./gc_keys.json"
      FIREBASE_TOKEN: ${{ inputs.firebase-token }}
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "11"
      - uses: subosito/flutter-action@v2.4.0
        with:
          flutter-version: "3.3.0"
      - name: Import Code-Signing Certificates
        uses: Apple-Actions/import-codesign-certs@v1
        with:
          p12-file-base64: ${{env.P12_FILE}}
          p12-password: ${{env.P12_PASSWORD}}
      - name: Prepare ios build
        working-directory: ./app/app_safi
        run: |
          rm -f ${{ github.workspace }}/app/app_safi/lib/env.dart
          mv ${{ github.workspace }}/app/scripts/tools/env/env_${{ inputs.environment-variable }}.dart ${{ github.workspace }}/app/app_safi/lib/env.dart
          rm -f ${{ github.workspace }}/app/app_safi/ios/Runner/GoogleService-Info.plist
          mv ${{ github.workspace }}/app/scripts/tools/ios/GoogleService-Info-${{ inputs.environment-variable }}.plist ${{ github.workspace }}/app/app_safi/ios/Runner/GoogleService-Info.plist
          export environment=${{ inputs.environment-variable }}
          npm install -g firebase-tools
          dart pub global activate flutterfire_cli
          APP_BUNDLE="ph.safibank.app.${{ inputs.environment-variable }}"
          flutterfire configure -y -t "${{env.FIREBASE_TOKEN}}" "--platforms=ios,android" -p "${{ inputs.google-cloud-project-id }}" -a "${APP_BUNDLE}" -i "${APP_BUNDLE}" -m "${APP_BUNDLE}"
      - name: Set up ruby env
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.8
          bundler-cache: false
      - name: build ios app
        working-directory: ./app/app_safi/ios
        run: |
          bundle install
          echo "$GOOGLE_CLOUD_KEY" | base64 --decode > gc_keys.json
          GOOGLE_CLOUD_KEY_FILE=${{env.GOOGLE_CLOUD_KEY_FILE}}
          GOOGLE_CLOUD_PROJECT_ID=${{env.GOOGLE_CLOUD_PROJECT_ID}}
          bundle exec fastlane ${{ inputs.fastlane-env }}
      - name: Upload app-store ipa and dsyms to artifacts
        uses: actions/upload-artifact@v3
        with:
          name: app-store-${{ inputs.environment-variable }}
          path: |
            ${{ github.workspace }}/app/app_safi/ios/SaFi.ipa
            ${{ github.workspace }}/app/app_safi/ios/*.app.dSYM.zip

safi-mobile-app-upload-ios-ci.yml :

name: Reusable upload ios

on:
  workflow_call:
    inputs:
      key-id:
        required: true
        type: string
      issuer-id:
        required: true
        type: string
      key-path:
        required: true
        type: string
      api-key:
        required: true
        type: string
      environment-variable:
        required: true
        type: string

jobs:
  publish_to_testflight:
    runs-on: macos-latest
    env:
      IOS_KEY_ID: ${{ inputs.key-id }}
      IOS_ISSUER_ID: ${{ inputs.issuer-id }}
      IOS_API_KEY_PATH: ${{ inputs.key-path }}
      IOS_API_KEY: ${{ inputs.api-key }}
    steps:
      - uses: actions/checkout@v3
      - name: Set up ruby env
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6.8
          bundler-cache: true
      - name: Download Artifact
        uses: actions/download-artifact@master
        with:
          name: app-store-${{ inputs.environment-variable }}
      - name: Publish iOS binary
        run: |
          mv SaFi.ipa ${{ github.workspace }}/app/app_safi/ios/SaFi.ipa
          mv *.app.dSYM.zip ${{ github.workspace }}/app/app_safi/ios/*.app.dSYM.zip
          cd app/app_safi/ios/
          echo "$IOS_API_KEY" | base64 --decode > ios.p8
          bundle install
          IOS_KEY_ID=${{env.IOS_KEY_ID}}
          IOS_ISSUER_ID=${{env.IOS_ISSUER_ID}}
          IOS_API_KEY_PATH=${{env.IOS_API_KEY_PATH}}
          bundle exec fastlane upload

Variable name and description

Variable

Description

fastlane-env

environment that we setup in /SaFiMono/app/app_safi/ios/fastlane/Fastfile e.g : dev, stage, etc

environment-variable

environment that we setup in /SaFiMono/app/app_safi/android/app/build.gradle e.g : nonprod, prod, stage, etc

p12-file

base64 of p12 file, to generate p12 file, run fastlane {env}, e.g : fastlane stage

in it will generated on google cloud bucket :

before you run fastlane {env} , make sure you change read only to false (only for the first time, on FastFile, on line 131) :


p12-password

change password that you want

google-cloud-project-id

On Firebase console, goto project setting :

and you will get the value :


google-cloud-key

base64 of google cloud key json.

firebase-token

you can get token by run command :
firebase login:cilogin:ci

keystore-alias

app package name, e.g : ph.safibank.app.nonprod

keystore-file

base64 of kaystore file (jks), follow this instruction : https://docs.flutter.dev/deployment/android#create-an-upload-keystore

keystore-password

keystore password on keystore file

keystore-store-password

keystore store password on keystore file

app-id

groups

tester groups in Firebase App Distribution, e.g : "Dkatalis, Testers, Vacuumlabs"

key-id

key id of Apple Developer

issuer-id

issuer id of Apple Developer

key-path

key path file name just hardcoded to ios.p8

api-key

base64 of ios.p8 file (download it on Apple Developer console)

refferences :

https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions

https://firebase.google.com/docs/app-distribution