ブログ記事の簡単なリライト作業を、手元のMacではなくサーバー側でも動かすようにしてみました。

やりたいことは、サーバーにブログ用リポジトリを clone して、その中で Codex CLI に記事リライト系の作業を任せ、さらにコミット・push・PR作成までを自動化することです。

実際には gh の権限、privateリポジトリのclone、Codexのログイン、sandbox など、細かいところでつまずいたことがいくつかありました。それらを中心に、備忘録として残しておきます。

やりたかったこと

まずはサーバーにSSHでログインし、GitHubリポジトリを用意します。

仮に、clone先は以下とします。

/home/appuser/repos/private-blog

ここにブログのprivateリポジトリをcloneして、記事リライト用のバッチ処理から Codex CLI を呼び出します。最終的には、このバッチ処理をcronで定期実行する想定です。

当然、git / gh / codex がシェルで使えるように、サーバー側へ各コマンドをインストールしておく必要があります。

本番サーバーでやるのが怖い場合は、もう1台サーバーを用意するのも現実的です。さくらVPSなら比較的安いですし、私も本格運用するなら別サーバーを用意する予定です。

もちろん、ラズパイなどを使って自鯖で処理させるのも十分ありだと思います。

サーバー側でCodexに作業させるので、本番公開ディレクトリとは必ず分けておきます。

/home/appuser/public-site      # 公開用
/home/appuser/repos/private-blog # 作業用clone

また、私の場合は、リポジトリのmainブランチも直接編集させず、worktreeを使った別ブランチで作業させるようにしています。

全体の関係は、次のようなイメージです。

ローカルMac、GitHub、サーバー、Codex、本番公開ディレクトリの関係図

今のところ、PRのレビューからbuild / deploy作業までは、人間が担当するようにしています。

そのためローカルMacの出番は、最初の初期設定とPR確認が中心です。そこまで整えば、毎回SSHでサーバーに入って手作業する必要はかなり減ります。

なぜGitHub Actionsではなくサーバー上のcloneで回したか

ところで、この手の自動化をChatGPTへ相談すると、かなりの確率で GitHub Actions 案が出てきます。

MicrosoftはGitHubを買収済みで、OpenAIにも大きく投資しています。

  • Microsoft → GitHubを買収済み
  • Microsoft → OpenAIへ大規模投資

だからActionsが推されるのでは、と邪推したくなるくらい毎回出てきます。もちろん半分冗談です。実際には、定期実行やPR作成までGitHub側で完結できるので、CI/CDの定番案として出てくるのだと思います。以前、VPSへのデプロイでは GitHub CI/CD で VPS へ自動デプロイするまで にまとめたように、Actionsの便利さも確認しています。

それでも今回は、サーバー上のcloneで動かす方を選びました。

理由は主に3つです。

1つ目は、すでにサーバー上で日次処理を動かす前提があったことです。記事リライトはサイト運用のローカルな作業に近く、実行ログや失敗時のworktreeをサーバー上に残せる方が追いやすいと考えました。

2つ目は、秘密情報を増やしたくなかったことです。GitHub Actionsに渡すsecretを増やすより、サーバー側のenvファイルに閉じた方が、今回の小さな運用では見通しがよくなります。

3つ目は、OpenAI API key を新しく使わずに進めたかったことです。Codex CLIはChatGPTログインでも使えるため、今回の確認ではAPI keyを新規発行せず、手元で使っているアカウントのログインで試しました。もちろん、公式にはAPI key認証もあり、CIや完全自動化ではAPI keyの方が向く場面もあります。ここでは「今回の運用では使わなかった」という整理です。

privateリポジトリをcloneするためにghとPATを整理した

サーバーからGitHubへアクセスする方法はいろいろありますが、Codexも扱う以上、できるだけ権限を絞りたいところです。

設定を雑にして、関係ないリポジトリまでcloneできたり、編集・commit・pushできたりする状態は避けたいです。

何でもGitHubへアクセスできる状況を作らないためにも、認証方法を先に整理しました。

今回は、サーバー上で広いOAuth権限を持つGitHub CLIログインを使い続けるより、対象リポジトリだけに絞った fine-grained personal access token を使う方針にしました。

