blog
走らせない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 が見ている。