13
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

GitHub Actions で Android プロジェクトのプルリクのチェックを行う(ゆめみ社の事例)

これは ゆめみ Advent Calendar 2020 の9日目の記事です。今回は、ゆめみの Android プロジェクトでよく導入している、GitHub Actions でのプルリクチェックのワークフローを紹介したいと思います :muscle:

ゆめみの Android グループは、約20以上の多種多様なお客様の Android プロジェクトに携わっています。一部例外はありますが、多くのプロジェクトの CI は、Bitrise と GitHub Actions を併用して構築しています。以前はプルリクのチェックは Bitrise を利用して実施していましたが、最近は GitHub Actions に乗り換えつつあります。

プルリクのチェックでは ktlint、Android Lint、Local unit test を実行し、Danger を利用してチェック結果をコメントさせています。これを実現するワークフローは次の通りです。(この記事の為にコメントを多めにいれています。)

.github/workflows/check-pull-request.yml
name: Check pull request

on: pull_request

env:
  # 実行する Gradle コマンド(プロジェクトによって調整してください。)
  GRADLE_KTLINT_TASK: 'ktlint'
  GRADLE_ANDROID_LINT_TASK: 'lintDevelopDebug'
  GRADLE_UNIT_TEST_TASK: 'testDevelopDebugUnitTest'

jobs:
  check:
    name: Check pull request
    runs-on: ubuntu-18.04 # Java や Ruby を利用するので念の為、環境を固定
    steps:
      - name: Check out
        uses: actions/checkout@v2
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Set up JDK
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Restore gradle cache # Gradle のキャッシュをリストア
        uses: actions/cache@v2
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }}
      - name: Set up Ruby # gem を利用するので Ruby をセットアップ
        uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.6'
      - name: Get gem info
        env: # Danger で利用する gem をここで列挙
          PACKAGES: danger:6.2.0 danger-checkstyle_format:0.1.1 danger-android_lint:0.0.8 danger-junit:1.0.0
        id: gem-info
        run: |
            echo "::set-output name=dir::$(gem environment gemdir)" # キャッシュするgemのディレクトリ
            echo "::set-output name=packages::$PACKAGES" # install 用の文字列
            echo "::set-output name=key::$(echo $PACKAGES | tr ' ' '-')" # キャッシュのキー文字列
      - name: Restore gem cache # gem のキャッシュをリストア
        uses: actions/cache@v2
        with:
          path: ${{ steps.gem-info.outputs.dir }}
          key: ${{ runner.os }}-gem-${{ steps.gem-info.outputs.key }}
      - name: Run ktlint
        run: ./gradlew $GRADLE_KTLINT_TASK
      - name: Run Android Lint
        run: ./gradlew $GRADLE_ANDROID_LINT_TASK
      - name: Run Unit Test
        run: ./gradlew $GRADLE_UNIT_TEST_TASK
      - name: Set up and run Danger
        if: cancelled() != true # 中断されない限り、エラーでも実行
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 標準で利用できるトークンを利用
          JOB_STATUS: ${{ job.status }} # jobのステータスを Danger へ受け渡す
        run: |
          gem install ${{ steps.gem-info.outputs.packages }}
          danger --dangerfile='.github/workflows/check-pull-request.danger' --remove-previous-comments --fail-on-errors=true

このワークフローでは、Danger のプラグインとして danger-checkstyle_formatdanger-android_lintdanger-junit を利用しています。

gem でインストールした Danger をコマンドラインで起動しています。コマンドラインのオプションについては GitHub の runner.rb ファイルを参照ください。(ドキュメントは見つけられませんでした。)

Danger 用のスクリプトファイルは次のものを用意し、上で説明したのワークフローの yaml ファイルと同じディレクトリに格納します。(こちらも、この記事の為にコメントを多めにいれています。)

.github/workflows/check-pull-request.danger
# GitHub Actions の job のステータスを受け取る
job_status = ENV['JOB_STATUS']

# 追加・変更していないコードはコメント対象外とするか
github.dismiss_out_of_range_messages({
  error: false, # エラーは追加・変更していないコードでもコメント
  warning: true,
  message: true,
  markdown: true
})

# ktlint の結果ファイルの解析とコメント
Dir.glob("**/build/reports/ktlint-results.xml").each { |report|
  checkstyle_format.base_path = Dir.pwd
  checkstyle_format.report report.to_s
}

# Android Lint の結果ファイルの解析とコメント
Dir.glob("**/build/reports/lint-results*.xml").each { |report|
  android_lint.skip_gradle_task = true # 既にある結果ファイルを利用する
  android_lint.report_file = report.to_s
  android_lint.filtering = false # エラーは追加・変更したファイルでなくてもコメント
  android_lint.lint(inline_mode: true) # コードにインラインでコメントする
}

