Androidアプリは規模が大きくなるとビルドにも時間がかかる。特にビルド待ち時間はbranchを切り替えて作業するわけにもいかず、とても効率が悪いため自動化をして楽をしよう。
CI/CDの実現方法
やり方はいくつか考えられ、それぞれメリットやデメリットがあるので要件や費用、運用の手間を考えて選択したいところ。
- 自前サーバーで運用
- サーバー立ててJenkinsなどのApplicationをインストールして運用する
- Macなどにインストールして社内ネットワーク上で使う
- 物理サーバーだと電源とかマシン自体のメンテナンスもやる必要があって非効率
- CI/CD専用のサービスを利用する
- CircleCIやBitriseなどのサービスを契約する
- お金がかかるが、コンテナ上でビルドできる
- 無料プランもあるが、制限はある
- VCSサービスに付属の機能を利用する
- お金がかかる場合があるが、コンテナ上でビルドできる
- 無料枠あり
- Gitlab CI/CD Pipeline(使ったことない)
- GithubActions(今回使う)
物理サーバーへの依存は極力さけたいのと、昨今盛り上がってきているGithubActionsですべて対応することにしてみた。
GithubActions
https://github.co.jp/features/actions
2019年に正式に発表された、Githubで使えるCI/CDツール。
Github上から直接コードをビルド、テスト、デプロイできる。
CircleCIなどを使ったことがあるのであれば、比較的簡単に入門できると思われる。
git管理されているプロジェクトルートに
.github/workflows/hogehoge.yaml
を追加し、YAML形式で内容を記載して行く流れになる。
※「workflows」でないと正しく認識されないため注意(最後のsを忘れがち)
workflowのトリガーイベント
workflowのトリガーにはいくつか種類があるので、最適なものを選択してYAMLに記載すると、適切なタイミングでworkflowを実行することができる。
詳しくは本家ドキュメントにて。
on:
push:
branches: # 指定branchへのpush / mergeをトリガーとしてworkflowを起動。マージ後に自動デプロイしたい場合など
- master
- feature/hoge
pull_request: # 指定branchへのpull request作成をトリガーとしてworkflowを起動。レビュー前にテスト実行や自動レビューを行いたい場合など
branches:
- master
jobs:
buid:
name: hoge...
新たなトリガー: workflow_dispatch
https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
2020年7月、workflow_dispatchという新たなトリガーがアナウンスされ、workflowの手動実行が可能になった。
これにより、githubイベントによるトリガーだけではなく、必要なときにworkflowを起動することができる。
inputパラメーターも受け付けるため、外部から値を受け取り動的に挙動を変えることも可能。
on:
workflow_dispatch: # 手動トリガーとする場合
inputs:
some_param_1:
required: true
description: 必須パラメーター
some_param:
required: false
description: オプショナルパラメーター
jobs:
buid:
name: hoge...
そうすると、このようにgithubのActionsタブに行くと、workflowをマニュアル実行できるボタンが出現する。
今回やること
今回やることは大きく分けて3つ。
1. githubのイベントトリガーによるDeploygate自動配布
2. workflow_dispatchトリガーによるDeploygateマニュアル配布
3. workflow_dispatchトリガーによるPlayストアアップロード
1. githubのイベントトリガーによるDeploygate自動配布
用途としては、QAをクリアしリリース待機状態になっているビルドを常に最新に保つ。
確認したければ固定のDeploygate配布ページから確認すればいつでも最新のビルドを確認することが可能になる。
以下のようなYAMLを .github/workflows/
直下に作成。
name: Deploygate Auto Distribution
on:
push:
branches:
- master # master branchへのpush / mergeをトリガーとしてworkflow開始
jobs:
build:
runs-on: ubuntu-latest
# skip ciの場合は実行しない
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2 # branchチェックアウト
- uses: actions/cache@v1 # gradle依存関係をキャッシュする(次回以降高速化のため)
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: setup JDK # JDKをセットアップする
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: sensitive
shell: bash
env:
DECRYPT_PASS: ${{ secrets.DECRYPT_PASS }} // github secretsから復号化キーを取得
run: |
# opensslが1.1.1gのためMacでLibreSSL使っている場合はコマンドを変える必要があるかもしれない
# 復号化コマンド
- name: Build Debug // デバッグビルドの実行
shell: bash
run: |
./gradlew assembleDebug
- name: Distribute Debug
env:
DEP_API_KEY: ${{ secrets.DEP_API_KEY }} # 以下のcurlで利用するAPIキーやdistributionキーなどをsecretsからgithub 取得
run: |
# DeploygateのAPIリファレンスに従って、curl処理
- name: Notification on Success # workflow処理成功時の処理
if: success()
run: |
# slackなどのチャットツールに通知するcurl
- name: Notification on Failure # workflow処理失敗時の処理
if: failure()
run: |
# slackなどのチャットツールに通知するcurl
ここでのポイントは
-
if: "!contains(github.event.head_commit.message, '[skip ci]')"
によりCIスキップを実現- CI処理が不要な場合にコミットメッセージに [skip ci] を入れるとworkflowがスキップされるようになる
-
actions/cache@v1
によりgradle依存関係をキャッシュ- 次回以降はキャッシュから読み出すため、場合によっては数分程度処理が短縮される
- ただし、workflow_dispatchの場合はキャッシュサポートされていないため注意が必要
- センシティブなキーは直書きせずに、github secretsに登録
-
{{ secrets.HOGE }}
により環境変数に読み出すようにする
-
-
if: success()
if: failure()
により成功・失敗をSlack等に通知する
2. workflow_dispatchトリガーによるDeploygateマニュアル配布
用途としては、開発中branchでエンジニア以外のメンバーに一旦確認をしてもらいたいときなど、branchを指定してworkflowを手動実行する。
配布終了するとチャットツールに通知されるため、誰でもそのビルドを確認できるようになる。
workflow_dispatchを使う上での変更点は以下のみ
name: Deploygate Manual Distribution
on:
workflow_dispatch:
inputs:
distribution_name:
required: true
description: deploygateの配信名
jobs:
# 中略
- name: Distribute Debug
run: |
# Deploygate APIのパラメーターで distribution_name=${{github.event.inputs.distribution_name}} のようにinputからの文字列を指定
ここでのポイントは
3. workflow_dispatchトリガーによるPlayストアアップロード
Play Publishing APIを使ってPlayストアにアップロードを行う。
なお、Github Actionsマーケットプレイスにすでに対応されたActionsが公開されているため、一番早いのはこの方法。
https://github.com/marketplace/actions/upload-android-release-to-play-store
今回は、将来的にいろいろと拡張しやすいのと、そこまで大変な作業ではないため自前で準備することにした。
Play Publishing APIはpythonのサンプルがあるため、こちらを参考に。最新版のv3を利用。
しかし、サンプルのコード自体が古く、今ではremoveされているメソッドや、サービスアカウント認証が推奨ではないp12キーファイルによる認証となっているため、新しい環境でも動くように改変。
まずは以下のファイルを .github/workflows/scripts
などのディレクトリに配置
# pipで依存関係を一括インストール用
google-api-python-client ~= 1.11.0
oauth2client ~= 4.1.3
"""Uploads an aab to the internal track."""
import argparse
import sys
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
import httplib2
from oauth2client import client
from oauth2client.service_account import ServiceAccountCredentials
TRACK = 'internal' # or can be 'alpha', beta', 'production' or 'rollout'
# workflow.yamlからの入力受付用
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
default='com.examole.app',
help='The package name. Example: com.android.sample')
argparser.add_argument('aab_file',
nargs='?',
default='app-release.aab',
help='The path to the AAB file')
argparser.add_argument('service_account_json',
nargs='?',
help='The path to the key file of service account.')
def main(argv):
scopes = ['https://www.googleapis.com/auth/androidpublisher']
flags = argparser.parse_args()
service_account_json = flags.service_account_json
# サンプルではここがp12ファイルを利用していたため、サービスアカウントのjsonキーファイルで認証するように変更
credentials = ServiceAccountCredentials.from_json_keyfile_name(service_account_json, scopes=scopes)
http = httplib2.Http()
http = credentials.authorize(http)
service = build('androidpublisher', 'v3', http=http)
package_name = flags.package_name
aab_file = flags.aab_file
try:
edit_request = service.edits().insert(body={}, packageName=package_name)
result = edit_request.execute()
edit_id = result['id']
print('Edit ID : "%s"' % edit_id)
# aabのアップロード(apkとはアップロード方法が異なるため注意)
media = MediaFileUpload(aab_file, mimetype='application/octet-stream', resumable=True)
aab_response = service.edits().bundles().upload(
editId=edit_id,
packageName=package_name,
media_body=media).execute()
print('Version code %d has been uploaded' % aab_response['versionCode'])
# Trackの更新(internal track)
track_response = service.edits().tracks().update(
editId=edit_id,
track=TRACK,
packageName=package_name,
body={u'releases': [{
u'name': u'アップロード時の文言を指定',
u'versionCodes': [str(aab_response['versionCode'])],
u'status': u'completed',
}]}).execute()
print('Track %s is set with releases: %s' % (
track_response['track'], str(track_response['releases'])))
# Transactionのcommit
commit_request = service.edits().commit(
editId=edit_id, packageName=package_name).execute()
print('Edit "%s" has been committed' % (commit_request['id']))
except client.AccessTokenRefreshError:
print ('The credentials have been revoked or expired, please re-run the '
'application to re-authorize')
if __name__ == '__main__':
main(sys.argv)
続いてworkflow。Deploygateと同様な部分は記載割愛。
name: Upload To Play Store
on:
workflow_dispatch:
jobs:
build-and-upload-store:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch_name }}
# 中略
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build Release # リリース用ビルド
shell: bash
run: |
./gradlew bundleRelease
- name: Install python dependencies # requirements.txtから依存一括インストール
run: |
python -m pip install --upgrade pip
pip install -r ./.github/workflows/scripts/requirements.txt
- name: Upload to play store # ストアの内部テストトラックへアップロード
run: |
python './.github/workflows/scripts/upload_aab.py' \
'com.example.your.package' \
'app/build/outputs/bundle/release/app-release.aab' \ # ここは環境によって変更
'./.github/workflows/account.json' # 必要に応じて暗号化したjsonを事前に復号化する
# 成功失敗の通知
ここでのポイントは
- サービスアカウントの認証をjsonキーファイルを利用するように変更
- ただし、github secretsはjsonなどの構造化ファイルを置けないため、暗号化してpushし、使う前に復号化する等の対策が必要
- apkではなくaabアップロードできるように少し変更
- 自前の実行ファイル(python)を準備して、workflowから直接呼び出し
最後に
今回はAndroidビルドによるサンプルだったが、それ以外でもCI/CDとしてかなり使えそうなため、どんどん使って行きたい。
毎回ローカルPCでの手動ビルドによる待ち時間やビルドミスの削減にもつながるため、効率化はかなりされたと思う。
python2でしか動作しないので早いとこpython3に移行しないと・・・