0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

pre-push フックでRustプロジェクトの「CI落ち→修正→再push」の無限ループがゼロになった話

0
Posted at

pre-push フックでRustプロジェクトの「CI落ち→修正→再push」の無限ループがゼロになった話

はじめに

Rust を書いている方なら、一度はこんな経験があるのではないでしょうか。

  1. コードを書いて git push する
  2. CI が走る
  3. cargo clippy で警告が出て CI が落ちる 😇
  4. ローカルで修正して再 push
  5. 今度は cargo fmt のチェックに引っかかる
  6. また修正して再 push……

この 「push → CI落ち → 修正 → 再push」の無限ループ、めちゃくちゃストレスですよね。

しかも CI の実行には数分かかるので、待ち時間がどんどん積み重なっていきます。

今回は、この問題を pre-push フックで解決した方法を紹介します!

この記事を読むメリット

  • git push する前に fmt / check / clippy / test を自動で回せるようになる
  • CI と同じチェックをローカルで実行できるので、CI落ちがほぼゼロになる
  • make check 一発で手動チェックもできる
  • コピペですぐ使えるスクリプトを公開しています

解決策: pre-push フックで push 前にローカルチェック

Git には フック(hooks) という仕組みがあり、commitpush などのタイミングで任意のスクリプトを実行できます。

今回使うのは 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 付きなので、フォーマットの自動修正もやってくれてめちゃくちゃ便利です。

実行イメージ

スクリーンショット_2026_04_01_21_28.png
(省略)
スクリーンショット_2026_04_01_21_26.png

全部グリーンで表示されるの、気持ちいいですよね!

おわりに

この pre-push フックを導入してから、CI が落ちて再push する回数がほぼゼロになりました。

「push する前にローカルでチェックすればいいじゃん」と思うかもしれませんが、人間なので忘れるし、Cargoはチェックコマンドが多い(cargo check, cargo clippy, cargo test)ので、フックにしておけば自動でチェックが走るからうっかりミスがなくなります。

ちなみに、このフックは Jarvish という Rust 製の AI シェルの開発で使っています。アイアンマンの J.A.R.V.I.S. をターミナルに降臨させたくて作り始めたプロジェクトで、こういった開発体験の改善にもこだわっています。興味があればぜひ覗いてみてください!

スクリプト自体はシンプルなので、どんな Rust プロジェクトにもコピペで導入できます。みなさんもぜひ試してみてください!

最後まで読んでいただきありがとうございました!バグ報告や改善の PR も大歓迎です 🙌

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?