# 最終結果でレポートするワーニング数は Android Lint と ktlint のみの合計としたいのでここで変数に保存
lint_warning_count = status_report[:warnings].count

# Local unit test の結果ファイルの解析とコメント
Dir.glob("**/build/test-results/*/*.xml").each { |report|
  junit.parse report
  junit.show_skipped_tests = true # スキップしたテストをワーニングとする(状況により適宜変更)
  junit.report
}

# プルリクの body が空の場合はエラー
fail 'Write at least one line in the description of PR.' if github.pr_body.length < 1

# プルリクが大きい場合はワーニング
warn 'Changes have exceeded 500 lines. Divide if possible.' if git.lines_of_code > 500

# 追加で独自のチェックをする場合はこのあたりで実施する
# ...

# Danger でエラーがある場合は既に何かしらコメントされているのでここで終了
return unless status_report[:errors].empty?

# GitHub Actions のワークフローのどこかでエラーがあった場合はその旨をコメントして終了
return markdown ':heavy_exclamation_mark:Pull request check failed.' if job_status != 'success'

# 成功時のコメント(もし不要な場合は省いてもいいと思います)
comment = ':heavy_check_mark:Pull request check passed.'
if lint_warning_count == 0
  markdown comment
else
  # ktlint と Android Lint のワーニング数の合計をレポート
  markdown comment + " (But **#{lint_warning_count}** warnings reported by Android Lint and ktlint.)"
end

仮に Android プロジェクトのコンパイルが通らない場合は Android Lint あたりでエラーになるので、一応そこで気づくことができます :eyes:(ビルドできるかのステップを追加してチェックしてもよいのですが、そうすると時間がかかってしまうので追加していません。)

この2ファイルを含むブランチでプルリクエストを作成してみてください。そうするとプルリク上でワークフローが実行中の状態になります。

ワークフローの実行状況や結果は、上図の Details リンクから確認することができます。どこでエラーになったかもここで確認することができます。

Danger のコメント例をいくつか紹介します。ワーニング(:warning: マークのコメント)に関しては、エラーと違い警告だけなので、いくらワーニングがあってもプルリクのチェック結果としては成功ステータスとなります。

ktlint または Android Lint のワーニング:

対象のコードにインラインでコメントされます。(ボットがレビューしているようで楽しいです :robot:

Android Lint のエラー:

Local unit test のエラー:

ワークフロー上で何かしらのエラーがある場合:

この場合はワークフローの実行結果画面からエラーの原因を探すことになります。(が、コンパイルエラーの場合が殆どです。)

エラーが無い場合:

成功の旨がコメントされます。

このように、GitHub Actions を利用することで、特別な CI サービスの契約なしに簡単に CI のワークフローを構築することができます。ワークフローがファイルとしてリポジトリで管理でき、実行状況や結果の確認も GitHub 内で完結しているのもいいですね :smiley:

そのほかの設定

Gradle と gem のキャッシュの作成

紹介したワークフローでは actions/cache で Gradle と gem のキャッシュを利用するようにしていますが、キャッシュはブランチに紐づいています。プルリクで新しいブランチを作成して push しても、その新しいブランチには当然、キャッシュが存在しません。コードの追加 push 時にはキャッシュが適用されはするのですが、キャッシュを十分に利用できておらず微妙な感じです。

ただ actions/cache は、プルリクのブランチにキャッシュが無ければマージ先のブランチのキャッシュを、マージ先に無ければデフォルトブランチのキャッシュを探索し利用する仕様になっています。よって、それらのブランチに更新があったタイミングでキャッシュを作成するワークフローを用意しておけば、プルリクの新規作成のタイミングでもキャッシュを利用できる状態にすることができます :thumbsup:

このワークフローは次のとおりです。(内容は先程のワークフローとだいたい同じなので細かい説明は省きます。)

.github/workflows/generate-cache.yml
name: Generate cache

on:
  push:
    branches: # デフォルトブランチや主たるマージ先ブランチを指定
      - master
      - develop*

jobs:
  create:
    name: Generate cache
    runs-on: ubuntu-18.04
    steps:
      - name: Check out
        uses: actions/checkout@v2
      - name: Set up JDK
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Restore gradle cache
        id: gradle-cache
        uses: actions/cache@v2
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }}
      - name: Download dependencies
        if: steps.gradle-cache.outputs.cache-hit != 'true' # キャッシュが無い場合だけ実行
        run: ./gradlew androidDependencies
      - name: Set up Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.6'
      - name: Get gem info
        env:
          PACKAGES: danger:6.2.0 danger-checkstyle_format:0.1.1 danger-android_lint:0.0.8 danger-junit:1.0.0
        id: gem-info
        run: |
          echo "::set-output name=dir::$(gem environment gemdir)"
          echo "::set-output name=packages::$PACKAGES"
          echo "::set-output name=key::$(echo $PACKAGES | tr ' ' '-')"
      - name: Restore gem cache
        id: gem-cache
        uses: actions/cache@v2
        with:
          path: ${{ steps.gem-info.outputs.dir }}
          key: ${{ runner.os }}-gem-${{ steps.gem-info.outputs.key }}
      - name: Set up Danger
        if: steps.gem-cache.outputs.cache-hit != 'true' # キャッシュが無い場合だけ実行
        run: |
          gem install ${{ steps.gem-info.outputs.packages }}

