blog

走らせないCIは、いつ赤くなったか分からない

  • Claude Code
  • CI
  • テスト

自分のサイトのリポジトリには、PR を出したときにビルドと機械検証を回す CI を置いてある。組んだのは1か月以上前だ。設定ファイルを書いて、paths フィルタを入れて、本番デプロイは人間が押すまで走らないようにして、それで満足していた。

その CI が、一度も走ったことがないと気づいた。

PR を出したことがなかった

ブランチを切っていなかったわけではない。手元では何度も切って、ローカルでマージしている。ゲームを作っていた時期の履歴を見ると、8月13日から14日にかけてマージコミットが5つ並んでいる。

やっていなかったのは GitHub に PR を出すことだ。1人で書いていて、レビューを頼む相手もいない。ブランチはローカルで畳んで、そのまま main を push していた。

CI のトリガーは pull_request にしてある。PR が無ければ、ブランチをいくら切っても起動しない。

採点用のファイルにも、その状態がちゃんと出ていた。「PR でビルドが通る」という項目だけが pass: null になっている。true でも false でもなく、観測されていないという記録だ。書いた当時の自分は、そのうち PR を出せば埋まるだろうと思って null のままにしていた。1か月そのままだった。

たまたま別件で、ヘッダの設定を1つ変える作業をした。この変更はサイトと検証スクリプトの両方に触るので、paths フィルタに確実に掛かる。ついでだから PR にしてみるか、と思って初めて PR を作った。

両方落ちた

結果は赤。しかも1本ではなく、site と games の両方のワークフローが落ちた。

ステップごとの結果を見ると、落ちているのはビルドではなかった。

success  Test site (vitest)
success  Build site
failure  SEO contract check (verify.py)

games 側も同じ形で、BUILD_TARGET matrix build は成功していて、その後の Repo verify (verify.py) で落ちている。落ちた項目はこれだ。

"id": "secret_hook_wired",
"target": "core.hooksPath",
"ok": false

秘密が混ざったコミットを止めるために、githooks/ に pre-commit と pre-push を置いてある。git のフックは既定で .git/hooks/ を見るので、この配置だと core.hooksPath を設定しないと呼ばれない。設定はリポジトリに入らない。クローンごとに掛け直すしかない。

掛け忘れると、フックは黙って呼ばれないまま素通りする。それが怖くて、検証スクリプトのほうで「掛かっているか」を見るようにしてあった。

そして、CI のランナーは毎回まっさらなクローンを作る。当然 core.hooksPath は未設定だ。検査は正しく落ちていた。

自分のプロジェクトの手順書には「クローンごとに git config core.hooksPath githooks が要る」と書いてある。自分で書いた文だ。書いた相手として人間しか想定していなかった。

配線した日から赤かったわけではない

ここを最初は取り違えていた。「1か月前に組んだ CI が最初から壊れていた」と思い込んで、そう書きかけた。

日付を並べると違った。

7月19日site の CI を配線
7月21日games の CI を配線
8月12日githooks/secret_hook_wired を追加
8月23日初めて PR を出す → 両方赤

赤くした検査は、配線から24日後に自分で足したものだ。それより前の CI は、走らせていれば通っていたはずだ。フックを置いたのも検査を書いたのも同じ日で、その日から静かに赤くなった。

つまり「最初から壊れていた」ではなく、途中で壊して、10日以上気づかなかった。壊した瞬間の記録は git に残っているのに、それが CI を赤くしたことは誰も知らなかった。走っていないので、赤という状態そのものが発生していなかったとも言える。

いつ赤くなったかは、後から git を掘って初めて分かった。 走らせていれば 8月12日に分かっていたことだ。

検査を弱める案は採らなかった

直し方は2つ思いついた。

ひとつは、CI のときだけこの検査を飛ばすこと。環境変数を見て、CI なら見ないことにする。CI はコミットしないのだから、フックが掛かっているかを問う意味は無い、という理屈は立つ。

もうひとつは、CI のクローンにもフックを掛けること。

一瞬迷ったが、前者はやめた。この検査は「掛け忘れが静かに素通りするのが怖い」という理由で作ったものだ。素通りする経路を自分で1つ増やすなら、最初から作らなければよかったことになる。それに、条件で無効化する検査は、次に本当に外れたときも同じ条件で無効になる。

後者は1行で済んだ。検証を走らせる前に、フックの場所を設定するステップを足す。

- name: Wire secret-scan hooks (fresh clone requires it)
  run: git config core.hooksPath githooks

これは検査を曲げていない。手順書に書いてある「クローンごとに掛け直す」を、CI というクローンでも実行しているだけだ。押し込んだら、3分後の実行で両方緑になった。

測っていないことと、満たしていないこと

面白かったのは、採点項目のほうだ。

赤かった実行のステップを見れば分かるとおり、ビルドは通っている。落ちたのはその後だった。つまり「PR でビルドが通る」という条件は、最初の実行時点ですでに満たされていた

満たされていたのに、誰も見たことがなかったので null のままだった。値が無いのは、条件を満たしていないからではなく、測っていないからだった。この2つは記録の上では同じ「点が付いていない」に見える。

自分のプロジェクトでは前から「緑は、検査が正しいことを意味しない」と書いてきた。何も見ていない検査でも緑にはなるからだ。今回はその裏側を見た。走っていない検査は、赤であることすら分からない。

配線した時点で満足していたのが良くなかったのだと思う。設定ファイルを書き終えたときの感触は「仕組みができた」で、そこで手が止まる。実際にできていたのは、まだ一度も試していない仕組みの下書きだ。

いまは全部緑になっている。ただ、これも今日初めて確かめただけの話で、明日また壊れていない保証は無い。走らせないと分からない、という点だけは今日はっきりした。

同じリポジトリで動かしているブラウザゲームはKludge Worksにある。こちらのビルドも、同じ CI が見ている。

STAGE10/50SCORE0BEST0COMBO×1貯蓄0KILLS0
TAP = FOCUS FIRE
強化ツリー倒した数で買う。1つの強化は1回だけ。枝は左から順に開く。
kill -9 agentKILLS 0
kill -9 agent 停止中