ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

CircleCI 2.0 で deploygate に Android アプリをアップロード

Android アプリを DeployGate
CircleCI を使って簡単にデプロイしたいな〜、と記事を探したら、
1.0 の記事が多かったので CircleCI 2.0 向けの記事を書くことにしました。

異なる証明書で署名されたアプリと言われ、deploygate に毎回アンインストールを求められる件についても、
「6. 異なる証明書で署名されたアプリと言われないために」の項で解決策を記しています。

実験台となったのは、
前回の『AndroidのgRPCクライアントをKotlinで実装しました』の記事で取り扱ったこちらのリポジトリ

1. config.yml はこうなった

まずは出来上がりから。
.circleci ディレクトリを作った上で、その中に config.yml という名前で保存します。

ただ、このあとの解説で書きますが、 DG_API_KEY を設定しないといけないので、
このままだとまだデプロイが出来ません。

aliases:
  android_docker: &android_docker
    docker:
      - image: circleci/android:api-28
        environment:
          TZ: Asia/Tokyo
  steps:
    - restore_cache: &restore_cache
        key: &jars_key jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }}
    - run: &download_deps
        name: Download dependencies
        command: ./gradlew androidDependencies
    - save_cache: &save_cache
        paths:
          - ~/.gradle
        key: *jars_key
    - run: &build_debug_apk
        name: Build debug APK
        command: ./gradlew assembleDebug
    - run: &test
        name: test
        command: ./gradlew test
    - run: &upload_to_deploy_gate
        name: Upload to DeployGate # Set DG_API_KEY in CircleCI
        command: |
          APK_PATH=app/build/outputs/apk/debug/app-debug.apk
          TIME=$(date "+%Y/%m/%d %H:%M")
          COMMIT_HASH=$(git log --format="%H" -n 1 | cut -c 1-8)
          USERNAME=nekonenene
          curl -F "file=@${APK_PATH}" -F "token=${DG_API_KEY}" -F "message=Build by CircleCI <${COMMIT_HASH}> (${TIME})" https://deploygate.com/api/users/${USERNAME}/apps

version: 2
jobs:
  build:
    <<: *android_docker
    steps:
      - checkout
      - restore_cache: *restore_cache
      - run: *download_deps
      - save_cache: *save_cache
      - run: *build_debug_apk
  test:
    <<: *android_docker
    steps:
      - checkout
      - restore_cache: *restore_cache
      - run: *download_deps
      - save_cache: *save_cache
      - run: *test
  deploy:
    <<: *android_docker
    steps:
      - checkout
      - restore_cache: *restore_cache
      - run: *download_deps
      - save_cache: *save_cache
      - run: *build_debug_apk
      - run: *upload_to_deploy_gate

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - test
      - deploy:
          requires:
            - build
            - test
          filters:
            branches:
              only: master
          context: deploygate

(長いので gist で見たい人用リンク)

あとで再び書きますが、このリポジトリでは gradle 設定を Kotlin で書いているために、
「build.gradle.kts」と書いている箇所があります。
たいていの方は「build.gradle」と書き直してください。

また、docker image は「circleci/android:api-28」を使っていますが、
compileSdkVersion に合わせたものをお使いください。

2. master 以外のブランチではデプロイしない

他のブランチで push されたときもテストはしたいけど、
deploygate にアップロードするのは master ブランチへコミットがあったときに限定したいな〜
という希望があると思います。

それを実現しているのが workflows の部分です。

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - test
      - deploy:
          requires:
            - build
            - test
          filters:
            branches:
              only: master
          context: deploygate

workflows としてはこうなっています。
ここの jobs で、実行する job を指定でき、各々にはさらに設定を書くことが出来ます。

- deploy:
    requires:
      - build
      - test
    filters:
      branches:
        only: master
    context: deploygate

filters: に branches: only: master と書かれていることによって、
master ブランチへコミットがあったときだけ、deploy job は実行されるようになっています。

