😸

Mac miniとcodexで自動リファクタリングPRを量産する仕組み

に公開

はじめに

日々のコード管理において、リファクタリングは重要な作業ですが、手動で行うには時間がかかります。そこで、Mac miniとChatGPT Plusを組み合わせて、自動でリファクタリングPRを生成する仕組みを構築したので紹介します。

システム構成

必要な環境

  • ChatGPT Plus:codexとcodex cliが使用可能
  • Mac mini M1 16GB:自宅サーバーとして24時間稼働

基本的な流れ

  1. Mac miniでcronを使ってスクリプトを定期実行
  2. 最新のリリースブランチを自動検出
  3. codex cliを実行してリファクタリング
  4. PRを自動作成
  5. セルフホストランナーでビルド確認

運用の特徴

効率的なフィードバックループ

PRの品質が微妙な場合は、深追いせずにすぐにPRをクローズします。その代わり、iPhoneのChatGPTアプリからcodexに対してAGENTS.mdの更新指示を出すことで、次回のリファクタリング精度を向上させています。
GitHubの公式アプリとChatGPTアプリの組み合わせで、提案のPRのマージ・クローズ・AGENTS.mdの改修のPR作成・マージもiPhoneで完結します。

実際の運用では、1日2〜3のPRがマージされており、継続的なコード改善が行われています。また、失敗したPRから学習してAGENTS.mdを改善することで、普段のcodex cli使用時の精度も向上しました。リファクタリング自体はおまけ程度の位置づけですが、コードの複雑さに目を向けるきっかけとして有効に機能しています。

Swift特有の課題

実際の運用では、ネイティブのiOSアプリの開発に利用しています。
SwiftのConcurrency周りでビルドできないケースが多いため、Mac miniのセルフホストランナーでビルド可能性を確認できる体制を整えています。

メリット

  • ネイティブアプリの利便性:codexはネイティブアプリが提供されており使いやすい
  • 自然な学習システム:AGENTS.mdが自然に育ち、リファクタリング品質も向上
  • 継続的改善:失敗から学習する仕組みが組み込まれている

今後の展望

OpenAI Codex Cloudによると、今後はCLIからクラウドのcodexを実行できるようになる予定のようです。これが実現すれば、cronでAPIを叩くだけで済むようになり、さらにシンプルな構成になると期待しています。

運用上の注意点

cronは失敗しても気づきにくいため、ログ出力を設定しておくことを強く推奨します。

自動化スクリプト

以下が実際に使用しているスクリプトです:

#!/usr/bin/env bash
set -euo pipefail

# Ensure PATH when running under cron (minimal env)
export PATH="/opt/homebrew/bin:/usr/local/bin:${PATH:-/usr/bin:/bin:/usr/sbin:/sbin}"

# ==== 設定 ====
REPO_SLUG="${REPO_SLUG:-noppefoxwolf/repo-name}"  # "owner/repo" 形式
REPO_NAME=$(basename "${REPO_SLUG}")
CLONE_DIR="${CLONE_DIR:-}"                     # 空なら一時ディレクトリにクローン

CODEX_PROMPT=${CODEX_PROMPT:-"可読性を上げるための小さなリファクタリングをしてください"}
CODEX_CMD="codex exec --full-auto \"$CODEX_PROMPT\""

PR_TITLE_PREFIX="${PR_TITLE_PREFIX:-refactor: codex automated changes}"
PR_BODY_EXTRA="${PR_BODY_EXTRA:-This PR was generated by an automated script running Codex refactoring.}"

# ==== 事前チェック ====
command -v git >/dev/null 2>&1 || { echo "git が見つかりません"; exit 1; }
command -v gh  >/dev/null 2>&1 || { echo "GitHub CLI(gh) が見つかりません: https://cli.github.com/"; exit 1; }
command -v jq  >/dev/null 2>&1 || { echo "jq が見つかりません: brew install jq などで導入してください"; exit 1; }
command -v codex >/dev/null 2>&1 || { echo "codex が見つかりません"; exit 1; }

