デイキャンプとカメラにハマっているネイティブアプリグループの堤下(@roana0229)です。
みなさんはCI導入していますか?
今まで弊社のiOS,AndroidアプリではCIが導入できていない現状がありましたが、2月末にリリースしたマッハバイトiOS版の開発を機に導入して、自動化していることを紹介します。
CIサービスは色々ありますが最近はよくBitriseを導入しているのを見かけます。ビルド環境への対応の早さやGUIでの操作しやすさから弊社もBitriseを導入しました。
静的なチェックの自動化
UnitTest以外では、静的なチェックでdangerを利用しています。dangerではgitの差分が扱いやすいようになっています。例えば、Swaggerを利用していてるのでAPI定義のyamlとswagger-codegenによって生成されるコードの差分をチェックしています。
swagger_file = "swagger/api_doc.yaml" swagger_auto_generated_files = ".+/Data/API/.+" has_swagger_changes = !git.modified_files.grep(/#{swagger_file}/).empty? || !git.deleted_files.grep(/#{swagger_file}/).empty? has_swagger_auto_generated_changes = !git.modified_files.grep(/#{swagger_auto_generated_files}/).empty? || !git.deleted_files.grep(/#{swagger_auto_generated_files}/).empty? if has_swagger_changes if has_swagger_auto_generated_changes message("`api_doc.yaml`,`自動生成されたファイル(Data/API/**)`の変更が含まれています。") else warn("`api_doc.yaml`が変更されていますが、`自動生成されたファイル(Data/API/**)`に変更がありません。`Data/API/**`ファイルを変更する必要がないか確認してください。") end end
PRには、このようにdangerによるコメントが付けられます。
Swaggerの導入は今回が初めてだったこともあり、自動生成されるだけのファイルやAPI定義だけ更新されていたり、意図しないAPI実装の変更をしてしまわないように気付くことができるようになりました。
他にも、開発ブランチの最新の状態なのにXcodeで開くと警告が出ていたり、実ディレクトリとXcode上のディレクトリがズレている経験ってありませんか?
Dangerfile
はrubyで記述されているため任意のコードを実行しやすく、realm/SwiftLintの警告やvenmo/synxが適応されているかなど、他のツールを用いたチェックを行っています。
def check_synx `bundle exec synx app.xcodeproj >/dev/null` synx_diff = `git diff app.xcodeproj/project.pbxproj` fail("`synx`を実行すると差分が発生しました。`bundle exec synx app.xcodeproj`し、`app.xcodeproj/project.pbxproj`の差分をコミットする必要があります。") unless synx_diff.empty? end swiftlint.config_file = '.swiftlint.yml' swiftlint.binary_path = 'Pods/SwiftLint/swiftlint' swiftlint.lint_files inline_mode: true, fail_on_error: true, additional_swiftlint_args: '--no-force-exclude' check_synx
静的なチェックであるためCI実行してからPRに適応されるまでかかる時間も短く、GithubのBranch protection rules
を設定しておくことで、簡単にブランチの最低限の品質を保つことができます。
細かいところが気になって、本質的なコードレビューから意識がズレてしまうことが過去にあったのですが、これらによりレビュー時に意識しなくてはいけないことを減らしコードに集中したレビューができるようになりました。
アプリ配布の自動化
アプリをリリースする時、一定の決まった作業がありますよね。これらはfastlaneを利用して、リリースのために必要なことは下記の3つで完了するようにしています。
- fastlaneでリリース準備用のコマンド実行する
- リリースPRのレビューを経てmasterにマージする
- (申請後に)Firebase CrashlyticsへdSYMをアップロードする
fastlaneでリリース準備用のコマンド実行する
リリース準備の処理はこのようになっています。
lane :release_update_version do |options| update_commit_to_latest(branch: options[:branch] || "develop") unless is_ci? update_version release_summary = get_done_cards_summary(github_token: ENV["Githubのトークン"]) app_version = get_version_number(xcodeproj: "app.xcodeproj", target: "app") build_version = get_build_number git_add git_commit(path: "*", message: "v#{app_version} (build version: #{build_version})") push_to_git_remote pr_url = create_pull_request( repo: "リポジトリ", title: "#{app_version}(#{build_version})", base: "master", body: release_summary ) sh("open #{pr_url}") unless is_ci? end
今後CIでやることを見据えていますが、現在はローカルで実行しています。
そのため最初に実行される処理であるprivate_lane :update_commit_to_latest
により、期待しているブランチで実行されているか、ブランチを最新にして差分が発生していないかをチェックしています。
private_lane :update_commit_to_latest do |options| UI.user_error!("branch:ブランチ名 が必要です") unless options[:branch] base_branch = options[:branch] ensure_git_branch(branch: base_branch) rescue UI.user_error!("#{base_branch}ブランチで実行してください") ensure_git_status_clean rescue UI.user_error!("差分がない状態にしてください") sh("git pull origin #{base_branch}") ensure_git_status_clean rescue UI.user_error!("#{base_branch}を最新にした状態で差分が発生しました、差分がない状態にしてください") end
次に実行されるバージョンのアップデートはprivate_lane :update_version
のようにしておけば、ビルドバージョンの更新 or アプリバージョンを更新した時にビルドバージョンを1にすることができます。
private_lane :update_version do |options| bump_type = options[:bump_type] || UI.select("バージョンをアップデートしますか?: ", ["only build_version", "major", "minor", "patch"]) if bump_type == "only build_version" then increment_build_number( xcodeproj: "app.xcodeproj" ) else increment_build_number( build_number: 1, xcodeproj: "app.xcodeproj" ) increment_version_number( bump_type: bump_type ) end end
その後にget_done_cards_summary
というRubyスクリプトにより、完了したタスクを整形しdevelop to master
PRが作成されます。
開発タスクはGithub Projectsで管理していてるため、APIを利用して完了したタスクを吸い上げ、その内容を整形した状態をPRに記載した状態で下記のようなPRが生成されるようになっています。
整形しやすいようにPRには機能追加 or 修正が判別できるラベルを付けて判別しています。
これにより、今回のリリースに含まれる内容を手作業でまとめ直す必要もなく、PRを共有するだけで良いため、リリース前にエンジニア以外が実機でテストするタイミングにも役立てています。
PRが作成されると、CIによりdeploygateへstaging
環境のアプリを配布され、slackに通知されます。このタイミングで修正が必要になった場合、通常通りPRを作り、developにマージすることでリリース用PRでもCIが再度走り、アプリが配布されます。
developへのマージ前でも任意のタイミングでBitriseをGUI上から操作して、ブランチを指定しdevelopment
環境のアプリを配布しています。
リリースPRのレビューを経てmasterにマージする
リリースビルドを手元で行うと時間がかかり、ターミナルをまだ終わってないかとチラ見して集中力が削がれてしまっていました。そのため、今はマージするだけでCIによりリリース作業が行われるようにしています。
lane :release_to_store do |options| slack( slack_url: ENV["Slackの通知用URL"], pretext: "ストアへのリリース処理を開始します(is_ci?: #{is_ci?})", message: "ビルド時の情報 Git Commit Hash が正しいか確認してください", channel: "#チャンネル" ) app_version = get_version_number(xcodeproj: "app.xcodeproj", target: "app") build_version = get_build_number release_summary = get_done_cards_summary(github_token: ENV["Githubのトークン"]) build_ios_app( workspace: "app.xcworkspace", configuration: "Release", scheme: "app", clean: true, export_method: "app-store", output_directory: "./fastlane/generated", output_name: "app.ipa" ) upload_to_app_store( force: true, team_id: "チームID", app_identifier: "アプリID", ipa: "./fastlane/generated/app.ipa", skip_screenshots: true, skip_metadata: true, skip_app_version_update: true, run_precheck_before_submit: false ) set_github_release( repository_name: "リポジトリ", api_token: ENV["Githubのトークン"], name: app_version, tag_name: "#{app_version}(#{build_version})", description: release_summary, commitish: "master", is_prerelease: true ) slack( slack_url: ENV["Slackの通知用URL"], pretext: "Release版のストアへのアップロードが完了しました!", message: "GitHub Releases: https://github.com/リポジトリ/releases", channel: "#チャンネル" ) end
※環境変数FASTLANE_USER
,FASTLANE_PASSWORD
でupload_to_app_store
時に利用するアカウントを指定することができます。
リリース作業が開始されたこと・終了したことをSlackに流しておくことで、他の人もリリース作業のステータスがどうなっているのかを知ることできます。
また、リリースPR作成時にも使用したスクリプトを使い、Github Releasesにリリースの履歴を残しています。リリース履歴にPRのURLも載っている状態になるので、過去の類似した変更やどういう意図で変更が入ったのかが追いやすくなりました。
リリース作業が完了した後にWorkflowのScriptステップでdevelopをmasterに追従するようにしています。忘れそうになることが多いかつ必ずしないといけないことなので、1ステップ追加するだけでサクッとできてとても助かっています。
Firebase CrashlyticsへdSYMをアップロードする
クラッシュ解析にはFirebase Crashlyticsを利用していて、dSYMをアップロードする必要があり、これにもコマンドを用意しています。
lane :upload_dsym_to_crashlytics do |options| app_version = get_version_number(xcodeproj: "app.xcodeproj", target: "app") build_version = get_build_number download_dsyms( team_id: "チームID", app_identifier: "app-identifier", output_directory: "./fastlane/generated/", version: app_version, build_number: build_version ) upload_symbols_to_crashlytics( dsym_path: "./fastlane/generated/app-identifier-#{app_version}-#{build_version}.dSYM.zip", gsp_path: "./GoogleService-Info.plist" ) end
初めはdSYMのダウンロードを手動でやっていたのですが、同じことを毎回繰り替えすことになってしまうので、リリースするアプリのバージョンを元にApp Store ConnectからdSYMのダウンロードとFirebase Crashlyticsへアップロードまで行っています。
本来はこれも任意のタイミングではなく、自動で行いたいのですがupload_to_app_store
のあとApp Store Connect側での処理の完了を知ることができず、ストアの申請をしたあとに実行するようにしています。
fastlaneで自動化するために気をつけていること
最初はローカルで実行していたこともあり、CI上での動作の殆どはローカルでも同じことが試せるようにfastlaneでロジックをまとめています。
一つ気を付けた方が良いなと思ったのは全てをFastfileに書こうとしない
ということです。最初はごりごりFastfileに書いていったのですが、いわゆる神クラスのように何でも行われる便利ツールに近づいていってしまいました。例えば、ライブラリの更新処理などbash記述のみで完結する場合はupdate_library.sh
という感じにするだけで十分でした。
任意のbashスクリプトを一覧から呼び出せるfastlaneのコマンドを用意したりするのは便利かもしれませんが、各処理の詳細をfastlaneが知る必要がないのであればその方がFastfileの肥大化を防ぐことができます。継続的に管理していくファイルなので、可読性を保てるように意識した結果この考えになりました。
Bitriseで設定している全体像
紹介してきたもので実際に構成されているWorkflow,Triggerはこのようになっています。
Workflow
Workflow | Description |
---|---|
primary | 静的なチェック、UnitTestの実行 |
primary-with-deploy-beta | 静的なチェック、UnitTestの実行deploy-beta の実行 |
deploy-debug | development 環境のアプリを配布 |
deploy-beta | staging 環境のアプリを配布 |
release | AppStoreConnectへアップロードmerge-master-into-develop の実行 |
merge-master-into-develop | develop をmaster に追従する |
Trigger
Pull Request
Push
BitriseはWorkflowのあとに特定のWorkflowを実行できる仕組みがあるため、各Workflowは細かく別けて必要な時につなげています。Triggerに指定されていないdeploy-debug
, deploy-beta
は任意のタイミングで手動実行しています。
また、developへのPush時にはPR時と同じprimary
を実行していますが、これはBitriseのBuild Caches
がPRのTriggerではキャッシュ更新ができないため、Push時にも行っています。
おわりに
以上、Bitrise + fastlaneで自動化していることの紹介でした。
今回初めてのCI導入でしたが思い切って入れてみて、体験の良さを感じています。過去の開発では、UnitTestの準備ができていないしCIを導入しても効果なさそうだな…ということがあったのですが、今ではUnitTestの有無に関係なくCIを導入すべきだなと思っています。
また、今回紹介したようなCI環境を用意したことで、開発以外のビルドをローカルで行うことがなくなり待ち状態でPCリソースが奪われることが減ったため、結果的に自由に使うことが出来る時間が多くなりました。
他にも、ライブラリの自動更新検知やその時のキャッシュをローカルでも利用できるようにしたりなど、まだやれていないことが今後も新しく出てきたりすると思うので、自動化出来ることを増やして行こうと思います。