権限は、最小限から始めます。

fine-grained PAT は、次の画面から作成できます。

https://github.com/settings/personal-access-tokens

設定画面では、まず Only select repositories を選び、対象リポジトリを1つだけに絞ります。そのうえで、必要な権限だけを Read and write にします。

fine-grained personal access tokenで対象リポジトリと必要権限だけを選ぶ説明図

Repository access: Only selected repositories
Selected repository: owner/private-blog
Contents: Read and write
Issues: Read and write
Pull requests: Read and write
Metadata: Read-only

token は env ファイルに入れて、権限を絞ります。

mkdir -p /home/appuser/.config/blog
chmod 700 /home/appuser/.config/blog

vi /home/appuser/.config/blog/github.env
chmod 600 /home/appuser/.config/blog/github.env

リポジトリが増えたら、/home/appuser/.config/ 配下でディレクトリを切り、リポジトリごとにenvファイルを分ける運用にします。

envファイルの中身は例として次のようにします。

export GH_TOKEN="<fine-grained-pat>"
export GITHUB_TOKEN="$GH_TOKEN"
export GH_REPO="owner/private-blog"

このあたりは、これが唯一の正解というわけではありません。

実際、gh はこのファイルを自動で読みに行くわけではありません。使う直前に環境変数を明示的に読み込ませる必要があります。

. /home/appuser/.config/blog/github.env
gh repo view "$GH_REPO"
gh issue list --repo "$GH_REPO" --limit 5

設定がうまくいっていれば、GitHubへアクセスできるようになります。

. /home/appuser/.config/blog/github.env

gh auth status --hostname github.com
gh repo view "$GH_REPO"
gh api repos/owner/private-blog --jq '.permissions'

git remote -v
git status --short

ここで permissions.pushtrue になっていれば、少なくともGitHub API側ではpush権限があります。逆に、cloneはできるのにpushだけ403になる場合は、fine-grained PAT の Contents 権限が read-only のままになっていないかを疑います。

Codex CLIはAPIキーではなくChatGPTログインで試した

次にCodex側の設定です。ここでも考え方が2つあり、ChatGPTログインを使うのか、OpenAI API key を使うのかで分かれます。

今回は新たなAPI課金を増やしたくなかったので、ChatGPTログインを選びました。

Codex CLIの認証は、公式ドキュメントでもChatGPTログインとAPI keyログインの2通りが説明されています。CLIやIDE拡張では両方を使えますが、API keyの場合はOpenAI Platform側のAPI課金になります。今回の狙いは、すでに使っているChatGPT/Codexアカウントでサーバー上の確認を進めることだったので、API keyは使いませんでした。

参考:

GUIのないリモートサーバーでは、通常のブラウザログインがそのまま使いにくい場合があります。その場合は、デバイスコード認証を使うのが分かりやすいです。

codex login --device-auth

コマンドを実行すると、ブラウザで開くURLと入力用コードが表示されます。手元のMacなど、ブラウザを開ける端末でURLへアクセスし、表示されたコードを入力して許可します。

環境によっては、通常の codex login をSSHポートフォワードで通す方法もあります。

ssh -L 1455:localhost:1455 [email protected]
codex login

この場合も、サーバー側に保存される認証情報は秘密情報として扱います。

ログイン後の認証情報は、環境によって ~/.codex/auth.json やOS側のcredential storeに保存されます。auth.json ができる場合、その中身はtokenなので、GitHubのPATと同じように扱います。Issue、PR、記事本文などに、くれぐれもコミットしないよう注意してください。

最初は小さな読み取りタスクで確認した

CLIからGitHub操作もできて、Codexのログインも終わったら、いきなり記事を書き換えさせるのではなく、読み取りだけの小さなタスクで確認します。

cd /home/appuser/repos/private-blog

codex exec --ephemeral "このリポジトリの構成を3行で説明してください。ファイルは変更しないでください。"

codex exec はスクリプトからCodexを呼ぶための非対話モードです。公式ドキュメントでは、初期状態では読み取り寄りに動かし、編集が必要なときだけ --sandbox workspace-write のように明示する流れが説明されています。

