iOS 自動(dòng)化雜談

前言:可持續(xù)集成自動(dòng)化的話題已經(jīng)老生常談了抡四。目前市面上比較流行的自動(dòng)化流程工具——Fastlane,F(xiàn)astlane是用Ruby語(yǔ)言編寫的一套自動(dòng)化工具集和框架,F(xiàn)astlane的工具集基本上涵蓋了打包胸私,簽名,測(cè)試阔涉,部署瑰排,發(fā)布暖侨,庫(kù)管理等等用起來(lái)比較方便,配合Jenkins可持續(xù)化集成京郑,基本可以滿足大部分的流程自動(dòng)化。

image

一. 打包

實(shí)現(xiàn)打包有很多種跟狱,例如xcodebuild户魏,但已經(jīng)有好用的工具集為何不用呢?
跟著打包的流程寫腳本关翎,例如我想打包笤休,得提供給別人選擇哪個(gè)分支症副,采用什么類型,及時(shí)通知等

image
  • Jenkins上裝了Git parameter plug-In 0.9.12版本的插件進(jìn)行分支選擇
  • 想暴露什么參數(shù)在Jenkins上自定義
  • 利用fastlane gym
  • 上傳蒲公英
  • 由于之前蒲公英掛過一次闹啦,不能完全依賴第三方分發(fā)平臺(tái)窍奋,自己再自建一個(gè)OTA服務(wù)器來(lái)內(nèi)測(cè)分發(fā)
  • 自定義內(nèi)測(cè)的二維碼采用python myqr生成
  • 消息通知:我司采用企業(yè)微信酱畅,那就搞個(gè)機(jī)器人webhook一下,當(dāng)然也可以腳本發(fā)個(gè)郵件
  • 符號(hào)表選擇是否上傳
desc "ad_Hoc 版本"
  lane :beta do  |options|
    # 新建build號(hào)
    new_build = options[:new_build]
    time = Time.new.strftime("%Y-%m-%d-%H:%M:%S")
    increment_build_number(
      build_number: new_build,
      xcodeproj: "xxxxx.xcodeproj"
    )
    sh("pod repo update")
    # 拉取代碼
    cocoapods
    # 獲取版本號(hào)
    version = get_version_number(
      xcodeproj: "xxxxx.xcodeproj",
      target: "xxxxx"
    )
    # 打包環(huán)境
    configuration = (options[:configuration] ? options[:configuration] : "Release")
    ipaName="xxxxx"
    ipaPath=configuration + "/" + version + "." + new_build + "/"
    # 導(dǎo)出ipa包地址
    output_directory = "/Users/admin/WebSites/app/ipa/" + ipaPath
    #manifest.plilst需要的參數(shù)
    ipaUrl='https://10.104.33.114/app/ipa/' + ipaPath + ipaName + '.ipa'
    plistPath = 'https://10.104.33.114/app/ipa/' + ipaPath + 'manifest.plist'
    pngName = version + "." + new_build + '.png'
    disImg ='https://10.104.33.114/app/icon/' + pngName
    gym(
      scheme: "xxxxx",
      workspace: "xxxxx.xcworkspace",
      export_method:"ad-hoc",
      output_directory: output_directory,#文件路徑
      clean: true,
      configuration: configuration,
      export_options:{
         manifest: {
             appURL: ipaUrl,
             displayImageURL: disImg,
             fullSizeImageURL: disImg
             },
         }
    )
    # 參數(shù)傳給內(nèi)測(cè)分發(fā)網(wǎng)頁(yè)
    size =`echo $(wc -c < #{output_directory}#{ipaName}.ipa)`
    desc = URI::encode(options[:desc])
    appBuildURL = "http://10.104.33.114/app/index.html?" + "version=" + version + "&" + "build=" + new_build + "&" + "size=" + size.strip + "&" + "time=" + time + "&" + "desc=" + desc + "&" + "pngName=" + pngName + "&" +  "plistUrl=" + plistPath
    myqrAppBuildURL = "http://10.104.33.114/app/index.html?" + "version=" + version + "\\&" + "build=" + new_build + "\\&" + "size=" + size.strip + "\\&" + "time=" + time + "\\&" + "desc=" + desc + "\\&" + "pngName=" + pngName + "\\&" + "plistUrl=" + plistPath
    appQRCodeURL = "http://10.104.33.114/app/icon/" + pngName
    cpath = sh("pwd").strip
    `rm -rf #{cpath}/qrcode.png`
    # myqr生成二維碼
    `myqr #{myqrAppBuildURL}`
    `mv #{cpath}/qrcode.png /Users/admin/WebSites/app/icon/#{pngName}`
    UI.message "appBuildURL:#{appBuildURL}"
    UI.message "appQRCodeURL:#{appQRCodeURL}"
    # 上傳蒲公英
    uploadPgy(options[:desc])
    versionDes = version + " ( build "+ new_build + " )"
    description = "打包完成,版本:"+ versionDes + ",包體積:" + size.strip
  end