また、 requires: に build と test が書かれていることによって、
build job と test job の両方が終わった後に deploy job が走るようになっています。

これを書かなかった場合、他の job と deploy job がパラレルに(並行して)走ります。

つまり、フローとしては以下のようになっています。

build ──┬── deploy (only: master)
test  ──┘

context については後で説明します。

3. 各 job を定義する

workflows から先に説明しましたが、もっとも大事なのは jobs の部分です。
ここで、何がおこなわれるかを定義します。

version: 2
jobs:
  build:
    <<: *android_docker
    steps:
      - checkout
      - restore_cache: *restore_cache
      - run: *download_deps
      - save_cache: *save_cache
      - run: *build_debug_apk

と書かれている部分ですね。

簡潔に書くため、YAMLのAnchors and Aliases 機能を使って書きましたが、
省略せずに書くと以下のようになっています。

jobs:
  build:
    docker:
      - image: circleci/android:api-28
        environment:
          TZ: Asia/Tokyo
    steps:
      - checkout
      - restore_cache:
          key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }}
      - run:
          name: Download dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }}
      - run:
          name: Build debug APK
          command: ./gradlew assembleDebug

今回はビルドに使う Android SDK が 28 であるため、
使用する Docker image も circleci/android:api-28 を使用しています。

他のバージョンもありますので、ふさわしいものを探してください。
https://hub.docker.com/r/circleci/android/tags

restore_cache では、
build.gradle.kts および app/build.gradle.kts が更新されているかを確認し、
変更がなければ以前 save_cache で保存した .gradle を使用します。
これにより、2回目以降の CircleCI の実行時間が短くなります。

前回の記事でも触れましたが、 Gradle Kotlin DSL により
gradle 設定を Kotlin で書いているためにファイル名が「build.gradle.kts」になっています。

Gradle Kotlin DSL を使っていない場合は、 key: のところは
jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} と書きましょう。

4. deploygate にアップロード

肝心の deploygate へのアップロードですね!

このようにコマンドを作成しました。

- run: &upload_to_deploy_gate
    name: Upload to DeployGate # Set DG_API_KEY in CircleCI
    command: |
      APK_PATH=app/build/outputs/apk/debug/app-debug.apk
      TIME=$(date "+%Y/%m/%d %H:%M")
      COMMIT_HASH=$(git log --format="%H" -n 1 | cut -c 1-8)
      USERNAME=nekonenene
      curl -F "file=@${APK_PATH}" -F "token=${DG_API_KEY}" -F "message=Build by CircleCI <${COMMIT_HASH}> (${TIME})" https://deploygate.com/api/users/${USERNAME}/apps

deploygate の API ドキュメント によれば、
アプリのアップロードは

curl \
  -F "token=xxx" \
  -F "file=@sample.apk" \
  -F "message=sample" \
  https://deploygate.com/api/users/_your_name_/apps

で出来ると書かれていますのでそれに従っています。

変数 APK_PATH にデバッグビルドで作られるアプリのパスを指定、
message に入れる文字列として変数 TIME, COMMIT_HASH を定義、
変数 USERNAME には deploygate のユーザー名を代入したあと、API を叩いています。

"message=Build by CircleCI <${COMMIT_HASH}> (${TIME})" となっていますので、
「Build by CircleCI <02f18181> (2019/02/23 19:07)」のようなメッセージが付けられることになります。

deploygate の管理画面のスクリーンショット
deploygate の管理画面

メッセージに日時を入れているのは、
deploygate アプリからバージョン一覧を見たときに、どれが何か全然わからなかったためですね…。

↓ 入れないとこんな感じ

deploygate の Android アプリのスクリーンショット
deploygate の Android アプリのスクリーンショット

コミットメッセージを入れようかとも思いましたが、
コミットメッセージに特殊文字が含まれる場合に curl が失敗しないよう
処理を考えなくては…なので今回はやりませんでした。

5. deploygate API Key の指定

