作ったもの
ghfanout は、.gitignore・pom.xml・pyproject.toml・package.json のような共有設定ファイルを 1 か所で一元管理(single source of truth)し、多数の GitHub リポジトリへ Pull Request として配布する CLI ツールです。
ファイルを 1 回編集してコマンドを 1 回叩けば、影響するすべてのリポジトリに PR が立ちます。
- GitHub: https://github.com/seiya-koji/ghfanout
- PyPI: https://pypi.org/project/ghfanout/
- ドキュメント: https://seiya-koji.github.io/ghfanout/
pip install ghfanout # または uv tool install ghfanout
解決したい課題
マイクロサービスをリポジトリ分割で運用していると、こんな経験はないでしょうか。
-
.gitignoreに 1 行足したいだけなのに、対象リポジトリが 10 個ある - CI の設定を直したはずなのに、直っていないリポジトリが後から見つかる
- 「あのリポジトリだけ
pyproject.tomlの lint ルールが古い」が定期的に発生する
モノレポに統合できればこの問題は消えますが、組織やデプロイの都合でリポジトリを分けざるを得ないケースは多くあります。ghfanout はそういう「モノレポにできないリポジトリ群」のための、設定ファイルの一元管理・配布ツールです。
既存ツールを検討した結果
私のニーズは「あるべき状態を宣言的に管理して、差分が出たときだけ PR を配り続ける。」こと。
加えて、私の環境では GitHub Actions が使えないという制約もありました。
既存ツールはそれぞれ少しずつ噛み合いませんでした。
- safe-settings — ブランチ保護やラベルなどリポジトリ「設定」の Policy-as-Code。リポジトリ内のファイルは配布できない
- multi-gitter / git-xargs — clone してスクリプトを実行するワンショットの一括変更ツールで、あるべき状態を継続的に管理するものではない
- repo-file-sync-action — 一番近い存在だが GitHub Actions 前提
この隙間を埋めるのが ghfanout です。
safe-settings がリポジトリ設定(メタデータ)の Policy-as-Code なら、ghfanout はリポジトリ内ファイルの Configuration-as-Code。両者は組み合わせて使えます。
特徴
1. 対象リポジトリを git clone しない
配布はすべて GitHub Git Data API 経由で行います。対象リポジトリをローカルに clone しないので、リポジトリが何十個あってもディスクも時間も消費しません。pip install するだけで動きます。
2. Kustomize 風の base + overlay 構成
Kubernetes の Kustomize を使ったことがある人にはおなじみの、「base に共通の内容を置き、overlay で配布先ごとの選択をする」構成を採用しています。
ghfanout.yaml # 配布先の host / org を宣言
base/
common/ # 全リポジトリ共通。無条件に配布される
.gitignore
java-service/ # プロファイル: opt-in したリポジトリだけに配布
pom.xml.tmpl # .tmpl = Jinja2 テンプレート。リポジトリごとに描画される
python-service/
pyproject.toml
node-service/
package.json
overlays/
user-service/ # ディレクトリ名 = 配布先リポジトリ名
manifest.yaml # このリポジトリが受け取るプロファイル・ブランチを宣言
api-gateway/
manifest.yaml
manifest.yaml には以下を記載します。
bases: # 受け取るプロファイル(common/ は常に含まれる)
- java-service
branches: # 配布先ブランチ(省略時はデフォルトブランチ)
- main
- release-1.x
values: # テンプレートから参照する値
version: "1.2.3"
3. Jinja2 テンプレートでリポジトリごとの値を埋め込める
拡張子が .tmpl のファイルだけが Jinja2 テンプレートとして描画され、拡張子を除いた名前で配布されます(pom.xml.tmpl → pom.xml)。
<!-- base/java-service/pom.xml.tmpl -->
<groupId>com.example</groupId>
<artifactId>{{ repo }}</artifactId>
<version>{{ values.version | default("0.1.0") }}</version>
.tmpl が付いていないファイルはそのままコピーされます。
使ってみる
# 1. 設定リポジトリの雛形を生成(動くサンプル付き)
ghfanout init ./my-config --org myorg
cd my-config
# 2. base + overlay をローカルで合成して確認(git 操作なし・ネットワーク不要)
ghfanout build example-service # 出力: dist/example-service/
# 3. 認証して、実リポジトリとの差分をプレビューしてから配布
export GHFANOUT_TOKEN=ghp_xxxx
ghfanout deploy example-service --dry-run
ghfanout deploy example-service # 差分があれば PR が開く
# 全リポジトリへ一括配布も可能
ghfanout deploy --all
deploy は配布先ブランチごとに差分を計算し、差分のあるブランチにだけ PR を作ります。
作業ブランチ(ghfanout/update-<branch>)がすでにあれば作り直し、open な既存 PR があれば新規作成せず再利用するので、何度実行しても PR が乱立しません。
PR を経由せず対象ブランチへ直接 push する deploy_mode: push も選べます(信頼できる社内リポジトリ向け)。
おわりに
「設定ファイルを直すたびに 10 個のリポジトリを回る」作業から解放されたい方は、ぜひ試してみてください。
pip install ghfanout
ghfanout init --org <your-org>