iOS持续集成 CICD

最近这两天生完Xcode发现云可以用了,但是碍于把公司代码直接放到公网不太合适。但是也是一个机会偷偷自动化一下打包逻辑,毕竟人为操作的话,有几个时间节点需要等,年纪也大了,一打断就忘记接着干嘛了。还是能机器一气呵成的就机器来吧。
之前的打包逻辑主要有以下几个步骤。

  1. 修改版本号、build号、项目名称等信息;
  2. 使用xcode执行build archive;
  3. 在organizer中导出包,手动上传到 fir.im;
  4. 在organizer中导出包,手动上传到testflight;

按照这个思路逐步把逻辑脚本化,脚本的话当然还是用python比较好,人生苦短。

文件结构

主要有以下几个文件及文件结构:

- cicd.py # 主要执行脚本
- cicd # 放打包产物及配置等信息文件夹
   |- changelog.txt # 版本修改信息,可以在fir.im中查看
   |- ExportOptions.plist # 测试版本自动打包配置
   |- ReleaseExportOptions.plist # 正式版本自动打包配置
   |- build # 编译产物
   |- output # ipa包

修改版本号、build号、项目名称等信息

使用到命令行工具 agvtool
详细原理及配置参考链接https://blog.csdn.net/xo19882011/article/details/121204208
配置好相关项目之后转到命令上来:

agvtool new-marketing-version version # 相应的发布版本
agvtool new-version -all buildnum # 相应的buildnum

这样打版本的准备就做好了。

打版本

使用到命令行工具 xcodebuild,这个命令的详细介绍还是看帮助文档比较好,然后再结合不太理解的参数搜搜,也参考了一下别人的博客

xcodebuild archive \
    -workspace xxx.xcworkspace \ # 项目里边一般都有cocospod,所以一般都是 xcworkspace
    -scheme xxx \ # debug 跟 release 不同,使用的时候采用不同的scheme
    -destination generic/platform=iOS \
    -configuration xxx \ # build setting中配置的configuration,一般来说可能是 Debug / Release
    -archivePath cicd/build/xxx.xcarchive \
    -allowProvisioningUpdates \
    -allowProvisioningDeviceRegistration

最后可能会报错,但是只要是产物在一般来说没啥问题。

导出ipa

使用到命令行工具 xcodebuild

xcodebuild -exportArchive \
    -archivePath cicd/build/debug/xxx.xcarchive \
    -exportOptionsPlist cicd/ExportOptions.plist \
    -exportPath cicd/output/debug/

其中 ExportOptions.plist文件内容如下,也是对应着xcode导出包的时候每个步骤对应的配置,当前用到的是自动签名。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>compileBitcode</key>
    <false/>
    <key>method</key>
    <string>development</string>
    <key>signingStyle</key>
    <string>automatic</string>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>teamID</key>
    <string>xxxx</string>
    <key>thinning</key>
    <string>&lt;none&gt;</string>
</dict>
</plist>

上传ipa到fir.im

结合 fir.im 的文档 https://www.betaqr.com/docs,使用到发布应用那项。代码如下:

print(f'\033[36m--- 上传 ---\033[0m')
res = requests.post('http://api.bq04.com/apps', json={
  'type': 'ios',
  'bundle_id': 'com.xxxx', # bunldid
  'api_token': 'xxxx' # 相应的 fir.im token
})
print('token is ', res.text)
res = json.loads(res.text)
binary_key = res['cert']['binary']['key']
binary_token = res['cert']['binary']['token']
binary_upload_url = res['cert']['binary']['upload_url']
icon_key = res['cert']['icon']['key']
icon_token = res['cert']['icon']['token']
icon_upload_url = res['cert']['icon']['upload_url']
res = requests.post(icon_upload_url, data={
  'key': icon_key,
  'token': icon_token,
}, files={'file': open('图标地址', 'rb')})
print('upload icon res ', res.content)
res = requests.post(binary_upload_url, data={
  'key': binary_key,
  'token': binary_token,
  'x:name': 'xxxx',
  'x:version': args.version,
  'x:build': buildnum,
  'x:changelog': open('cicd/changelog.txt', 'r').read(),
}, files={'file': open('cicd/output/debug/xxxx.ipa', 'rb')})
print('upload binary res ', res.content)

changelog记录一下版本更新内容,方便测试人员确认信息。

上传 Test Flight

使用到命令行工具 xcrun altool

  xcrun altool \
    --upload-app \
    -f cicd/output/release/xxx.ipa \
    -t ios \
    --apiKey xxx \
    --apiIssuer xxx \
    --verbose 

其中apiKey及apiIssuer在在 app store connect 的用户和访问 -> 密钥中新建及查看。
新建一个密钥,记录密钥 ID,并下载相应的私钥。把密钥放在以下目录其中之一中

'./private_keys'
'~/private_keys'
'~/.private_keys'
'~/.appstoreconnect/private_keys'

apiKey => 密钥ID
apiIssuer => Issuer ID

之后没啥问题就能直接上传了。

完整脚本

# coding = utf8
import os
import sys
import argparse
import requests
import json
import datetime