注意 myqr 是生成二維碼的python 工具碎紊,需要設(shè)置環(huán)境變量

image

以上已經(jīng)實(shí)現(xiàn)了打包樊诺,接下來(lái)上傳蒲公英

  # 上傳蒲公英
  def uploadPgy(desc)
    begin
      pgyer(api_key: "xxx",user_key: "xxx",update_description:"xxx")
      rescue
      retry
    xxx
  end

如果實(shí)現(xiàn)企業(yè)微信通知,其實(shí)就是發(fā)送一個(gè)請(qǐng)求秃嗜,此時(shí)要注意的是fastlane 是ruby 環(huán)境,執(zhí)行shell腳本的 & 或是 雙引號(hào)需要轉(zhuǎn)義:\ 螺句,并非一個(gè)\橡类,例如轉(zhuǎn)義&:\&
<img src="https://zhonghphuan.github.io/images/iOS-自動(dòng)化-打包/WX20200820-170210@2x.png" width = "300" alt="" align=center />
基本以上已經(jīng)實(shí)現(xiàn)了打包的日常需求了,gym中的export_options是自建內(nèi)測(cè)分發(fā)的manifest配置

export_options:{
         manifest: {
             appURL: ipaUrl,
             displayImageURL: disImg,
             fullSizeImageURL: disImg
             },
         }

以下簡(jiǎn)單描述一下自建OTA服務(wù)

  1. 啟動(dòng)Web服務(wù) - Mac自帶Apache
?  ~ httpd -v
Server version: Apache/2.4.41 (Unix)
Server built:   Apr 17 2020 19:06:36
  • 啟動(dòng):sudo apachectl start
  • 停止:sudo apachectl stop
  • 重啟:sudo apachectl restart

啟動(dòng)sudo apachectl start后瀏覽器http://127.0.0.1取劫,顯示It Works即成功

  1. SSL簽名證書
?  ~ cd /private/etc/apache2/
?  apache2 sudo mkdir ssl
?  apache2 cd ssl
?  ssl sudo openssl genrsa -out ip211.key 2048
Generating RSA private key, 2048 bit long modulus
...................+++
..............................................................+++
e is 65537 (0x10001)
?  ssl sudo openssl req -new -key ip211.key -out ip211.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:127.0.0.1(此處填具體的ip地址)
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
?  ssl sudo openssl x509 -req -days 365000 -in ip211.csr -signkey ip211.key -out ip211.crt
Signature ok
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=127.0.0.1
Getting Private key
?  ssl sudo openssl rsa -in ip211.key -out ip211-nopass.key
writing RSA key
?  ssl ls -l
total 32
-rw-r--r--  1 root  wheel  1679  8 20 17:26 ip211-nopass.key
-rw-r--r--  1 root  wheel  1168  8 20 17:25 ip211.crt
-rw-r--r--  1 root  wheel   985  8 20 17:23 ip211.csr
-rw-r--r--  1 root  wheel  1679  8 20 17:20 ip211.key

只有Common Name填寫具體的ip地址

  1. 修改conf文件
