※ 現在(2015/01/07)Circle CIのiOSビルドはベータとなっているため、利用するにはサポートへの連絡が必要です。

できること

GitHubへのコミット毎に以下のタスクを実行します。

  • ユニットテストの実行
  • ipaの作成
  • ipaのバリデーション
  • CrittercismにdSYMをアップロード
  • Circle CIにテストレポートを出力
  • iTunes Connectにipaをアップロード

これらの作業を自動化すると、masterブランチにgit pushするだけでテストが通ったアプリがiTunes Connectに登録され、 TestFlight(新)からダウンロードできるようになり、そのままApp Storeに申請することまで出来るようになります。

Circle CIのメリット

  • 無料(1 container)
  • 見た目がきれい
  • private repositoryもビルドできる
  • GitHubのhookを自動的に設定してくれる
  • GitHubのcommit statusを自動的に更新してくれる
  • 毎回フレッシュな環境でビルドできる
  • なんでも実行できる

これらの特徴はTravis CIと似ていますが、無料のTravis CIはprivate repositoryのビルドができず、 private repositoryに対応しているTravis Proは$129/monthと個人利用には高価です。

やること

  • xcodebuild testでテストを実行し、CIRCLE_TEST_REPORTSにjunitのテストレポートを出力
  • securityで署名関係のアイテムをコンテナにインストール
  • agvtoolで一意なCFBundleVersionをつける
  • xcodebuild archiveでxcarchiveを作成
  • xcodebuild -exportArchiveでipaを作成
  • curlでCrittercismにdSYMをアップロード
  • altoolでiTunes Connectにipaをアップロード

Rakefile

WORKSPACE    = 'YourApp.xcworkspace'
SCHEME       = 'YourApp'
PRODUCT_NAME = 'YourApp'
BUILD_DIR    = 'build'

PROVISIONING_PROFILE_UUID = '00000000-0000-0000-0000-000000000000'
KEYCHAIN_NAME             = 'custom.keychain'

PROVISIONING_PROFILE_PATH = '/path/to/provisioning_uuid.mobileprovision'
APPLE_CERTIFICATE_PATH    = '/path/to/apple.cer'
DISTRIBUTION_CER_PATH     = '/path/to/distribution.cer'
DISTRIBUTION_P12_PATH     = '/path/to/distribution.p12'

P12_PASSPHRASE          = ENV['P12_PASSPHRASE']
ITUNES_CONNECT_ID       = ENV['ITUNES_CONNECT_ID']
ITUNES_CONNECT_PASSWORD = ENV['ITUNES_CONNECT_PASSWORD']
CRITTERCISM_APP_ID      = ENV['CRITTERCISM_APP_ID']
CRITTERCISM_KEY         = ENV['CRITTERCISM_KEY']

PRODUCT_DIR    = File.join(BUILD_DIR, 'product')
XCARCHIVE_PATH = File.join(PRODUCT_DIR, "#{SCHEME}.xcarchive")
IPA_PATH       = File.join(PRODUCT_DIR, "#{SCHEME}.ipa")
DSYM_DIR       = File.join(XCARCHIVE_PATH, "dSYMs")
DSYM_PATH      = File.join(DSYM_DIR, "#{PRODUCT_NAME}.app.dSYM.zip")

desc 'delete build directories and execute xcodebuild clean.'
task :clean do
  rm_rf(BUILD_DIR) if File.exist?(BUILD_DIR)
  sh 'set -o pipefail && xcodebuild clean | xcpretty -c'
end

desc 'install developer identity profile.'
task :install_signing_items do
  sh "cp #{PROVISIONING_PROFILE_PATH} $HOME/Library/MobileDevice/Provisioning\\ Profiles"
  sh "security create-keychain -p circle #{KEYCHAIN_NAME}"
  sh "security import #{APPLE_CERTIFICATE_PATH} -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import #{DISTRIBUTION_CER_PATH} -k #{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import #{DISTRIBUTION_P12_PATH} -k #{KEYCHAIN_NAME} -P #{P12_PASSPHRASE} -T /usr/bin/codesign"
  sh "security default-keychain -s #{KEYCHAIN_NAME}"
