通ったビルドが、何も直していなかった
ビルドが通った。テストも落ちていない。デプロイも成功した。
それでも、直したかったものは1つも直っていませんでした。
変更が丸ごと消えた状態でコミットが成功してしまった話と、それがなぜ「成功」に見えたのか。そして、こういう失敗を見つけるための確認が「動くかどうか」ではなく「入っているかどうか」だという話です。
やろうとしていたこと
2つの修正を、1回の作業でまとめる予定でした。
| 直したいもの | 理由 |
|---|---|
| 記事一覧の取得上限(20→100) | 公開記事がちょうど20本。次の1本から漏れ始める |
| 構造化データの日時をGMTに | 時差の情報が無い時刻を、機械に渡していた |
どちらもGraphQLのクエリを変える作業です。このプロジェクトはクエリから型を自動生成しているので、クエリを変えたら型を作り直す必要があります。作り直しにはCMSへの接続が要るため、手順はこうなりました。
1. ソースを5ファイル書き換える
2. npm run codegen (型を作り直す)
3. npm run build (手元で通ることを確認)
4. commit → push
全部が「成功」した
実行結果は、どこにも問題がありませんでした。
codegenは通りました。ビルドも通りました。コミットもpushも成功し、Vercelのデプロイも緑になりました。本番サイトは変わらず表示されています。
ところが本番の構造化データを見ると、日時が変わっていません。
期待 "datePublished": "2026-08-22T04:20:00Z"
実際 "datePublished": "2026-08-22T13:20:00"
デプロイの遅れかと思い、キャッシュを避けて何度か取り直しました。変わりません。
コミットの中身を見た
ここでようやく、コミットに何が入っているかを確認しました。
frontend/src/gql/gql.ts | 5 +++++
frontend/src/gql/graphql.ts | 8 +++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
2ファイル。しかも両方とも、自動生成されたファイルです。手で書き換えたはずの5ファイルが、1つも入っていません。
作業ディレクトリを見ると、posts(first: 20) のまま戻っていました。書き換えたはずの内容が、どこにも残っていません。
何が起きていたか
git reflog に答えがありました。
d370f37 HEAD@{0}: commit: fix: 記事一覧の上限を100に上げ...
682faf3 HEAD@{1}: reset: moving to HEAD ← これ
682faf3 HEAD@{2}: pull: Fast-forward
pullの直後、コミットの前に reset が挟まっています。git reset --hard は、コミットしていない変更を問答無用で消します。元に戻す手段はありません。
つまり、書き換え → リセットで消滅 → その状態でcodegen → コミット、という順番で進んでいました。
直前の操作を取り消したいときに git reset --hard を使いたくなりますが、これはコミット済みの履歴を戻す道具です。まだコミットしていない作業は、履歴に無いので戻せません。作業を残したまま状態を整えたいなら git stash のほうです。
なぜ「成功」してしまったのか
ここが今回いちばん考えさせられた点です。
変更が消えているのに、なぜビルドが通ったのか。答えは、消えた結果として、全部が整合していたからです。
| クエリ側 | 生成された型 | 結果 | |
|---|---|---|---|
| 意図した状態 | first: 100 | first: 100 | 通る |
| 実際の状態 | first: 20 | first: 20 | 通る |
| もし片方だけ残っていたら | first: 100 | first: 20 | 落ちる |
中途半端に消えていれば、ビルドが落ちて気づけました。きれいに全部消えたので、辻褄が合ってしまったわけです。
ビルドが検証しているのは「矛盾が無いこと」であって、「意図が反映されていること」ではありません。何も変えていないコードは、当然のように矛盾しません。
CIやビルドの成功は、壊れていないことの証明です。直っていることの証明ではありません。この2つを同じものだと思っていると、今回のように「全部が緑なのに何も進んでいない」状態を見逃します。
では、生成されたファイルの差分は何だったのか
もうひとつ不思議だったのが、消えているのに gql.ts だけ差分が出ていたことです。
調べると、生成されていたのは別のクエリの型でした。sitemap用に書いた、こういう文字列です。
const POSTS_FOR_SITEMAP = /* GraphQL */ `
query GetPostsForSitemap {
posts(first: 100) { ... }
}
`;
このファイルは、型生成に依存したくないからこそ素の文字列で書いていました。ところが先頭に付けた /* GraphQL */ という目印を、codegenが「これはクエリだ」と認識して走査対象に含めていました。
結果として、型生成を避けるために作ったファイルが、型生成の対象になっていたわけです。意図とは正反対でした。
この矛盾は、単体では表に出ません。使われない型がひっそり生成されるだけで、何も壊れないからです。別の失敗を調べていて、たまたま見つかりました。目印を外して、対象から除きました。
次から何を見るか
再発を防ぐために増やした確認は、1つだけです。
git status --short
並んでいるファイルの数を数えます。5ファイル触ったなら、5ファイル並んでいるはずです。2つしか無ければ、その時点でおかしい。
やり直したときは、実際にこれで確認しました。7ファイル(手で書いた5つ + 生成された2つ)が並んでいることを見てからコミットしています。
| 1回目 | 2回目 | |
|---|---|---|
| コミットに入ったファイル | 2 | 7 |
| ビルド | 通った | 通った |
| 本番の日時 | 変わらず | Z が付いた |
1行目だけが、両者を区別できていました。
今回の学び
まず、ビルドの成功は「壊れていない」であって「直っている」ではないこと。今回は変更が丸ごと消えたことで整合が取れてしまい、すべての工程が緑になりました。矛盾を検査する仕組みは、何もしていない状態を歓迎します。
次に、結果ではなく、入力を確認すること。「本番の表示が変わったか」で気づくのは最後です。コミットに何ファイル入ったかを見れば、pushする前に止められました。確認は下流より上流に置くほうが安いです。
そして、git reset --hard は取り消しではないこと。コミットしていない作業に対しては、単なる破棄です。状態を整えたいだけなら stash を使う、という区別を持っておくと事故が減ります。
最後に、避けたはずの仕組みに、目印ひとつで巻き込まれること。型生成から独立させるつもりのファイルが、コメント1つで走査対象になっていました。意図は、コードのどこかに具体的な形で表れていないと守られません。
関連記事

推測させるのをやめる
sitemap.xml・robots.txt・構造化データ。バラバラの施策に見えて、やっていることは同じだった。これまで相手に推測させていたことを、こちらから明示的に伝える。そして実装中に見つけた、一覧用のクエリを流用すると21本目から静かに漏れ始めるという落とし穴。
読む体験をつくる — 目次の自動生成から読了状態の保存まで
記事を5本公開したあと、読む側の体験を作り込みました。目次の自動生成、追従する目次、読了状況の表示と保存、スマートフォンのメニュー、読了目安の時間。あわせて、トップページの見た目も見直しています。 この記事は、その実装記 […]
本番公開でつまずいた11のこと — 仕組みから解説
前回、ローカルで作ったサイトを本番公開するまでの記録を書きました。今回はその中で出てきた仕組みを、初めて触る人向けに整理して解説します。 作業記録だけだと「こう直した」で終わってしまい、次に似た場面で応用が効きません。な […]