AndroidプロジェクトのCI状況を晒す

やっと半年ほど前からfastlaneとCicle CIを使い初めた。
自身が書いたビルドフローを晒すことによって誰かに突っついてもらえたらラッキーというモチベーションで晒してみる。

## ビルドに関わるサードパーティ製品・ライブラリ
* Circle CI Enterprise
* Github Enterprise
* Crashlytics Beta
* fastlane（bundlerからフック)
* Gradleスクリプト

## circle.yml
```yaml

machine:
  timezone: Asia/Tokyo
  java:
    version: oraclejdk8
  environment:
    JAVA_OPTS: "-Xms256m -Xmx1024m -XX:MaxPermSize=512m"
    ADB_INSTALL_TIMEOUT: 60

dependencies:
  cache_directories:
    - ~/.android
    - /usr/local/android-sdk-linux
  pre:
    - mkdir -p $ANDROID_HOME/licenses
    - cp -f ./android-sdk-license $ANDROID_HOME/licenses
    - echo y | android update sdk --no-ui --all --filter "platform-tools, tools"
    - echo y | android update sdk --no-ui --all --filter "android-25, build-tools-25.0.2"
    - echo y | android update sdk --no-ui --all --filter "extra-android-m2repository"
    - echo y | android update sdk --no-ui --all --filter "extra-android-support"
    - echo y | android update sdk --no-ui --all --filter "extra-google-m2repository"

test:
  override:
    - ./gradlew testDevelopmentDebugUnitTest -PdisablePreDex -PdisableDevMinSdk
    - ./gradlew testDevelopmentReleaseUnitTest -PdisabltePreDex

deployment:
  deliver-develop:
    branch: develop
    commands:
      - bundle exec fastlane dev type:Debug
      - cp -r app/build/outputs $CIRCLE_ARTIFACTS
  deliver-staging:
    branch: /release\/.*/
    commands:
      - bundle exec fastlane stg 
      - cp -r app/build/outputs $CIRCLE_ARTIFACTS
  create-tag:
    branch: master
    commands:
      - ./gradlew assembleStandbyRelease -PdisabltePreDex -PdisableDevMinSdk
      - ./gradlew assembleProductionRelease -PdisabltePreDex -PdisableDevMinSdk
      - ./gradlew assemblePlaystoreRelease -PdisabltePreDex -PdisableDevMinSdk
      - git fetch origin --prune 'refs/tags/*:refs/tags/*'
      - git config user.email "circleci@"
      - git config user.name "CircleCI bot"
      - bundle exec fastlane tag
  deliver-production:
    tag: /builds\/androidtag\/.*/
    commands:
      - bundle exec fastlane stby # publish to crashlytics beta
      - bundle exec fastlane prd # publish to crashlytics beta
      - bundle exec fastlane store # make archive apk for PlayStore
      - cp -r app/build/outputs $CIRCLE_ARTIFACTS
```
#### 環境(Product Flavor)
* development
* staging
* standby
* production
* store

standby → プロジェクトがBlueGreenのため、待機しているサーバーにアクセスできるアプリを用意している。
production → ストアにリリースする前に触るアプリ。コンテンツを公開用アプリと同様のサーバーに向けて見れるようにしている。デバッグログなどが取れるもの
store → 公開用アプリ。

↑は各々のプロジェクトで変わると思いますが、ほとんどのプロジェクトではdev, stg, storeは最低でもあると思います。

#### ビルドのピタゴラスイッチ
Circle CIのブランチにマッチさせてビルドルールを変えるという概念はシンプルで
git-flowのようなブランチ名がルール化されている業務フローととても相性がいいです。
ブランチ名の条件には、正規表現が使えるので柔軟に分岐させることが出来ます。

#### ビルドルールリスト

| ビルドルール | ビルド |
| --- | --- |
| `branch: develop` | developブランチに変更があればビルドを行う。develop向けのアプリをCrashlyticsにデプロイする |
| `branch: /release\/.*/` | `release/`をプレフィックスとするブランチに変更があればビルドを行う。staging向けのアプリをCrashlyticsにデプロイする |
| `branch: master` | masterブランチに変更があればビルドを行う。タグ作成とGHEへのプッシュを行う |
| `tag: /builds\/androidtag\/.*/` | `builds/androidtag/`をプレフィックスとするタグが作成されたらビルドを行う。タグ作成とGHEへのプッシュを行う |

#### 全体フロー
1. `feature/.*`ブランチで開発を行いdevelopにプルリク
2. developアプリがデプロイされる
3. 開発が終わったら`release/.*`ブランチを作成する。
4. stagingアプリがデプロイされる
5. stagingのリグレッションテストが一通り終わる
6. `release/.*`ブランチをmasterにマージする
7. `builds/androidtag/.*`タグがアプリのバージョンなどの情報を元に生成される
8. タグが出来たので、standby, productionがデプロイ、storeアプリがアーカイブされる

ということで、ブランチの操作をするだけで本番アプリができるようになっています。

## Fastfile

途中からFastfileが肥大化してしまい、見通しが悪くなったため、Fastfileを役割毎に切り出しています。

