- 公開日
- 最終更新日
pre-commit が重くなったのでコミット前チェックを整理した話
この記事を共有する
目次
はじめに
こんにちは!パーソル&サーバーワークスの野間です。
Python を勉強しながらサーバーレスのプロジェクトを書いています。
ミスを早い段階で見つけたくて、リンターやテストをコミット前に実行する仕組みを少しずつ足していきました。
ところが増やすほどコミットが遅くなり、チェックを飛ばしたい気持ちが出てくるようになりました。
実行時間で置き場所を決める
いろいろ試した結果、私はチェックの種類ではなく実行時間で置き場所を決めるようにしました。
早く知りたい指摘ほど手前に置きたくなります。
ただ、手前に置けるのは速いものだけです。
遅いチェックを手前に置くと、結局そのチェック自体を外したくなってしまいます。
ローカルで止めるもの
ローカルのチェックは pre-commit というツールで管理しています。
コミット時とプッシュ時で実行するものを分けられるので、ここで 2 段に分けました。
コミット時に走らせるのは、数秒で終わるものだけです。
- ruff によるリンターとフォーマットの確認
- pyright による型チェック
- ユニットテストとカバレッジの下限チェック
- cfn-lint と sam validate によるテンプレートの検証
- 認証情報の混入チェック
ユニットテストをコミット時に置けるのは、今のプロジェクト規模では全件実行しても数秒で終わるからです。
テストが増えて待ち時間が気になり始めたら、プッシュ時や CI に移すことを検討します。
設定は .pre-commit-config.yaml に書きます。
簡略化すると以下のような形です。
repos:
- repo: local
hooks:
- id: ruff-lint
name: ruff (lint)
entry: ruff check src tests
language: system
pass_filenames: false
types: [python]
- id: pytest
name: pytest (unit)
entry: pytest -q
language: system
pass_filenames: false
types: [python]
- id: cfn-lint
name: cfn-lint (CloudFormation)
entry: cfn-lint template.yaml
language: system
pass_filenames: false
files: \.(yaml|yml)$
types: [python] を指定した hook は、ステージングされたファイルに Python ファイルが含まれるときだけ起動します。
cfn-lint のようにテンプレートを対象にする hook は files で YAML の正規表現パターンを書いて、対象ファイルの変更時に起動するようにしています。
いずれも手元のファイルを読むだけで完結するので、コミットの体感を損ないません。
テンプレートの検証をここに入れているのは、書き間違いをデプロイまで持ち越さないためです。
プッシュ時に回したのは、時間がかかるものです。
- sam build
- 実際の AWS 環境を使う統合テスト
同じ .pre-commit-config.yaml の中で、stages に pre-push を指定すると、プッシュ時にだけ実行されます。
- id: pre-push-checks
name: sam build + integration test
entry: bash scripts/pre-push.sh
language: system
pass_filenames: false
stages: [pre-push]
always_run: true
この指定を入れるかどうかだけで置き場所を変えられるので、あとから移すのも簡単です。
sam build は関数の数だけビルドが走るので、コミットのたびに実行すると待たされます。
一方、プッシュは 1 日に何度もするものではないので、ここなら許容できます。
環境が無い人のプッシュを止めない
私が触っているリポジトリの統合テストは、実際の AWS 環境にデプロイした関数を呼び出す作りになっています。
そのため実行には AWS の認証情報が必要です。
これをプッシュ時のチェックにそのまま入れると、環境を用意していない人がプッシュできなくなります。
統合テストの作り方によっては認証情報が不要な場合もあるので、あくまで私の環境での話です。
そこで、環境変数が設定されているときだけ統合テストを実行するようにしました。
PROFILE="${EXAMPLE_AWS_PROFILE:-}"
if [ -z "$PROFILE" ]; then
echo "統合テストをスキップしました"
exit 0
fi
python tests/integration_test.py --profile "$PROFILE"
環境がある人は自動で実行され、無い人は素通りします。
チェックを増やすときは、動かせない人が詰まらないようにすることも一緒に考える必要がありました。
CI で止めるもの
ローカルのチェックは、意図的に回避できます。
そのため CI でも同じチェックをもう一度かけています。
今回は AWS CodeBuild を使っていて、ビルドの前にユニットテストとカバレッジの下限チェックを実行しています。
カバレッジとは、テストがコードのどれくらいの割合を通ったかを示す数値です。
下限チェックは「この割合を下回ったら失敗にする」という設定で、テストを書かずに機能だけ足すことを防ぐために入れています。
# buildspec.yml(抜粋)
pre_build:
commands:
- pytest tests/unit -q --cov=layers/common/python --cov-fail-under=85
build:
commands:
- sam build --parallel
- sam deploy --no-confirm-changeset --no-fail-on-empty-changeset
ローカルと同じ下限値を CI にも書いているので、片方だけ通ることはありません。
同じ基準を 2 か所に置くのは重複に見えますが、ローカルは回避できるという前提に立つと必要な重複です。
--cov=layers/common/python としているのは、このプロジェクトでは共通ロジックをレイヤーに集約しているためです。
まずはここのカバレッジを優先的に確保し、対象範囲は段階的に広げていく予定です。
カバレッジの下限は最初から高くせず、実際の値を見ながら段階的に上げました。
最初から高い値を入れるとテストを書くまでコミットできなくなるので、現状より少し低い値から始めるのがおすすめです。
まとめ
今回はサーバーレス開発のチェックを、ローカルと CI に振り分けました。
やってみて分かったのは、チェックは足せば良いというものではない、ということでした。
遅いチェックを手前に置くと、そのチェック自体を外したくなります。
実行時間で置き場所を決めるようにしてから、飛ばしたい気持ちが出てこなくなりました。
コミット前のチェックが増えて重くなってきた方の参考になれば幸いです。
この記事は私が書きました
野間 太一
記事一覧猫とCloudFormationが好きです。