この段階で見たいのは、次の点です。

  • Codex CLIが起動するか
  • OpenAI側の認証が通っているか
  • cloneしたリポジトリをGit repositoryとして認識できるか
  • 読み取りだけの依頼で止まれるか

ここが通ってから、書き込み確認へ進みました。

書き込みは必ず作業ブランチとworktreeに閉じ込めた

冒頭で説明した通り、Codexに書き込みを任せる場合、mainの作業ディレクトリで直接動かすのは避けました。

代わりに、作業ごとに feature/ ブランチと git worktree を作ります。

なぜわざわざworktreeで作業させるかというと、今後、並列に同時作業が入った場合に、作業場所がぶつかってトラブルになるのを避けるためです。 このあたりのことは git worktreeでAI時代の並列開発を試す。Codexに別ブランチを同時に任せるには にまとめています。もっと良い方法もあるかもしれませんが、今のところ、この運用で大きなトラブルは起きていません。

cd /home/appuser/repos/private-blog

git fetch origin main
git worktree add -b feature/article-rewrite-YYYYMMDD \
  /tmp/worktrees/article-rewrite-YYYYMMDD \
  origin/main

Codexを実行する場所は、このworktree側になるようにします。

cd /tmp/worktrees/article-rewrite-YYYYMMDD

codex exec --sandbox workspace-write \
  "対象ファイルだけを編集してください。git reset --hard、git clean、force push、submodule操作は実行しないでください。"

参考: Codex non-interactive mode

重要なのは、Codexに渡すプロンプトで、対象ファイル、禁止操作、完了条件を具体的に書くことです。

今回の記事リライト用のバッチ処理も、細かいプロンプトや選定ロジックはここでは伏せます。ただ、運用方針としては「作業場所を分ける」「mainへ直pushしない」「危険なgit操作を禁止する」「コミットやPRは外側の処理で確認する」という形に寄せています。

何でもCodexに操作させるのではなく、シェルで決め打ちできるところは外側のスクリプトに任せる。この切り分けの方が、運用はスムーズになると思います。

運用で守ることとまとめ

この手の自動化は、動かすこと自体よりも、その後に事故らない仕組みをどう組み込むかの方が大事だと思います。

運用では、最低限これを守ることにしました。

  • GitHubのPATは対象リポジトリだけに絞る
  • PAT、GH_TOKEN~/.codex/auth.json をIssue、PR、記事、ログへ貼らない
  • 作業cloneと本番公開ディレクトリを分ける
  • mainの作業ディレクトリでCodexに書き込みさせない
  • git status --shortgit worktree list を作業前後に見る
  • git reset --hardgit clean、force push、submodule操作を自動処理に入れない
  • Codexに任せる範囲を、対象ファイルと完了条件まで絞る
  • 成功時だけPRを作り、失敗時はworktreeとログを残す

特に、workspace-write は「作業ディレクトリ内なら書ける」設定です。便利ですが、雑なプロンプトで広い範囲を任せると、意図しないファイルまで触る可能性があります。だからこそ、worktreeを分け、対象ファイルを絞り、外側のスクリプトでgit操作を管理する方が安心でした。

片付けも条件付きにしました。成功してPRが確認できたら、その実行で作ったworktreeだけを git worktree remove します。途中で失敗した場合は、原因調査のためにworktreeを残します。既存worktreeをまとめて消したり、git worktree prune を自動で走らせたりはしません。

今回のポイントは、サーバーにCodexを入れること自体ではなく、cloneしたリポジトリを安全な作業場所として扱うことでした。

GitHubの認証は fine-grained PAT で対象リポジトリに絞る。Codexは、今回の確認ではOpenAI API keyを新規発行せず、ChatGPTログインで動かす。書き込みはmainではなくworktreeに閉じ込める。成功したらPR、失敗したらログとworktreeを残す。

この形にしておくと、GitHub Actionsにsecretを増やさず、手元のMacだけにも依存せず、サーバー上のcloneで記事リライトのような定期作業を回せます。

AIに自動で書き換えさせるほど、作業場所、権限、禁止操作、失敗時の残し方を先に決めるのが重要だと感じました。