?  ssl sudo cp /private/etc/apache2/httpd.conf /private/etc/apache2/httpd.conf.bak
Password:
?  ssl sudo cp /private/etc/apache2/extra/httpd-ssl.conf /private/etc/apache2/extra/httpd-ssl.conf.bak
?  ssl sudo cp /private/etc/apache2/mime.types /private/etc/apache2/mime.types.bak
?  ssl sudo vim /private/etc/apache2/httpd.conf
?  ssl sudo vim /private/etc/apache2/extra/httpd-ssl.conf
?  ssl sudo vim /private/etc/apache2/mime.types

1)修改/private/etc/apache2/httpd.conf谱邪,去掉以下兩個(gè)模塊的注釋
LoadModule socache_shmcb_module libexec/apache2/mod_socache_shmcb.so
LoadModule ssl_module libexec/apache2/mod_ssl.so
Include /private/etc/apache2/extra/httpd-ssl.conf

2)修改/private/etc/apache2/extra/httpd-ssl.conf惦银,去掉以下三處的注釋
ServerName 127.0.0.1(具體的ip地址)
SSLCertificateFile "/private/etc/apache2/ssl/ip211.crt"
SSLCertificateKeyFile "/private/etc/apache2/ssl/ip211-nopass.key"

3)修改/private/etc/apache2/mime.types扯俱,加入以下兩條
application/octet-stream ipa
text/xml plist

  1. 重啟服務(wù):sudo apachectl restart喇澡,瀏覽器輸入具體ip地址
  2. 配置目錄
$ sudo mkdir ipa
$ sudo mkdir icon
$ sudo mkdir ssl
$ sudo mkdir plist
image

拷貝/private/etc/apache2/ssl/ip211.crt 到 這個(gè)ssl目錄下:
sudo cp /private/etc/apache2/ssl/ip211.crt ~/WebSites/app/ssl/ip211.crt

  1. 制作一個(gè)簡(jiǎn)單的頁(yè)面

解析鏈接中的itms-services:// 實(shí)現(xiàn)OTA

<!DOCTYPE html>
<html>

<head lang="zh-cmn-Hans">
  <meta charset="UTF-8">
  <title>分發(fā)ipa包管理</title>
  <meta name="renderer" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width,initial-scale=0.5,user-scalable=no" />
</head>
<style>
  .img {
    text-align: center;
  }

  .btn {
    text-align: center;
    background: #35AF5D;
    color: #000;
    padding: 20px;
    margin: 30px;
    font-size: 24px;
    border-radius: 4px;
    box-shadow: 4px 2px 10px #999;
  }

  .btn:active {
    opacity: .7;
    box-shadow: 4px 2px 10px #555;
  }
</style>

<body>
  <h1 style="text-align:center;color:#35AF5D">
    工程名
  </h1>
  <p id="p1" style="text-align:center">
    版本:
  </p>
  <p id="p2" style="text-align:center">
    大星缇痢:
  </p>
  <p id="p3" style="text-align:center">
    更新時(shí)間:
  </p>
  <p id="p4" style="text-align:center">
    更新描述:
  </p>
  <div class="img">
    <img id="imgid" src="./icon/*.png" height="300" width="300" />
  </div>
  <div class="btn" onclick="installApp()">安裝app</div>
  <script>
    document.getElementById("p1").innerHTML = "版本:" + getUrlParam("version") + " ( build " + getUrlParam("build") + " )"
    document.getElementById("p2").innerHTML = "大信皇骸:" + getUrlParam("size") + " KB"
    document.getElementById("p4").innerHTML = "更新時(shí)間:" + getUrlParam("time")
    document.getElementById("p3").innerHTML = "更新描述:" + decodeURIComponent(getUrlParam("desc"))
    document.getElementById('imgid').src = "./icon/" + getUrlParam("pngName")
    function getUrlParam(variable) {
      let query = window.location.search.substring(1);
      let vars = query.split("&");
      for (let i = 0; i < vars.length; i++) {
        let pair = vars[i].split("=");
        if (pair[0] == variable) { return pair[1]; }
      }
      return (false);
    }
    function installApp() {
      var plistUrl = decodeURI(getUrlParam("plistUrl"));
      window.location.href = "itms-services://?action=download-manifest&url=" + plistUrl;
    }
  </script>
  <a style="display:block;margin: 30px;" >下載證書</a>
  <p style="display:block;margin: 30px;">點(diǎn)擊下載證書,下載安裝配置文件</p>
  <p style="display:block;margin: 30px;">在設(shè)置-通用-描述文件與設(shè)備管理中尔当,選擇已下載的配置文件琅催,進(jìn)行安裝</p>
  <p style="display:block;margin: 30px;">在設(shè)置-通用-關(guān)于本機(jī)-證書信任設(shè)置中將完全信任打開</p>