さて、上のコマンドで "token=${DG_API_KEY}" となっている箇所がありました。
ここの変数にどうやって値を入れているかの話をします。

CircleCI には、設定画面からプロジェクトごと、もしくは Context ごとに環境変数を設定できるようになっています。
今回はそれの後者、Context を使った環境変数の設定をおこなっています。
参考 : https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-context

以下の画像のように、「SETTINGS」の「Contexts」へと進みます。

CircleCI の設定ページ
SETTINGS → Contexts

右上に「Create Context」のボタンがあると思いますので、それを押して Context の新規作成をします。
Name はわかりやすく「deploygate」としておきましょう。

f:id:nekonenene:20190223202240p:plain
「deploygate」の名前で CircleCI Context を作成

すると、以下の画面が出ますので「Add Environment Variable」ボタンから環境変数の追加をおこないます。

f:id:nekonenene:20190223202531p:plain

入力画面が出たら、Nameに「DG_API_KEY」、Valueに deploygate の API キーを入力します。

API キーは https://deploygate.com/settings の下の方に書かれています。

f:id:nekonenene:20190223202942p:plain
DG_API_KEY として deploygate の API key を設定

この設定を完了させたことにより、
(2. で説明を省きました) context: deploygate のコンフィグにより、
deploy job の実行時にいま設定した環境変数が読み込まれるようになります。

これで、 deploygate へ Android アプリを永遠にアップロードすることができる
CircleCI config が完成しました! おつかれさまです!!

6. 異なる証明書で署名されたアプリと言われないために

これで一件落着かと思いきや罠があります。

異なる証明書で署名されたアプリ (バージョン 1.0=1) が既にインストールされています。新しいアプリをインストールする前に、一度アンインストールしてください。

と、アンインストールが毎回求められてしまうことが発生します。

f:id:nekonenene:20190223203807j:plain
異なる証明書で署名されたアプリが既にインストールされています

なんで起きるかというと、デバッグビルドの署名は通常 ~/.android/debug.keystore にある証明書が使われるのですが、
CircleCI が使う Docker イメージが毎回異なるため、署名に使う証明書も一致しないのです。

ということで、 デバッグビルドに使う証明書を指定しましょう!!

まずはプロジェクトのルートディレクトリにて以下のコマンドを実行します。

keytool -v -genkey -keystore debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US" -keyalg RSA -validity 36500

これで 36500 日(およそ100年)有効の証明書が作成できます。

次に app ディレクトリの build.gradle.kts に以下のような設定が含まれるよう修正します。
参照: https://github.com/nekonenene/grpc_android_client/blob/master/app/build.gradle.kts

android {
    // 中略
    signingConfigs {
        getByName("debug") {
            storeFile = rootProject.file("debug.keystore")
            storePassword = "android"
            keyAlias = "androiddebugkey"
            keyPassword = "android"
        }
    }

    buildTypes {
        getByName("debug") {
            isMinifyEnabled = false
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}

Gradle Kotlin DSL を使わない場合の書き方は異なります。公式のドキュメントなどを参照してください。
storePassword, keyAlias, keyPassword は、さっきのコマンドで指定した値(-storepass android -alias androiddebugkey -keypass android)と一致するように気を付けてください。

これにより、デバッグビルド時に、プロジェクトのルートディレクトリに作った
debug.keystore が毎回使われ、同一の証明書による署名となってくれるため、
deploygate にアンインストールが毎回求められる問題はなくなります。

7. おしまい

しっかり書いたらけっこう長くなりましたが、以上!
Android アプリを deploygate にデプロイし続ける方法でしたー!

異なる証明書で署名されたアプリ 問題は、昔「毎回アンインストールはめんどいな〜」って思いながらも
しぶしぶやっていましたが、今回調べたらちゃんと対処法が発見できてよかったです。

DroidKaigi 2018 のアプリ もよく見ると、
debug.keystore がトップディレクトリに置いてありますね。なるほどなー。

らくらく実機テストをして、楽しいアンドロイド開発ライフをお過ごしください!