end

desc 'run unit tests on main target.'
task :test do
  report_dir = ENV['CIRCLE_TEST_REPORTS']
  report_option = ""
  if report_dir
    report_option = "-r junit -o #{report_dir}/test-report.xml"
  end

  sh "set -o pipefail && xcodebuild test \\
      -sdk iphonesimulator \\
      -workspace #{WORKSPACE} \\
      -scheme #{SCHEME} | xcpretty -c #{report_option}"
end

desc 'archive ipa.'
task :archive do
  rm_rf(PRODUCT_DIR) if File.exist?(PRODUCT_DIR)
  mkdir_p(PRODUCT_DIR)

  sh "set -o pipefail && xcodebuild archive \\
      -sdk iphoneos \\
      -configuration Release \\
      -workspace #{WORKSPACE} \\
      -scheme #{SCHEME} \\
      -archivePath #{XCARCHIVE_PATH} \\
      PROVISIONING_PROFILE=#{PROVISIONING_PROFILE_UUID} | xcpretty -c"

  provisioning_path = "~/Library/MobileDevice/Provisioning\\ Profiles/#{PROVISIONING_PROFILE_UUID}.mobileprovision"
  provisioning_name = `/usr/libexec/PlistBuddy -c 'Print Name' /dev/stdin <<< $(security cms -D -i #{provisioning_path})`.chomp

  sh "set -o pipefail && xcodebuild \\
      -exportArchive \\
      -exportFormat ipa \\
      -archivePath #{XCARCHIVE_PATH} \\
      -exportPath #{IPA_PATH} \\
      -exportProvisioningProfile \"#{provisioning_name}\" | xcpretty -c"

  sh "(cd #{DSYM_DIR}; zip -r #{PRODUCT_NAME}.app.dSYM.zip #{PRODUCT_NAME}.app.dSYM)"

  # Xcode 6.1 and 6.1.1 cannot archive ipa contains SwiftSupport correctly
  # sh "sh package_ipa.sh #{XCARCHIVE_PATH}/Products/Applications/#{PRODUCT_NAME}.app #{IPA_PATH}"
end

desc 'submit dSYM to Crittercism'
task :crittercism do
  sh "curl https://app.crittercism.com/api_beta/dsym/#{CRITTERCISM_APP_ID} \\
      -F dsym=@#{DSYM_PATH} \\
      -F key=#{CRITTERCISM_KEY}"
end

desc 'validate ipa.'
task :validate_ipa do
  sh "altool \\
      --validate-app \\
      --file #{IPA_PATH} \\
      --username #{ITUNES_CONNECT_ID} \\
      --password #{ITUNES_CONNECT_PASSWORD}"
end

desc 'upload ipa.'
task :upload_ipa do
  sh "altool \\
      --upload-app \\
      --file #{IPA_PATH} \\
      --username #{ITUNES_CONNECT_ID} \\
      --password #{ITUNES_CONNECT_PASSWORD}"
end

desc 'run deploy tasks.'
task :deploy => [:install_signing_items,
                 :clean,
                 :archive,
                 :crittercism,
                 :validate_ipa,
                 :upload_ipa]

circle.yml

machine:
  environment:
    PATH: $PATH:/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support
    LC_CTYPE: en_US.UTF-8

dependencies:
  override:
    - sudo gem install xcpretty

test:
  override:
    - rake test

deployment:
  master:
    branch: master
    commands:
      - agvtool new-version -all $CIRCLE_BUILD_NUM
      - rake deploy

参考

今回は細かい作業の説明を省きましたが、それぞれのコマンドの説明はk_katsumiさんが書いたTravis CIでiOSアプリのリリース作業を自動化するで詳しく説明されています。 また、Circle CIにベータ利用の連絡するまでの細かい手順についてはsakuさんが書いた大晦日〜正月にiOSでCircleCIを試したので振り返ってみたで詳しく説明されています。