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

alpha-release-automation

0e8279dc67437cb9eb0562620d9be061?s=47 もん
August 21, 2018

 alpha-release-automation

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

0e8279dc67437cb9eb0562620d9be061?s=128

もん

August 21, 2018
Tweet

Transcript

  1. Cookpad.apk #1 alpha release automation 技術部 モバイル基盤グループ 門田

  2. 自己紹介 • かどたふくお • 技術部モバイル基盤グループ ◦ 主な仕事:Kotlin大臣、リリース自動化など • twitter: @_litmon_

    github: @litmon speakerdeck: @litmon
  3. 今までのリリースフローおさらい

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

    • fastlane/supply は知っている前提で話します ◦ 分からない人はこの機会に調べてみてください ◦ 雑に言うと「Google PlayにリリースするAPIをいい感じにラップしてくれてる便利ツール」
  5. 「クックパッドアプリのリリースフローに関して」P25 から抜粋

  6. 軽くおさらい v18.4 開発開始 リリース コードフリーズ 開発期間 テスト期間 リリース期間 v18.5 開発開始

  7. 軽くおさらい v18.4 開発開始 リリース コードフリーズ 開発期間 テスト期間 リリース期間 v18.5 開発開始

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

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

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

    開発者のPCを使って apkをアップロードする ◦ apkはJenkins Jobでビルドしているのに、わざわざ開発者の手元に持ってくるのが手間 ◦ Google Publishing APIを叩くためのAPI Key(Publisher.json)をリリースを行う開発者が持つ必 要があり危険 ◦ ミスタイプとかしそうでヒューマンエラーが怖い
  11. 難しい・・・

  12. 他にも問題はある • パッチリリースを行うときに、バージョンの更新を行う必要がある ◦ 1回1回は大したことない作業だが重なると大変 ◦ 大規模な修正を加えたときはパッチリリースが増えがちなので、毎度毎度面倒が発生する ◦ masterにマージする際にコンフリクトが発生しがち

  13. 他にも問題はある • RCブランチを運用すると、masterとRCで差分が生じた際に面倒になる ◦ RCに入れるべき変更を masterに間違えて投げてしまったり、 RCの差分をmasterに適用しよう としたときにコンフリクトが発生したり、 …… ◦

    マイルストーンごとにこの作業を行う人を立てているが、負担が大きい • 各マイルストーンに入れたい施策のために開発時期を調整するオペレーション が何度も発生していた ◦ 1イテレーションが2週間ほどなので、「どうしてもこの時期に出したいが、開発がちょっと間に合 わない」みたいな場合に調整が発生する
  14. そもそも人間が 作業するのは面倒!!!

  15. こうしたい • apkアップロード作業は特定のタイミングに勝手に実行されていればいい • 品質チェックが通った時点で、リリース作業は公開ボタンを押すだけに したい • RCブランチを廃止したい

  16. 自動でリリースする

  17. 自動アルファリリースを導入する • さまざまな手作業からの開放 ◦ リリース用apk作成のためのJenkins Job実行 ◦ リリースのためのPublishing.jsonファイルの用意 ◦ リリース用apkを手元にダウンロード

    ◦ リリース用スクリプトの実行 ◦ バージョンの更新作業 • 逆に言うと、これらを機械的に実行できるよう調整する必要がある
  18. 自動アルファリリースを導入する Before • RCブランチにリリースに必要なものが揃ってからapkをビルド • リリースするときに手動でスクリプトを実行しリリース After • RCブランチに変更があった時点でapkをビルドし自動でアップロード •

    リリースの判断をしたときにPlay Consoleから手作業で昇格
  19. 自動アルファリリースを導入するために • 一番の技術的な障害は「versionCode」の取扱い ◦ Google Playの仕様上、各トラックにアップロードする apkは以前アップロードしたものよりも大き い必要がある • 現在リリースされているversionCodeはAPIで取得することができるため、ビルド

    時にversionCodeを渡すようにした
  20. 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) } } }
  21. 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` のように渡せる
  22. 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
  23. 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
  24. 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の値は今までのリリースフロー通りの値を使うようにしたので、引数 に渡ってきた値とこれからリリースしたいバージョンの大小を比較しビルドするようにした
  25. 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から組み立てられるのでそれ用のメソッド を用意
  26. 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を 指定するようにしている
  27. 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
  28. 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の中で最も大きいものをま ず取り出す
  29. 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 → ...
  30. 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 # => [] となってしまう
  31. 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 の中から最も高いものを選択して使用する必要があ る
  32. 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
  33. 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をビルドする
  34. 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を分けている
  35. 実行するときはこんな感じ $ bundle exec fastlane android build_newer_version_release_apk $ bundle exec

    fastlane android upload_newer_version_apk_to_google_play_alpha fastlane便利・・・ あとはこれをJenkins Jobで実行するようにトリガの設定などを行うだけ
  36. 導入してみてどうだったか • apkのビルド・アップロードを人間が意識しなくて良くなったので、 リリースの作業を行うエンジニア(自分)が楽になった ◦ 人間に依存しないようになったので属人性が排除された • コンソール上から公開率の操作を行うだけになったので、非エンジニアでも簡単 に変更が加えられるようになった ◦

    エンジニアが作業する必要がなくなったため、開発に集中できるように ◦ なったらいいなぁ
  37. 今後どうしていきたいか • まだまだ人間が管理している箇所が多い ◦ versionMajor, versionMinorは人間が管理している ◦ コードフリーズ・RCブランチの運用など • 「人間がスケジュールを管理する」のではなく

    「スケジュールによって人間が動く」未来を作りたい ◦ リリースフローに関する人間の作業を徹底的に排除 ◦ これによって、調整業や面倒な仕事から脱却したい
  38. 余談 • 段階リリースを中止しているときにalphaリリースを実行しようとすると、APIから 謎のエラーが返ってきて出来ない ◦ なにか知っている人いたら教えてください :pray:

  39. 余談 • 段階リリースを中止しているときにalphaリリースを実行しようとすると、APIから 謎のエラーが返ってきて出来ない ◦ なにか知っている人いたら教えてください :pray:

  40. 余談 • 段階リリースを中止しているときにalphaリリースを実行しようとすると、APIから 謎のエラーが返ってきて出来ない ◦ なにか知っている人いたら教えてください :pray: • アルファリリース自動化を導入後、Playコンソールから段階リリースを操作して 間違って100%リリースをしてしまったことがあった

    ◦ 事故怖い。コンソール怖い。 ◦ 今後はアルファリリースからの昇格も Slack bot上で完結できるようにしたい ……
  41. おわり!

  42. 次回予告 • Dangerを使ってKotlinのコンパイル時エラーや警告を表示してみた 〜プロジェクトkyoto-sensei〜 • Kotlinをクックパッドアプリに導入したときのあれこれ • Android版クックパッドアプリで採用している技術の現状確認 2018年版 inspired

    by https://techlife.cookpad.com/entry/2015/06/25/093507