はじめに
こんにちは、WEARバックエンド部バックエンドブロックの塩足です。普段は弊社サービスであるWEARのバックエンド開発・保守を担当しています。
WEARのバックエンドでは、これまで自動テスト環境としてCircleCIを使用していましたが、運用保守の改善を目的にGitHub Actionsへ移行しました。
今回は、GitHub Actionsへ移行する際に取り組んだ以下の3点について紹介します。
- 効率的にテストを分割してテストを並列実行する方法
- 失敗したテストのみを再実行する仕組みの構築
- GitHubのCheck annotationsを活用して、失敗したテスト情報を表示
また、最後に今回行ったテストカバレッジのレポーティングとGitHub Pagesでのホスティングの方法について紹介します。
目次
- はじめに
- 目次
- 背景
- 課題
- 課題解決の方法
- テストカバレッジを活用できる環境整備の方法
- 今後の展望
- まとめ
背景
前述の通り、WEARのバックエンドではこれまで自動テスト環境としてCircleCIを使用していました。
なぜ自動テスト環境をCircleCIからGitHub Actionsに移行したのか
では、なぜ自動テスト環境をCircleCIからGitHub Actionsに移行したのか、その理由を説明します。
テストカバレッジのレポーティングやGitHub Pagesでのホスティングがシンプルに実現可能
octocov
の導入が容易
octocovはコードメトリクスを収集するツールキットです。octocov
には前回のテストカバレッジと比較するdiff機能があります。この機能を使うにはデータストレージが必要です。
CircleCIで利用する際は、外部ストレージを利用する必要があります。一方、GitHub Actionsでは、octocov
がGitHub Actionsアーティファクトをデータストレージとしてサポートしています。また、k1LoW/octocov-action@v1
アクションが用意されているため、簡単に導入できます。
ワークフローをトリガーする豊富なイベント
これまで、CircleCIではpush
をトリガーにワークフローを実行していました。しかし、octocov
を利用するにはpull_request
をトリガーにワークフローを実行する必要があります。pull_request
をトリガーにすると、Pull Requestを作成するまでテスト結果を確認できないため、開発生産性が落ちてしまいます。
そこで、CircleCIでpush
とpull_request
の両方をトリガーする方法を検討しました。しかしながら、両方をトリガーするにはGitHub ActionsからCircleCIのAPIでトリガーする必要があり、全体のワークフローが複雑になってしまいます。一方、GitHub Actionsであれば柔軟なトリガーが可能なため、シンプルに解決できます。
全社的にGitHub Actionsを推奨
全社的に新規のプロジェクトに関しては、基本的にGitHub Actionsの利用を推奨しています。そのため、今後はGitHub Actionsに揃えることで、ナレッジの共有やメンテナンスが容易になると考えました。
上記2つの理由から、自動テスト環境をCircleCIからGitHub Actionsへ移行することを決定しました。
課題
CircleCIからGitHub Actionsに移行するにあたって、以下の3つの課題がありました。
- CircleCIのタイミングベースでテスト分割する仕組みがGitHub Actionsにない
- CircleCIの失敗したテストのみを再実行する機能がGitHub Actionsにない
- GitHub Actionsで失敗したテスト詳細情報へのアクセシビリティが低い
1. CircleCIのタイミングベースでテスト分割する仕組みがGitHub Actionsにない
CircleCIは標準でタイミングデータに基づいたテストの分割が可能です。GitHub Actionsには同様の機能が備わっていないので、別途仕組みを考える必要があります。
2. CircleCIの失敗したテストのみを再実行する機能がGitHub Actionsにない
CircleCIには標準で失敗したテストのみを再実行する機能を提供しています。GitHub Actionsには失敗したテストのみ再実行する機能がないため、Flaky test等でテストが失敗した際、全テストを実行する必要があり開発生産性に影響します。
3. GitHub Actionsで失敗したテスト詳細情報へのアクセシビリティが低い
CircleCIはJob詳細画面のTESTSタブで失敗したテストを一覧で閲覧できます。
一方、GitHub Actionsには失敗したテストを一覧で閲覧する機能はありません。並列で実行して複数のtestジョブでテストが失敗している場合、失敗しているtestジョブの数だけログを確認する必要があります。
課題解決の方法
まずは以下のようなTestワークフロー(.github/workflows/test.yml
)を用意し、これを拡張することで3つの課題を解決していきます。
name: Test on: push: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: TEST_JOB_PARALLEL_COUNT: 2 RAILS_ENV: test defaults: run: shell: bash jobs: test: runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read actions: read strategy: fail-fast: false matrix: group_index: ['0,1', '2,3'] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Setup DB run: bundle exec rails "parallel:setup[`nproc`]" - name: Run rspec in parallel run: bundle exec parallel_rspec -n $((${TEST_JOB_PARALLEL_COUNT} * `nproc`)) --only-group ${{ matrix.group_index }} - name: Upload test result if: ${{ success() || failure() }} uses: actions/upload-artifact@v4 with: name: test-result-${{ matrix.group_index }} path: | test_results/ coverage/.resultset*.json include-hidden-files: true if-no-files-found: ignore
このワークフローはmatrix strategyを使用して2つ(TEST_JOB_PARALLEL_COUNT
)のtestジョブを並列実行します。さらに、各testジョブはparallel_rspec
で2つ(= nproc
から取得したCPU数)のrspecプロセスを実行します。テストはparallel_rspec
によってファイルサイズをベースに4つ(= testジョブ並列数 × CPU数)のグループに分割され、各testジョブで2グループずつ実行されます。テスト結果は test-result-${{ matrix.group_index }}
という名前でアーティファクトに保存します。
testジョブのワークフローのフローチャートは以下のようになります。
1. r7kamura/split-tests-by-timings
アクションを使用したタイミングベースのテスト分割
課題1を解決するための、タイミングベースのテスト分割について説明します。
parallel_rspec
のオプション--group-by
にruntime
を指定することで、タイミングベースでテスト分割できます。しかし、今回はより汎用的に利用できるr7kamura/split-tests-by-timings
アクションを採用しました。
r7kamura/split-tests-by-timingsアクションはJUnit XMLファイルを元にタイミングベースでテスト分割するアクションです。つまり、過去のテスト結果からJUnit XMLファイルを抽出し、r7kamura/split-tests-by-timings
アクションに渡す必要があります。
タイミングベースでテスト分割するために、testジョブを以下のように修正します。
@@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - group_index: ['0,1', '2,3'] + test_job_index: [0, 1] steps: - name: Checkout uses: actions/checkout@v4 @@ -35,13 +35,30 @@ jobs: bundler-cache: true - name: Setup DB run: bundle exec rails "parallel:setup[`nproc`]" + - name: Download all test results for default branch + uses: dawidd6/action-download-artifact@v6 + with: + name: test-result-* + name_is_regexp: true + path: ${{ runner.temp }}/default-branch-test-results + branch: ${{ github.event.repository.default_branch }} + workflow_conclusion: success + if_no_artifact_found: warn + - name: Split tests by timings + uses: r7kamura/split-tests-by-timings@v0 + id: split-tests + with: + reports: ${{ runner.temp }}/default-branch-test-results/**/test_results + glob: spec/**/*_spec.rb + index: ${{ matrix.test_job_index }} + total: ${{ env.TEST_JOB_PARALLEL_COUNT }} - name: Run rspec in parallel - run: bundle exec parallel_rspec -n $((${TEST_JOB_PARALLEL_COUNT} * `nproc`)) --only-group ${{ matrix.group_index }} + run: bundle exec parallel_rspec -n `nproc` ${{ steps.split-tests.outputs.paths }} - name: Upload test result if: ${{ success() || failure() }} uses: actions/upload-artifact@v4 with: - name: test-result-${{ matrix.group_index }} + name: test-result-${{ matrix.test_job_index }} path: | test_results/ coverage/.resultset*.json
testジョブにおいて以下の点線で囲った2つのステップが追加されます。
追加した2つのステップについて説明します。
Download all test results for default branch
ステップ
このステップではデフォルトブランチで成功している最新のテスト結果をダウンロードします。テストは複数のジョブで分散して実行されるため、その全てのテスト結果を取得する必要があります。
公式で用意されているactions/download-artifact
アクションでは、ブランチやワークフロー実行のステータス等を指定してダウンロードできません。そこで、より柔軟に指定できるdawidd6/action-download-artifactアクションを使用することにしました。
dawidd6/action-download-artifact
アクションの各入力パラメータについては以下の表で説明します。
キー | 値 |
---|---|
name | ダウンロードするアーティファクト名のパターンを指定します。 |
name_is_regexp | nameで正規表現を利用できるように、true を設定します。 |
path | ダウンロードするファイルパスを指定します。 |
branch | ブランチでアーティファクトを検索します。今回はデフォルトブランチを指定します。 |
workflow_conclusion | ワークフローのステータスでアーティファクトを検索します。今回は成功しているワークフローのみに絞り込むため、success を指定します。 |
if_no_artifact_found | アーティファクトが見つからない場合の挙動を定義します。 初回実行を考慮してアーティファクトが存在しない場合でも動作するように warn を指定しています。 |
Split tests by timings
ステップ
r7kamura/split-tests-by-timings
アクションはJUnit XMLファイルを元にタイミングベースでテストを分割します。r7kamura/split-tests-by-timings
アクションの各入力パラメータについては以下の表で説明します。
キー | 値 |
---|---|
reports | JUnit XMLのファイルパスを指定します。 指定したパスにある全てのXMLファイルからタイミングデータを取得します。 |
glob | テストファイルのglobパターンを指定します。 |
index | 分割したグループのインデックスを指定します。 |
total | 分割するグループ数を指定します。 今回はtestジョブの数だけグループを作成します。 |
分割されたファイルパスのリストは${{ steps.split-tests.outputs.paths }}
でアクセスできます。
Split tests by timings
ステップで分割したファイルパスのリストをRun rspec in parallel
ステップで使用することで、タイミングベースのテスト分割ができます。タイミングベースのテスト分割によりtestジョブは約30秒高速化しました。
2. RSpecの--only-failures
オプションで失敗したテストのみ再実行
課題2を解決するため、失敗したテストのみを再実行する方法を説明します。
CircleCIではcircleci tests run
を使用することで、失敗したテストのみ再実行できましたが、GitHub Actionsにそういった機能は用意されていません。一方で、RSpecには--only-failures
オプションが用意されています。--only-failures
オプションは、実行されるテストをフィルタリングして、前回実行時に失敗したテストだけが実行されるようにします。
失敗したテストのみを再実行するために、testジョブを以下のように修正します。
@@ -33,9 +33,35 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true + - name: Download previous test result + uses: actions/download-artifact@v4 + with: + pattern: test-result-${{ matrix.test_job_index }} + path: ${{ runner.temp }} + - name: Place previous test result + id: previous-test-result + env: + TEST_RESULT_DIR: ${{ runner.temp }}/test-result-${{ matrix.test_job_index }} + run: | + if [ -f ${TEST_RESULT_DIR}/spec/examples.txt ]; then + mv ${TEST_RESULT_DIR}/spec/examples.txt spec/examples.txt + echo "failed-tests-only=true" >> $GITHUB_OUTPUT + fi + suffix="_`date +%s`" + mkdir -p test_results coverage + if [ -e ${TEST_RESULT_DIR}/test_results ]; then + mv ${TEST_RESULT_DIR}/test_results/* test_results/ + find test_results -type f -name "*.xml" | sed "p;s/.xml/${suffix}.xml/" | xargs -n2 mv + bundle exec rails runner "Dir['test_results/**/*.xml'].each { |path| File.write(path, Nokogiri(File.read(path)).tap { _1.css('testcase:has(failure)').remove }.to_s) }" + fi + if [ -e ${TEST_RESULT_DIR}/coverage ]; then + mv ${TEST_RESULT_DIR}/coverage/.resultset*.json coverage/ + find coverage -type f -name "*.json" | sed "p;s/.json/${suffix}.json/" | xargs -n2 mv + fi - name: Setup DB run: bundle exec rails "parallel:setup[`nproc`]" - name: Download all test results for default branch + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} uses: dawidd6/action-download-artifact@v6 with: name: test-result-* @@ -45,6 +71,7 @@ jobs: workflow_conclusion: success if_no_artifact_found: warn - name: Split tests by timings + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} uses: r7kamura/split-tests-by-timings@v0 id: split-tests with: @@ -53,7 +80,11 @@ jobs: index: ${{ matrix.test_job_index }} total: ${{ env.TEST_JOB_PARALLEL_COUNT }} - name: Run rspec in parallel + if: ${{ !steps.previous-test-result.outputs.failed-tests-only }} run: bundle exec parallel_rspec -n `nproc` ${{ steps.split-tests.outputs.paths }} + - name: Re-run rspec only failures + if: ${{ steps.previous-test-result.outputs.failed-tests-only }} + run: bundle exec rspec --only-failures - name: Upload test result if: ${{ success() || failure() }} uses: actions/upload-artifact@v4 @@ -61,6 +92,7 @@ jobs: name: test-result-${{ matrix.test_job_index }} path: | test_results/ + spec/examples.txt coverage/.resultset*.json include-hidden-files: true if-no-files-found: ignore
testジョブにおいて以下の点線で囲った3つのステップが追加されます。
追加した3つのステップについて説明します。
Download previous test result
ステップ
actions/download-artifact
アクションで前回のテスト結果のアーティファクトをダウンロードします。
GitHub Actionsのジョブの実行はいくつか方法がありますが、通常はイベントトリガーで実行されます。また、再実行はGitHub Actionsの実行詳細ページにある以下のボタンから「Re-run all jobs」と「Re-run failed jobs」を選択して実行できます。
このジョブの実行方法の違いによりアーティファクト取得の挙動が変化します。
実行方法 | アーティファクト取得の挙動 |
---|---|
イベントトリガー | アーティファクトがまだ存在しないため、取得できません。 |
Re-run all jobs | すべてのジョブのアーティファクトがリセットされます。 つまり、挙動としてはイベントトリガーと同じになります。 |
Re-run failed jobs | 成功したジョブのアーティファクトは変更されず、そのまま保存されます。 失敗したジョブのアーティファクトは再利用でき、再実行後にそのジョブの新しいアーティファクトで上書きできます。 |
Re-run failed jobs
の場合、失敗したジョブのアーティファクトは再利用できるため、前回のテスト結果を利用できることになります。
Place previous test result
ステップ
Download previous test result
ステップで前回のテスト結果をダウンロードできた場合、以下の3つのファイルを適切に配置する必要があります。
- spec/examples.txt
- JUnit XMLファイル
- カバレッジデータ
それでは1つずつ説明します。
spec/examples.txt
spec/examples.txt
は、RSpecの実行結果に関する情報を記録するためのファイルです。このファイルには、前回のRSpec実行時にどのテストが成功したか、失敗したかなどの情報が保存されます。
また、--only-failures
オプションを使用するには以下のように、spec/examples.txt
をspec/spec_helper.rb
に設定する必要があります。
RSpec.configure do |config| config.example_status_persistence_file_path = 'spec/examples.txt' end
JUnit XMLファイル
JUnit XMLファイルは以下の2箇所で利用します。
r7kamura/split-tests-by-timings
アクションを使用したタイミングベースのテスト分割- 後述するGitHubのCheck annotationsで失敗したテスト結果を表示
bundle exec rspec --only-failures
を実行すると、JUnit XMLのファイルは前回失敗したテストだけの結果に上書きされます。タイミングベースでテスト分割する際に、全体のテスト結果が必要になるため、別の名前にリネームして残しておく必要があります。
当然、このファイルには前回失敗したテスト結果が含まれています。前回失敗したテストは再実行されるため、前回失敗したテスト結果だけ事前に削除しておく必要があります。
path = 'test_results/rspec.xml' File.write(path, Nokogiri(File.read(path)).tap { _1.css('testcase:has(failure)').remove }.to_s)
カバレッジデータ
このファイルはテストカバレッジをレポートする際に利用します。このファイルもJUnit XMLファイルと同様にbundle exec rspec --only-failures
を実行すると、上書きされてしまいます。そのため、カバレッジデータも別のファイル名にリネームして残しておく必要があります。
前回のテスト結果がある場合は${{ steps.previous-test-result.outputs.failed-tests-only }}
にtrue
が設定され、後続する処理で利用されます。
Re-run rspec only failures
ステップ
前回のテスト結果がある場合のみbundle exec rspec --only-failures
を実行します。
これでRe-run failed jobs
から再実行した場合に、失敗したテストのみを再実行する方法が実現できました。
3. JUnit XMLファイルとreviewdogを用いて、GitHubのCheck annotationsで失敗したテスト情報を表示
課題3を解決するために、GitHubのCheck annotationsで失敗したテスト情報を表示する方法について説明します。
reviewdog
はReviewdog Diagnostic Format(RDFormat)
という独自のフォーマットを利用して任意のlinterと連携できます。
JUnit XMLのデータは、エラーが発生したテストのファイルパス、テスト名(full_description)、エラーメッセージを保持しています。reviewdog
を使用し、JUnit XMLのデータだけでエラー情報を表示すると以下のようになります。
対象ファイルの1行目にエラー情報が全て表示されるため、どのテストの情報なのか分かりにくくなってしまいます。
そこで、各テスト名の行番号を取得してエラー情報を見やすくする方法を検討しました。今回はRSpec::Core::ExampleGroup
オブジェクトのmetadata
から行番号を取得する方法を採用しました。
以下のファイル(scripts/generate_rspec_reviewdog_json.rb
)はJUnit XMLファイルからRDFormatファイルを生成するスクリプトとなります。
require 'nokogiri' $LOAD_PATH.unshift 'spec' require 'rails_helper' # RSpecのExampleGroupを再帰的に辿り、全てのExampleを取得する # @param example_group [RSpec::Core::ExampleGroup] # @return [Array<RSpec::Core::Example>] def collect_all_examples(example_group) example_group.examples + example_group.children.flat_map { collect_all_examples(_1) } end # ExampleGroupからfull_descriptionとline_numberの対応関係を生成する # @param example_group [RSpec::Core::ExampleGroup] # @return [Hash{String => Integer}] def map_description_to_line_number(example_group) collect_all_examples(example_group) .each_with_object({}) { |example, obj| obj[example.metadata[:full_description]] = example.metadata[:line_number] } end # キャッシュを利用して、ファイルパスに対応するfull_descriptionとline_numberの対応関係を生成する # @param path [String] # @return [Proc] def cached_description_to_line_number cache = {} ->(path) { cache[path] ||= begin example_group = eval(File.read(path)) # rubocop:disable Security/Eval map_description_to_line_number(example_group) end } end # JUnit XMLファイルからRDFormatのデータを生成する # @param junit_xml_file_path [String] # @return [Array<Hash>] def parse_junit_failures(junit_xml_file_path) description_mapper = cached_description_to_line_number Nokogiri(File.open(junit_xml_file_path)).css('testsuite testcase failure').map do |failure_elem| elem = failure_elem.parent path = elem.attr('file') description_to_line = description_mapper.call(path) { message: failure_elem.text, location: { path: path, range: { start: { line: description_to_line[elem.attr('name')] } } } } end end File.open(ENV.fetch('REVIEWDOG_JSON_FILE_PATH'), 'w') do |f| Dir[ENV.fetch('JUNIT_XML_FILE_PATH_PATTERN')].each do |junit_xml_file_path| rows = parse_junit_failures(junit_xml_file_path) f.puts(rows.map(&:to_json).join("\n")) if rows.present? end end
上記スクリプトを利用して、TestワークフローにGitHubのCheck annotationsで失敗したテスト情報を表示するreport-failed-tests
ジョブを追加しました。
report-failed-tests: needs: test runs-on: ubuntu-latest timeout-minutes: 5 continue-on-error: true permissions: contents: read pull-requests: write if: ${{ success() || failure() }} env: REVIEWDOG_JSON_FILE_NAME: rspec_reviewdog.jsonl steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Setup reviewdog uses: reviewdog/action-setup@v1 with: reviewdog_version: v0.20.0 - name: Download all test results uses: actions/download-artifact@v4 with: pattern: test-result-* path: ${{ runner.temp }}/test-results - name: Generate RSspec reviewdog json env: JUNIT_XML_FILE_PATH_PATTERN: ${{ runner.temp }}/test-results/**/test_results/*.xml REVIEWDOG_JSON_FILE_PATH: ${{ runner.temp }}/${{ env.REVIEWDOG_JSON_FILE_NAME }} run: bundle exec ruby scripts/generate_rspec_reviewdog_json.rb - name: Run rspec reviewdog env: REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} REVIEWDOG_JSON_FILE_PATH: ${{ runner.temp }}/${{ env.REVIEWDOG_JSON_FILE_NAME }} run: | cat $REVIEWDOG_JSON_FILE_PATH | reviewdog -f=rdjsonl -reporter=github-check
report-failed-tests
ジョブではscripts/generate_rspec_reviewdog_json.rb
を実行してRDFormatのファイルを作成します。作成されたRDFormatファイルはreviewdog
に渡されて、reviewdog
内でGitHub APIのUpdate a check runを実行します。
report-failed-tests
ジョブを実行した結果、以下のように各テストの行にエラー情報が表示されるようになりました。
テストカバレッジを活用できる環境整備の方法
最後に、テストカバレッジを活用できる環境整備の方法について説明します。
octocov
を使用してテストカバレッジをPull Requestにレポートする
まずアプリケーションに以下の.octocov.yml
ファイルを用意します。
coverage: paths: - coverage/.resultset.json acceptable: 60% codeToTestRatio: acceptable: 1:1.2 code: - "app/**/*.rb" - "lib/**/*.rb" test: - "spec/**/*_spec.rb" diff: datastores: - artifact://${GITHUB_REPOSITORY} comment: if: is_pull_request && !is_default_branch hideFooterLink: false deletePrevious: true report: if: is_default_branch datastores: - artifact://${GITHUB_REPOSITORY}
.octocov.yml
ファイルは以下のような設定になっています。
- テストカバレッジは60%未満の時、exist status 1で終了する
- コードとテストの割合が
1 : 1.2
未満の時、exist status 1で終了する - デフォルトブランチとのテストカバレッジの差分を表示する
- コメントはデフォルトブランチ以外のPull Requestの場合に行う
次にTestワークフローに以下のreport-coverage
ジョブを追加します。
report-coverage: needs: test runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read pull-requests: write if: ${{ success() || failure() }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Download all test results uses: actions/download-artifact@v4 with: pattern: test-result-* path: ${{ runner.temp }}/test-results - name: Aggregate all coverage resultsets run: bundle exec rails runner "require 'simplecov'; SimpleCov.collate(Dir['${{ runner.temp }}/test-results/**/coverage/.resultset*.json'], 'rails')" - name: Report coverage by octocov uses: k1LoW/octocov-action@v1 - name: Upload coverage uses: actions/upload-artifact@v4 with: name: coverage path: coverage include-hidden-files: true
report-coverage
ジョブはテスト結果をダウンロードし、複数test
ジョブで生成されたカバレッジデータをSimpleCov.collate
で集計します。集計したカバレッジデータをk1LoW/octocov-action@v1
に与えることでPull Requestにテストカバレッジをレポートすることが出来ます。
しかし、この方法ではテストカバレッジをレポートできないケースがあります。Testワークフローはpush
をトリガーに実行されます。つまり、このTestワークフローのreport-coverage
ジョブが実行している時点でPull Requestが存在しない場合、テストカバレッジをレポートできません。そこで、pull_request
をトリガーに実行するReport coverageワークフロー(.github/workflows/report-coverage.yml
)を用意します。
name: Report coverage on: pull_request: types: opened concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: TEST_WORKFLOW_FILE_NAME: test.yml COVERAGE_ARTIFACT_NAME: coverage defaults: run: shell: bash jobs: report-coverage: runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read pull-requests: write actions: read env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v4 - name: Get run_id of test workflow id: get-run-id run: | gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/${GITHUB_REPOSITORY}/actions/workflows/${TEST_WORKFLOW_FILE_NAME}/runs?head_sha=${{ github.event.pull_request.head.sha }}&status=completed" | \ jq '.workflow_runs | sort_by(.id)[] | select(.conclusion == "success" or .conclusion == "failure") | .id' | \ jq -sr '"test-run-id=\(last)"' >> $GITHUB_OUTPUT - name: Download coverage if: ${{ steps.get-run-id.outputs.test-run-id != 'null' }} uses: actions/download-artifact@v4 with: name: ${{ env.COVERAGE_ARTIFACT_NAME }} path: coverage run-id: ${{ steps.get-run-id.outputs.test-run-id }} github-token: ${{ github.token }} - name: Coverage Report by octocov if: ${{ hashFiles('coverage/.resultset.json') }} uses: k1LoW/octocov-action@v1
Report coverageワークフローのreport-coverage
ジョブはTestワークフローのreport-coverage
ジョブで保存したcoverageアーティファクトを利用します。ただし、Report coverageワークフローがトリガーされた時点でTestワークフローのcoverageアーティファクトが存在しない場合は処理をスキップします。
このように、push
とpull_request
の両方のトリガーを利用することで、テストカバレッジをPull Requestにレポートできました。
テストカバレッジの結果をGitHub Pagesでホスティングする
続いて、テストカバレッジの結果をGitHub Pagesでホスティングする方法を説明します。
Testワークフローに以下のbuild-github-pages
ジョブとdeploy-github-pages
ジョブを追加します。どちらのジョブもデフォルトブランチの場合のみ実行します。
build-github-pages: needs: report-coverage runs-on: ubuntu-latest timeout-minutes: 5 if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} steps: - name: Download coverage uses: actions/download-artifact@v4 with: name: coverage path: coverage - name: Upload pages artifact uses: actions/upload-pages-artifact@v3 with: path: coverage deploy-github-pages: needs: build-github-pages runs-on: ubuntu-latest timeout-minutes: 5 if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4
build-github-pages
ジョブはactions/upload-pages-artifact@v3
アクションでcoverageをアーカイブします。そして、アーカイブしたファイルをgithub-pages
というアーティファクト名でアップロードします。
deploy-github-pages
ジョブはactions/deploy-pages@v4
アクションでgithub-pages
アーティファクトをGitHub Pagesにデプロイします。
これでテストカバレッジを活用できる環境整備ができました。
今後の展望
以上の取り組みによって、テストカバレッジを活用できる環境を整えつつ、CircleCIからGitHub Actionsへ移行出来ました。
しかし、まだ改善余地があり、以下のような課題があります。
Flaky testを検出する機能がない
CircleCIにはFlaky testを検出できるテスト インサイト機能があります。GitHub ActionsにはFlaky testを検出できる機能がないため、別途用意する必要があります。
追加・変更した実装コードのソースコードカバレッジ
テストカバレッジの結果をGitHub Pagesでホスティングすることで、デフォルトブランチのテストカバレッジをソースファイル単位で確認できるようになりました。しかし、Pull Requestのテストカバレッジを確認するにはアーティファクトからダウンロードする必要があるため、Pull Request上で確認するには仕組みを考える必要があります。
今後は、これらの課題を解決する方法を検討したいと思います。
まとめ
本記事ではRailsアプリケーションの自動テスト環境をCircleCIからGitHub Actionsへ移行した際に発生した課題とその解決方法に関して紹介しました。Railsアプリケーションの自動テスト環境をCircleCIからGitHub Actionsへ移行を検討している方がいれば、ぜひ参考にしてみてください。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。