WORKSPACE = 'xxxx.xcworkspace'
BUNDLEID = 'com.xxxx'
FIRIMTOKEN = 'xxxx'
ICONPATH = 'Images.xcassets/AppIcon.appiconset/iphone1024.png'
DISPLAYNAME = 'xxxx'
TARGETNAME = 'xxxx'
API_KEY = 'xxxx'
API_ISSUER = 'xxxx'

DEBUG_SCHEME = 'xxxx'
DEBUG_CONFIGURE = 'Debug'
DEBUG_ARCHIVEPATH = 'cicd/build/debug/xxxx.xcarchive'
DEBUG_exportPath = 'cicd/output/debug/'
DEBUG_exportOptionsPlist = 'cicd/ExportOptions.plist'
DEBUG_archivePath = 'cicd/build/debug/xxxx.xcarchive'

RELEASE_SCHEME = 'xxxxDistribute'
RELEASE_CONFIGURE = 'xxxxDistribute'
RELEASE_ARCHIVEPATH = 'cicd/build/release/xxxxRelease.xcarchive'
RELEASE_exportPath = 'cicd/output/release/'
RELEASE_exportOptionsPlist = 'cicd/ReleaseExportOptions.plist'
RELEASE_archivePath = 'cicd/build/release/xxxxRelease.xcarchive'

def clean():
  print(f'\033[36m--- 清理文档 ---\033[0m')
  os.system('rm -rf cicd/build/*')
  os.system('rm -rf cicd/output/*')

def configuration(version, buildnum):
  print(f'\033[36m--- 设置版本号 ---\033[0m')
  os.system('agvtool new-marketing-version {}'.format(version))
  os.system('agvtool new-version -all {}'.format(buildnum))

def build(mode):
  print(f'\033[36m--- 打版本 ---\033[0m')
  scheme = ''
  configure = ''
  archivePath = ''
  if mode == 'debug':
    scheme = DEBUG_SCHEME
    configure = DEBUG_CONFIGURE
    archivePath = DEBUG_ARCHIVEPATH
  else:
    scheme = RELEASE_SCHEME
    configure = RELEASE_CONFIGURE
    archivePath = RELEASE_ARCHIVEPATH

  os.system('''
    xcodebuild archive \
    -workspace {} \
    -scheme {} \
    -destination generic/platform=iOS \
    -configuration {} \
    -archivePath {} \
    -allowProvisioningUpdates \
    -allowProvisioningDeviceRegistration
  '''.format(WORKSPACE, scheme, configure, archivePath))

def export(mode):
  print(f'\033[36m--- 打ipa版本 ---\033[0m')
  exportPath = ''
  exportOptionsPlist = ''
  archivePath = ''
  if mode == 'debug':
    exportPath = DEBUG_exportPath
    exportOptionsPlist = DEBUG_exportOptionsPlist
    archivePath = DEBUG_archivePath
  else:
    exportPath = RELEASE_exportPath
    exportOptionsPlist = RELEASE_exportOptionsPlist
    archivePath = RELEASE_archivePath

  os.system('''
    xcodebuild -exportArchive \
    -archivePath {} \
    -exportOptionsPlist {} \
    -exportPath {}
  '''.format(archivePath, exportOptionsPlist, exportPath))

def uploadFim(buildnum):
  print(f'\033[36m--- 上传 ---\033[0m')
  res = requests.post('http://api.bq04.com/apps', json={
    'type': 'ios',
    'bundle_id': BUNDLEID,
    'api_token': FIRIMTOKEN
  })
  print('token is ', res.text)
  res = json.loads(res.text)
  binary_key = res['cert']['binary']['key']
  binary_token = res['cert']['binary']['token']
  binary_upload_url = res['cert']['binary']['upload_url']
  icon_key = res['cert']['icon']['key']
  icon_token = res['cert']['icon']['token']
  icon_upload_url = res['cert']['icon']['upload_url']
  res = requests.post(icon_upload_url, data={
    'key': icon_key,
    'token': icon_token,
  }, files={'file': open(ICONPATH, 'rb')})
  print('upload icon res ', res.content)
  res = requests.post(binary_upload_url, data={
    'key': binary_key,
    'token': binary_token,
    'x:name': DISPLAYNAME,
    'x:version': args.version,
    'x:build': buildnum,
    'x:changelog': open('cicd/changelog.txt', 'r').read(),
  }, files={'file': open('cicd/output/debug/{}.ipa'.format(TARGETNAME), 'rb')})
  print('upload binary res ', res.content)

def uploadTestFlight():
  print(f'\033[36m--- 上传 Test Flight ---\033[0m')
  os.system('''
  xcrun altool \
    --upload-app \
    -f cicd/output/release/{}.ipa \
    -t ios \
    --apiKey {} \
    --apiIssuer {} \
    --verbose 
  '''.format(TARGETNAME, API_KEY, API_ISSUER))

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='cicd parser')
  parser.add_argument('-v', type=str, dest='version', metavar='版本号', default='', required=True)
  parser.add_argument('-m', type=str, dest='mode', metavar='模式', default='debug')
  args = parser.parse_args()
  if args.version == '':
    exit(0)

  buildnum = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M')
  print('build num ', buildnum)

  # 清理
  clean()
  # 配置
  configuration(args.version, buildnum)
  # build
  build(args.mode)
  # export
  export(args.mode)
  
  if args.mode == 'debug':
    # 上传
    uploadFim(buildnum)
  else:
    # 上传testflight
    uploadTestFlight()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容