iOS 自动化杂谈

前言:可持续集成自动化的话题已经老生常谈了。目前市面上比较流行的自动化流程工具——Fastlane,Fastlane是用Ruby语言编写的一套自动化工具集和框架,Fastlane的工具集基本上涵盖了打包,签名,测试,部署,发布,库管理等等用起来比较方便,配合Jenkins可持续化集成,基本可以满足大部分的流程自动化。

image

一. 打包

实现打包有很多种,例如xcodebuild,但已经有好用的工具集为何不用呢?
跟着打包的流程写脚本,例如我想打包,得提供给别人选择哪个分支,采用什么类型,及时通知等

image
  • Jenkins上装了Git parameter plug-In 0.9.12版本的插件进行分支选择
  • 想暴露什么参数在Jenkins上自定义
  • 利用fastlane gym
  • 上传蒲公英
  • 由于之前蒲公英挂过一次,不能完全依赖第三方分发平台,自己再自建一个OTA服务器来内测分发
  • 自定义内测的二维码采用python myqr生成
  • 消息通知:我司采用企业微信,那就搞个机器人webhook一下,当然也可以脚本发个邮件
  • 符号表选择是否上传
desc "ad_Hoc 版本"
  lane :beta do  |options|
    # 新建build号
    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
    # 获取版本号
    version = get_version_number(
      xcodeproj: "xxxxx.xcodeproj",
      target: "xxxxx"
    )
    # 打包环境
    configuration = (options[:configuration] ? options[:configuration] : "Release")
    ipaName="xxxxx"
    ipaPath=configuration + "/" + version + "." + new_build + "/"
    # 导出ipa包地址
    output_directory = "/Users/admin/WebSites/app/ipa/" + ipaPath
    #manifest.plilst需要的参数
    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
             },
         }
    )
    # 参数传给内测分发网页
    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 工具,需要设置环境变量

image

以上已经实现了打包,接下来上传蒲公英

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

如果实现企业微信通知,其实就是发送一个请求,此时要注意的是fastlane 是ruby 环境,执行shell脚本的 & 或是 双引号需要转义:\ ,并非一个\,例如转义&:\&
<img src="https://zhonghphuan.github.io/images/iOS-自动化-打包/WX20200820-170210@2x.png" width = "300" alt="" align=center />
基本以上已经实现了打包的日常需求了,gym中的export_options是自建内测分发的manifest配置

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

以下简单描述一下自建OTA服务

  1. 启动Web服务 - Mac自带Apache
➜  ~ httpd -v
Server version: Apache/2.4.41 (Unix)
Server built:   Apr 17 2020 19:06:36
  • 启动:sudo apachectl start
  • 停止:sudo apachectl stop
  • 重启:sudo apachectl restart

启动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,去掉以下两个模块的注释
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. 重启服务:sudo apachectl restart,浏览器输入具体ip地址
  2. 配置目录
$ sudo mkdir ipa
$ sudo mkdir icon
$ sudo mkdir ssl
$ sudo mkdir plist
image

拷贝/private/etc/apache2/ssl/ip211.crt 到 这个ssl目录下:
sudo cp /private/etc/apache2/ssl/ip211.crt ~/WebSites/app/ssl/ip211.crt

  1. 制作一个简单的页面

解析链接中的itms-services:// 实现OTA

<!DOCTYPE html>
<html>

<head lang="zh-cmn-Hans">
  <meta charset="UTF-8">
  <title>分发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">
    更新时间:
  </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 = "更新时间:" + 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;" href="http://10.104.33.114/app/ssl/ip211.crt">下载证书</a>
  <p style="display:block;margin: 30px;">点击下载证书,下载安装配置文件</p>
  <p style="display:block;margin: 30px;">在设置-通用-描述文件与设备管理中,选择已下载的配置文件,进行安装</p>
  <p style="display:block;margin: 30px;">在设置-通用-关于本机-证书信任设置中将完全信任打开</p>


</body>

</html>
image
二. testflight 自动化公测
  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就不自动提审了
            skip_waiting_for_build_processing: false,
            beta_app_feedback_email:"xxxxx@xxx.net",
            beta_app_description:options[:desc],
            demo_account_required: true,
        #构建是否应该分发给外部测试人员?
            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]
              }
            }
    )

但这样有个问题,需要双重验证,通过fastlane spaceauth 生成的session一个月就过期了