</body>

</html>
image
二. testflight 自動(dòng)化公測(cè)
  1. 方案一: 使用fastlane的upload_to_testflight
    upload_to_testflight(
          beta_app_review_info: {
            contact_email: "xxxxx@xxx.net",
            contact_first_name: "xx",
            contact_last_name: "xx",
            contact_phone: "+xxxxxx",
            demo_account_name: "xxxxxx",
            demo_account_password: "xxxxx"
          },
            first_name: "xxx",
            last_name: "xxxx",
            email: "xxxxx@xxx.net",
        #  true就不自動(dòng)提審了
            skip_waiting_for_build_processing: false,
            beta_app_feedback_email:"xxxxx@xxx.net",
            beta_app_description:options[:desc],
            demo_account_required: true,
        #構(gòu)建是否應(yīng)該分發(fā)給外部測(cè)試人員藤抡?
            distribute_external: true,
            notify_external_testers: true,
            groups: groups,
            changelog:options[:desc],
            ipa: ipa_path,
            localized_app_info: {
              "default": {
                feedback_email: "xxxxx@xxx.net",
                description: "xxxxxxxxxxx"
              },
              "zh-Hans": {
                feedback_email: "xxxxx@xxx.net",
                description: "xxxxxxxxx缠黍。"
              }
            },
            localized_build_info: {
              "default": {
                 whats_new: options[:desc]
              },
              "zh-Hans": {
                 whats_new: options[:desc]
              }
            }
    )

但這樣有個(gè)問題药蜻,需要雙重驗(yàn)證替饿,通過fastlane spaceauth 生成的session一個(gè)月就過期了

#!/bin/bash
# 雙重驗(yàn)證session一個(gè)月過期视卢,執(zhí)行下面方法輸入驗(yàn)證碼繼續(xù)一個(gè)月
# fastlane spaceauth -u ios-develop@xxxxx.net
export FASTLANE_SESSION='---\n- !ruby/object:HTTP::Cookie\n ........'
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=nqfn-rljf-jipw-kevb

那這個(gè)方法其實(shí)不太能完美廊驼,只能利用 蘋果自動(dòng)化api來(lái)實(shí)現(xiàn)

  1. 方案二:蘋果自動(dòng)化api

先ruby封裝幾個(gè)函數(shù)