ktlint の導入

基本的には ktlint の README に導入方法が書かれていますが、

  • マルチモジュールへの対応
  • checkstyle 形式のレポートの追加(Danger 用)
  • ktlint からの指摘があっても CI(GitHub Actions)上で異常終了させない

というのを考慮して、プロジェクトルートの build.gradle:app 配下のではなく)に次のように記載します。

build.gradle

// ...

subprojects { // 配下のモジュール全てに適用される

    configurations { ktlint }

    dependencies { ktlint "com.pinterest:ktlint:0.39.0" }

    task ktlint(type: JavaExec, group: "verification") {
        description = "Check Kotlin code style."
        main = "com.pinterest.ktlint.Main"
        classpath = configurations.ktlint
        args "--android", "--color", "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/reports/ktlint-results.xml", "src/**/*.kt"
        ignoreExitValue true
    }

    task ktlintFormat(type: JavaExec, group: "formatting") {
        description = "Fix Kotlin code style deviations."
        main = "com.pinterest.ktlint.Main"
        classpath = configurations.ktlint
        args "-F", "--android", "src/**/*.kt"
        ignoreExitValue true
    }

    afterEvaluate {
        check.dependsOn ktlint
    }
}

ktlint から指摘があったらワークフローを失敗ステータスにする厳しい運用にする場合は ignoreExitValue true の記載は削除してください。

もしモジュールによって動作を変えたい場合は、it.name でモジュール名が取得できるので、判定処理をいれるとよいでしょう。例えばライブラリ導入用のモジュールがあったりすると、Gradle の check タスクがなくエラーになったりするので、次のような判定で回避します。

afterEvaluate {
    if (it.name != "awesome_module") {
        check.dependsOn ktlint
    }
}

次に、プロジェクトルートに .editorconfig ファイルを用意して、ktlint の追加の設定をします。

.editorconfig
[*]
insert_final_newline = true

[*.{kt, kts}]
max_line_length = 128
disabled_rules = import-ordering

insert_final_newline を有効にすると、ファイルの末尾が改行文字であるかチェックされるようになります。Android Studio のコードフォーマッタもこの設定を参照しているので、ファイルの末尾が改行文字ではない場合は自動的に補完されるようになります。(この例では、Java のファイルなど、Kotlin 以外のファイルでも自動的に補完したいので [*] で全ファイルを対象にしています。)

max_line_length は、コード一行の最大文字数の設定です。デフォルトだと 100 なのですが、弊社では 128 にしています。このあたりはプロジェクトによって調整してください。

disabled_rules には、無効にする ktlint のルールを指定します。この例では import-ordering(コードの import 文の並び順のチェック)を無効にしています。現状、Android Studio のコードフォーマッタで並び替えた順序が ktlint の指摘の対象になってしまうからです。このあたり、最新バージョンの ktlint を利用(私が試したのは 0.39.0 まで)したり、フォーマッタの設定を調整したり、.editorconfig ファイルを調整したりすれば回避できるのかもしれません。(私は何をしても上手くいきませんでした..)

また、Android Studio のコードフォーマッタも、なるべく ktlint の指摘が少なくなるように設定されてあるものを利用する方がよいでしょう。おすすめの設定は ktlint の README に書いてあります。

プルリクのマージのブロック

リポジトリの Branch protection rules の設定で、今回のワークフローのジョブ(Check pull request)のステータスをチェックするようにします。ワークフローの実行中やエラー時はマージボタンを非活性にすることができるので、中途半端な状態でマージされるのを防げます。

Danger(この画面での表示は danger/danger)もステータスを更新しますが、Danger でエラーの場合は今回のワークフローもエラーになるので、ここでの Danger のステータスのチェックは特に不要です。

おわりに

いかがでしたでしょうか。今回は、ゆめみの Android プロジェクトでよく導入しているプルリクチェックのワークフローを紹介させて頂きました。他にも紹介したいワークフローはまだまだあるのですが、それはまた別の機会に紹介させて頂きます :alien:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
13
Help us understand the problem. What are the problem?