• [Flutter] Fastlane 이용해서 배포 자동화 하기 #1 iOS 설정

    2023. 6. 22.

    by. dundin

    반응형

    Flutter를 사용한다고 해도, 결국 각 플랫폼 폴더 /ios /android 에서 각각 설정을 해줘야 한다. 

    그래서 이번에 진행해 본 김에 전체적으로 정리해보기로 했습니다 

     

     

    기본 설정 

    일단 Fastlane은 ruby로 설치되기 때문에 ruby version을 통일 시켜줄 필요가 있다. 

    asdf 설정하는 방법은 아래 글을 참고해주세욥 

     

    asdf 적용해서 ruby/node 버전 통일하기

    다른 사람들이랑 협업 하다보면 ruby/node version이 다른 경우가 존재한다. 계속 설치하다보면 루비 버전 계속 늘어나고 .. 갑자기 버전 꼬이고 .. 악몽이 될 수 있기 때문에 asdf 라는 툴을 이용해서

    jooeungen.tistory.com

     

    아래 명령어등을 실행 해서 로비가 잘 설치되어 있는지 확인. 

    $ ruby -v 
    $ which ruby

     

    ruby 로 설치되는 애들의 버전관리를 위한 bundler로 설치해준다. 

    https://bundler.io/

    gem install bundler

     

    bundler를 설치 했으면 이제 /ios 폴더로 이동 

     

    iOS  에서 Fastlane setup 하기 

    Bundler는 Gemfile을 바라본다. 

    ios/Gemfile 을 생성하고 아래 내용을 붙인다. 

    source "https://rubygems.org" 
    gem "fastlane"
    gem "cocoapods", '1.12.1'

     

    bundle update 를 실행 하면 Gemfile.lock 이 생긴다. 

    bundle update

     

     

    이제 아래 초기화 명령어를 통해 초기화를 진행 해도 되는데, 

    bundle exec fastlane init

     

    단순히 폴더를 만들어 주는것이라 mason brick 같은데 그냥 넣어두는게 편할것 같기도 하다. 
    공식 문서: https://docs.fastlane.tools/getting-started/ios/setup/

     

    여튼 fastlane init 을 하고 나면 아래처럼 파일이 추가된다. 

     

     

    iOS  에서 Appfile 살펴보기 

    Appfile 에는 bundle id, apple id, team id등 기본적인 정보들이 들어간다. 

    나중에 fastfile, matchfile 에서 정의되는 함수들이 여기의 정보를 가져다 쓰게 된다. 

    app_identifier("com.dundinstudio.test") # The bundle identifier of your app
    apple_id("admin@dundinstudio.com") # Your Apple Developer Portal username
    
    itc_team_id("1111111") # App Store Connect Team ID
    team_id("xxxxxxx") # Developer Portal Team ID

     

    .env 파일 추가하기 

    환경변수는 보통 .bash_profile, .zshrc에 저장하게 되지만 폴더별로 환경변수를 정의해서 팀원들과 공유해쓰는 것도 가능하다. 

    물론 아래처럼 .env.template 파일만 git에 추가해야 하고 .env 파일은 .gitignore에 추가한다. 

    // .env.template
    
    # Copy this file to '.env' and replace <values> with real ones.
    # Fastlane variables
    export FASTLANE_USER="<yourappleid@yourdomain.com>"
    export FASTLANE_PASSWORD="<your_apple_id_password>"
    export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="<your_app_specific_password>"

     

    이렇게 해두면 계정/비번을 계속 물어보는 일이 없어진다. 

     

    Match 설정하기 

    iOS는 fastlane match를 이용해서 provisioning 관리해 주는게 꿀이다 

    문서: https://docs.fastlane.tools/actions/match/#match

     

    일단 github에 private repository를 하나 파준다. 

    그리고 ios/ 폴더 경로에서 다음 명령어 실행 

    bundle exec fastlane match init

     

    요것도 사실 폴더구성 해주는게 전부라서, 귀찮으면 fastlane match init 스킵하고 그냥 

    Fastlane/Matchfile 경로에 바로 파일 생성해줘도 무관하다  

    git_url("git@github.com:dundinstudio/ios-certificate.git")
    storage_mode("git")
    type("development")

     

    이렇게 하고 나서 아래 명령어를 차례대로 적어주면 되는데, 

    새로운 기기에서 처음 match를 돌리는 경우 repository 비밀번호를 물어보는데, 이건 최초 1번만 입력하면 된다.

    처음 매치돌리는 사람이 기억해뒀다가 다른 사람한테 알려주세욤 

    fastlane match appstore
    fastlane match development
    fastlane match adhoc

     

    처음 match를 돌린 사람이 cert와 prfoile 을 생성하게 되고, 

    다른 사람은 이미 존재하는 cert와 profile을 다운받아서 사용하게 된다. 

     

    match를 사용하면서 꼬이는 경우에 대한 포스팅이 있으니 참고 

    https://jooeungen.tistory.com/entry/Fastlane-인증서-싱크-오류-해결-nuke

     

    [Fastlane] 인증서 싱크 오류 해결 (nuke)

    여러명의 팀에서 작업하다보면 Certification이 꼬이는 경우가 있다. 일단 가장 중요한 것은 모든 팀원이 fastlane을 이용해서 인증서를 관리해야 한다는 점이다. Dev Portal 에서 revoke 하고 새로 발급받

    jooeungen.tistory.com

     

    Firebase 로그인 셋업 

    일단 목표는 Firebase distribution을 통해서 배포를 하는 것이기 때문에, firebase cli를 설치해야 한다. 

    공식문서: https://firebase.google.com/docs/cli#sign-in-test-cli

     

    우리는 asdf를 통해 nodejs를 설치해뒀기 때문에, npm을 설치하겠습니당 

    npm install -g firebase-tools
    firebase login

    이렇게 하면 web prompt가 뜰 것이고, 여기에 프로젝트가 등록되어 있는 Firebase에 로그인된 google id를 선택하면 된다. 

     

    그 다음 cli로 Firebase를 사용 할 수 있도록 plugin을 추가해 준다. 

    공식 문서: https://firebase.google.com/docs/app-distribution/ios/distribute-fastlane

    bundle exec fastlane add_plugin firebase_app_distribution

     

    이러면 Gemfile이 아래 플러그인 관련 코드가 추가된다. 

    source "https://rubygems.org" 
    gem "fastlane"
    gem "cocoapods", '1.12.1'
    
    // 추가된 코드
    plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
    eval_gemfile(plugins_path) if File.exist?(plugins_path)

     

    Fastfile 수정 하기전에 두가지만 더, 

    1. Firebase > 출시 및 모니터링 > AppDistribution 으로 가서 시작하기 클릭 

    2. 우측에 잘보면 토글 버튼이 있음 .. iOS / Android 모두 선택해서 시작하기 눌러주기 

     

    3. 프로젝트 설정 > iOS 앱 > 앱 ID 복사해두기 

    자 이제 준비 끝, fastfile을 수정하러 가보자. 

     

     

    Fastlane을 통해 Firebase App Distribution 앱 배포하기 

     

    Fastfile을 아래와 같이 수정 한다. 

    APP_ID = "com.dundinstudio.testApp"
    NOTIFICATION_APP_ID = "com.dundinstudio.testApp.NotificationService"
    FIREBASE_APP_ID = "xxxxxxxxxxx.ios.xxxxxx"
    archiveDir = '../build/ios/archive/Runner.xcarchive'
    buildDirBeta = "../build/ios/output/beta"
    ipaPathBeta = "#{buildDirBeta}/Runner.ipa"
    
    ##########################################
    ###### Firebase Beta Distribution ########
    ##########################################
    
    desc "Firebase QA용빌드 "
    
    lane :firebase do
      # Firebase 에 배포하는 것은 adhoc 빌드 입니다 
      match(type: "adhoc", app_identifier: [APP_ID, NOTIFICATION_APP_ID], force_for_new_devices: true)
    
      sh("(cd .. && flutter build ipa --release --flavor=beta --dart-define=app.flavor=beta)")
    
      build_app(
        scheme: "beta",
        configuration: "Release-beta",
        # Use archive from flutter build. 'build_app' only converts the archive to ipa.
        skip_build_archive: true,
        archive_path: archiveDir,
        output_directory: buildDirBeta,
        export_method:"ad-hoc",
        export_options: {
          provisioningProfiles: {
            APP_ID_BETA => "match AdHoc " + APP_ID,
            NOTIFICATION_APP_ID_BETA => "match AdHoc " + NOTIFICATION_APP_ID,
          }
        }
        # uncomment this for detailed build error
        #xcargs: "-verbose"
      )
        
      upload_firebase(appId: FIREBASE_APP_ID, ipaPath: ipaPathBeta)
      upload_dsyms()
    end
    
    lane :upload_firebase do |values|
      firebaseAppId = values[:appId]
      ipaPath = values[:ipaPath]
    
      firebase_app_distribution(
        app: firebaseAppId,
        groups: "inhouse-tester",
        ipa_path: ipaPath,
        debug: true
      )
    end
    
    lane :upload_dsyms
      dsymPath = "#{buildDir}/Runner.app.dSYM.zip"
      upload_symbols_to_crashlytics(
        gsp_path: "./Runner/GoogleService-Info.plist", # Will use the postfix if provided, otherwise default
        dsym_path: dsymPath, # Replace with the correct path if needed
        # debug: true,
      )
    
    end

     

    일단 순서는 

    1. match adhoc: 앱이 추가되면 그때마다 provisioning이 새로 생겨야 해서 force_for_new_devices 옵션을 넣었다. 
    2. bundle exec cocoapod install을 실행 하는 함수 
    3. 앱을 빌드 하면 /build 폴더 안에 Runner.ipa 라는 이름으로 빌드가 된다.
    4. 빌드한 앱을 upload_firebase lane을 통해서 firebase에 업로드하기, 이건 firebase_app_distrubution이 알아서 해준다.
    5. upload_dsyms까지 친절하게 ~ 

     

    요기까지 했으면 문제없이 아래처럼 Firebase distribution에 뜨게된다.  

     

     

    Fastlane을 통해 Testflight 앱 배포하기

    Testflight은 오히려 Firebase 배포 보다는 신경쓸게 적다.

    appstore 릴리즈 배포로 적절하게 세팅하고 deliver를 이용해서 배포 하면 되는데, 

    여기서 제일 중요한 포인트는 

     

    FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD 를 환경변수에 꼭 설정해야 한다는 것이다. 

    모든 애플 계정은 이제 2FA 가 필수이고, cli에서 배포하는 경우 이를 우회하기위해서 비밀번호를 설정해야 하는 것이다. 

    export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="<your_app_specific_password>"

     

    AppSpecificPassword는 

    https://appleid.apple.com/account/manage 

    위 링크에서 생성할 수 있습니다.

     

    ##########################################
    ########## AppStore Distribution #########
    ##########################################
    
    desc "AppStore 리뷰 등록"
    lane :release do
      match(type: "appstore", app_identifier: [APP_ID])
      
    sh("(cd .. && flutter build ipa --release --flavor=production --dart-define=app.flavor=production)")
          
      build_app(
        scheme: "production",
        configuration: "Release-production",
        # Use archive from flutter build. 'build_app' only converts the archive to ipa.
        skip_build_archive: true,
        output_directory: "./release",
        export_method:"app-store",
        export_options: {
          provisioningProfiles: {
            "com.dundinstudio.testApp" => "match AppStore com.dundinstudio.testApp",
          }
        }
      )
      
      version = get_version_number(target: "Runner")
      
      deliver(
        app_identifier: APP_ID,
        ipa: "./release/Runner.ipa",
        app_version: version,
        edit_live: false,
        skip_screenshots: true,
        skip_metadata: true,
        submit_for_review: false,
        automatic_release: false,
        phased_release: false
      )
    end
    반응형

    댓글