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


この記事は Akatsuki Advent Calendar 2017 の 7 日目の記事です。
6日目: Unityで出力したipa, apkの容量を削減するためのチェックリスト

はじめに

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

去年はこういうのを書いていました。
「Akatsuki Advent Calendar 2016 5日目 安易にiOSのUUIDをアカウント認証ロジックに使ってはいけない」

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

背景

アプリ開発ではリリース時やバージョンアップ時に
各プラットフォームに提出するバイナリ(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は利用しています。
Jenkinsのジョブは「プラットフォーム数 * サーバ数」用意しています。
これらのジョブを回してビルドが成功したら、
アプリ開発&テスト支援サービスのDeploygateに自動でバイナリがアップされます。

完全自動化前のタイミングでも、このジョブを毎日1つ1つ実行しているはずはなく、
そこは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を共有するので、コピペ用のテキストも用意しています。
上の表に書き込まれれば下のテキストに反映されます。

Google Apps Scriptの準備

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

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

function doPost(e) {

// ここに処理を書く

}

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

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

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にしても良いと思います

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

提出バイナリを自動でGoogleDriveにアップロードする

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

使うもの

  • GoogleDriveAPI
  • Rake

GoogleDriveAPIの準備

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

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

Rakefileの準備

APIを叩くために、セッションを張ります。

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にタスクを書きます。

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で共有するだけ。

まとめ

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