Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

今までのリリースフローおさらい

Slide 4

Slide 4 text

クックパッドのリリースフロー ● 実は以前にも紹介したことがある ○ 「クックパッド リリースフロー」で検索したら出てきます ○ https://speakerdeck.com/litmon/kutukupatudoapurifalseririsuhuroniguan-site ● 今回は、ここで話していた「自動化」周りの話をします ● fastlane/supply は知っている前提で話します ○ 分からない人はこの機会に調べてみてください ○ 雑に言うと「Google PlayにリリースするAPIをいい感じにラップしてくれてる便利ツール」

Slide 5

Slide 5 text

「クックパッドアプリのリリースフローに関して」P25 から抜粋

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

軽くおさらい リリース リリース期間 v18.5 開発開始 ● テスト期間が終了し、リリース可能な品質であることが確認できたらそのバー ジョンを公開する ● リリース用の署名を付けた apkはJenkins Jobによって作成 ● fastlane/supply をラップしたRubyスクリプトを使用し、 開発者のPCを使って apkをアップロードする ● 20, 50, 100% と段階的に公開率を操作し、新たな不具合が見つかったら必要 に応じてリリースを中止したりパッチリリースを行う

Slide 10

Slide 10 text

以前のリリースフローの問題点 ● fastlane/supply をラップしたRubyスクリプトを使用し、 ○ メンテナンスが大変 ○ Rubyスクリプト内でコマンドラインツールとしての supplyを実行していてひたすら冗長 ● 開発者のPCを使って apkをアップロードする ○ apkはJenkins Jobでビルドしているのに、わざわざ開発者の手元に持ってくるのが手間 ○ Google Publishing APIを叩くためのAPI Key(Publisher.json)をリリースを行う開発者が持つ必 要があり危険 ○ ミスタイプとかしそうでヒューマンエラーが怖い

Slide 11

Slide 11 text

難しい・・・

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

他にも問題はある ● RCブランチを運用すると、masterとRCで差分が生じた際に面倒になる ○ RCに入れるべき変更を masterに間違えて投げてしまったり、 RCの差分をmasterに適用しよう としたときにコンフリクトが発生したり、 …… ○ マイルストーンごとにこの作業を行う人を立てているが、負担が大きい ● 各マイルストーンに入れたい施策のために開発時期を調整するオペレーション が何度も発生していた ○ 1イテレーションが2週間ほどなので、「どうしてもこの時期に出したいが、開発がちょっと間に合 わない」みたいな場合に調整が発生する

Slide 14

Slide 14 text

そもそも人間が 作業するのは面倒!!!

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

自動でリリースする

Slide 17

Slide 17 text

自動アルファリリースを導入する ● さまざまな手作業からの開放 ○ リリース用apk作成のためのJenkins Job実行 ○ リリースのためのPublishing.jsonファイルの用意 ○ リリース用apkを手元にダウンロード ○ リリース用スクリプトの実行 ○ バージョンの更新作業 ● 逆に言うと、これらを機械的に実行できるよう調整する必要がある

Slide 18

Slide 18 text

自動アルファリリースを導入する Before ● RCブランチにリリースに必要なものが揃ってからapkをビルド ● リリースするときに手動でスクリプトを実行しリリース After ● RCブランチに変更があった時点でapkをビルドし自動でアップロード ● リリースの判断をしたときにPlay Consoleから手作業で昇格

Slide 19

Slide 19 text

自動アルファリリースを導入するために ● 一番の技術的な障害は「versionCode」の取扱い ○ Google Playの仕様上、各トラックにアップロードする apkは以前アップロードしたものよりも大き い必要がある ● 現在リリースされているversionCodeはAPIで取得することができるため、ビルド 時にversionCodeを渡すようにした

Slide 20

Slide 20 text

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) } } }

Slide 21

Slide 21 text

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` のように渡せる

Slide 22

Slide 22 text

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は (1桁) で構成されている 例: 180500011

Slide 23

Slide 23 text

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...” となっている 例: v18.5.0.1

Slide 24

Slide 24 text

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の値は今までのリリースフロー通りの値を使うようにしたので、引数 に渡ってきた値とこれからリリースしたいバージョンの大小を比較しビルドするようにした

Slide 25

Slide 25 text

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から組み立てられるのでそれ用のメソッド を用意

Slide 26

Slide 26 text

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を 指定するようにしている

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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の中で最も大きいものをま ず取り出す

Slide 29

Slide 29 text

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 → ...

Slide 30

Slide 30 text

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 # => [] となってしまう

Slide 31

Slide 31 text

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 の中から最も高いものを選択して使用する必要があ る

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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をビルドする

Slide 34

Slide 34 text

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を分けている

Slide 35

Slide 35 text

実行するときはこんな感じ $ bundle exec fastlane android build_newer_version_release_apk $ bundle exec fastlane android upload_newer_version_apk_to_google_play_alpha fastlane便利・・・ あとはこれをJenkins Jobで実行するようにトリガの設定などを行うだけ

Slide 36

Slide 36 text

導入してみてどうだったか ● apkのビルド・アップロードを人間が意識しなくて良くなったので、 リリースの作業を行うエンジニア(自分)が楽になった ○ 人間に依存しないようになったので属人性が排除された ● コンソール上から公開率の操作を行うだけになったので、非エンジニアでも簡単 に変更が加えられるようになった ○ エンジニアが作業する必要がなくなったため、開発に集中できるように ○ なったらいいなぁ

Slide 37

Slide 37 text

今後どうしていきたいか ● まだまだ人間が管理している箇所が多い ○ versionMajor, versionMinorは人間が管理している ○ コードフリーズ・RCブランチの運用など ● 「人間がスケジュールを管理する」のではなく 「スケジュールによって人間が動く」未来を作りたい ○ リリースフローに関する人間の作業を徹底的に排除 ○ これによって、調整業や面倒な仕事から脱却したい

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

余談 ● 段階リリースを中止しているときにalphaリリースを実行しようとすると、APIから 謎のエラーが返ってきて出来ない ○ なにか知っている人いたら教えてください :pray: ● アルファリリース自動化を導入後、Playコンソールから段階リリースを操作して 間違って100%リリースをしてしまったことがあった ○ 事故怖い。コンソール怖い。 ○ 今後はアルファリリースからの昇格も Slack bot上で完結できるようにしたい ……

Slide 41

Slide 41 text

おわり!

Slide 42

Slide 42 text

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