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

  • 3
    Like
  • 0
    Comment

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

ビルドに関わるサードパーティ製品・ライブラリ

  • Circle CI Enterprise
  • Github Enterprise
  • Crashlytics Beta
  • fastlane(bundlerからフック)
  • Gradleスクリプト

circle.yml


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の中身です。

=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の中で開発用のminSdkをdisableにするというフラグで、自前で定義したフラグです。
このプロジェクトではminSdkが16なのですが、フラグを立てない時(つまり、Android Studioでのビルド時)にminSdkを21にすることによって普段のビルドをなるべく高速化させるという試みです。
理由はこちらの2016年、KotlinでAndroid開発する方へで述べられています。

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

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個追加すれば簡単に切り替え出来る