iOS项目的 fastlane / jenkins 实践

本文内容包括:

1.TestFlight lane

用途、fastlane构建过程、每一步用到的fastlane命令

2.HockeyApp lane

用途、fastlane构建过程、每一步用到的fastlane命令

3.注意事项
  • 关于更新dSYMs的问题

  • 关于waiting for processing中断的问题

  • 关于manage code signing的最佳方式

  • “采用 TestFlight 取代 hockeyapp 作为团队的日常测试” 调研

4.附录
  • Fastfile Demo

  • Jenkinsfile Demo


第一部分:TestFlight lane

用途:内测、公测、正式发布

证书类型:Production

provisionning profile类型:Distribution - app store

fastlane构建过程:

安装依赖
运行测试
更新 build number
打包
上传到testflight
从iTunes Connect下载dSYMs、并上传至Crashlytics

每一步用到的fastlane命令对应如下:

cocoapods

scan

更新 build number: 
    get_version_number
    latest_testflight_build_number  (从testflight上读取最新的build number,并加 1,为新的build number)
    update_info_plist  (CFBundleVersion的值)

gym

upload_to_testflight (同 pilot)

更新 dSYMs: 
    download_dsyms (from itunes connect) 
    upload_symbols_to_crashlytics

第二部分:HockeyApp lane

用途:团队日常测试

证书类型:Production

provisionning profile类型:Distribution - ad hoc

fastlane构建过程:

安装依赖
运行测试
更新 build number
打包
上传到hockeyapp 
上传dSYMs至Crashlytics

每一步用到的fastlane命令对应如下:

cocoapods

scan

更新 build number: 
    从jenkins的环境变量里读出build number为新的build number (ENV["BUILD_NUMBER"] ) 
    update_info_plist (CFBundleVersion的值)

gym

upload_to_testflight (同 pilot)

upload_symbols_to_crashlytics

第三部分:注意事项

关于更新dSYMs:

对于testflight lane,在项目enable bitcode后,apple会对我们上传的app进行重新编译打包,因此,在本地编译的时候生成的dSYMs就没有用了,需要往crashlytics(或者其他crash report平台)上传你从iTunes Connect下载的最新的文件,因此才有了第6.1步。关于更新dSYMs,这里有来自fastlane作者的详细的解释。

关于waiting for processing中断:

问题:由于从iTunes Connect上下载dSYMs,需要等待苹果processing这个过程完成后才能生成新的dsyms,而往往processing的过程非常漫长且时间不固定(短一点也要半小时左右)。因此,第5步 upload_to_testflight命令中,如果没有设置 skip_waiting_for_build_processing 的值为 true,则默认是false,那么这一步在上传成功后,就会一直等待processing的完成。然而processing过程太长,有可能造成命令行中断,进而造成CI失败,影响后面任务的执行。

解决办法:这里 建议将 skip_waiting_for_build_processing 设置为true,同时将第6步与前面几步独立开来,放在一个单独的CI任务中执行

关于manage code signing:

问题:由于构建TestFlight和构建HockeyApp的包,需要的provisioning profile的类型不一样,第一个是app store类型,第二个是adhoc类型,因此,manually set provisioning profile无法同时满足两种情况。

解决:建议采用(也是苹果推荐的方式)xcode automatically manage signing 的方式,并在fastlane的gym脚本中配置不同的provisioning profile,让xcode自动去选择相应的provisioning profile文件。

注意:

如果只是在fastlane的gym命令中配置了provisioning profile,但是xcode上依然选择的是manual,最终依然会用xcode中配置的来打包,因此如果xcode配置的是adhoc的provisioning profile,但是fastlane执行的是打一个app store类型的包,就会因为类型错误而失败。

自动管理的方式要求在CI上用一个包含在开发team中的apple id 登录xcode,这样xcode就可以通过这个账号自动获得所有的provisioning profile了。

采用 TestFlight 取代 hockeyapp 作为团队的日常测试,是否可行:

问题:由于采用 hockeyapp 做测试分发平台,本质是用ad-hoc的方式打包和分发,因此需要注册所有测试设备的UDID。这给团队的日常管理带来了很多麻烦,尤其是每次新增测试设备,或者测试设备即将达到每年100个上限的时候,都会带来额外的工作量。还有一个问题就是,用hockeyapp测好的包,要发到app store的时候,必须重新打一个 app store类型的包,而不能把测好的包直接发布到app store,中间必然增加了出错的风险。

解决办法:尝试采用 TestFlight 取代 hockeyapp 作为团队的日常测试,用iTunes Connect test user apple id(内测人员ID)登录到所有测试设备的testflight中,这样每次新上传的包,不需要等待apple beta review,即可开始测试。同时,不需要UDID。

实测:但是实测了一下,打好的包,上传到 TestFlight 用了 8分钟左右,而TestFlight processing的过程用了 30分钟(还可能更长,不可预测)。(但是上传到 hockeyapp 只需要3分钟左右)也就是每次开发人员给测试人员出一个可测试的包,采用 TestFlight 要比 hockeyapp 多用至少35分钟。再加上编译打包等其他时间,总共得一个小时以上了。这个对于团队的日常开发测试来说基本是不可接受的。

