日常のテストは手元で回し、gh-signoff を必須チェックにする

業務システム開発案件で、通常の feature PR の lint とテストを GitHub Actions から手元での実行に戻しました。 GitHub Actions では、ステージングと本番のデプロイ、リリース PR と Renovate PR の検証を行います。 手元の検証に通ったコミットには gh signoff で成功ステータスを付け、このステータスを GitHub の必須チェックにしています。

この運用を検討したきっかけは、David Heinemeier Hansson の記事「We’re moving continuous integration back to developer machines」です。

この記事では、この成功ステータスを署名と呼びます。 GPG や SSH を使うコミット署名とは別のものです。

署名には、Basecamp が公開している gh-signoff を使いました。 gh-signoff は、開発者が検証済みだと申告したコミットに成功ステータスを付けます。 GitHub が手元のテスト結果を確認する仕組みではないため、署名する人と手順を信頼できる範囲で使います。

実行の切り分け

イベントごとの実行内容は次のとおりです。

イベント lint / test デプロイ 通過条件
feature PR(develop 宛) 手元で実行 なし bin/ci が付ける署名
push to develop スキップ staging マージ前の署名
Renovate PR(develop 宛) GitHub Actions で実行 なし CI の成功を確認した人が付ける署名
Release PR(develop → main) GitHub Actions で実行 なし lint と test
push to main GitHub Actions で実行 production deploy-productionneeds: test

通常の feature PR では、lint と test のジョブを if でスキップします。 ワークフローは起動するため、Checks には skipped と表示されます。 develop への push では、ステージングへのデプロイだけを実行します。

署名するタイミング

署名は、最新の develop を取り込んでから付けます。 署名後に develop が更新された場合も、取り込み直して lint とテストを実行します。

この手順を守るため、GitHub では Require branches to be up to date before merging を有効にします。 または、最新の develop との統合結果を検証する merge queue を使います。 gh signoff install だけではこの条件が有効にならないため、GitHub 側で別に設定が必要です。

ブランチ単体で問題がなくても、develop を取り込むと失敗することがあります。 実際に、二つの変更が同じモデルへ行を追加し、統合後のクラスが RuboCop の上限である 300 行を超えた例がありました。 テストでも、一方の変更が前提条件を変えると、もう一方のコードが統合後に失敗することがあります。

署名の対象は、実際にマージするコミットと同じでなければなりません。 rebase やマージでコミットが変わったら、lint とテストを実行し直してから署名します。

本番デプロイの検証経路

deploy-production には needs: test を付けています。

deploy-production:
  needs: test
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'

push to main では test も実行します。 テストが失敗すると、deploy-production はスキップされます。 そのため、GitHub Actions から本番へデプロイする経路では、マージ前の署名とマージ後のテストを両方確認できます。

ステージングへのデプロイには needs: test を付けていません。 この案件では、手元と GitHub Actions の実行環境をそろえたうえで、ステージングへの反映時間を優先しました。 環境差を検出する必要がある案件では、ステージングの前にも GitHub Actions でテストするほうが安全です。

Renovate PR の検証経路

Renovate PR では、例外として GitHub Actions の lint と test を実行します。 依存関係の更新は実行環境の差による問題が起こりやすいためです。

この運用では、Renovate に署名を付ける権限を与えていません。 CI が成功したら、人が結果と対象コミットを確認し、そのコミットへ署名してからマージします。 CI の成功だけでは、必須チェックにしている署名の代わりにはなりません。

bin/ci の事前検査

bin/ci は RuboCop と parallel:spec を実行し、両方に通った場合だけ gh signoff を実行します。 その前に、実行環境の不足を調べる事前検査を三つ置きました。 事前検査は問題を検知しますが、修復はしません。

  • gh-signoff 拡張の有無:テスト後の署名で初めて失敗することを防ぎます
  • アセットのビルド結果app/assets/builds/application.css がなく、system spec や request spec がまとめて失敗することを防ぎます
  • テスト DB の初期データ:seed で投入するマスターデータがなく、多数の spec が失敗することを防ぎます

テスト後に設定不足が分かると、それまでの実行時間が無駄になります。 失敗件数も多いため、セットアップではなくコードの問題だと誤解しやすくなります。 最初に数秒で検査すれば、この遠回りを避けられます。

修復を自動化しないのは、開発者の作業を勝手に変えないためです。 yarn builddb:seed_fu を自動実行すると時間がかかり、意図して空にした DB にデータを投入することもあります。 問題を検知した場合は、必要なコマンドを表示して終了します。

Error: test DB が未 seed です。次を実行してください:
  RAILS_ENV=test bundle exec rake db:seed_fu
  RAILS_ENV=test bundle exec rake 'parallel:rake[db:seed_fu]'  # 並列 DB も seed

parallel:spec 実行後のテスト DB

parallel:spec は、複製元になるテスト DB のテーブルを空にします。 このとき、seed で投入したマスターデータも削除されます。 直後に単一プロセスで rspec を実行する場合は、先に seed を入れ直します。

paths-ignore と本番デプロイ

本番デプロイを push to main に結び付ける場合、そのワークフローでは paths-ignore を使いません。 Markdown などを除外すると、それらだけを変更した push ではワークフロー自体が起動しないためです。 ワークフローが起動しなければ、deploy-production も実行されません。

テストを省略したい場合は、デプロイとテストのワークフローを分けます。 本番のデプロイ経路については、Fly.io への移行作業とカットオーバーで紹介しています。

この運用に向く条件

手元のテストが現実的な時間で終わることが前提です。 並列実行で数分なら署名を待つ負担は小さいですが、1 時間かかるなら GitHub Actions に任せるほうが効率的です。

手元と GitHub Actions の実行環境をそろえられることも必要です。 依存バージョンや環境変数に差があると、手元の成功だけでは十分な確認になりません。

署名は開発者による申告です。 参加者が増え、同じ手順を守ることが難しくなった場合は、必須チェックを GitHub Actions の実行に戻します。

テスト時間、実行環境、署名への信頼のどれかが変わったら、運用を見直します。

参考リンク

CONTACT

フレイズに相談してみる

開発からデザインまで、構想段階からでも大丈夫です。
まずはお気軽にご相談ください。

[email protected]