この記事はくふうカンパニー Advent Calendar 2019の18日目の記事となります<(_ _)>
おはこんばんにちは!
アプリのリリースは自動化されていますか?
私の個人的な Android アプリも多数の先人の知恵をお借りして、 fastlane で自動化しています。
この記事は、その運用のご紹介です。
(そういうやり方もあるんだな…程度にご覧いただけたら嬉しいです。)
はじめに
rbenv や gem をはじめとする Ruby の環境構築や、assembleRelease など Android の設定、Fastfile などの fastlane の記入内容に関しては細かく触れません。
ある程度、個人や会社でリリースまでできてる状態を前提に、私なりの変更点を記載させていただきます。
流れ
以下の流れでリリースされます。
- master (とか fix )にリリース最終版を用意
- Google スプレッドシートでリリース内容を記載
- Google スプレッドシートでリリース(キーワードで発火してタグを打つまで)
- タグを検知して Circle CI でビルド
- workflow で fastlane を使用してリリース
Google スプレッドシート
内容は以下の通りです。
表
GAS
function release() {
var sheet = SpreadsheetApp.getActiveSheet()
if (sheet.getRange(sheet.getLastRow(), 6).getValue() != 'release') {
return
}
var relaeseSelect = Browser.msgBox(sheet.getName() + ' を Play ストアにリリースします。', 'よろしいですか?', Browser.Buttons.OK_CANCEL)
if (relaeseSelect == 'cancel') {
Browser.msgBox('リリースを取り止めました。')
return
}
if (relaeseSelect == 'ok') {
if (!sheet.getRange(sheet.getLastRow(), 1).getValue()
|| !sheet.getRange(sheet.getLastRow(), 2).getValue()
|| !sheet.getRange(sheet.getLastRow(), 3).getValue()
|| !sheet.getRange(sheet.getLastRow(), 4).getValue()
|| !sheet.getRange(sheet.getLastRow(), 5).getValue()) {
Browser.msgBox('記載内容が不足しています。')
return
}
// リリースタグを打つ
var releasePushUrl = 'https://api.github.com/repos/bvlion/' + sheet.getName() + '/releases'
var headers = {
'Authorization': 'token -----token-----'
}
var data = {
tag_name: 'v' + sheet.getRange(sheet.getLastRow(), 2).getValue(),
target_commitish: sheet.getRange(sheet.getLastRow(), 5).getValue(),
name: 'v' + sheet.getRange(sheet.getLastRow(), 2).getValue(),
body: sheet.getRange(sheet.getLastRow(), 4).getValue()
}
var options = {
method : 'post',
contentType: 'application/json',
headers : headers,
payload : JSON.stringify(data)
}
var response = UrlFetchApp.fetch(releasePushUrl, options)
sheet.getRange(sheet.getLastRow(), 7).setValue(response)
}
}
トリガー
Google スプレッドシートでやっていること
見ての通りですが、シートを更新するとスクリプトが発火します。
スクリプトでは、最終行の特定カラムが「 release 」になっていて、かつ全ての項目が埋まっているかを確認し、 GitHub API 経由でリリースタグを打ちます。
Circle CI
Circle CI ではタグの変更を検知して、 GAS の内容を見に行き、ビルドからリリースまでを行います。
GAS
GAS では doGet を公開し、 versionCode や versionName を返すようにしています。
function doGet(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet = ss.getSheetByName(e.parameter.app)
if (sheet == null) {
return ContentService.createTextOutput(JSON.stringify({error: 'sheet is null [' + e.parameter.app + ']'})).setMimeType(ContentService.MimeType.JSON);
}
var data = {
code: sheet.getRange(sheet.getLastRow(), 1).getValue(),
name: sheet.getRange(sheet.getLastRow(), 2).getValue(),
english: sheet.getRange(sheet.getLastRow(), 3).getValue(),
japanese: sheet.getRange(sheet.getLastRow(), 4).getValue()
}
return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON)
}
取得して整形
GAS から返される json を整形して、所定のファイルに書き出します。
require "json"
require "uri"
require "net/http"
uri = URI.parse("https://script.google.com/macros/s/${hash}/exec?app=${app_name}")
redirect_url = Net::HTTP.get_response(uri)["location"]
response = Net::HTTP.get_response(URI.parse(redirect_url))
json = JSON.parse(response.body)
if json["error"] then
File.open("exit_message", mode = "w") {|f|
f.write(json["error"])
}
exit
end
File.open("dependencies/ext.gradle", mode = "w") {|f|
f.write("ext {\n")
f.write(" appVersionCode = ")
f.write(json["code"])
f.write("\n")
f.write(" appVersionName = '")
f.write(json["name"])
f.write("'\n")
f.write("}")
}
File.open("fastlane/metadata/android/en-US/changelogs/" + json["name"] + ".txt", mode = "w") {|f|
f.write(json["english"])
}
File.open("fastlane/metadata/android/ja-JP/changelogs/" + json["name"] + ".txt", mode = "w") {|f|
f.write(json["japanese"])
}
名称は何でもいいんですが、私は「 dependencies/ext.gradle 」という名称で versionCode と versionName を外出ししています。
Circle CI
バージョンを設定、ビルド、 fastlane でリリース、を行います。
version: 2
jobs:
version_setting:
docker:
- image: circleci/ruby:2.5.1
steps:
- checkout
- run:
name: set version
command: ruby create-version.rb
- run:
name: Error Check
command: |
if [ -f exit_message ]; then
echo `cat exit_message`
circleci step halt
fi
- persist_to_workspace:
root: .
paths:
- .
build:
docker:
- image: circleci/android:api-29
environment:
JVM_OPTS: -Xmx3200m
steps:
- attach_workspace:
at: .
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: gradle dependencies
command: ./gradlew androidDependencies
- save_cache:
paths: ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Build apk
command: ./gradlew :app:assembleRelease
- persist_to_workspace:
root: .
paths:
- .
deploy:
docker:
- image: circleci/ruby:2.5.1
steps:
- attach_workspace:
at: .
- run:
name: bundler
command: bundle install
- restore_cache:
key: gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- run:
name: up play store
command: bundle exec fastlane play_store
- save_cache:
key: gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
paths: vendor/bundle
workflows:
version: 2
build_and_deploy:
jobs:
- version_setting:
filters:
tags:
only: /v.*/
branches:
ignore: /.*/
- build:
requires:
- version_setting
- deploy:
requires:
- build
最後に…
個人のアプリなので、ほぼエラーハンドリングは入れていません。
また、 Circle CI の persist_to_workspace で「 . 」を指定して全体を渡してしまっているのも、いつか直さないとな〜とは感じつつ、大変便利に運用しています。
良し悪しがあるとは思いますが、個人的にバージョンコードや Play ストアへ表示するメッセージを git で管理しなくて良くなっている点がお気に入りです!
参考
配置の参考程度にリポジトリを作成しました。
GitHub