開発記事公開日
AIが書いたプルリクエストの検査が、3秒で終わっていた
この記事で分かること
AIが書いた変更を受け止めるはずのCIが、9月20日から毎回数秒で失敗していました。落ちていたのは検査ではなく、検査が始まる前の段階です。赤のまま7件をマージした経緯と、別のアプリで同じような停止の間に壊れた変更が入っていた件を記録します

目次 読みたいところから
BenriWorksでは、コードの大半をClaude CodeやCodexに書かせ、プルリクエストで受け取っています。受け取った変更は、GitHub上のCI(型検査からビルドまでを自動で走らせる仕組み)を通してからマージする決まりで、このブログのAI向けの執筆ルールにも「全て成功しなければマージしない」と書いてあります。ところが9月20日から、そのCIが毎回数秒で失敗するようになりました。落ちていたのは検査ではなく、検査が始まる前の段階です。そのまま7件のプルリクエストをマージした経緯と、別のアプリでは同じような停止の間に壊れた変更が入っていた件を記録します。
CIがAIの変更に対して担っているもの
このブログのCIは、プルリクエストを出したときとmainへ反映したときに、6つの検査を順に実行します。TypeScriptの型検査、ESLint、単体テスト、記事のfrontmatterの検証、内部リンクの検査、本番と同じ設定でのビルドです。1回の実行は1分前後で終わります。
同じ6つの検査は手元でも実行でき、Claude Codeは作業の終わりにこれを流して結果を報告してきます。それでもCIを別に持つのは、書いた本人ではない場所で、同じ検査を同じ手順でやり直すためです。AIの「通りました」は報告であって、検査結果そのものではありません。CIの緑のチェックは、その報告を書いた本人以外が確かめた印になります。
一方で、本番への公開はCIとは別の経路です。mainへマージするとVercelが自動でビルドして公開するので、CIが通っていなくても、マージさえすれば公開は進みます。流れの全体は「Codex と GitHub と Vercel で、Webアプリを公開するまでの流れ」に書きました。
1分かかっていた検査が、数秒で終わる
9月15日までは、CIは毎回成功していました。直近の13回は48秒から79秒で終わっています。初めて失敗したのは9月20日、広告のコードを入れるプルリクエストでした。以降、9月24日までの13回はすべて失敗で、12回は3秒から6秒、残る1回も40秒で終わっています。
失敗した実行を開いても、検査の結果は1つもありません。CIの1回分の実行単位(ジョブ)には、それを実際に動かすマシン(ランナー)が割り当てられておらず、工程の一覧は空で、ログも残っていませんでした。9月24日に1回だけ再実行しましたが、同じく5秒で失敗しています。
赤い×が示しているもの
プルリクエストの画面では、検査の結果が緑のチェックか赤い×で示されます。赤い×になるのは、検査が実行されて何かを見つけた場合だけではありません。検査がそもそも実行されなかった場合も、同じ×になります。前者なら、落ちた工程とログが残り、何が壊れたかを読めます。後者には、読むものがありません。
見分ける手がかりは、実行時間と工程の数でした。9月15日に成功した実行を開くと、依存関係のインストールから本番ビルドまでの工程が並び、ビルドだけで21秒かかっています。9月24日の失敗には工程が1つもなく、全体で3秒です。検査の中身に1分かかる以上、3秒の失敗が検査の結果であるはずはありません。
赤のまま、7件をマージした
9月20日の広告のプルリクエストは、赤のままマージされました。9月24日には、講座のページやプライバシーポリシーの修正を入れる作業で、さらに6件をマージしています(うち3件は、同じ変更を本番のブランチへ移すためのものです)。7件はいずれもClaude Codeが書いた変更で、24日の6件は、この記事を書いたのと同じ作業の中で出したものです。
24日のマージの前に、Claude Codeは失敗の中身を調べ、ランナーが割り当てられていないこと、9月20日以降のすべての実行で同じ症状が出ていること、再実行しても変わらないことを、プルリクエストのコメントにまとめました。そのうえでCIが実行するはずの検査を手元で流し、すべて通ったことを確かめて、「このPRの変更が原因ではないと判断し、手元の検証結果をもってマージします」と書いています。判断の材料としては、筋が通っていました。
引っかかるのは、その材料を誰が用意したかです。変更を書いたのも、失敗を調べたのも、手元で検査を流したのも、同じClaude Codeでした。CIを別に置いた理由は、書いた本人ではない場所で検査をやり直すことだったはずです。CIが止まると、その役目は何の知らせもなく、書いた本人の報告に戻ります。手元の検査は実際に走っていましたが、運営者の側から見えていたのは、通ったという報告だけでした。
停止の間にmainへ入った変更
ブログの7件は、少なくとも手元の検査を通してからマージしています。同じような停止の間に、壊れた変更がそのまま入った例が、別のアプリにありました。
複線図道場(第二種電気工事士の技能試験に向けた練習アプリ)のリポジトリでは、8月11日から21日まで、CIのすべてのジョブが数秒で失敗していました。その間の8月14日に、配線問題を10問追加する変更がmainへ入っています。この変更から、ブラウザで画面を操作して動作を確かめるE2Eテストが、ChromiumとWebKitの両方で落ちるようになりました。
気づいたのは9月2日です。計測タグを入れるプルリクエストでE2Eテストが赤くなり、原因を追うと、計測タグの変更を除いたコードでも同じ失敗が再現しました。壊れていたのはプルリクエストではなく、8月14日からのmainでした。Claude Codeに、最後にテストが通っていた8月8日からのコミットを二分探索させると、壊れた地点は8月14日の変更で、中身は古くなったテスト7件と、製品側の不具合2件でした(接続箱の設定の識別子が変わったのに追従していなかった箇所と、狭い画面で採点パネルがナビゲーションを覆っていた箇所)。経緯は「Claude Fable 5.1 で13リポジトリにGA4を入れた記録」にも書いています。
製品側の不具合のうち接続箱の件は、公開中の10問すべてに出ていました。「接続ボックスへ」のボタンを押しても、何も起きなくなっていたのです。直したのは9月4日で、BenriWorksのアプリはマージすると公開される流れなので、それまでの3週間ほど、この状態が本番に出ていたはずです。CIの停止は8月21日に終わっていましたが、その日を最後に9月3日までmainへの変更はなく、検査が走る機会そのものがありませんでした。CIが戻った日にmainで検査を走らせ直していれば、その日に見つかっていたはずです。
記録用に使っている別のリポジトリでも、8月27日に同じ止まり方を確認しています。ジョブの工程が空で、ランナーの番号が0でした。そちらでは、CIで自動生成していた索引を、同じ生成スクリプトを手元で実行してコミットする形に切り替えています。
気づく手がかりと、止まっている間の手当て
振り返ると、止まっていることを示す手がかりは、どの件でも最初の実行から出ていました。
- 1分前後かかる検査が、数秒で失敗している
- 失敗したジョブに、工程とログが1つもない
- プルリクエストと関係のないmainへの反映でも、同じように失敗している
ブログで今回とった手当ては2つです。1つは、CIが実行するはずの検査を手元で流し、結果をプルリクエストに残すことでした。これで、少なくとも何を確かめたかは後から読めます。もう1つは、CIが戻ったらmainで1回走らせ直すと、同じコメントに書いておくことです。複線図道場で12日かかった発見を、戻った当日に済ませるためです。
残っている選択もあります。CIの成功をマージの必須条件にしておけば、止まった時点でマージもできなくなり、見過ごしようがなくなります。その代わり、CIの側の不具合で開発全体が止まります。このブログのリポジトリは、今回、赤のままでもマージできる状態でした。どちらを取るかは、まだ決めていません。

止まった原因
ランナーが割り当てられる前に失敗しているため、原因を示すログがありません。プルリクエストのコメントでは、確認先としてGitHub Actionsの支払いと利用上限の設定、リポジトリのActionsの有効化状態を挙げました。運営者の見立ては、支払い(Billing)の設定です。3つのリポジトリで、時期をずらして数秒で失敗する停止が起きていることも、リポジトリの中のコードではなくアカウントの側に原因があることと合っています。ただ、設定の画面を開いて確かめたわけではなく、原因はまだ見立ての段階です。
このブログのCIは、この記事を公開する時点でも、数秒で失敗し続けています。戻ったときに最初に確かめるのは、止まっていた間にマージした変更が、書いた本人ではない場所でも本当に通るかどうかです。

