ChatGPTからPRを作り、安全にマージできるようにした

連載「ChatGPTと育てる、自分専用AI基盤」第4回。chatgpt/*ブランチからPull Requestを経てmainへ安全にマージする流れを描いたシンプルな水彩画調サムネイル。 AI

※「ChatGPTと育てる、自分専用AI基盤」第4回です。

前回は、Docker操作や長時間処理をDevelopment MCPから切り離し、Development Runnerへ分けたところまで書きました。

第3回:AIに自分自身を再起動させたら止まった。Development Runnerを分離した

ここまで来ると、ChatGPTからコードを直し、テストし、必要ならサービスを更新するところまでかなりつながります。

でも、次に気になったのはもっと根本的なことでした。

「AIが直したコードを、そのままmainへ入れていいのか?」

便利になったからこそ、ここは怖い。

コードの変更はできる。テストも通る。だからといって、その変更をそのまま本番の基準へ反映してよいとは限りません。

そこで、AIが作る変更と、正式に採用する変更の間にGitHubのPull Requestを置くことにしました。

今では毎日のように使っている流れですが、最初からきれいにできていたわけではありません。

PRの向きを間違え、古いブランチを再利用して競合させ、Gitの表示上の状態と実体がずれたこともありました。

その失敗を一つずつ潰していった結果、

「AIは変更案を作る。mainへ入れる境界は別にする」

という形が固まりました。

コードを書き換えられることと、採用してよいことは別だった

Development MCPができる前は、ChatGPTにコードを書いてもらっても、実際にファイルへ反映するのは自分でした。

第2回でその運搬作業をかなり減らしました。

その結果、別の問題が見えるようになります。

人間がコピペしていた頃は、良くも悪くも人間が途中にいました。

ところがChatGPTから直接コードを書き換えられるようになると、変更までが速い。

速いのは良いのですが、そのままmainまで触らせると、

AIが考える
↓
AIが変更する
↓
AIがmainへ反映する

となってしまいます。

これは自分の使い方には合いませんでした。

私が欲しかったのは、AIを完全放置することではありません。

実装や反復はかなり任せたい。でも、正式な変更として採用する境界は見える形で残したい。

そこでGitHubのPull Requestがちょうどよかったわけです。

最初のPRはCodex Cloudからだった

Development MCP自体がまだ存在しない段階では、Development MCPをDevelopment MCPで作ることはできません。

そのため初期実装にはGitHubのprivate repositoryとCodex Cloudを使いました。

2026年7月29日に作られた最初のPull Requestは、

PR #1「Add Development MCP service, deployment/CI tooling, and security guards」

です。

この時点のhead branchは、まだchatgpt/*ではなくCodex Cloud側のブランチでした。

Development MCP本体、deployment、CI、安全ガードなど、初期構成をまとめて入れた比較的大きなPRです。

その後、VPS上でもChatGPTから開発できるようになり、PRを作る主体がCodex Cloudから自分のDevelopment MCP側へ移っていきました。

ここで、GitHubが単なるコード保管場所ではなく、AIが作った変更を正式化する境界になり始めます。

PRのbaseとcompareを逆にした

最初の頃はGitHubのPull Request自体にも慣れていませんでした。

実際、PRを作るときにbaseとcompareの向きを逆にしたことがあります。

今なら、

base = main
compare = 変更を入れたbranch

と迷いません。

でも当時は、「どっちからどっちへ?」が直感的ではありませんでした。

これは小さな失敗ですが、AIに操作させるときには重要でした。

人間が毎回UIを見て確認するなら、その場で気づけます。

一方、自動化するなら、向きを仕様として固定しないと同じ間違いを繰り返すからです。

そこで、PRは常にmainをbaseにし、作業branchをheadにする形へ固定しました。

古いブランチを使い回したら、普通に競合した

もっと分かりやすい失敗がPR #2です。

PR #2では、最初のCodex Cloud由来の古いブランチをもう一度使いました。

しかし、その間にmain側はすでに進んでいます。

結果、PR #2はmergeableではなく、GitHub上でも競合した状態になりました。

要するに、昔の作業台をそのまま引っ張り出して、今のmainへもう一度載せようとしたわけです。

これを見て、長く生きる作業branchを使い回すのはやめました。

毎回、最新のmainから新しい小さなbranchを作る。

この方針へ切り替えます。

実際、次のPR #3は、

chatgpt/development-mcp-phase2

というbranchからmainへ出しています。

ここから、AI側の作業branchはchatgpt/*に限定する形が定着していきました。

mainへ直接pushさせなかった

branchを分けても、AIが自由にGit操作できるならまだ怖い。

そこでGit操作自体も、任意のshell commandではなく目的別の専用ツールへ寄せました。

基本ルールはかなり単純です。

  • mainへ直接pushしない
  • force pushしない
  • pushできるのはchatgpt/*だけ
  • 対象repositoryを固定する
  • 作業ツリーがcleanであることを確認する
  • 「さっき確認したHEAD」と現在のHEADが同じことを確認する

最後の条件が意外と重要です。

AIが状態を読んでから変更を確定するまでの間に、別の作業でGitの状態が変わることがあります。

その時、古い前提のままpushやmergeを続けると事故になります。

そこで、事前に見ていたHEADをexpected HEADとして渡し、現在値が違えば止めるようにしました。

これはKnowledge MCPでSHA-256が一致しなければ文書更新を拒否したのと、かなり似ています。

「前に見た状態が今も同じ」と確認できた時だけ変更する。

対象がMarkdownからGitへ変わっただけで、考え方は同じでした。

テストが通ったらPRを作る

標準フローは、だんだん次の形へ固まりました。

ChatGPTで要件を整理
  ↓
Development MCPで変更
  ↓
Ruff / mypy / pytest
  ↓
local commit
  ↓
chatgpt/* branchへguarded push
  ↓
GitHub Pull Request
  ↓
CIと差分を確認
  ↓
merge
  ↓
VPSのmainをfast-forwardで同期
  ↓
対象serviceだけ更新
  ↓
health確認

ここで大事なのは、テストとmergeを同じ操作にしなかったことです。

テストが通るのは「機械的な条件を満たした」というだけです。

その変更を採用するかは別問題です。

設計意図が合っているか。

変更範囲が大きすぎないか。

余計なファイルまで触っていないか。

この確認地点としてPRを残しました。

非エンジニアの自分にとっても、GitHub上で差分がまとまって見えるのはかなり助かりました。

mainの同期もfast-forward onlyにした

PRをmergeした後は、VPS側のmainを最新へ合わせます。

ここでも、何でもmergeできるようにはしませんでした。

git_sync_mainでは、

  • working treeがclean
  • 事前に確認したHEADと一致
  • fast-forward only

を条件にしました。

VPS側に独自の変更が残っていたり、履歴が分岐していたりしたら、その場で無理に合わせません。

「最新mainにしたい」という目的より、分からない状態で履歴を書き換えないことを優先しました。

また一時期、@@INLINE0@@だけを使って同期していたため、実際には更新されているのに@@INLINE1@@の追跡表示が古いまま残ることもありました。

これもremote tracking refを明示的に更新するよう修正しています。

実体だけ合っていればよいのではなく、次にAIが読む状態表示まで現実と一致していることが重要でした。

「承認します」が増えた理由もここにある

このGitHub境界を作ったことで、開発はかなり安全になりました。

一方で、その後別の問題も増えます。

PRが増えるたびに、私が内容を確認し、mergeを承認する必要があります。

開発初期の数日だけでも、PR #1〜#30が作られ、競合したPR #2を除く29件がmergeされています。

小さなPRを積み重ねる方針は安全ですが、その分だけ「確認」「承認」の回数も増える。

この時点では、それで問題ありませんでした。

まずは、

AIが勝手に確定しないこと

の方が重要だったからです。

ただ、開発速度が上がると、今度はこの承認往復そのものがボトルネックになります。

それが後のStanding Delegationや承認UIにつながっていきます。

後から、merge自体もさらに厳密になった

初期のPR運用では、「branchを分ける」「main直push禁止」「expected HEADを見る」というところが中心でした。

その後、開発が並列化し、複数の作業が同時に動くようになると、merge境界はさらに厳しくなりました。

現在は、merge前にread-only previewを作り、

  • PRのhead SHA
  • base SHA
  • previewした内容

を固定し、その状態が変わっていれば古い承認を使わず止める方向へ進んでいます。

これは、PRという仕組みを変えたというより、最初からあった原則を強くしたものです。

見たものと、実際に確定するものが同じ時だけ進める。

AIが速くなり、並列に動くほど、この原則は重要になりました。

ただ、このあともう一段別の境界が必要になりました。PR、component scope、workspace leaseで技術的な競合を避けても、「そのworkspaceを今どのChatが担当してよいか」は別問題だったからです。複数Chatの並列運用で、主開発Chatが別Chat所有のPublishing MCP workspaceまで進めそうになり、ownership boundaryを追加しました。

AIが別のChatの仕事まで進めそうになった話――能力向上で生まれた新しい失敗モード

振り返ると、GitHubは「人間を残す場所」だった

第2回では、人間のコード運搬作業を減らしました。

第3回では、強い実行処理をRunnerへ分離しました。

ここまで、人間を作業の途中から少しずつ外しています。

でも第4回では、逆に人間を残す場所を決めました。

AI
  → 実装
  → テスト
  → commit
  → branchへpush
  → PRを作る

人間
  → 差分を見る
  → 採用するか判断する

完全自動化を急がず、まず「ここまではAI」「ここから先は確定」という境界を作る。

この考え方は、その後のPublishing MCPでもそのまま使います。

記事を作れることと、公開してよいことは別。

コードを直せることと、mainへ入れてよいことも別。

生成と確定を分ける。

GitHub PRを使い始めたことで、その原則がかなり明確になりました。

次回:記事を書けても、公開までがまだ遠かった

開発側では、Knowledge MCP、Development MCP、Development Runner、GitHub PRという流れが整いました。

次に本格的に取り組んだのが、ブログとSNSへの発信です。

当初はn8nを中心に考えていましたが、ChatGPTとの会話で内容が変わり続ける記事制作を固定workflowへ押し込むと、思ったより扱いにくい。

そこで、対話型の発信を専用のPublishing MCPへ分けることになりました。この続きは第5回にまとめています。

連載ナビ

関連記事

コメント

タイトルとURLをコピーしました