| ファイル | 役割 |
| --- | --- |
| Env | SlackのWebhookURL, テスターグループ, CrashlyticsのAPIトークンをオブジェクトとして定義しておく |
| App | アプリのビルドlaneを定義 |
| Release | バージョンのbumpupスクリプトをフックするlaneを定義 |
| Fastfile | フックされるファイル。Envをload, AppとReleaseをimportする。`before_all`, `after_all`, `error`のハンドリングを共通で定義 |

依存関係はこんな感じです

```
  Env
   ↓オブジェクトのload
Fastfile
   ↑laneのimport
[App, Release]
```

下記に示すのは`App`の中身です。

```ruby
=begin
# Lane for build application
=end

class AppContext
  @@flavor_hash = {
    'store' => 'playstore',
    'stby' => 'standby',
    'prd' => 'production',
    'stg' => 'staging',
    'dev' => 'development'
  }

  def self.validate_env(env)
    unless @@flavor_hash.has_key?(env)
      raise [
        "Invalid environment: #{env}\n",
        "Valid environment is one of them: #{@@flavor_hash.keys}",
      ].join("\n")
    end
  end

  def initialize(env, build_type)
    AppContext.validate_env(env)
    @env = env
    @build_type = build_type
  end

  def task
    'assemble'
  end

  def flavor
    @@flavor_hash[@env]
  end

  def apk_path
    "app/build/outputs/apk/app-#{flavor}-#{@build_type.downcase}.apk"
  end

  def build_type
    @build_type
  end

end

platform :android do

  #=Test
  desc "Runs all the tests"
  lane :test do
    gradle(task: "test")
  end

  #=Build apps each environment
  desc "Build store app and create apk"
  lane :store do |options|
    options[:env] = 'store'
    beta(options)
  end

  desc "Build stby app, create apk and deploy to beta"
  lane :stby do |options|
    options[:env] = 'stby'
    beta(options)
  end

  desc "Build prd app, create apk and deploy to beta"
  lane :prd do |options|
    options[:env] = 'prd'
    beta(options)
  end

  desc "Build stg app, create apk and deploy to beta"
  lane :stg do |options|
    options[:env] = 'stg'
    beta(options)
  end

  desc "Build dev app, create apk and deploy to beta"
  lane :dev do |options|
    options[:env] = 'dev'
    beta(options)
  end

  #=Actual build operation
  desc "Build app for current env, create apk and deploy to beta"
  private_lane :beta do |options|
    archive(options)
    crashlytics(groups: Fabric.tester_group)
  end

  desc "Build app for current env  create apk"
  private_lane :archive do |options|
    env = options.fetch(:env, 'dev')
    build_type = options.fetch(:type, 'Release')

    begin
      AppContext.validate_env(env)
    rescue => e
      UI.user_error!(e.message)
      Kernel.abort
    end

    context = AppContext.new(env, build_type)
    gradle(
      task: context.task,
      flavor: context.flavor,
      build_type: context.build_type,
      flags: "-PdisablePreDex -PdisableDevMinSdk"
    )

    copy_artifacts(
      target_path: "artifacts",
      artifacts: [context.apk_path]
    )
  end

end

# vim: ft=ruby sw=2 ts=2 sts=2
```

#### 普段のビルドはminSDKを21に設定。CIでは16にする
archiveレーンのgradleをフックしている所で以下のフラグを追加しています。

`-PdisableDevMinSdk`

これはgradleの中で開発用のminSdｋをdisableにするというフラグで、自前で定義したフラグです。
このプロジェクトではminSdkが`16`なのですが、フラグを立てない時(つまり、Android Studioでのビルド時)にminSdkを`21`にすることによって普段のビルドをなるべく高速化させるという試みです。
理由はこちらの[2016年、KotlinでAndroid開発する方へ](http://qiita.com/satorufujiwara/items/871c5b7b66c7691d82a8#%E7%8F%BE%E5%9C%A8%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)で述べられています。

設定を反映するbuild.gradleは以下の通りです。こうすることによってAndroid Studioで開発している時は21, CIではにするというフラグ16、他のBuildVariantsでは16のようなスイッチが実現しています。大まかな実装は21で。互換性の確認は16にして・・・という形で開発しています。
プロジェクトの開発の方針によって変わってくるところなので一概にコレが正解とは言えないでしょう。


```groovy
def getDevMinSdk() {
  return hasProperty('disableDevMinSdk') ? 16 : 21
}

android {
  defaultConfig {
    minSdkVersion 16
    targetSdkVersion 25
  }

  productFlavors {
    development {
      minSdkVersion = devMinSdk
    }
  }
}
```

#### AppContextクラス
gradleをフックするための変数を一括管理しておくクラスを作っています。
また、その変数からビルドのパスが一意に決まるので、文字列の解決をメソッド化しておくことによって`private_lane :archive`をスッキリ書けるようにしています

## まとめ
* circle.ymlはブランチ名の分岐には正規表現で自動化を
* Fastlaneのlaneはrubyで便利に
* Fastfileから切り出してlaneを書いたり、環境変数を定義するとスッキリする
* `minSdk`はフラグ1個追加すれば簡単に切り替え出来る