原文:CI AND AUTOMATIC DEPLOYMENT TO ITUNES CONNECT WITH XCODE SERVER
译者:CocoaChina--刘行知
这篇文章中,我将介绍在Xmartlabs项目中,使用Xcode Server进行持续集成,并自动部署到iTunes Connect的一些经验,以及我所遇到问题。本文将描述我是如何解决其中的一些问题的,以期它可以帮助一些遇到相似情况的人。
已经有很多博客讲述如何设置Xcode Server,创建一个集成 bot(译者注:机器人,为便于理解与实践,本文中不翻),以及在Xcode上浏览其结果(问题跟踪,测试代码覆盖率等)。然而,当你尝试一些更复杂的东西,你可能会遇到一些错误时,而这些错误一般很难找到描述解决办法的资源。
为什么我们需要有自己的CI(continuous integration)服务器?
几乎每个人都知道拥有CI服务器的好处:它可以自动分析代码,运行单元和UI测试,在其他有价值的任务中构建项目。如果代码出现问题,它会将结果通知可能引入该问题的人。 Xcode bot跟踪每个集成的所有新问题以及已解决的问题。对于新的问题,bot将显示一系列可能产生问题的提交。此外,我们不再需要处理所部署环境的配置文件和证书,从而允许团队中的任何人轻松发布新版本的应用程序。
总之,这允许程序员花更多的时间在应用程序开发上,而在应用程序集成和部署上花更少的时间。同时,确保代码有质量问题的可能性保持在最低。
设置Xcode Server
苹果公司的[Xcode Server和持续集成指南],很好地介绍了如何设置和使用Xcode Server的知识,我们建议您首先阅读该指南,我们不再详细介绍关于设置Xcode Server的基础知识。
Cocoapods&Fastlane
安装Xcode Server应用程序并启用Xcode Server服务,下一步便是安装 Cocoapods 和 Fastlane 。Fastlane将帮助我们完成许多常规任务,这些任务是构建项目和将应用程序上传到iTunes Connect所必需的。为了防止它们运行过程中出现权限问题,我们将仅仅为对应的构建者(译者注:builder user,构建用户,本文中简称构建者),安装所有gem,使用 gem install --user-install some_gem 命令来完成安装。另外,我们需要创建符号链接,来访问 Cocoapods 和 Fastlane 二进制文件,以便在我们的bot运行时访问它们。
在开始之前,通过将下面的这一行加入到`~/.bashrc`和`~/.bash_login`文件内,将ruby bin文件夹包含到构建者的路径中:
# It may change depending on the ruby's version on your system(请根据你系统中ruby的版本来修改此处的版本号) export PATH="$PATH:/var/_xcsbuildd/.gem/ruby/2.0.0/bin"
现在开始安装gems:
$ sudo su - _xcsbuildd $ gem install --user-install cocoapods $ pod setup $ ln -s `which pod` /Applications/Xcode.app/Contents/Developer/usr/bin/pod $ gem install --user-install fastlane $ ln -s `which fastlane` /Applications/Xcode.app/Contents/Developer/usr/bin/fastlane
邮件 & 通知
Xcode Server有一个很好的功能,能够根据集成结果向选定的人发送电子邮件。例如,如果因为项目没有编译通过,或者一些测试没有通过,而导致的集成失败,bot将发送电子邮件到最后提交者,通知其构建已经失败了。
由于我们使用Gmail帐户发送电子邮件,因此需要更改Xcode Server上的邮件服务的设置。首先在服务器上启用邮件服务,然后检查选项“Relay outgoing mail through ISP”。在选项对话框中,添加smtp.gmail.com:587,启用身份验证并输入有效的凭据。这就是让 Xcode Server使用您的Gmail帐户发送电子邮件需要的所有设置。
创建bot
现在我们已经启动并运行了Xcode Server,现在该创建我们的Xcode bots了。在Xmartlabs,我们为每个Xcode项目设立了两个不同的bot。
持续集成 bot
为了确保项目正确构建,以及代码分析,单元和UI测试相应地都通过。每当一个拉取请求合并到开发分支中时,这个bot都将被自动触发。如果出现问题,它将通知提交者。
我们可以通过以下简单的步骤创建bot:
在Xcode项目中,选择菜单选项“Product”>”Create Bot”。
依照创建向导,比较简单就可以完成。在设置git凭据时,你可能会遇到一些困难。我们选择创建一个ssh密钥,并将其用于我们的bot。于是我们最终选择现有的SSH密钥,并对所有的bot使用相同的密钥。
集成它,看看一切是否运行良好。
> 比较好用的一点是,电子邮件将被发送到所有可能导致该问题的提交者,你也可以指定其他接收者。
部署型bot
第二个bot负责构建和上传应用程序IPA到iTunes Connect。它还将负责使用最新的代码仓库创建和推送新的git标签,而这我们将使用Fastlane来实现。因为我们通常需要每周发布一次测试版本,因此通常我们将其配置为按需运行或每周运行。
证书和私钥
我们必须确保在系统钥匙串上已经安装了分发/开发证书及其对应的私钥。
要构建IPA,我们必须在以下文件夹中放入必需的配置文件,因为bot在其自己的用户`_xcsbuildd `上运行,并在此文件夹中搜索配置文件:
/Library/Developer/XcodeServer/ProvisioningProfiles
集成前的脚本
Xcode集成时,允许我们提供,集成前和集成后的脚本。
在我们的部署型Bot开始集成之前,我们必须执行一些触发型的命令:
递增编译版本号
下载所需的配置文件
安装项目使用的库的正确版本
Fastlane工具将在`Appfile`文件中查找有用信息,以修改诸如 Apple ID 和 application Bundle Identifier 。下面的代码片段,介绍了`Appfile`:
app_identifier "(MY_APP_BUNDLE_ID)" # The bundle identifier of your app(因识别问题,本段代码中用圆括号替代尖括号) apple_dev_portal_id "(apple_dev_program_email@server.com)" # Your Apple email address itunes_connect_id "(itunes_connect_email@server.com)" # You can uncomment the lines below and add your own # team selection in case you are on multiple teams # team_name "(TEAM_NAME)" # team_id "(TEAM_ID)" # To select a team for iTunes Connect use # itc_team_name "(ITC_TEAM_NAME)" # itc_team_id "(ITC_TEAM_ID)"
lane :before_integration do # fetch the number of commits in the current branch build_number = number_of_commits # Set number of commits as the build number in the project's plist file before the bot actually start building the project. # This way, the generated archive will have an auto-incremented build number. set_info_plist_value( path: './MyApp-Info.plist', key: 'CFBundleVersion', value: "#{build_number}" ) # Run `pod install` cocoapods # Download provisioning profiles for the app and copy them to the correct folder. sigh(output_path: '/Library/Developer/XcodeServer/ProvisioningProfiles', skip_install: true) end
如果我们运行`fastlane before_integration`,它将连接到iOS Member Center,并使用在`Appfile`中的bundle id,下载该应用程序的配置信息文件。此外,我们必须将密码发送到fastlane。从而使这些操作配合Xcode bots工作,我们通过环境变量`FASTLANE_PASSWORD`上传密码:
$ export FASTLANE_PASSWORD="(APPLE_ID_PASSWORD)"(圆括号替换尖括号) $ fastlane before_integration
> 注意,在调用`fastlane`之前,我们切换到了`myapp`文件夹,这是git远程仓库名称。**触发器在父项目文件夹中运行**。
lane :after_integration do plistFile = './MyApp-Info.plist' # Get the build and version numbers from the project's plist file build_number = get_info_plist_value( path: plist_file, key: 'CFBundleVersion', ) version_number = get_info_plist_value( path: plist_file, key: 'CFBundleShortVersionString', ) # Commit changes done in the plist file git_commit( path: ["#{plistFile}"], message: "Version bump to #{version_number} (#{build_number}) by CI Builder" ) # TODO: upload to iTunes Connect add_git_tag( tag: "beta/v#{version_number}_#{build_number}" ) push_to_git_remote push_git_tags end
lane :after_integration do plistFile = './MyApp-Info.plist' # ... ipa_folder = "#{ENV['XCS_DERIVED_DATA_DIR']}/deploy/#{version_number}.#{build_number}/" ipa_path = "#{ipa_folder}/#{target}.ipa" sh "mkdir -p #{ipa_folder}" # Export the IPA from the archive file created by the bot sh "xcrun xcodebuild -exportArchive -archivePath /"#{ENV['XCS_ARCHIVE']}/" -exportPath /"#{ipa_path}/" -IDEPostProgressNotifications=YES -DVTAllowServerCertificates=YES -DVTSigningCertificateSourceLogLevel=3 -DVTSigningCertificateManagerLogLevel=3 -DTDKProvisioningProfileExtraSearchPaths=/Library/Developer/XcodeServer/ProvisioningProfiles -exportOptionsPlist './ExportOptions.plist'" # Upload the build to iTunes Connect, it won't submit this IPA for review. deliver( force: true, ipa: ipa_path ) # Keep committing and tagging actions after export & upload to prevent confirm the changes to the repo if something went wrong add_git_tag( tag: "beta/v#{version_number}_#{build_number}" ) # ... end
for_platform :ios do for_lane :before_integration_staging do app_identifier "com.xmartlabs.myapp.staging" end for_lane :after_integration_staging do app_identifier "com.xmartlabs.myapp.staging" end for_lane :before_integration_production do app_identifier "com.xmartlabs.myapp" end for_lane :after_integration_production do app_identifier "com.xmartlabs.myapp" end end apple_dev_portal_id "" itunes_connect_id "" # team_name ""(此处圆括号替代尖括号) # team_id ""(此处圆括号替代尖括号)
require './libs/utils.rb' fastlane_version '1.63.1' default_platform :ios platform :ios do before_all do ENV["SLACK_URL"] ||= "https://hooks.slack.com/services/#####/#####/#########" end after_all do |lane| end error do |lane, exception| reset_git_repo(force: true) slack( message: "Failed to build #{ENV['XL_TARGET']}: #{exception.message}", success: false ) end # Custom lanes desc 'Do basic setup, as installing cocoapods dependencies and fetching profiles, before start integration.' lane :before_integration do ensure_git_status_clean plist_file = ENV['XL_TARGET_PLIST_FILE'] # This is a custom action that could be find in the libs/utils.rb increase_build_number(plist_file) cocoapods sigh(output_path: '/Library/Developer/XcodeServer/ProvisioningProfiles', skip_install: true) end desc 'Required tasks before integrate the staging app.' lane :before_integration_staging do ENV['XL_TARGET_PLIST_FILE'] = './MyAppStaging-Info.plist' before_integration end desc 'Required tasks before build the production app.' lane :before_integration_production do ENV['XL_TARGET_PLIST_FILE'] = './MyApp-Info.plist' before_integration end desc 'Submit a new Beta Build to Apple iTunes Connect' lane :after_integration do branch = ENV['XL_BRANCH'] deliver_flag = ENV['XL_DELIVER_FLAG'].to_i plist_file = ENV['XL_TARGET_PLIST_FILE'] tag_base_path = ENV['XL_TAG_BASE_PATH'] tag_base_path = "#{tag_base_path}/" unless tag_base_path.nil? || tag_base_path == '' tag_link = ENV['XL_TAG_LINK'] target = ENV['XL_TARGET'] build_number = get_info_plist_value( path: plist_file, key: 'CFBundleVersion', ) version_number = get_info_plist_value( path: plist_file, key: 'CFBundleShortVersionString', ) ENV['XL_VERSION_NUMBER'] = "#{version_number}" ENV['XL_BUILD_NUMBER'] = "#{build_number}" tag_path = "#{tag_base_path}release_#{version_number}_#{build_number}" tag_link = "#{tag_link}#{tag_path}" update_changelog({ name: tag_path, version: version_number, build: build_number, link: tag_link }) ENV['XL_TAG_LINK'] = "#{tag_link}" ENV['XL_TAG_PATH'] = "#{tag_path}" sh "git config user.name 'CI Builder'" sh "git config user.email 'builder@server.com'" git_commit( path: ["./CHANGELOG.md", plist_file], message: "Version bump to #{version_number} (#{build_number}) by CI Builder" ) if deliver_flag != 0 ipa_folder = "#{ENV['XCS_DERIVED_DATA_DIR']}/deploy/#{version_number}.#{build_number}/" ipa_path = "#{ipa_folder}/#{target}.ipa" sh "mkdir -p #{ipa_folder}" sh "xcrun xcodebuild -exportArchive -archivePath /"#{ENV['XCS_ARCHIVE']}/" -exportPath /"#{ipa_path}/" -IDEPostProgressNotifications=YES -DVTAllowServerCertificates=YES -DVTSigningCertificateSourceLogLevel=3 -DVTSigningCertificateManagerLogLevel=3 -DTDKProvisioningProfileExtraSearchPaths=/Library/Developer/XcodeServer/ProvisioningProfiles -exportOptionsPlist './ExportOptions.plist'" deliver( force: true, ipa: ipa_path ) end add_git_tag(tag: tag_path) push_to_git_remote(local_branch: branch) push_git_tags slack( message: "#{ENV['XL_TARGET']} #{ENV['XL_VERSION_NUMBER']}.#{ENV['XL_BUILD_NUMBER']} successfully released and tagged to #{ENV['XL_TAG_LINK']}", ) end desc "Deploy a new version of MyApp Staging to the App Store" lane :after_integration_staging do ENV['XL_BRANCH'] = current_branch ENV['XL_DELIVER_FLAG'] ||= '1' ENV['XL_TAG_BASE_PATH'] = 'beta' ENV['XL_TARGET_PLIST_FILE'] = './MyApp Staging-Info.plist' ENV['XL_TARGET'] = 'MyApp Staging' ENV['XL_TAG_LINK'] = 'https://github.com/xmartlabs/MyApp/releases/tag/' after_integration end desc "Deploy a new version of MyApp to the App Store" lane :after_integration_production do ENV['XL_BRANCH'] = current_branch ENV['XL_DELIVER_FLAG'] ||= '1' ENV['XL_TARGET_PLIST_FILE'] = './MyApp-Info.plist' ENV['XL_TARGET'] = 'MyApp' ENV['XL_TAG_LINK'] = 'https://github.com/company/MyApp/releases/tag/' after_integration end end
关于前一个`Fastfile`文件的注意事项:
为生产环境和多阶段环境定义两个`before_integration` lane,以便使用`Appfile`设置正确的应用程序标识符。
编译,版本控制操作和部署操作封装在`after_integration` lane中。这使得我们可以产品和分阶段的`after_integration` lane,设置了不同的参数和内部调用。
ensure_git_status_clean`将检查bot的工作文件夹是否有更改,若更改,则运行失败。这将确保bot的工作副本与远程存储库文件完全相同。由于我们正在更新我们`after_integration` lane上的本地文件,如果出现问题,我们将需要重置所有文件。因此,我们在`error`块中添加了`reset_git_repo`操作。
命令`xcrun xcodebuild -exportArchive`需要使用选项`-exportOptionsPlist`指定的配置文件。我们在`fastlane`文件夹中创建了`ExportOptions.plist`文件,其内容类似于:
最后一步,添加一个新的在集成后触发器(After Integration Trigger),执行我们的`after_integration_staging` lane:
您可以在 [Fastlane CI files](https://github.com/xmartlabs/Fastlane-CI-Files)这个github仓库中,找到上面列出Fastlane文件的模板。
我们试图解锁它,然后运行`sigh`时,结果如下所示:
# Try to unlock the keychain to be accessed by fastlane actions $ security -v unlock-keychain -p `cat /Library/Developer/XcodeServer/SharedSecrets/PortalKeychainSharedSecret` /Library/Developer/XcodeServer/Keychains/Portal.keychain # Will download profiles using sigh $ fastlane before_integration_staging
我们根本无法在运行Fastlane时访问钥匙串。我们选择仅将密码保存为系统环境变量。
[!] Unable to satisfy the following requirements: - `SwiftDate` required by `Podfile` - `SwiftDate (= 3.0.2)` required by `Podfile.lock`
$ sudo rm -fr /var/_xcsbuildd/.cocoapods $ sudo su - _xcsbuildd $ gem install --user-install cocoapods $ pod setup $ ln -s `which pod` /Applications/Xcode.app/Contents/Developer/usr/bin/pod
Fastlane - Sigh & Gym 无法访问钥匙串
结果就是这样,它们不能访问钥匙串。看到这条消息(或类似的),当运行`gym`或`sigh`产生的结果如下:
security:SecKeychainAddInternetPassword :User interaction is not allowed.
它们无法访问存储的登录密码,必须使用`FASTLANE_PASSWORD`通过env变量传递密码至`sigh`。
$ sudo /Applications/Xcode.app/Contents/Developer/usr/bin/xcscontrol --initialize
/Library/Developer/XcodeServer/IntegrationAssets/$XCS_BOT_ID-$XCS_BOT_NAME/$XCS_INTEGRATION_NUMBER/$TARGET_NAME.ipa
$ echo 'eval "$(ssh-agent -s)"' >> ~/.bash_login $ echo 'ssh-add ~/.ssh/id_rsa_github' >> ~/.bash_login
Host github.com HostName github.com IdentityFile ~/.ssh/id_rsa_github
parameter ErrorMessage = ERROR ITMS-90035: "Invalid Signature. A sealed resource is missing or invalid. Make sure you have signed your application with a distribution certificate, not an ad hoc certificate or a development certificate. Verify that the code signing settings in Xcode are correct at the target level (which override any values at the project level). Additionally, make sure the bundle you are uploading was built using a Release target in Xcode, not a Simulator target. If you are certain your code signing settings are correct, choose "Clean All" in Xcode, delete the "build" directory in the Finder, and rebuild your release target. For more information, please consult https://developer.apple.com/library/ios/documentation/Security/Conceptual/CodeSigningGuide/Introduction/Introduction.html