CIの無料枠が尽きると自前ランナーまで止まる — 自動マージをローカル常駐プロセスへ移した話
今回やったこと
GitHub Actions の無料実行枠を使い切った際、セルフホスト(self-hosted)ランナーを用意しているにも関わらず、すべての CI ジョブが止まることに気づいた。恒久策として、ドラフト PR の自動マージを GitHub Actions から切り離し、常駐マシンで動くポーリング型の daemon(scripts/automerge_daemon.py)に移設した記録。
なぜ自前ランナーがあるのに止まったか
private リポジトリの GitHub Actions には月あたりの無料分(2,000分/月 for Free プラン)が存在する。これが尽きると 課金ホールド がかかり、「ジョブのキューへの登録」そのものが止まる。self-hosted ランナーは実行機のコスト問題は解消するが、「ジョブが割り当てられるかどうか」は GitHub Actions のスケジューラが決める。課金ホールド中はこのスケジューラが動かないため、ランナーがいくら準備完了状態でも仕事が回ってこない。
この事業ではすでに記事の起稿・公開・公開後レビューの重い処理を全部ローカル常駐マシンの launchd で回しており、GHA に残っていた必須機能は実質「ドラフト PR の自動マージ」1つだけだった。GHA への依存を断つことで根本解消できる、という判断になった。
daemon が担う仕事(*-auto-merge.yml 5本の移植)
daemon は以下の流れで 15 分ごとに走る。
gh pr listでオープン PR を最大 100 件取得- 変更ファイルがちょうど 1 媒体の許可パスに全部収まるか確認(複数媒体にまたがる PR は人間が判断)
- 媒体ごとのゲートを順番に通す
- 全通過なら
gh pr merge --mergeしてブランチを削除 - Discord に通知
媒体別ゲートの構成
各媒体は以下のフィールドで設定されている。
| フィールド | 役割 |
|---|---|
path_allow | 変更ファイルが全てこの正規表現に一致する PR だけを対象にする |
safety_var | このリポ変数が 'on' の時だけ動く(None の媒体は常時 on) |
require_label | このラベルが無ければ skip(起稿 AI の自己ゲートに連動) |
draft_guard | frontmatter の draft: true を全変更記事で確認(装填レーン専用・Astro 媒体のみ) |
validators | 記事 1 本ごとに実行するバリデータ(非 0 で不合格) |
repo_validators | リポ全体で 1 回実行するバリデータ |
例えば the_ntm は require_label: "autodraft:高" と draft_guard: True を持ち、ラベルがない PR と draft: false の記事を含む PR を自動的に弾く。WP 媒体(frontmatter draft 概念がない)はラベル不要だが、変更パスが非常に限定的な正規表現で縛られている。
隔離: 専用 detached worktree
バリデータを実行する際、メインの作業ツリー(他の drip やエージェントが使っている)の HEAD を汚さないため、専用の detached worktree で PR 先端を checkout してから検査する。git worktree add --detach で作った一時ツリーに PR の fetch 先端を checkout --detach し、検証が終わっても cleanup せず使い回す設計(毎回作り直すとオーバーヘッドが大きい)。
なぜ Astro build を daemon では省いたか
各媒体の起稿ランナー(run-autodraft.sh 等)が PR 作成前に astro build まで通しているため、daemon での再ビルドは冗長になる。build は上流で担保済み、という前提で daemon は軽く・堅牢に保つ方針にした。node_modules の有無に依存しないため、daemon を走らせる常駐マシンに Node.js 環境が不要になる副次効果もある。
dry-run と本番の切り替え
# 判定のみ(マージしない)
python3 scripts/automerge_daemon.py --dry-run
# 本番(ゲート通過 PR をマージ)
python3 scripts/automerge_daemon.py
dry-run は自動化の前に手で走らせて「どの PR が通るか」を確認するために使う。
launchd の設定(常駐マシン)
scripts/com.<project>.automerge-daemon.plist を常駐マシンの ~/Library/LaunchAgents/ に配置し、launchctl load で登録する。plist の StartInterval に 900(秒)を指定して 15 分間隔で起動する設定になっている。
AUTOMERGE_REPO_DIR と AUTOMERGE_WORKTREE を環境変数として plist に渡すことで、リポジトリの場所と検証用 worktree のパスを動的に変更できる。
移設後の動作
GHA から切り離したことで、無料枠の残量を気にせず装填 PR が自動でマージされるようになった。GHA に残る用途は push / PR トリガーの検証(validate_mdx、brand leak check 等)のみになり、マージ判断ロジックは完全にローカルで完結している。
試した環境
- Python 3.12(
~/.pyenv/shims/python3経由) ghCLI 2.50.x- macOS 上の launchd(常駐マシン)
- 対応媒体: the_ntm を含む計5媒体
更新履歴
- 2026-07-27: 初稿