Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Gradle + Kotlin + CircleCIによるAndroid Google Playデプロイの自動化

こんにちは、グノシーAndroidアプリの開発担当のLiangです。

この記事はGunosy Advent Calendar 2022の23日目の記事です。前回の記事は Rui さんの 広告レコメンドでIncrementalトレーニングを実践し、学習コストを大幅に削減した話 でした。

今回では、Androidアプリの開発において、CIツールを用いたAPKのデプロイ作業を自動化する仕組みについて、お話したいと思います。

背景

Androidアプリの開発に当り、通常のリリースでは、従来以下の手作業で行います:

  1. DeployGateでビルドされた最新版のリリースAPKをダウンロード
  2. 該当リリースAPKをGoogle Playコンソールにてβ公開する。
  3. release/xxxxxxmasterにマージする。
  4. masterにversionCodeでタグを切る。

リリース頻度が高くなるに連れて、手動によるミスもあり得る状況を防ぐために、1と2の作業の自動化を検討し始めました。

やり方

前提として私達はCircleCIを用いて、継続的にビルドとテストの運用をしています。 では、どのような仕組みで自動化すれば、今の環境に一番相応しいのでしょうか?

  • fastlanedocs.fastlane.tools

    • fastlaneがない環境で、最初から導入する必要がある。
    • ただデプロイをするためだけのために、fastlaneによる膨大なライブラリーの依存が発生する。
    • デプロイ失敗した時のログが完全ではない。
  • Gradle Play Publishergithub.com

    • Gradle 7以上が必要で、検討した時点ではGradle 6だった。 
    • build.gradleの設定が複雑になりがち。
    • リリースノートの書き込みは別途ファイルを作成する必要がある。
  • Google Play Developer APIgithub.com

    • Google公式のライブラリーなので、余計な処理を入れない。
    • ライブラリー自体はJavaなので、Kotlinで書けば互換出来る。
    • こちらでフローを作成して制御するため、エラーなどは素早く対応出来る。

というわけで、Google Play Developer APIを利用することに決定しました。ワークフローとしては、Kotlinで書いたデプロイ用のGradleタスクをCircleCIに実行させます。

仕組み

https://blog.gradle.org/images/kotlin-dsl-1.0/kotlin-dsl-1.0.png Gradle Blogより

buildSrcのタスク用Gradle設定

project/buildSrc/build.gradle

  • Kotlinファイルのためのプラグイン追加org.jetbrains.kotlin.jvm
  • dependenciesにGoogle Play Developer API ライブラリーの依存関係を追加
plugins {
    id('org.jetbrains.kotlin.jvm') version '1.7.20'
}

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation 'com.google.apis:google-api-services-androidpublisher'
    implementation 'com.google.api-client:google-api-client'
    implementation 'com.google.auth:google-auth-library-oauth2-http'
}
デプロイ用のカスタムタスク

project/buildSrc/src/main/kotlin/GooglePlayDeployTask.kt

  • open classの指定でproject層のbuild.gradleにtaskをimport
  • annontation@Optionを付けた変数は、コメントラインで渡す
    • versionCode: 対象バージョンAPKのダウンロード用
    • googleDeployKey: Base64でエンコードしたAPI用のサービスアカウントJson
  • DefaultTask()の継承により、annontation@TaskActionを付けたメソッドでデプロイのタスクを実行
open class GooglePlayDeployTask : DefaultTask() {

    @Input
    @Option(option = "versionCode", description = "")
    lateinit var versionCode: String

    @Input
    @Option(option = "googleDeployKey", description = "")
    lateinit var googleDeployKey: String

    @TaskAction
    fun deploy() {
    }
}
デプロイのコードフロー
  1. サービスアカウントJsonでGoogleCredentialsの認証を行う
    • 保存したgoogleDeployKeyをBase64デコードし、InputStreamに転換
  2. 認証を通ったら、デプロイ用のクラスAndroidPublisherを作成
  3. editIdを作成、APIを叩く時に必要な識別ID
  4. Apks.uploadで対象APKをInputStreamContentのフォマットでアップロード
  5. Tracks.updateで作成したTrackにリリース情報をアップロード
    • Update.track = betaでTrackをオープンテストに指定
    • TrackRelease.status = completedで100%公開に指定
  6. 最後にCommit.commitでAPIを叩く一連の作業を実行して完了
fun deploy() {
    val keyInputStream = ByteArrayInputStream(Base64.getDecoder().decode(googleDeployKey))
    val credentials = GoogleCredentials.fromStream(keyInputStream).createScoped(scopes)
        ...
    val androidPublisher = AndroidPublisher
        .Builder(httpTransport, jsonFactory, httpRequestInitializer)
        .setApplicationName(PACKAGE_NAME)
        .build()

    val edits = androidPublisher.edits()
    val editId = edits
        .insert(PACKAGE_NAME, null)
        .execute()
        .id

    val apkInputStreamContent = InputStreamContent(
        "application/vnd.android.package-archive", 
        ByteArrayInputStream(apkResponse)
    )
    val apk = edits
        .apks()
        .upload(PACKAGE_NAME, editId, apkInputStreamContent)
        .execute()

    val trackRelease = TrackRelease()
        .setVersionCodes(apkVersionCodes)
        .setReleaseNotes(latestReleaseNotes)
        .setStatus("completed")
    val updateTrack = Track()
        .setReleases(listOf(trackRelease))
    val track = edits
        .tracks()
        .update(PACKAGE_NAME, editId, "beta", updateTrack)
        .execute()

    edits
        .commit(PACKAGE_NAME, editId)
        .execute()
}
実行のtaskを定義

project/build.gradle

import GooglePlayDeployTask
task deployGooglePlay(type: GooglePlayDeployTask)
CircleCIの設定でGradleタスクを追加

project/circle.yml

  • jobsでGradleタスクの実行用Jobを追加:deploy_google_play
    • VERSION_CODEは今回リリースのVersionCode
    • GOOGLE_DEPLOY_KEYは環境変数で保存されたサービスアカウントJson
  deploy_google_play:
  steps:
    - run: ./gradlew deployGooglePlay 
      --versionCode=$VERSION_CODE 
      --googleDeployKey=$GOOGLE_DEPLOY_KEY
  • workflowsでデプロイJobdeploy_google_playを追加
    • ブランチをmasterに指定、マージする度に実行
- deploy_google_play:
    name: deploy_google_play
        filters:
            branches:
                only: master

結論

以上の仕組みの導入によって、β公開のリリース作業を自動化し、かなりの手間を省けました。従来GradleタスクはGroovyで書くことが多いですが、Kotlinで書けるのもAndroidエンジニアにとって嬉しいことです。使う場面がございましたら、是非皆さんも利用してみましょう!

次回は aita さんの記事です。お楽しみに!