if ! gh auth status >/dev/null 2>&1; then
  echo "gh が未ログインです: gh auth login を実行してください"
  exit 1
fi

# ==== クローン ====
if [[ -z "${CLONE_DIR}" ]]; then
  WORKDIR="$(mktemp -d)"
else
  WORKDIR="${CLONE_DIR%/}"
fi
echo "Working directory: ${WORKDIR}"
if [[ -d "${WORKDIR}/${REPO_NAME}" ]]; then
  echo "既存ディレクトリがあるため再利用します"
else
  gh repo clone "${REPO_SLUG}" "${WORKDIR}/${REPO_NAME}"
fi
cd "${WORKDIR}/${REPO_NAME}"

# ==== 最新の release ブランチを特定 ====
git fetch --prune origin

# origin にある "release*" のブランチ一覧取得
RELEASE_BRANCHES=()
while IFS= read -r branch; do
  RELEASE_BRANCHES+=("$branch")
done < <(git ls-remote --heads origin 'release*' | awk '{print $2}' | sed 's#refs/heads/##g')

if [[ ${#RELEASE_BRANCHES[@]} -eq 0 ]]; then
  echo "release ブランチが見つかりませんでした"
  exit 1
fi

# "release/*" の */以降をバージョン文字列として自然順ソートして最大を選ぶ
LATEST_RELEASE_VERSION="$(printf "%s\n" "${RELEASE_BRANCHES[@]}" \
  | sed -E 's#^release/##' \
  | sort -V \
  | tail -n1)"

LATEST_RELEASE_BRANCH="release/${LATEST_RELEASE_VERSION}"
echo "Latest release branch: ${LATEST_RELEASE_BRANCH}"

# ==== 派生ブランチ作成 ====
TS="$(date +%Y%m%d-%H%M%S)"
NEW_BRANCH="chore/codex-refactor-${LATEST_RELEASE_VERSION//\//-}-${TS}"

# origin/release/* から新規ブランチを切る
git checkout -b "${NEW_BRANCH}" "origin/${LATEST_RELEASE_BRANCH}"

# ==== codex リファクタ実行 ====
echo "Running Codex: ${CODEX_CMD}"
eval "${CODEX_CMD}"

# ==== 変更があればコミット ====
if git status --porcelain | grep -q .; then
  git add -A
  COMMIT_MSG="${PR_TITLE_PREFIX} (base: ${LATEST_RELEASE_BRANCH})"
  git commit -m "${COMMIT_MSG}"
else
  echo "codex による差分はありませんでした。PR 作成をスキップします。"
  exit 0
fi

# ==== Push & PR 作成 ====
git push -u origin "${NEW_BRANCH}"

PR_TITLE="${PR_TITLE_PREFIX} (${LATEST_RELEASE_BRANCH})"
PR_BODY=$(cat <<EOF
This PR targets \`${LATEST_RELEASE_BRANCH}\`.

- Source branch: \`${NEW_BRANCH}\`
- Generated at: $(date -u +"%Y-%m-%d %H:%M:%S UTC")

${PR_BODY_EXTRA}
EOF
)

gh pr create \
  --title "${PR_TITLE}" \
  --body "${PR_BODY}" \
  --base "${LATEST_RELEASE_BRANCH}" \
  --head "${NEW_BRANCH}" \
  --label "refactor" \
  --label "automated"
gh pr view --web || true

echo "Done."

cron設定

スクリプトを5時間ごとに実行するcron設定:

% crontab -e 
0 */5 * * * (date; /path/make_refactoring_pr.sh) >> /logs/make_refactoring_pr.log 2>&1

この設定により、5時間ごとに自動でリファクタリングPRが生成され、ログファイルに実行結果が記録されます。

Discussion