结论:因此,暂时放弃了这个想法。 所以我们团队采用的是,日常story级别的测试,采用hockeyapp;上线前的PAT测试(此时需要外部人员介入帮忙一起测试),采用 TestFlight。


第四部分:附录

附录一:Fastfile

fastlane_version "2.85.0"
default_platform :ios
require_relative 'actions/xctestrun'

platform :ios do
  desc "Runs all the tests"
  lane :update_dependences do
    cocoapods
  end

  desc "Runs all the tests"
  lane :test do
    unit_test
    ui_tests
  end

  desc "Build and Upload a new beta build to HockeyApp"
  lane :beta_hockeyapp do
    update_build_number_jenkins
    build_adhoc
    upload_hockeyapp
    upload_symbols_to_crashlytics(gsp_path: "./xxxxx/GoogleService-Info.plist")
  end

  desc "Build and upload a PROD app to TestFlight"
  lane :release_testflight do
    update_build_number_testflight
    build_appstore
    upload_testflight
  end

  desc "Download dSYM files from iTunes Connect and upload to Crashlytics."
  lane :upload_dsyms do
    download_dsyms(username: "XXX", app_identifier: "XXX")
    upload_symbols_to_crashlytics(gsp_path: "./xxxxx/GoogleService-Info.plist")
  end


  # -------------- private lanes below -------------- #

  # -------------- test -------------- #
  lane :ui_tests do
    xctestrun(testcase: 'xxxxxx')
    xctestrun(testcase: 'xxxxxx')
  end

  lane :unit_test do
    scan(
      workspace: "XXX",
      scheme: "XXX",
      clean: true,
      device: "iPhone 6",
      output_types: "html",
    )
  end
  # -------------- test -------------- #


  # -------------- hockeyapp -------------- #
  lane :update_build_number_jenkins do
    latestBuildNumber = ENV["BUILD_NUMBER"]
    update_info_plist(
      scheme: 'XXX',
      block: lambda { |plist|
        plist["CFBundleVersion"] = latestBuildNumber
      }
    )
  end

  lane :build_adhoc do
    gym(
      configuration: "Release",
      scheme: "XXX",
      clean: true,
      include_bitcode: true,
      include_symbols: true,
      export_method: 'ad-hoc',
      export_options: {
        provisioningProfiles: {"com.xx.xxx" => "use adhoc provisioning profile"}
      }
    )
  end

  lane :upload_hockeyapp do
    change_logs = sh "git log -5 --pretty=oneline --abbrev-commit"
    hockey(
      api_token: "XXX",
      public_identifier: "XXX",
      ipa: "XXX",
      release_type: '2',
      status: "2",
      strategy: "replace",
      notes: change_logs,
      notes_type: "1",
      bypass_cdn: true,
      timeout: 3000
    )
  end
  # -------------- hockeyapp -------------- #


  # -------------- testflight -------------- #
  lane :update_build_number_testflight do
    currentVersion = get_version_number(
      xcodeproj: "XXX"
    )
    latestBuildNumber = latest_testflight_build_number(
      username: "XXX",
      app_identifier: "XXX",
      version: currentVersion
    )
    update_info_plist(
      scheme: 'XXX',
      block: lambda { |plist|
        plist["CFBundleVersion"] = (latestBuildNumber.to_i + 1).to_s
      }
    )
  end

  lane :build_appstore do
    gym(
      configuration: "Release",
      scheme: "XXX",
      clean: true,
      include_bitcode: true,
      include_symbols: true,
      export_method: 'app-store',
      export_xcargs: "-allowProvisioningUpdates",
      export_options: {
        provisioningProfiles: {"com.xx.xxx" => "use appstore provisioning profile"}
      }
    )
  end

  lane :upload_testflight do
    upload_to_testflight(
      skip_waiting_for_build_processing: true,
      username: "XXX",
      app_identifier: "XXX",
      ipa: "XXX")
  end
  # -------------- testflight -------------- #

end

附录二:Jenkinsfile (HockeyApp lane)

pipeline {
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git submodule update --init'
            }
        }

        stage('Update dependences') {
            steps {
                sh "fastlane update_dependences"
            }
        }

        stage('Run all test') {
            steps {
                sh "fastlane test"
            }
        }

        stage('Build and Upload to testflight') {
            steps {
                sh "fastlane beta_hockeyapp"
            }
        }
    }
}

附录三:Jenkinsfile (TestFlight lane)

pipeline {
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git submodule update --init'
            }
        }

        stage('Update dependences') {
            steps {
                sh "fastlane update_dependences"
            }
        }

        stage('Run all test') {
            steps {
                sh "fastlane test"
            }
        }

        stage('Build and Upload to testflight') {
            steps {
                sh "fastlane release_testflight"
            }
        }
    }
}

附录四:Jenkinsfile (更新dSYMs)

pipeline {
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git submodule update --init'
            }
        }
        stage('Update dependences') {
            steps {
                sh "fastlane upload_dsyms"
            }
        }
    }
}

你可能感兴趣的:(iOS项目的 fastlane / jenkins 实践)