require "base64"
require "jwt"
require 'json'
# 準(zhǔn)備分支信息
  def prepare(branch,version,new_build,channel)
    sh "git checkout #{branch}"
    sh "git pull origin #{branch}"
    increment_build_number(
      build_number: new_build,
      xcodeproj: "xxxx.xcodeproj"
    ) 
    increment_version_number(version_number: version)  
    tag_string = "#{channel}_#{version}.#{new_build}"
    sh 'git add .'
    git_commit(path: '.', message: tag_string)
    push_to_git_remote(tags: false)
    add_git_tag(tag: tag_string)
  end

  # 上傳蒲公英
  def uploadPgy(desc)
    begin
      pgyer(api_key: "xxxx",user_key: "xxxx",update_description:"#{desc}")
      rescue
      retry
    end
  end

  # 審核狀態(tài)
  def getBuildState(buildid)
    begin
      jwt_token = getToken()
      externalBuildState =  %x(curl  -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -s -X GET   https://api.appstoreconnect.apple.com/v1/buildBetaDetails/#{buildid}  )
      state = JSON.parse(externalBuildState)
      buildstate = state["data"]["attributes"]["externalBuildState"]
      rescue
      retry
    end
  end

  # 內(nèi)審狀態(tài)
  def getInternalBuildState(buildid)
    begin
      jwt_token = getToken()
      externalBuildState =  %x(curl  -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -s -X GET   https://api.appstoreconnect.apple.com/v1/buildBetaDetails/#{buildid}  )
      state = JSON.parse(externalBuildState)
      buildstate = state["data"]["attributes"]["internalBuildState"]
      rescue
      retry
    end
  end

   # 獲取build
 def getBetaBuild(new_build)
  begin
    jwt_token = getToken()
    buildJson =  %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X GET https://api.appstoreconnect.apple.com/v1/builds?filter[version]=#{new_build})
    buildJsonParse = JSON.parse(buildJson)
    buildid = buildJsonParse["data"][0]["id"]
    rescue
      sleep 5 * 60
    retry
  end
end

     # 測(cè)試人員添加測(cè)試組中
  def getBetaTesters(groupid)
    begin
      jwt_token = getToken()
      betaTesters = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X POST -d '{"data": {"type": "betaTesters","attributes": {"firstName":"xx","lastName":"xx","email":"xx@xxx.net"},"relationships": {"betaGroups":{"data":[{"type":"betaGroups","id":"#{groupid}"}]}}}}' https://api.appstoreconnect.apple.com/v1/betaTesters)
      puts "將測(cè)試人員添加到組中: #{betaTesters}"
      betaTestersData = JSON.parse(betaTesters)
      id = betaTestersData["data"]["id"]
      rescue
        sleep 5 * 60
      retry
    end
  end

  # 創(chuàng)建組
  def createGroup(groups)
    jwt_token = getToken()
    puts "令牌:#{jwt_token}"
    # 創(chuàng)建組
    groupJson = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X POST -d '{"data": {"type": "betaGroups","attributes": {"name":"#{groups}"},"relationships": {"app": {"data":{"type":"apps","id":"xxxx"}}}}}' https://api.appstoreconnect.apple.com/v1/betaGroups)
    groupJsonParse = JSON.parse(groupJson)
    groupid = groupJsonParse["data"]["id"]
  end


  # build添加測(cè)試組中
  def addBetaGroups(groupid,buildid)
      jwt_token = getToken()
      # 將版本添加到組中
      insertBetaGroups = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X POST -d '{"data": [{"type": "builds","id":"#{buildid}"}]}' https://api.appstoreconnect.apple.com/v1/betaGroups/#{groupid}/relationships/builds)
      puts "將版本添加到組中: #{insertBetaGroups}"
  end

  # 獲取本地化id
  def getBetaBuildLocalizationsid(buildid,desc)
      jwt_token = getToken()
      createBetaBuildLocalizationsJson =  %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X POST -d '{ "data": {"type": "betaBuildLocalizations","attributes": {"whatsNew": "#{desc}","locale":"zh-Hans"},"relationships": {"build":{"data":{"id":"#{buildid}","type":"builds"}}}}}' https://api.appstoreconnect.apple.com/v1/betaBuildLocalizations)
      puts "createBetaBuildLocalizationsJson: #{createBetaBuildLocalizationsJson}"
      betaBuildLocalizationsJson =  %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X GET https://api.appstoreconnect.apple.com/v1/betaBuildLocalizations?filter[build]=#{buildid}&filter[locale]=zh-Hans)
      betaBuildLocalizationsParse = JSON.parse(betaBuildLocalizationsJson)
      puts "betaBuildLocalizationsJson: #{betaBuildLocalizationsJson}"
      betaBuildLocalizationsid = betaBuildLocalizationsParse["data"][0]["id"]
  end

  # 本地化信息
  def patchBetaBuildLocalizations(betaBuildLocalizationsid,desc)
      jwt_token = getToken()
      patchBetaBuildLocalizations = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X PATCH -d '{ "data": {"type": "betaBuildLocalizations","attributes": {"whatsNew": "#{desc}"},"id": "#{betaBuildLocalizationsid}"}}' https://api.appstoreconnect.apple.com/v1/betaBuildLocalizations/#{betaBuildLocalizationsid})
      puts "本地化信息: #{patchBetaBuildLocalizations}"
  end
  # 啟用公測(cè)鏈接
  def getPublic_link(groupid,groups)
    begin
      jwt_token = getToken()
      public_link_json = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X PATCH -d '{"data": {"type": "betaGroups","id": "#{groupid}","attributes": {"name": "#{groups}","publicLinkEnabled": true,"publicLinkLimitEnabled": false,"publicLinkLimit": null,"feedbackEnabled": true}}}' https://api.appstoreconnect.apple.com/v1/betaGroups/#{groupid})
      puts "鏈接請(qǐng)求: #{public_link_json}"
      public_link_json_parse = JSON.parse(public_link_json)
      public_link = public_link_json_parse["data"]["attributes"]["publicLink"]
      rescue
        sleep 5 * 60
      retry
    end
  end
 
  # 獲取蘋果憑據(jù)token
  def getToken 
    private_key = OpenSSL::PKey.read(File.read("/Users/admin/AuthKey_xxxxx.p8"))
    token = JWT.encode(
    {
      iss: "xxxxx-xxxx-xxxxxx-xxxx-xxxxxx",
      exp: Time.now.to_i + 20 * 60,
      aud: "appstoreconnect-v1"
    },
    private_key,
    "ES256",
    header_fields={kid: "xxxxx" }
  )
  end

此處根據(jù) 蘋果自動(dòng)化api文檔先本地通過postman去調(diào)試驗(yàn)證,如下圖酝掩,header中的Authorization為key,value為 "Bearer 蘋果憑據(jù)token"

image

具體實(shí)現(xiàn)

desc "發(fā)布testflight版本"
  lane :testflight do  |options|
    #新建build號(hào)
    new_build = options[:new_build]
    desc = options[:desc]
    puts "desc:#{desc}"
    time = Time.new.strftime("%Y-%m-%d-%H:%M:%S")
    increment_build_number(
      build_number: new_build,
      xcodeproj: "xxxx.xcodeproj"
    )

    new_version = options[:new_version]
    if !new_version.empty?
     increment_version_number(version_number: new_version)
    end
    sh("pod repo update")
    # 拉取代碼
    cocoapods
    # 獲取版本號(hào)
    version = get_version_number(
      xcodeproj: "xxxx.xcodeproj",
      target: "xxxx"
    )
    # 打包環(huán)境
    configuration = "Release"
    ipaName="xxxx"
    ipaPath=configuration + "/" + version + "." + new_build + "/"
    # 導(dǎo)出ipa包地址
    output_directory = "/Users/admin/WebSites/app/ipa/" + ipaPath
    #manifest.plilst需要的參數(shù)
    ipaUrl='https://10.104.33.114/app/ipa/' + ipaPath + ipaName + '.ipa'
    plistPath = 'https://10.104.33.114/app/ipa/' + ipaPath + 'manifest.plist'
    pngName = version + "." + new_build + '.png'
    disImg ='https://10.104.33.114/app/icon/' + pngName
    gym(
      scheme: "xxxx",
      workspace: "xxx.xcworkspace",
      export_method:"app-store",
      export_xcargs: "-allowProvisioningUpdates",
      output_directory: output_directory,#文件路徑
      clean: true,
      configuration: configuration,
      export_options:{
         manifest: {
             appURL: ipaUrl,
             displayImageURL: disImg,
             fullSizeImageURL: disImg
             },
         }
    )

    ipa_path = output_directory + ipaName + '.ipa'
    groups = version + "." + new_build
    apiIssuer = "xxxxxxxxxxxxxxx";
    apiKey = "xxxxxx";
    `xcrun altool --validate-app -f #{ipa_path}  -t ios --apiKey #{apiKey} --apiIssuer #{apiIssuer}`
    validate_status = `echo $?`
    puts "======================== validate ========================"
    puts "#{validate_status}"
    if Integer(validate_status) != 0
      puts "======================== 驗(yàn)證出錯(cuò) ========================"
      exit
    end
    puts "======================== 驗(yàn)證成功 ========================"
    `xcrun altool --upload-app -f #{ipa_path} -t ios --apiKey #{apiKey} --apiIssuer #{apiIssuer}`
    upload_status = `echo $?`
    puts "======================== upload ========================"
    puts "#{upload_status}"
    if Integer(upload_status) != 0
      puts "======================== 上傳出錯(cuò) ========================"
      exit
    end
    puts "======================== 上傳成功 ========================"
    size =`echo $(wc -c < #{output_directory}#{ipaName}.ipa)`
    desc = URI::encode(options[:desc])
    appBuildURL = "http://10.104.33.114/app/index.html?" + "version=" + version + "&" + "build=" + new_build + "&" + "size=" + size.strip + "&" + "time=" + time + "&" + "desc=" + desc + "&" + "pngName=" + pngName + "&" +  "plistUrl=" + plistPath
    myqrAppBuildURL = "http://10.104.33.114/app/index.html?" + "version=" + version + "\\&" + "build=" + new_build + "\\&" + "size=" + size.strip + "\\&" + "time=" + time + "\\&" + "desc=" + desc + "\\&" + "pngName=" + pngName + "\\&" + "plistUrl=" + plistPath
    appQRCodeURL = "http://10.104.33.114/app/icon/" + pngName
    cpath = sh("pwd").strip
    `rm -rf #{cpath}/qrcode.png`
    `myqr #{myqrAppBuildURL}`
    `mv #{cpath}/qrcode.png /Users/admin/WebSites/app/icon/#{pngName}`
    UI.message "appBuildURL:#{appBuildURL}"
    UI.message "appQRCodeURL:#{appQRCodeURL}"
    description = "公測(cè)包:"+ groups
    UI.message "description:#{description}"

    # 獲取build
    buildJson = getBetaBuild(new_build)
    puts "buildid:#{buildid}"

    # 輪詢
    internalBuildStat =  getInternalBuildState(buildid)
    puts "提交內(nèi)審狀態(tài):#{internalBuildStat}"
    
    while !(internalBuildStat.casecmp?("IN_BETA_TESTING"))  do
      sleep 5 * 60
      internalBuildStat =  getInternalBuildState(buildid)
      puts "提交內(nèi)審狀態(tài):#{internalBuildStat}"
    end

    # 發(fā)出企業(yè)微信通知:可以提交審核
    sleep 5 * 60
    jwt_token = getToken()
    # 提交審核
    betaAppReviewSubmissions = %x(curl -H "Authorization: Bearer #{jwt_token}" -H "Content-type: application/json" -X POST -d '{"data": {"type": "betaAppReviewSubmissions","relationships": {"build": {"data":{"type":"builds","id":"#{buildid}"}}}}}' https://api.appstoreconnect.apple.com/v1/betaAppReviewSubmissions)
    puts "審核請(qǐng)求結(jié)果:#{betaAppReviewSubmissions}"
    
    # 獲取審核狀態(tài)
    buildstate =  getBuildState(buildid)
    puts "審核狀態(tài):#{buildstate}"
    laststate = buildstate
    if buildstate.casecmp?("WAITING_FOR_BETA_REVIEW")
      # 發(fā)出企業(yè)微信通知:等待審核狀態(tài)
    end

    if buildstate.casecmp?("IN_REVIEW")
      # 發(fā)出企業(yè)微信通知
    end
    
    # 輪詢查看審核狀態(tài)(每隔10分鐘)
    while !(buildstate.casecmp?("IN_BETA_TESTING") || buildstate.casecmp?("APPROVED") || buildstate.casecmp?("REJECTED") || buildstate.casecmp?("BETA_APPROVED") || buildstate.casecmp?("BETA_REJECTED"))  do
      sleep 10 * 60
      buildstate =  getBuildState(buildid)
      if !laststate.casecmp?(buildstate)
        if (buildstate.casecmp?("IN_REVIEW") || buildstate.casecmp?("IN_BETA_REVIEW"))
          # 發(fā)出企業(yè)微信通知
          else
            # 發(fā)出企業(yè)微信通知
        end
      end
      laststate =  buildstate;
      puts "審核狀態(tài):#{buildstate}"
    end

    if (buildstate.casecmp?("REJECTED") || buildstate.casecmp?("BETA_REJECTED"))
      # 發(fā)出企業(yè)微信通知:等待審核狀態(tài)
        puts "#{groups} 公測(cè)審核被拒竿拆,請(qǐng)前往App Store查看原因"
        exit
    end

    if (buildstate.casecmp?("IN_BETA_TESTING") || buildstate.casecmp?("APPROVED") || buildstate.casecmp?("BETA_APPROVED"))

      jwt_token = getToken()
      puts "令牌:#{jwt_token}"
      # 創(chuàng)建組
      groupid = createGroup(groups)
      puts "獲取到組id:#{groupid}"
      sleep 5

      # 將測(cè)試人員添加到組中
      getBetaTesters(groupid)
      sleep 5

      # 將build添加到組中
      addBetaGroups(groupid,buildid)
      sleep 5

      #獲取本地化id
      betaBuildLocalizationsid = getBetaBuildLocalizationsid(buildid,options[:desc])
      puts "betaBuildLocalizationsid:#{betaBuildLocalizationsid}"
      
      #修改本地化測(cè)試信息
      patchBetaBuildLocalizations(betaBuildLocalizationsid,options[:desc])

      # 啟用公測(cè)鏈接
      public_link = getPublic_link(groupid,groups)
      puts "公測(cè)鏈接: #{public_link}"
      new_branch = options[:new_branch]
      prepare(new_branch,version,new_build,'testflight')
      push_git_tags
      # 上傳bugly
      dsymFilePath = output_directory + 'xxxx.app.dSYM.zip'
      upload_dsym_to_bugly(
        file_path: "#{dsymFilePath}",
        file_name: "%e8%b6%axxxxxxx%.app.dSYM.zip",
        app_key: "xxxxxxx",
        app_id:"xxxxxxx",
        api_version: 1,
        symbol_type: 2, # iOS => 2, Android => 1
        bundle_id: 'com.xxxx.xxxx',
        product_version: "#{groups}"
      ) 
    end
  end
三. 總結(jié)
  1. 公測(cè)自動(dòng)化實(shí)現(xiàn)后丙笋,App Store打包通過打包驗(yàn)證和上傳也很容易實(shí)現(xiàn)
  2. Jenkins + fastlane 較為方便的實(shí)現(xiàn)可持續(xù)集成自動(dòng)化的流程
  3. python煌贴、ruby、shell等語(yǔ)言實(shí)現(xiàn)腳本思想一樣牛郑,哪個(gè)方便用哪個(gè)
  4. 能工具化提高效率的盡量工具化自動(dòng)化,為公司節(jié)省人力笙各,提高工作效率
  5. 消息通知最終流程過程或結(jié)果可以采用郵件、webhook機(jī)器人消息等
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杈抢,一起剝皮案震驚了整個(gè)濱河市仑性,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歼捐,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贷盲,死亡現(xiàn)場(chǎng)離奇詭異剥扣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)朦乏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)吃引,“玉大人刽锤,你說(shuō)我怎么就攤上這事〔⑺迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵弄砍,是天一觀的道長(zhǎng)输涕。 經(jīng)常有香客問我,道長(zhǎng)莱坎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任碴卧,我火速辦了婚禮乃正,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烫葬。我一直安慰自己,他們只是感情好垢箕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布兑巾。 她就那樣靜靜地躺著,像睡著了一般蒋歌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上修档,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天府框,我揣著相機(jī)與錄音,去河邊找鬼迫靖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛照激,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播俩垃,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼汰寓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吆寨!你這毒婦竟也來(lái)了啄清?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辣卒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后睛榄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡场靴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咧欣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡魄咕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毛萌,到底是詐尸還是另有隱情,我是刑警寧澤阁将,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布右遭,位于F島的核電站,受9級(jí)特大地震影響言蛇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宵距,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望满哪。 院中可真熱鬧,春花似錦哨鸭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)只估。三九已至,卻和暖如春蛔钙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吁脱。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留攻冷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像孵班,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子篙程,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354