#!/bin/bash
# 双重验证session一个月过期,执行下面方法输入验证码继续一个月
# 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

那这个方法其实不太能完美,只能利用 苹果自动化api来实现

  1. 方案二:苹果自动化api

先ruby封装几个函数

require "base64"
require "jwt"
require 'json'
# 准备分支信息
  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

  # 审核状态
  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

  # 内审状态
  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

     # 测试人员添加测试组中
  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 "将测试人员添加到组中: #{betaTesters}"
      betaTestersData = JSON.parse(betaTesters)
      id = betaTestersData["data"]["id"]
      rescue
        sleep 5 * 60
      retry
    end
  end

  # 创建组
  def createGroup(groups)
    jwt_token = getToken()
    puts "令牌:#{jwt_token}"
    # 创建组
    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添加测试组中
  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
  # 启用公测链接
  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 "链接请求: #{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
 
  # 获取苹果凭据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

此处根据 苹果自动化api文档先本地通过postman去调试验证,如下图,header中的Authorization为key,value为 "Bearer 苹果凭据token"

image

具体实现

desc "发布testflight版本"
  lane :testflight do  |options|
    #新建build号
    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
    # 获取版本号
    version = get_version_number(
      xcodeproj: "xxxx.xcodeproj",
      target: "xxxx"
    )
    # 打包环境
    configuration = "Release"
    ipaName="xxxx"
    ipaPath=configuration + "/" + version + "." + new_build + "/"
    # 导出ipa包地址
    output_directory = "/Users/admin/WebSites/app/ipa/" + ipaPath
    #manifest.plilst需要的参数
    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 "======================== 验证出错 ========================"
      exit
    end
    puts "======================== 验证成功 ========================"
    `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 "======================== 上传出错 ========================"
      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 = "公测包:"+ groups
    UI.message "description:#{description}"

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

    # 轮询
    internalBuildStat =  getInternalBuildState(buildid)
    puts "提交内审状态:#{internalBuildStat}"
    
    while !(internalBuildStat.casecmp?("IN_BETA_TESTING"))  do
      sleep 5 * 60
      internalBuildStat =  getInternalBuildState(buildid)
      puts "提交内审状态:#{internalBuildStat}"
    end

    # 发出企业微信通知:可以提交审核
    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 "审核请求结果:#{betaAppReviewSubmissions}"
    
    # 获取审核状态
    buildstate =  getBuildState(buildid)
    puts "审核状态:#{buildstate}"
    laststate = buildstate
    if buildstate.casecmp?("WAITING_FOR_BETA_REVIEW")
      # 发出企业微信通知:等待审核状态
    end

    if buildstate.casecmp?("IN_REVIEW")
      # 发出企业微信通知
    end
    
    # 轮询查看审核状态(每隔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"))
          # 发出企业微信通知
          else
            # 发出企业微信通知
        end
      end
      laststate =  buildstate;
      puts "审核状态:#{buildstate}"
    end

    if (buildstate.casecmp?("REJECTED") || buildstate.casecmp?("BETA_REJECTED"))
      # 发出企业微信通知:等待审核状态
        puts "#{groups} 公测审核被拒,请前往App Store查看原因"
        exit
    end

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

      jwt_token = getToken()
      puts "令牌:#{jwt_token}"
      # 创建组
      groupid = createGroup(groups)
      puts "获取到组id:#{groupid}"
      sleep 5

      # 将测试人员添加到组中
      getBetaTesters(groupid)
      sleep 5

      # 将build添加到组中
      addBetaGroups(groupid,buildid)
      sleep 5

      #获取本地化id
      betaBuildLocalizationsid = getBetaBuildLocalizationsid(buildid,options[:desc])
      puts "betaBuildLocalizationsid:#{betaBuildLocalizationsid}"
      
      #修改本地化测试信息
      patchBetaBuildLocalizations(betaBuildLocalizationsid,options[:desc])

      # 启用公测链接
      public_link = getPublic_link(groupid,groups)
      puts "公测链接: #{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
三. 总结
  1. 公测自动化实现后,App Store打包通过打包验证和上传也很容易实现
  2. Jenkins + fastlane 较为方便的实现可持续集成自动化的流程
  3. python、ruby、shell等语言实现脚本思想一样,哪个方便用哪个
  4. 能工具化提高效率的尽量工具化自动化,为公司节省人力,提高工作效率
  5. 消息通知最终流程过程或结果可以采用邮件、webhook机器人消息等
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270