バイナリのビルド作業はそろそろボタンをポチるだけにしようぜ

この記事は [Akatsuki Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aktsk) の 7 日目の記事です。
6日目: [Unityで出力したipa, apkの容量を削減するためのチェックリスト](https://qiita.com/sanukin/items/39f804fe11054883ed88)

# はじめに
Akatsuki Inc.でクライアントエンジニアをやっているものです。

去年はこういうのを書いていました。
[｢Akatsuki Advent Calendar 2016 5日目 安易にiOSのUUIDをアカウント認証ロジックに使ってはいけない｣](https://qiita.com/tomotaka-yamasaki/items/048f7a9d53665f4aa808)

今回はバイナリ作成作業の全てを自動化したら色々幸せになったお話です。

# 背景
アプリ開発ではリリース時やバージョンアップ時に
各プラットフォームに提出するバイナリ(ipa、apk)を作成しますよね。
そして恐らく、開発中も開発用バイナリをゴリゴリ作成していると思います。

普段からバイナリを作っておかないと、いざリリースするときに
｢あぁぁ、Androidだけビルド失敗してる…いつもはiOSだけビルドしてたから気づかなかった…｣
なんてことになりかねません。

そうならないためにも、継続的に、毎日バイナリを作成することはとても大切です。
ただ、そこに毎日労力を割く訳にはいきません。自動化しましょう。

# 完全に自動化する前の状況
## バイナリの種類
私のチームでは環境ごとにバイナリを用意しています。
デイリーでビルドしているものとリリース時にビルドするものがあります。

### Daily-Buildするバイナリ
- 主に新規開発用にビルドしているもの
  - このバイナリを使って日々、新規開発された機能の検証を行っている
- 毎日12個のバイナリを作成する

||ios|ios|android| android | android |
|---|---|---|---|---|---|
||Debug|Release|Debug|Release-Log|Release|
|Aサーバ用バイナリ|1|2|3|4|5|
|Bサーバ用バイナリ|6|7|8|9|10|
|Cサーバ用バイナリ|✕|11|✕|✕|12|

※ Debug, Release, Release-log: ビルドオプション

### Release-Buildするバイナリ
- 各プラットフォームに提出する前にビルドするもの
  - 18個のバイナリを作成する

||ios|ios|android| android | android |
|---|---|---|---|---|---|
||Debug|Release|Debug|Release-Log|Release|
|Aサーバ用バイナリ|1|2|3|4|5|
|Bサーバ用バイナリ|6|7|8|9|10|
|Cサーバ用バイナリ|11|12|13|14|15|
|Dサーバ用バイナリ|✕|16|✕|✕|✕|
|提出用バイナリ|✕|17|✕|✕|18|

※ Debug, Release, Release-log: ビルドオプション

## ビルド方法
CIツールとして[Jenkins](https://jenkins-ci.org/)は利用しています。
Jenkinsのジョブは｢プラットフォーム数 * サーバ数｣用意しています。
これらのジョブを回してビルドが成功したら、
アプリ開発&テスト支援サービスの[Deploygate](https://deploygate.com)に自動でバイナリがアップされます。

完全自動化前のタイミングでも、このジョブを毎日1つ1つ実行しているはずはなく、
そこはJenkinsの[Build Flow Plugin](https://wiki.jenkins.io/display/JENKINS/Build+Flow+Plugin)で自動化していいます。
Build Flow Pluginを使えばジョブ実行のシーケンスが組めるので、
Daily-Buildのジョブ一式をフロー化することで毎日定期実行できます。
Release-Buildにおいても同様にフロー化しています。

## 課題
### DeploygateのRevisionを手動で確認する問題
Deploygateにバイナリをアップロードする際には、バイナリに対してRevisionが振られます。
そのRevisionは毎日開発チーム全体に伝える必要があるんですが、
それを手動でJenkinsジョブのコンソールから見つけて伝える、ということをやっていました。

> Jenkins > ビルドパラメータからジョブ探す > コンソール > Revision検索

これを12バイナリ分。しかも毎日。辛すぎますし、間違える可能性も大いにあります。
自動化しましょう。

### 提出バイナリを手動でGoogleDriveにアップロードする問題
Release-Buildで提出用バイナリが作成されますが、
それらは一度GoogleDriveに格納する必要があります。
その際に、Jenkinsジョブを探してバイナリダウンロード、手動でリネームしてGoogleDriveにアップロードしていました。

> Jenkins > ビルドパラメータからジョブ探す > バイナリDL > ファイル名リネーム > GoogleDriveアップロード

ここでミスすると、提出するバイナリが想定と違うという事故が発生します。
自動化しましょう。

# DeploygateのRevisionを自動で取得してまとめる
- RevisionはDeploygateにアップロードするAPIのレスポンスに含まれている
- 後はRevisionをどこかに送ってまとめればOK

## 使うもの
- Googleスプレッドシート
- Google Apps Script

## スプレッドシートの準備
ここで用意するシートはRevisionを書き込む受け皿です。
各プラットフォーム、ビルドオプションごとにバイナリが存在するのでRevisionを書き込むセルを用意します。
一応ビルドしたbranchとupdate_atも書き込めるようにしておきます。

毎日SlackでRevisionを共有するので、コピペ用のテキストも用意しています。
上の表に書き込まれれば下のテキストに反映されます。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/78128/4cd9f66b-1fc7-1f06-5c88-22d39fbd2bbb.png "図1.png")

## Google Apps Scriptの準備
スプレッドシートに対して外部(Jenkins)から書き込みを行いたいのでGoogle Apps Script(GAS)でPOSTを用意します。
GASはスプレッドシート上部のツール > スクリプトエディタで書けます。

GASはデフォルトで以下のようになっており、
POSTするとdoPost内の処理を行います。

```gas
function doPost(e) {

// ここに処理を書く

}
```

スプレッドシートのセルとバイナリとの紐付けは、バイナリ名とビルドオプションで判定できるので、
APIでパラメータを渡せば解決できます。

最終的にこんな感じになります。
```addBranchData```、```addUpdateAt```メソッドも```addRevisionData```と大体同じ内容なので省略します。

```gas
function doPost(e) {
  var revision = e.parameters.revision[0];  // Revision
  var scheme = e.parameters.scheme[0];      // バイナリ名
  var flavor = e.parameters.flavor[0];      // ビルドオプション
  var branch = e.parameters.branch[0];      // ビルドしたブランチ
  
  // ビルドしたブランチをシート名にしている
  var sheetName = branch;
  
  // セルの特定
  var row = getRow(scheme, branch);
  var col = getCol(flavor);
  
  // セルへの書き込み
  addRevisionData(row, col, revision, sheetName);
  addBranchData(row, branch, sheetName);
  addUpdateAt(row, sheetName);
  
  return ContentService.createTextOutput("success").setMimeType(ContentService.MimeType.JSON);
}

function addRevisionData(row, col, revision, sheetName){
  var sheet = SpreadsheetApp.openById("<my-document-id>").getSheetByName(sheetName);
  sheet.getRange(row, col).setValue(revision);
}
```

## Webアプリケーションの準備
GASが書き終わったら、上部の公開 > Webアプリケーションとして導入を選択します。
urlが発行されるので後はcurlでパラメータを渡してexecすればセルに書き込まれます。
※ WebアプリケーションではなくAPIにしても良いと思います

```bash
curl -F "revision=${REVISION}" -F "scheme=${SCHEME}" -F "flavor=${flavor}" -F "branch=${branch}"  https://script.google.com/macros/s/hogehoge/exec
```

![図2.png](https://qiita-image-store.s3.amazonaws.com/0/78128/63187417-f734-c2a9-ed79-3fd3fcc27fbd.png "図2.png")

# 提出バイナリを自動でGoogleDriveにアップロードする
- 提出バイナリはJenkinsを動かしているマシンにある
- ダウンロード > リネーム > アップロード の作業を全てマシン上で行えば良い

## 使うもの
- GoogleDriveAPI
- Rake

## GoogleDriveAPIの準備
説明している所はたくさんあるので省略しますが、
大体以下の手順で準備完了です。

1. Google Cloud Platform上でAPIを叩くためのアプリケーションを作成
2. client_id, client_secret, refresh_tokenを取得
3. GoogleDriveAPIを有効にする

## Rakefileの準備
APIを叩くために、セッションを張ります。

```rb
module GoogleDriveSession
  def self.getGoogleDriveSession
    credentials = Google::Auth::UserRefreshCredentials.new(
      client_id: ENV["GOOGLE_CLIENT_ID"],
      client_secret: ENV["GOOGLE_CLIENT_SECRET"],
      scope: [
        "https://www.googleapis.com/auth/drive"
      ],
      redirect_uri: "http://localhost/redirect")
    credentials.refresh_token = ENV["GOOGLE_REFRESH_TOKEN"]
    credentials.fetch_access_token!
    GoogleDrive.login_with_oauth(credentials)
  end
end
```

アップロードするためのバイナリはJenkinsジョブを実行すれば作成されるので
後はリネームしてアップロードするだけです。
Rakefileにタスクを書きます。

```rb
namespace :binary do
  desc 'upload release binary file'
  task :upload do
    # セッションの取得
    session = GoogleDriveSession.getGoogleDriveSession

    # マシン内のバイナリパス
    binary_path = ENV["BINARY_PATH"]
    binary_ext = File.extname(binary_path).delete!('.')

    upload_folder_path = ["Upload", "Folder", "Example"]
    upload_folder = session.file_by_title(upload_folder_path)
    
    # バイナリ名
    upload_binary_name = ENV["UPLOAD_BINARY_NAME"]

    # アップロード
    upload_folder.upload_from_file(binary_path, upload_binary_name + "." + binary_ext)
  end
end
```

後はビルド終了後にrakeタスクを叩くことでバイナリが
```Upload/Folder/Example/```内に格納されます。

# 全自動化した結果
## Daily-Build
定期実行後、スプレッドシートに自動で書き込まれます。
後は結果をSlackで共有するだけ。

```
Branch: test-branch
Server: A Server
iOS:     # 1000(Debug)   # 1001(Release)
Android: # 100(Debug)    # 102(Release)    # 101(Release-Log)
```

## Release-Build
ビルドボタンを押すだけでこちらもスプレッドシートに自動で書き込まれます。
更に、提出用バイナリは自動でGoogleDriveに格納されます。
こちらも結果をSlackで共有するだけ。

![図3.png](https://qiita-image-store.s3.amazonaws.com/0/78128/012e16f8-c3bb-7a8a-4743-638b0c0a1fd3.png "図3.png")

# まとめ
今回はアプリのリリース作業の完全自動化を行いました。
リリース準備は結構属人化する領域かと思いますが、
完全自動化することによって誰にでも(エンジニア以外でも)できる作業になります。
属人化も防げるし、楽になるし、事故も起きなくなるし、良いことばかりです。
リリース作業に限らず、単純作業はボタンをポチるだけのスマートなタスクにしましょう！