Upgrade to Pro — share decks privately, control downloads, hide ads and more …

alpha-release-automation

もん
August 21, 2018

 alpha-release-automation

2018-08-21 Cookpad.apk #1
Fukuo Kadota

もん

August 21, 2018
Tweet

More Decks by もん

Other Decks in Technology

Transcript

  1. クックパッドのリリースフロー • 実は以前にも紹介したことがある ◦ 「クックパッド リリースフロー」で検索したら出てきます ◦ https://speakerdeck.com/litmon/kutukupatudoapurifalseririsuhuroniguan-site • 今回は、ここで話していた「自動化」周りの話をします

    • fastlane/supply は知っている前提で話します ◦ 分からない人はこの機会に調べてみてください ◦ 雑に言うと「Google PlayにリリースするAPIをいい感じにラップしてくれてる便利ツール」
  2. 軽くおさらい v18.4 開発開始 リリース コードフリーズ 開発期間 テスト期間 リリース期間 v18.5 開発開始

    • あるバージョンの開発が開始し、 masterブランチに対して新規機能開発やバグ 修正などを行う ◦ 例: v18.4 の開発が開始 • バージョン名はそのまま Github Enterprise上でマイルストーンとして管理され、 追加したいfeatureごとにissueが切られて管理される ◦ そのマイルストーンの issueを見ればどういう機能が入るバージョンなの かが明確になる
  3. 軽くおさらい リリース コードフリーズ テスト期間 リリース期間 v18.5 開発開始 • あるバージョンの開発が一通り終了し、リリース前のテスト期間へと移行する •

    その際、ブランチを RC-<version> として切ることで、masterへは次のバージョ ンの新機能開発をマージ可能な状態にする ◦ コードフリーズ • テスト期間ではQITチームによる品質チェックを行い、 テスト期間中に見つかった不具合やバグなどは RCブランチに順次修正を加え ていく • ある程度修正が落ち着いたら RCブランチの差分をmasterに適用する
  4. 軽くおさらい リリース リリース期間 v18.5 開発開始 • テスト期間が終了し、リリース可能な品質であることが確認できたらそのバー ジョンを公開する • リリース用の署名を付けた

    apkはJenkins Jobによって作成 • fastlane/supply をラップしたRubyスクリプトを使用し、 開発者のPCを使って apkをアップロードする • 20, 50, 100% と段階的に公開率を操作し、新たな不具合が見つかったら必要 に応じてリリースを中止したりパッチリリースを行う
  5. 以前のリリースフローの問題点 • fastlane/supply をラップしたRubyスクリプトを使用し、 ◦ メンテナンスが大変 ◦ Rubyスクリプト内でコマンドラインツールとしての supplyを実行していてひたすら冗長 •

    開発者のPCを使って apkをアップロードする ◦ apkはJenkins Jobでビルドしているのに、わざわざ開発者の手元に持ってくるのが手間 ◦ Google Publishing APIを叩くためのAPI Key(Publisher.json)をリリースを行う開発者が持つ必 要があり危険 ◦ ミスタイプとかしそうでヒューマンエラーが怖い
  6. 他にも問題はある • RCブランチを運用すると、masterとRCで差分が生じた際に面倒になる ◦ RCに入れるべき変更を masterに間違えて投げてしまったり、 RCの差分をmasterに適用しよう としたときにコンフリクトが発生したり、 …… ◦

    マイルストーンごとにこの作業を行う人を立てているが、負担が大きい • 各マイルストーンに入れたい施策のために開発時期を調整するオペレーション が何度も発生していた ◦ 1イテレーションが2週間ほどなので、「どうしてもこの時期に出したいが、開発がちょっと間に合 わない」みたいな場合に調整が発生する
  7. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } }
  8. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } gradle実行時にpropertyを渡すようにする 実行時は `./gradlew assembleRelease -PversionCode=180501001` のように渡せる
  9. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } versionMajor, versionMinorなどの値は以下のように決まっている - versionMajor: 年度 - versionMinor: 年度内のリリース回数 (パッチリリース含まない ) - versionBuild100, versionBuild: パッチリリース回数 - marketId: リリース先(以前は色々あったが現在は 1つのみ) versionCodeは <versionMajor(2桁)><versionMinor(2桁)><versionBuild100(2桁)><versionBuild(2桁)><marketId>(1桁) で構成されている 例: 180500011
  10. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } versionMajor, versionMinorなどの値は以下のように決まっている - versionMajor: 年度 - versionMinor: 年度内のリリース回数 (パッチリリース含まない ) - versionBuild100, versionBuild: パッチリリース回数 - marketId: リリース先(以前は色々あったが現在は 1つのみ) versionNameも同様に “v<versionMajor(2桁)>.<versionMinor(2桁)>.<versionBuild100(2桁)>.<versionBuild(2桁)>” となっている 例: v18.5.0.1
  11. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } versionMajor, versionMinorの値は今までのリリースフロー通りの値を使うようにしたので、引数 に渡ってきた値とこれからリリースしたいバージョンの大小を比較しビルドするようにした
  12. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } versionNameは前述どおりの仕様なので versionCodeから組み立てられるのでそれ用のメソッド を用意
  13. build.gradleの変更 android { defaultConfig { if (project.hasProperty("versionCode")) { def newerVersionCode

    = Math.max( Integer.parseInt(project.property("versionCode")), getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) ) def newerVersionName = getVersionNameFromVersionCode(String.valueOf(newerVersionCode)) println("versionCode: " + newerVersionCode) println("versionName: " + newerVersionName) versionCode newerVersionCode versionName newerVersionName } else { versionCode getVersionCode(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) versionName getVersionName(versionMajor, versionMinor, versionBuild100, versionBuild, marketId) } } } versionCodeを引数から与えない場合 (開発時など)はデフォルトのversionCode, versionNameを 指定するようにしている
  14. fastlane/supplyでビルドに必要なversionCodeを取得 platform :android do lane :retrieve_newer_version_from_google_play do alpha_version_codes = google_play_track_version_codes(

    json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', ) alpha_version_code = alpha_version_codes.max&.to_i rollout_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'rollout' ) rollout_version_code = rollout_version_codes.max&.to_i production_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', ) production_version_code = production_version_codes.first [alpha_version_code, rollout_version_code, production_version_code].compact.max&.+10 || 0 end end
  15. fastlane/supplyでビルドに必要なversionCodeを取得 platform :android do lane :retrieve_newer_version_from_google_play do alpha_version_codes = google_play_track_version_codes(

    json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', ) alpha_version_code = alpha_version_codes.max&.to_i rollout_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'rollout' ) rollout_version_code = rollout_version_codes.max&.to_i production_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', ) production_version_code = production_version_codes.first [alpha_version_code, rollout_version_code, production_version_code].compact.max&.+10 || 0 end end fastlane/supplyには元々versionCodeを取得するためのlaneがあるので、それを使って取り出す ( https://docs.fastlane.tools/actions/supply/#retrieve-track-version-codes ) 引数にはAPI KeyになるJSONファイルのPATHと、対象になるアプリの packageName、あとは取 り出したいtrackを指定する アルファリリースを自動化するためには、 alphaトラックに上がっている apkのversionCodeよりも 大きくないといけないので、 alphaトラックに上がっている versionCodeの中で最も大きいものをま ず取り出す
  16. fastlane/supplyでビルドに必要なversionCodeを取得 platform :android do lane :retrieve_newer_version_from_google_play do alpha_version_codes = google_play_track_version_codes(

    json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', ) alpha_version_code = alpha_version_codes.max&.to_i rollout_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'rollout' ) rollout_version_code = rollout_version_codes.max&.to_i production_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', ) production_version_code = production_version_codes.first [alpha_version_code, rollout_version_code, production_version_code].compact.max&.+10 || 0 end end alphaリリースを自動化するために versionCodeを上げる必要があるため、 versionCode + 10 の値を返却するようにする + 10にしているのは、前述した marketIdは固定値で下1桁目に位置しているためその値を除いた 値を変える必要があるから 例: v18.5のアルファリリース時の versionCode 180500011 → 180500021 → 180500031 → ...
  17. fastlane/supplyでビルドに必要なversionCodeを取得 platform :android do lane :retrieve_newer_version_from_google_play do alpha_version_codes = google_play_track_version_codes(

    json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', ) alpha_version_code = alpha_version_codes.max&.to_i rollout_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'rollout' ) rollout_version_code = rollout_version_codes.max&.to_i production_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', ) production_version_code = production_version_codes.first [alpha_version_code, rollout_version_code, production_version_code].compact.max&.+10 || 0 end end さらにクックパッドでは段階リリースも行っているた め、たとえば180500011のversionCodeのアプリを alphaトラックからrolloutトラックに昇格した場合、 - alphaトラック: 公開中apkなし - rolloutトラック: 公開中apkあり(180500011) の状態になり、この状態でパッチリリースを行うため のビルドを行おうとしても alpha_version_codes # => [] となってしまう
  18. fastlane/supplyでビルドに必要なversionCodeを取得 platform :android do lane :retrieve_newer_version_from_google_play do alpha_version_codes = google_play_track_version_codes(

    json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', ) alpha_version_code = alpha_version_codes.max&.to_i rollout_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'rollout' ) rollout_version_code = rollout_version_codes.max&.to_i production_version_codes = google_play_track_version_codes( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', ) production_version_code = production_version_codes.first [alpha_version_code, rollout_version_code, production_version_code].compact.max&.+10 || 0 end end そのため、 - rolloutトラックに存在するversionCode - alphaトラックに存在するversionCode - productionトラック(rollout 100%)に存在する versionCode の中から最も高いものを選択して使用する必要があ る
  19. fastlane/supplyを使ってビルド→アップロード lane :build_newer_version_release_apk do version_code = retrieve_newer_version_from_google_play gradle(task: 'assembleProdExternalRelease', properties:

    { 'versionCode': version_code }) end lane :upload_newer_version_apk_to_google_play_alpha do version_code = retrieve_newer_version_from_google_play gradle(task: 'copyChangelog', properties: { 'versionCode': version_code }) supply( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', apk: 'cookpad/build/outputs/apk/cookpad-prod-external-release.apk', mapping: 'cookpad/build/outputs/mapping/prodExternal/release/mapping.txt', skip_upload_images: true, skip_upload_screenshots: true, ) gradle(task: 'releng', properties: { 'versionCode': version_code }) end
  20. fastlane/supplyを使ってビルド→アップロード lane :build_newer_version_release_apk do version_code = retrieve_newer_version_from_google_play gradle(task: 'assembleProdExternalRelease', properties:

    { 'versionCode': version_code }) end lane :upload_newer_version_apk_to_google_play_alpha do version_code = retrieve_newer_version_from_google_play gradle(task: 'copyChangelog', properties: { 'versionCode': version_code }) supply( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', apk: 'cookpad/build/outputs/apk/cookpad-prod-external-release.apk', mapping: 'cookpad/build/outputs/mapping/prodExternal/release/mapping.txt', skip_upload_images: true, skip_upload_screenshots: true, ) gradle(task: 'releng', properties: { 'versionCode': version_code }) end versionCodeは先程定義した retrieve_newer_version_from_google_play を使って取得 assembleタスクにプロパティを渡すようにして、新しいバージョンの apkをビルドする
  21. fastlane/supplyを使ってビルド→アップロード lane :build_newer_version_release_apk do version_code = retrieve_newer_version_from_google_play gradle(task: 'assembleProdExternalRelease', properties:

    { 'versionCode': version_code }) end lane :upload_newer_version_apk_to_google_play_alpha do version_code = retrieve_newer_version_from_google_play gradle(task: 'copyChangelog', properties: { 'versionCode': version_code }) supply( json_key: ENV.fetch('GOOGLE_PLAY_PUBLISHER_PATH'), package_name: 'com.cookpad.android.activities', track: 'alpha', apk: 'cookpad/build/outputs/apk/cookpad-prod-external-release.apk', mapping: 'cookpad/build/outputs/mapping/prodExternal/release/mapping.txt', skip_upload_images: true, skip_upload_screenshots: true, ) gradle(task: 'releng', properties: { 'versionCode': version_code }) end supplyを素直に使ってalphaトラックへアップロード アップロードするapkは先ほどビルドしたものを使用する クックパッドではアップロード前に apkの軽いチェックを挟んでいるため、 laneを分けている
  22. 実行するときはこんな感じ $ bundle exec fastlane android build_newer_version_release_apk $ bundle exec

    fastlane android upload_newer_version_apk_to_google_play_alpha fastlane便利・・・ あとはこれをJenkins Jobで実行するようにトリガの設定などを行うだけ
  23. 今後どうしていきたいか • まだまだ人間が管理している箇所が多い ◦ versionMajor, versionMinorは人間が管理している ◦ コードフリーズ・RCブランチの運用など • 「人間がスケジュールを管理する」のではなく

    「スケジュールによって人間が動く」未来を作りたい ◦ リリースフローに関する人間の作業を徹底的に排除 ◦ これによって、調整業や面倒な仕事から脱却したい