ClaudeCodeやCodexを使っていて、「外出中の作業が出来ない時間が勿体ない」と思ったことはありませんか?
あるいは大したことがないタスクなのに時間だけはかかって、
手元のCloneしたリポジトリを長時間占有してしまう・・・みたいな状況に陥ることはありませんか?
そこで、GitHubのセルフホステッドランナーという仕組みを利用して、
Issue を1本書いてラベルを貼るだけで、Mac Pro が勝手に動き出し、コードを直し、テストを回し、Pull Request を出してくれる・・・というシステムを作ってみました。
現在Unityプロジェクトで動かしていますが、Unity以外でも利用出来るはずです。
なぜ作ったか
- 手元の端末を占有する。 30分かかる作業の間、ターミナルが1つ塞がってしまう
- 依頼の記録が残らない。 「あれ何で直したんだっけ」が追えない
- 外出中に作業が出来ない。 常に動かしたい
- 複数並列で作業が出来ない。 AIが高速なのにプロジェクトを丸ごと複製しないとシングルスレッド
これらは全部自分の手元で作業していることが原因です。
まぁ当たり前と言えば当たり前ですが、効率を求めるとどうしても何もしていない時間が勿体なく感じてしまいます。
たまたま手に入ったMac Pro Late2013が思いの外高いメモリとストレージとコアを持っていたので、
MacOSを抜いてUbuntuを入れて、ClaudeCodeに丸ごと渡して、プロジェクトのIssue投稿をトリガーに自動で作業してPRを作ったり調査してくれる・・・というシステムを作ったら面白そうだなと思い作ってみました。
できたもの
人間: Issue を書く
↓ "Requested" ラベルを貼る
[ワークフロー] 着手してよいか判定(Close 済み? 既に動いてる?)
↓
[AI] Issue を読む → タスク一覧を Issue 本文に書く → 実装 → コミット
↓
[ワークフロー] テストを回す(落ちたら1回だけ AI に直させる)
↓
[ワークフロー] push して PR を作る
↓
人間: PR を読んでマージする
ポイントはAIはcommitする所までという部分で、
git push と gh pr create はワークフローが行います。
似たようなシステムを作ったことがある人は多いと思いますが、
AIにブランチコントロールまで任せるとトークンが勿体ないですし、
何より関係ないブランチを触って知らない間に沼っていたりします。
masterブランチにpushするなと言ってもpushするし、
サブエージェントを使うなと言っても使うし、
作業が終わったら報告しろと言っても報告しない時がたまにあります。
機械的に判断出来るところはスクリプトを用意し、ギリギリまで仕組みで解決しましょう。
構成
マシン
母艦は退役した Mac Pro Late2013(ゴミ箱)です。
macOSがもうサポート終了しているので、macOS を消して Ubuntu Server を入れ、専用機にしています。
| 本体 | Mac Pro (Late 2013) |
| CPU | Xeon E5-2697 v2 — 12 コア / 24 スレッド |
| メモリ | 64 GB |
| OS | Ubuntu Server 24.04 LTS / GUI なし / SSH のみ |
登場人物
| 登場人物 | 役割 |
|---|---|
| 人間 | Issue を書く / Requested を貼る / PR をマージする
|
github-actions[bot] (GITHUB_TOKEN) |
状態ラベルの付け替え・進捗の通知 |
worker[bot] (自作の GitHub App) |
実装・push・PR 作成・Issue への回答 |
GITHUB_TOKEN経由だとGitHubはトリガーを発火しないので、無限ループを防げます。
(自分の通知で自分が再起動しないようにする)
- 状態ラベル・進捗通知 →
GITHUB_TOKEN(何も起こしたくない) - PR 作成・ブランチ push → App トークン(後続の処理に繋げたい)
状態はラベル1枚で表す
Issue はラベル一枚貼ることで状態を表現します。
なし → Requested → Analyzing → InProgress → AwaitingReview → Completed
↕
NeedsInput
人間がやるのは Requested を貼ることと、最後に Issue を閉じることだけで、状態の更新(ラベルの張替え)はワークフローが行います。
なのでラベルを見ることで「いま誰にボールがあるか」が分かります。
複数並列で動かす
セルフホストランナーは 1インスタンスにつき同時に1ジョブしか受け取りません。
つまりもしランナーが1本しか無いと、Issue を2つ投げた時点で片方は待ちになります。
せっかくコアもメモリも余っているので限界まで並列化させます。
一般的な導入手順(マシンに直接インストールして systemd で常駐させる)のまま増やそうとすると、地味に面倒なので、ランナーごとコンテナにします。
イメージを1つ作っておいて、欲しいぶんだけ起動することで、自由に増減させられるし、他のマシンに引っ越す時もイメージをコピーするだけで簡単に引っ越せます。
- 増設は
docker run1回。ディレクトリもサービスも増えない - コンテナごとに独立しているので、ユーザー単位の状態を踏み合わない
- ビルド環境のバージョンがイメージのタグで固定される
上限を決めるのはメモリなので、今回は1コンテナに 9GB の上限を付けて6本にしています。
ランナー構成
イメージの中身は、ランナー本体・ビルド環境・Claude Code の3つです。
ホスト(Ubuntu)
├─ Docker
├─ 温度監視・ネットワークなど、カーネルに触るもの
│
├─ [コンテナ] ci-1 ┐
├─ [コンテナ] ci-2 │ 同じイメージから起動
├─ ... │ 中に runner + ビルド環境 + Claude Code
└─ [コンテナ] ci-6 ┘
ゲームプログラミングで行われるマルチスレッド・ジョブシステムに似てます。
増設の仕方
増設は docker run 1回(初回は鍵登録とライセンス認証)を実行するだけです。
docker run -d --name ci-7 --restart unless-stopped --memory 9g \
-e REPO_URL=... -e RUNNER_NAME=ci-7 -e REGISTRAR_APP_ID=... \
-v /srv/secrets/registrar.pem:/secrets/registrar.pem:ro \
ci-runner:<バージョン>
--name と RUNNER_NAME を変えてもう1回叩くだけ。
登録トークンはコンテナが起動のたびに自分で取るようにしていますが、
会社で環境を作る場合はセキュリティ上の理由で人間が手作業で登録したほうがいいかもしれません。
ホストに残すもの
コンテナに入れられないものだけです。
- Docker そのもの
- コンテナをまたいで残したい状態 — ライセンスファイル、登録用の秘密鍵
- カーネルやネットワークに触るもの — 温度監視(このマシンは発熱の持病があります)、ファイアウォール
やってみて
良かった点。
-
依頼がそのまま記録になる。 Issue を読み返せば、何を頼んで何が出てきたかが全部あります
- 過去のプロンプトを探してひっくり返すのが面倒でしたが、GitHub上で検索出来ます
-
手元が空く。 依頼を投げたらブラウザを閉じてよい
- スマホから依頼も!
- 境界が明示的。 「AI に出来ないこと」が権限と道具の有無で表現されていて、お願いベースになっていません
難しかった点。
プロンプトを書く時間より、AI が間違えたときにシステムが壊れない形を探す時間のほうが圧倒的に長かったです。
そしてその設計の不備は、だいたい実際に事故ってから分かります。
構築初期はフォールバックを一切書かずに、正常系のみ追い求めるのが良さそうです。
運用に入って、安定してきたらフォールバックなり握り潰すなりしてもいいと思います。
ほか書いたもの



