調査エージェントから「ライセンス API の PHP がメッセージを日本語固定で返している」というレポートを受け取って、修正に着手しようとしたら違和感がありました。本番サーバーで実際に走っているファイルを開いてみると、ローカル git の最新コミットと中身が違う。それも、ローカルより本番の方が 新しい機能を持っていた、というやや珍しい状況です。
掘ってみると数ヶ月前、別のユーザー報告に対応するために本番ファイルに直接 hot-patch を当てて、その変更が ローカル git に取り込まれないまま忘れられていたことが分かりました。今回はこの「ローカル repo と本番の乖離」をどう検出して、どう安全に合流させたかをまとめておきます。
ありえる経路で、本番の改善を巻き戻すリグレッションが発生する
このまま「ローカル git を起点に修正を書いて、本番にアップロード」する経路だと、何が起きるか。本番側にあって ローカルにない改善が、上書きアップロードで消える ことになります。
具体的には、本番には半年前から「早期利用特典」機能の言語対応(USD で買った英語ユーザーには client_name = 'Early Bird Bonus'、JPY 課金には '早期利用特典' を入れる分岐)が動いていました。これがローカル git には存在しません。普通に PR を作って merge → 本番アップロードすると、早期利用特典の言語対応が静かに消滅して、英語ユーザーへの特典表示が日本語に戻る経路ができてしまいます。
最初にこの違和感に気づけたのは半分偶然でした。修正対象のファイルを開いた時、「あれ、こんなロジックあったっけ」と思って git blame したら、そのコードが git のどこにも存在しなかった。「これは何かおかしい」となって調査が始まりました。
2 段階のロールフォワード — まず本番を真実とする
採用したのは 2 段階の合流戦略です。
第 1 段(ロールフォワード同期): 本番ファイルをそのままローカル git に取り込みます。差分を「ローカル → 本番」ではなく 「本番 → ローカル」方向で適用する。これでローカル git の最新コミットが、本番の現状と一致した状態になります。
# 本番ファイルを引き取って ローカル git に取り込む
scp -i ~/.ssh/key layer2024@host:wpmm.jp/public_html/license/api/register_free.php \
/tmp/register_free_prod.php
# まず MD5 で完全一致を確認(後でアップロード時の検証に使う)
md5 /tmp/register_free_prod.php
# → 94b9c5a7...
# 本番版をローカルパスにコピー → git commit
cp /tmp/register_free_prod.php server/wpmm-license/api/register_free.php
git add server/wpmm-license/api/register_free.php
git commit -m "rollforward sync: pull v1.5.9 hotpatch from production"
ここで重要なのは、機能の差分を理解する前に、まず本番状態を git に取り込むこと。「いったん本番を真実とみなす」コミットを 1 つ挟むだけで、その後の差分作業がずっと安全になります。
第 2 段(足し算): 第 1 段でローカルが本番と揃ったところで、新機能だけを「足し算」します。既存の早期利用特典セクションには一切手を入れず、新しく必要な行だけを追加。
# 第 2 段の commit を作る
# - 既存の早期利用特典セクション(71-111 行)は完全に温存
# - 新機能(i18n フォールバック・lang クエリ追加)だけを足す
git diff HEAD~1 -- server/wpmm-license/api/register_free.php
# diff の出力で「削除行」がゼロであることを目視確認
「削除行ゼロ」は機械的に検証できるシンプルな安全基準です。本番の動作を変えないことを diff の構造で保証できます。
MD5 で温存を担保する
「bit-for-bit 不変」を主張するなら、定量的な検証も必要です。アップロード前後で、温存対象セクションが厳密に同一であることを md5 ハッシュで確認しました。
# 温存対象セクション(早期利用特典・行 71-111)だけを抽出して md5
sed -n '71,111p' server/wpmm-license/api/register_free.php | md5
# → 期待: 元の本番ファイルの同じ範囲と完全一致
# アップロード後にもう一度確認
ssh user@host 'sed -n "71,111p" wpmm.jp/.../register_free.php | md5'
INSERT INTO licenses の SQL 文、bind 順、$client_name_label = $is_en ? 'Early Bird Bonus' : '早期利用特典' の三項演算子、try/catch、期間判定 — すべて temporally に変更されていないことを構造的に確認できます。
学び — 「自分の git が正史」という前提は危険
このラウンドから取り出せる原則は次の 3 つでした。
- 本番ファイルを起点に diff を取る習慣を組み込む — ローカル git は「正史」とは限らない。手動 hot-patch 文化があるプロジェクトでは特に、本番にローカルにない変更が存在する可能性を常に念頭に置く。修正に着手する前に
scpで本番ファイルを引いて、ローカルとの diff を必ず取る - 乖離を見つけたら 2 段階で合流する — 第 1 段「ロールフォワード同期」で本番を真実とする・第 2 段「足し算」で新機能を載せる。2 段階に分けることで「本番の改善を巻き戻すリグレッション」を構造的に避けられる
- 温存対象は md5 で定量検証する — 「bit-for-bit 不変です」と書面で主張するだけでなく、
sed -nで温存セクションを抽出して md5 を取り、アップロード前後で一致を確認する。diffの「削除行ゼロ」目視チェックと組み合わせれば、二重の保険になる
今回の最大の発見は 「ローカル repo が本番と乖離している可能性を、常に疑う」 という習慣の重要性でした。手動デプロイや hot-patch 文化が残っているプロジェクトでは、これは特に避けがたい現実です。git の履歴を 最新の真実とみなす癖 が付いていると、今回のような「本番の機能を巻き戻す」事故を一発で踏みます。
修正に着手する前の 1 分の scp + diff が、その後の数時間の事故対応を救ってくれることはよくあります。