pre-push フックでRustプロジェクトの「CI落ち→修正→再push」の無限ループがゼロになった話
はじめに
Rust を書いている方なら、一度はこんな経験があるのではないでしょうか。
- コードを書いて
git pushする - CI が走る
-
cargo clippyで警告が出て CI が落ちる 😇 - ローカルで修正して再 push
- 今度は
cargo fmtのチェックに引っかかる - また修正して再 push……
この 「push → CI落ち → 修正 → 再push」の無限ループ、めちゃくちゃストレスですよね。
しかも CI の実行には数分かかるので、待ち時間がどんどん積み重なっていきます。
今回は、この問題を pre-push フックで解決した方法を紹介します!
この記事を読むメリット
-
git pushする前に fmt / check / clippy / test を自動で回せるようになる - CI と同じチェックをローカルで実行できるので、CI落ちがほぼゼロになる
-
make check一発で手動チェックもできる - コピペですぐ使えるスクリプトを公開しています
解決策: pre-push フックで push 前にローカルチェック
Git には フック(hooks) という仕組みがあり、commit や push などのタイミングで任意のスクリプトを実行できます。
今回使うのは pre-push フック。git push を実行した瞬間、リモートにデータが送られる前にスクリプトが走ります。スクリプトが失敗(exit 1)すると push が中止されるので、壊れたコードがリモートに行くことがありません。
実装
チェック内容
pre-push フックでは以下の 4ステップ を順番に実行します:
| # | コマンド | 内容 |
|---|---|---|
| 1 | cargo fmt --all -- --check |
コードフォーマットのチェック |
| 2 | RUSTFLAGS="-Dwarnings" cargo check --all-targets |
コンパイルチェック(警告もエラー扱い) |
| 3 | cargo clippy --all-targets -- -D warnings |
lint チェック(警告もエラー扱い) |
| 4 | cargo test --all-targets |
テスト実行 |
ポイントは RUSTFLAGS="-Dwarnings" と -D warnings です。通常は警告で済むものも全てエラー扱いにすることで、CI と同じ厳しさでチェックできます。
スクリプト全文
githooks/pre-push.sh(クリックで展開)
#!/usr/bin/env bash
set -uo pipefail
# ── Colors ───────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
RESET='\033[0m'
# ── Options ───────────────────────────────────────────────
NO_HOOK=false
FIX=false
for arg in "$@"; do
case "$arg" in
--no-hook) NO_HOOK=true ;;
--fix) FIX=true ;;
esac
done
# ── Result tracking ───────────────────────────────────────
CHECK_NAMES=()
CHECK_RESULTS=()
FAIL_COUNT=0
CURRENT_STEP=""
# ── Helpers ──────────────────────────────────────────────
step() {
CURRENT_STEP="$1"
echo -e "${YELLOW}${BOLD}▶ $1${RESET}"
}
pass() {
echo -e "${GREEN} ✔ $1${RESET}"
CHECK_NAMES+=("$CURRENT_STEP")
CHECK_RESULTS+=("pass")
}
fail() {
echo -e "${RED} ✖ $1${RESET}"
CHECK_NAMES+=("$CURRENT_STEP")
CHECK_RESULTS+=("fail")
FAIL_COUNT=$((FAIL_COUNT + 1))
}
# ── Pre-push checks (mirrors CI) ────────────────────────
echo ""
if $NO_HOOK; then
echo -e "${BOLD}Running local checks${RESET}"
else
echo -e "${BOLD}Running pre-push checks${RESET}"
fi
echo ""
# 1. Format
if $FIX; then
step "cargo fmt (auto-fix)"
if cargo fmt --all; then
pass "Format applied"
else
fail "cargo fmt failed."
fi
else
step "cargo fmt --check"
if cargo fmt --all -- --check; then
pass "Format OK"
else
fail "Format check failed. Run 'cargo fmt' to fix."
fi
fi
# 2. Check (compile)
step "cargo check"
if RUSTFLAGS="-Dwarnings" cargo check --all-targets; then
pass "Check OK"
else
fail "cargo check failed."
fi
# 3. Clippy (lint)
step "cargo clippy"
if cargo clippy --all-targets -- -D warnings; then
pass "Clippy OK"
else
fail "Clippy found issues."
fi
# 4. Test
step "cargo test"
if cargo test --all-targets; then
pass "Tests OK"
else
fail "Tests failed."
fi
# ── Summary ──────────────────────────────────────────────
echo ""
if [ "$FAIL_COUNT" -eq 0 ]; then
echo -e "${GREEN}${BOLD}Summary: All checks passed${RESET}"
else
echo -e "${RED}${BOLD}Summary: ${FAIL_COUNT} check(s) failed${RESET}"
fi
echo ""
for i in "${!CHECK_NAMES[@]}"; do
if [ "${CHECK_RESULTS[$i]}" = "pass" ]; then
echo -e " ${CHECK_NAMES[$i]} : ${GREEN}✔ done${RESET}"
else
echo -e " ${CHECK_NAMES[$i]} : ${RED}✖ error${RESET}"
fi
done
echo ""
if [ "$FAIL_COUNT" -gt 0 ]; then
if $NO_HOOK; then
echo -e "${RED}${BOLD}Check failed.${RESET}"
else
echo -e "${RED}${BOLD}Push aborted.${RESET}"
fi
exit 1
fi
if ! $NO_HOOK; then
echo -e "${GREEN}${BOLD}Ready to push!${RESET}"
echo ""
fi
スクリプトの設計ポイント
いくつか工夫しているポイントを解説します。
1. カラー出力で結果が一目瞭然
各ステップの結果を 緑(✔ pass) と 赤(✖ fail) で表示します。ターミナルで見ると、どこで失敗したかが瞬時にわかります。
2. サマリ表示
全チェックが終わった後に、結果の一覧をまとめて表示します。途中のログに埋もれて「結局どこが失敗したの?」となることがありません。
3. CI ミラー設計
CI で実行しているチェックと完全に同じコマンドを使っています。ローカルで通れば CI でも通る、という安心感が得られます。
4. set -uo pipefail で安全なエラーハンドリング
-
-u: 未定義変数の参照をエラーにする -
-o pipefail: パイプラインの途中で失敗してもエラーを検出する
これにより、スクリプト自体のバグで「チェックが通ったように見えて実は実行されていなかった」という事故を防いでいます。
5. --no-hook / --fix オプション
| オプション | 効果 |
|---|---|
--no-hook |
フックではなくローカルチェックとして実行(メッセージが変わる) |
--fix |
cargo fmt を自動修正モードで実行 |
--no-hook があることで、git push のタイミング以外でも同じチェックを手動で回せます。
使い方
セットアップ
Makefile に以下のターゲットを用意しています:
HOOKS_DIR := $(shell git rev-parse --git-common-dir)/hooks
install-hooks: ## Install git hooks
@mkdir -p $(HOOKS_DIR)
cp githooks/pre-push.sh $(HOOKS_DIR)/pre-push
chmod +x $(HOOKS_DIR)/pre-push
@echo "Installed pre-push hook to $(HOOKS_DIR)/pre-push"
uninstall-hooks: ## Remove git hooks
rm -f $(HOOKS_DIR)/pre-push
@echo "Removed pre-push hook"
check: ## Run pre-push checks
bash githooks/pre-push.sh --no-hook --fix
# フックをインストール(一度だけ)
make install-hooks
# 手動でチェックを実行(fmt は自動修正)
make check
make install-hooks を実行すると、.git/hooks/pre-push にスクリプトがコピーされます。これ以降、git push するたびに自動でチェックが走ります。
「今すぐチェックだけ走らせたい」というときは make check を使います。--no-hook --fix 付きなので、フォーマットの自動修正もやってくれてめちゃくちゃ便利です。
実行イメージ
全部グリーンで表示されるの、気持ちいいですよね!
おわりに
この pre-push フックを導入してから、CI が落ちて再push する回数がほぼゼロになりました。
「push する前にローカルでチェックすればいいじゃん」と思うかもしれませんが、人間なので忘れるし、Cargoはチェックコマンドが多い(cargo check, cargo clippy, cargo test)ので、フックにしておけば自動でチェックが走るからうっかりミスがなくなります。
ちなみに、このフックは Jarvish という Rust 製の AI シェルの開発で使っています。アイアンマンの J.A.R.V.I.S. をターミナルに降臨させたくて作り始めたプロジェクトで、こういった開発体験の改善にもこだわっています。興味があればぜひ覗いてみてください!
スクリプト自体はシンプルなので、どんな Rust プロジェクトにもコピペで導入できます。みなさんもぜひ試してみてください!
最後まで読んでいただきありがとうございました!バグ報告や改善の PR も大歓迎です 🙌

