原文:CI AND AUTOMATIC DEPLOYMENT TO ITUNES CONNECT WITH XCODE SERVER
作者:Miguel Revetria
譯者:CocoaChina--劉行知(QQ1445152551)
這篇文章中,我將介紹在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 "([email protected])" # Your Apple email address itunes_connect_id "([email protected])" # 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 '[email protected]'" 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