ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Fastlane 으로 Firebase Distribution 연동하기
    iOS 2020. 8. 6. 10:02

    Fastlane 은 커맨드 라인으로 iOS 어플리케이션을 배포하게 하는 프로그램입니다.

    Firebase Distribution은 애드훅을 이용해 테스트플라이트의 빌드를 거치지 않고 배포하게 하는 툴입니다.

    두개를 조합하면, 커맨드라인에서 파이어베이스로 원큐에 올리는 프로그램을 작성할 수 있습니다.

    이번 포스트에서는 fastlane 사이닝 이슈, firebase distribution 연동 이슈 등은 생략합니다.

     

    https://sesang06.tistory.com/124

     

    Fastlane 으로 iOS 빌드 과정을 손쉽게 자동화하자

    Fastlane 으로 iOS 빌드하기 https://fastlane.tools/ fastlane은 루비 기반 클라이언트 자동 빌드 오픈소스 라이브러리입니다. iOS 프로젝트에서는 코드 사이닝 관리, 스크린샷, 테스트플라이트 업로드, 앱스

    sesang06.tistory.com

     

    https://sesang06.tistory.com/182

     

    애드훅보다 간편하다! 테스트플라이트보다 빠르다! 파이어베이스 iOS 배포를 활용해 보자

    파이어베이스 앱 배포를 이용하면 애드훅보다 간편하게 배포할 수 있습니다. 테스트플라이트 말고 굳이 서드파티 왜 쓰냐 생각하실 수 있는데, 파이어베이스 앱 배포를 쓰면 테스트플라이트에

    sesang06.tistory.com

     

    https://sesang06.tistory.com/187

     

    FastLane Match 를 이용해 사이닝 관리하기

    커맨드 라인으로 할 때 사이닝 이슈 Testflight 로 업로드할 때는 appstore, Firebase Distirbution 으로 배포할 때는 adhoc 의 사이닝이 적절하게 위 깃허브에 저장되어 있어야 합니다. 그렇지 않으면 사이닝�

    sesang06.tistory.com

    Firebase Distribution CLI 설치하기

    Firebase CLI(GitHub)는 Firebase 프로젝트를 관리, 조회, 배포할 수 있는 다양한 도구를 제공합니다.

    Firebase CLI 참조

    설치하기

    쉘에서 아래 명령어를 입력합니다.

    curl -sL https://firebase.tools | bash

    이 스크립트는 운영체제를 자동으로 감지하고 최신 Firebase CLI 릴리스를 다운로드한 다음 디렉터리에 관계없이 사용 가능한 firebase 명령어를 사용 설정합니다.

    로그인하여 Firebase CLI 테스트

    CLI를 설치한 후에는 인증해야 합니다. 그러면 Firebase 프로젝트를 나열하여 인증을 확인할 수 있습니다.

    다음 명령어를 실행하여 Google 계정으로 Firebase에 로그인합니다.

    firebase login

    이 명령어는 로컬 머신을 Firebase에 연결하고 Firebase 프로젝트에 대한 액세스 권한을 부여합니다.

    참고: firebase login 명령어는 머신에서 localhost에 연결되는 웹페이지를 엽니다. 원격 머신을 사용 중이며 localhost에 액세스할 수 없는 경우 --no-localhost 플래그를 사용하여 명령어를 실행합니다.

    Fastlane 설치하기

     

    Xcode 커맨드 라인 툴을 설치합니다.

    xcode-select --install

    fastlane을 설치합니다.

    # RubyGems 사용
    sudo gem install fastlane -NV
    
    # Homebrew 사용
    brew cask install fastlane

    Fastlane - Firebase Distribution 플러그인 최초 연동하기

    1. fastlane을 설치 및 설정합니다.

    2. 앱 배포를 fastlane 구성에 추가하려면 iOS 프로젝트의 루트에서 다음 명령어를 실행합니다.

      fastlane add_plugin firebase_app_distribution

      명령어에서 원하는 옵션을 선택하라는 메시지가 표시되면 Option 3: RubyGems.org를 선택합니다.

    fastlane을 사용하여 테스터에 iOS 앱 배포 | Firebase

    이미 연동된 프로젝트에 설정된 Firebase Distribution 설치하기

    이미 연동된 프로젝트에 새 머신에서 플러그인을 설치하려고 할 경우에, Firebase Distribution 플러그인이 깔려 있으므로, 아래 커맨드로 새 머신에서는 받아옵니다.

    fastlane install_plugins

    fastlane Plugins

    Sudo 명령어를 치지 않아도 Firebase Distribution 명령어를 칠 수 있도록 설정하기

    카탈리나부터 설치에 이슈가 생겨서 sudo 를 fastlane firebase plugin 에 붙여주지 않으면 읽지 못하는 오류가 생겨났습니다.

    아래 명령어로 읽기 권한을 일반 유저에게도 추가하여, sudo 를 치지 않아도 되도록 수정합니다.

    sudo chmod -R a+r /Library/Ruby/Gems/2.6.0/gems/fastlane-plugin-firebase_app_distribution-0.1.4

    How to fix fastlane-plugin-firebase_app_distribution undefined

    Fastfile에 배포 스크립트 작성하기

    ruby 기반으로 되어 있는 fastfile 에 스크립트를 작성하면, 쓸만한 자동화를 건져낼 수 있습니다.

    제 일반적인 배포는 다음과 같이 일어납니다.

    1. develop 브랜치에서 qa 브랜치를 땁니다.
    2. adhoc 사이닝을 진행합니다.
    3. 앱을 빌드합니다.
    4. 버전을 하나 올리고 커밋합니다.
    5. 애드훅으로 아카이브하여 파이어베이스에 업로드합니다.
    6. 파이어베이스에 릴리즈 노트를 작성하고, 테스트 그룹을 추가합니다.
    7. 관계자에게 슬랙으로 파이어베이스에 업로드되었음을 알리고, 변경된 내역을 정리합니다.
    8. 리모트 깃에 내역을 공유합니다.

    1부터 8까지의 내역을 모두 자동화할 수 있습니다.

    fastlane 도큐먼트에는 쓸만한 slack, git 관련 명령어들이 많기 때문입니다.

    이 과정을 전부 자동화한 것이 upload_to_firebase 함수입니다.

    # This file contains the fastlane.tools configuration
    # You can find the documentation at https://docs.fastlane.tools
    #
    # For a list of all available actions, check out
    #
    #     https://docs.fastlane.tools/actions
    #
    # For a list of all available plugins, check out
    #
    #     https://docs.fastlane.tools/plugins/available-plugins
    #
    
    # Uncomment the line if you want fastlane to automatically update itself
    # update_fastlane
    
    default_platform(:ios)
    
    module Constant
      # 프로젝트 이름
      PROJECT = "a.xcodeproj"
      # 심사 요청 스키마
      TARGET_RELEASE = "a"
      # 개발 버전 스키마
      TARGET_DEBUG = "a UrlConvertable"
      # 앱 올리고 나서 배포할 때 slack hook
      SLACK_URL = "https://hooks.slack.com/services/adsfkjas;dlkfjasdkl;fj"
      # 인증서를 가져올 app id
      APP_IDENTIFIERS = ["com.company.a"]
      # 파이어베이스 배포에 올릴 앱
      FIREBASE_APP_ID = "1:1234564654564:ios:1231321231"
      # 파이어베이스 배포 테스트 그룹
      FIREBASE_TEST_GROUP = "test"
      # 파이어베이스 배포를 하고 버전 범프를 푸시할 리모트의 이름
      REMOTE_NAME = "origin"
    end
    
    def get_version() 
      return get_version_number(xcodeproj: Constant::PROJECT, target: Constant::TARGET_RELEASE)
    end
    
    def get_build()
      return get_build_number(xcodeproj: Constant::PROJECT)
    end
    
    def set_version(version)
      increment_version_number(
          version_number: version,
        xcodeproj: Constant::PROJECT
      )
      update_info_plist(
        plist_path: "Settings.bundle/Root.plist",
        block: proc do |plist|
          versionSpecifiers = plist["PreferenceSpecifiers"].find{|specifiers| specifiers["Key"] == "Version"}
          versionSpecifiers["DefaultValue"] = version
        end
      )
    end
    def set_build(build)
      increment_build_number(
        build_number: build,
        xcodeproj: Constant::PROJECT
      )
    end
    
    def increase_build(increase_by = 1)
      build = get_build()
      splited = build.split('.')
      major = splited[0]
      minor = splited[1]
      patch = splited[2]
      build_number = splited[3]
      new_build = "#{major}.#{minor}.#{patch}.#{build_number.to_i + increase_by}"
      set_build(new_build)
      return new_build
    end
    
    def increase_version(increase_by = 1)
      version = get_version()
      splited = version.split('.')
      major = splited[0]
      minor = splited[1]
      patch = splited[2]
      new_version = "#{major}.#{minor}.#{patch.to_i + increase_by}"
      new_build = "#{new_version}.0"
      set_version(new_version)
      set_build(new_build)
      return new_version
    end
    
    def on_success(message_value)
      version = get_version()
      build = get_build()  
      slack(
        message: message_value,
        success: true,
        slack_url: Constant::SLACK_URL,
        attachment_properties: {
          fields: [
            {
                title: "버전",
                value: version
            },
            {
                title: "빌드",
                value: build
            }
          ]
        }
      )
    end
    
    def on_error(exception)
      version = get_version()
      build = get_build()
      slack(
        message: "<!here> fastlane 빌드 중 문제가 발생하였습니다.",
        success: false,
        slack_url: Constant::SLACK_URL,
        attachment_properties: {
          fields: [
            {
                title: "에러 내역",
                value: exception
            },
            {
                title: "버전",
                value: version
            },
            {
                title: "빌드",
                value: build
            }
          ]
        }
      )
    end
    
    def version_bump()
      current_build = increase_build()
      git_add
      commit_message = "버전 변경 #{current_build}"
      commit_version_bump(
        message: commit_message,
        xcodeproj: Constant::PROJECT,
        force: true
      )
    end
    
    def get_commit_log()
      last_tag = last_git_tag()
      add_git_tag
      changelog = changelog_from_git_commits(
        between: [last_tag, "HEAD"]
      )
      current_build = get_build()
      log = """
    #{current_build} 버전은 아래와 같은 수정사항이 반영되었습니다.
    #{changelog}
    """
      return log
    end
    
    def make_qa_branch_if_needed()
      current_branch = git_branch
      if current_branch.match(/^qa/)
        puts("현재 qa 브랜치 #{current_branch} 로 설정되어 있습니다.")
        return
      end
      qa_branch_name = "qa/#{major}_#{minor}_#{patch}"
      sh("git", "checkout", "-b", qa_branch_name)
      puts("현재 qa 브랜치 #{qa_branch_name} 를 생성했습니다.")
    end
    
    def upload_to_firebase()
      make_qa_branch_if_needed()
      match(app_identifier: Constant::APP_IDENTIFIERS, type: "adhoc")
      build_app(project: Constant::PROJECT, scheme: Constant::TARGET_DEBUG, configuration: "Debug")
      version_bump()
      release_notes = get_commit_log()
      firebase_app_distribution(
        app: Constant::FIREBASE_APP_ID,
        groups: Constant::FIREBASE_TEST_GROUP,
        release_notes: release_notes
      )
      push_to_git_remote(
        remote: Constant::REMOTE_NAME,
        tags: false
      )
      slack_message = """
    <!here> 파이어베이스에 배포 버전이 올라갔습니다.
    #{release_notes}
    """
      on_success(slack_message)
    
    end
    
    platform :ios do
    
      lane :release do
        begin
          match(app_identifier: Constant::APP_IDENTIFIERS, type: "adhoc")
          build_app(project: Constant::PROJECT, scheme: Constant::TARGET_RELEASE)
          on_success("심사버전 빌드를 완료했습니다. 업로드를 진행합니다.")
          upload_to_app_store(skip_metadata: true, skip_screenshots: true)
          on_success("심사버전 업로드를 완료했습니다.")
        rescue => exception
          on_error(exception)
        end
      end
    
      lane :develop do
        begin
          match(app_identifier: Constant::APP_IDENTIFIERS, type: "appstore")
          build_app(project: Constant::PROJECT, scheme: Constant::TARGET_DEBUG, configuration: "Debug")
          on_success("개발버전 빌드를 완료했습니다. 업로드를 진행합니다.")
          upload_to_testflight(skip_submission: true, skip_waiting_for_build_processing: true)
          on_success("개발버전 업로드를 완료했습니다.")
        rescue => exception
          on_error(exception)
        end
      end
    
      lane :firebase do
        begin
          upload_to_firebase()
        rescue => exception
          on_error(exception)
        end
      end
    
      lane :qa do
        make_qa_branch_if_needed()
      end
    
      # get_version 명령어
      def command_gv()
        version = get_version()
        puts("현재 버전은 #{version} 입니다. 🎉")
      end
      lane :get_version do
        command_gv()
      end
      lane :gv do
        command_gv()
      end
    
      # get_build 명령어
      def command_gb()
        build = get_build()
        puts("현재 빌드는 #{build} 입니다. 🎉")
      end
      lane :get_build do
        command_gb()
      end
      lane :gb do
        command_gb()
      end
    
      # set_version 명령어
      def command_sv(options)
        version = get_version()
        new_version = options[:number].strip
        new_build = "#{new_version}.0"
        set_version(new_version)
        set_build(new_build)
        puts("버전이 #{version} 에서 #{new_version} 으로 변경되었습니다. 🎉")
        puts("빌드가 #{new_build} 로 자동 설정되었습니다. 🎉")
      end
      lane :set_version do |options|
        command_sv(options)
      end
      lane :sv do |options|
        command_sv(options)
      end
    
      # set_build 명령어
      def command_sb(options)
        build = get_build()
        new_build = options[:number].strip
        set_build(new_build)
        puts("빌드가 #{build} 에서 #{new_build} 으로 변경되었습니다. 🎉")
      end
      lane :set_build do |options|
        command_sb(options)
      end
      lane :sb do |options|
        command_sb(options)
      end
    
      # increase_version 명령어
      def command_iv(options)
        increase_by = 1
        if options[:by]
          increase_by = options[:by].to_i
        end
        new_version = increase_version(increase_by)
        puts("버전이 #{new_version} 으로 올라갔습니다. 🎉")
        puts("빌드가 #{new_version}.0 으로 자동 설정되었습니다. 🎉")
      end
    
      lane :increase_version do |options|
        command_iv(options)
      end
      lane :iv do |options|
        command_iv(options)
      end
    
      # increase_build 명령어
      def command_ib(options)
        increase_by = 1
        if options[:by]
          increase_by = options[:by].to_i
        end
        new_build = increase_build(increase_by)
        puts("빌드가 #{new_build} 으로 올라갔습니다. 🎉")
      end
    
      lane :increase_build do |options|
        command_ib(options)
      end
      lane :ib do |options|
        command_ib(options)
      end
    
    
    
    end
Designed by Tistory.