CIの無料枠が尽きると自前ランナーまで止まる — 自動マージをローカル常駐プロセスへ移した話

  • #GitHub Actions
  • #CI/CD
  • #自動化
  • #運用
  • #ローカル常駐
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 分ごとに走る。

  1. gh pr list でオープン PR を最大 100 件取得
  2. 変更ファイルがちょうど 1 媒体の許可パスに全部収まるか確認(複数媒体にまたがる PR は人間が判断)
  3. 媒体ごとのゲートを順番に通す
  4. 全通過なら gh pr merge --merge してブランチを削除
  5. Discord に通知

媒体別ゲートの構成

各媒体は以下のフィールドで設定されている。

フィールド役割
path_allow変更ファイルが全てこの正規表現に一致する PR だけを対象にする
safety_varこのリポ変数が 'on' の時だけ動く(None の媒体は常時 on)
require_labelこのラベルが無ければ skip(起稿 AI の自己ゲートに連動)
draft_guardfrontmatter の 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_DIRAUTOMERGE_WORKTREE を環境変数として plist に渡すことで、リポジトリの場所と検証用 worktree のパスを動的に変更できる。

移設後の動作

GHA から切り離したことで、無料枠の残量を気にせず装填 PR が自動でマージされるようになった。GHA に残る用途は push / PR トリガーの検証(validate_mdx、brand leak check 等)のみになり、マージ判断ロジックは完全にローカルで完結している。

試した環境

  • Python 3.12(~/.pyenv/shims/python3 経由)
  • gh CLI 2.50.x
  • macOS 上の launchd(常駐マシン)
  • 対応媒体: the_ntm を含む計5媒体

更新履歴

  • 2026-07-27: 初稿