AI コーディングエージェント向けのルールを AGENTS.md に書いています。コミットメッセージは日本語で、とか、テストを勝手に skip するな、とか、そういう取り決めです。

リポジトリが 4 つあるので、このファイルも 4 つに置く必要がありました。コピペで配るとすぐズレるのは目に見えていたので、親リポジトリを正として子へ自動配布する GitHub Actions を書きました。

書いたのは 5 月です。そして一度はちゃんと配られました。ところが同じ日の夜に実装を書き換えていて、その書き換えた部分だけが 4 か月間、一度も走っていませんでした

その間ワークフローは実行されていて、ログはすべて成功です。ただしどれも、本体に入る前に抜けていました。

2026 年 9 月時点の話です(GitHub CLI 2.93.0、GitHub.com)。

配布の仕組み

構成は 4 つのリポジトリを submodule で束ねた に書いたとおりで、親 1 つと子 4 つ(api / blog / web / infra)です。

親の AGENTS.md などが更新されたら、各子に同じ内容を配ります。

on:
  push:
    branches: [main]
    paths:
      - 'AGENTS.md'
      - 'CLAUDE.md'
      - 'GEMINI.md'
      - '.github/copilot-instructions.md'

配るのは 4 ファイル。AGENTS.md が本体で、残り 3 つは各エージェント向けの入口です(中身は「AGENTS.md を読め」とだけ書いてあります)。

子ごとに並列で走らせて、単純にコピーします。

strategy:
  fail-fast: false
  matrix:
    repo: [api, blog, web, infra]

fail-fast: false にしているのは、1 つの子で失敗しても残りは配りたいからです。

PR を立てて即マージする

子の main にも保護をかけているので、直接 push はできません。そこで、PR を作ってその場で squash merge する形にしました。

env:
  GH_TOKEN: ${{ secrets.SYNC_PAT }}
  PARENT_SHA: ${{ github.sha }}
run: |
  git checkout -b "chore/sync-agent-rules-${SHORT_SHA}"
  git commit -m "親 repo の AI エージェントルールを sync (parent ${SHORT_SHA})"
  git push -u origin "$BRANCH"

  PR_URL=$(gh pr create --base main --head "$BRANCH" ...)
  gh pr merge "$PR_URL" --squash --delete-branch

GH_TOKEN に入れているのは後述する PAT です。権限は Contents と Pull requests の両方が要ります。 Contents だけだと push は通って gh pr create で落ちるので、少し分かりにくい失敗の仕方をします(私はこれを README に書き忘れていて、あとで直しました)。

ブランチ名に親のコミット SHA を入れているので、どの更新に対応する配布かが後から分かります。

リポジトリ設定の auto-merge を使う手もあります。ただしあれは「PR ごとに auto-merge を有効化できるようにする」設定なので、結局ボット側から API を叩くことになります。だったら作成と同時に明示的にマージするほうが、何が起きるか読みやすいかなと思いました。

GITHUB_TOKEN では PR を作れない

ここで 1 つ詰まりました。ワークフローに自動で渡される GITHUB_TOKEN を使うと、PR 作成が弾かれます。

GitHub Actions is not permitted to create or approve pull requests

組織やリポジトリの設定で「Actions による PR 作成」を禁止できるようになっていて、それが効いている状態でした。

参考: GitHub Docs - リポジトリの GitHub Actions 設定を管理する

設定を緩める手もありますが、全ワークフローに効いてしまうので、ここだけのために開けたくありませんでした。PAT を 1 つ作って、このワークフローに渡しています。

そもそも GITHUB_TOKEN自分のリポジトリにしか権限がないので、子へ push する時点でどのみち別のトークンが要ります。

同じところは submodule の pointer 追従でも踏んでいて、エラーの全文や .gitmodules の SSH URL を PAT 付き https に差し替える話は submodule pointer の追従を GitHub Actions で自動化した話 に書きました。ここでは繰り返しません。

差分がないときは何もしない

毎回 PR が立つと鬱陶しいので、コピーした結果が変わらなければ抜けるようにしています。

if [[ -z "$(git status --porcelain)" ]]; then
  echo "変更なし、PR スキップ"
  exit 0
fi

素直な実装です。そしてこれが 4 か月の空振りの正体でした。

PR が 1 つも立っていない

9 月になって子リポジトリの PR 一覧を見たとき、chore/sync-agent-rules-* という PR が 1 つも無いことに気づきました。

配布は動いていたはずなのに、なぜ PR が無いのか。履歴を追って分かりました。

5/3 の時刻できごと
18:36各子に手でコピー
18:37ワークフロー初版を追加(このときは PR ではなく直 push 方式)
22:31 / 22:40AGENTS.md を 2 回更新
22:42初版が発火し、bot が子へ直 push(成功)
22:58PR + 即 squash merge 方式に書き換え

つまり 5 月に配られたのは書き換える前の実装でした。子のログにもはっきり残っています。

$ git -C api log --format='%h %ad %an %s' --date=short
1b7a4ca 2026-05-03 github-actions[bot] Sync agent rules from parent repo
0d63a94 2026-05-03 ymzkryo            Sync agent rules from parent repo

PR が無いのは当然で、当時のワークフローは PR を作らない実装だったからです。

書き換えた部分が 4 か月走らなかった

問題は 22:58 の書き換え以降です。

配布対象の 4 ファイルを、私はその後 9 月 8 日まで一度も更新しませんでしたon.push.paths はこの 4 ファイルを見ているので、push 起因の発火は 0 回です。

$ git log --format='%ad %s' --date=short -- AGENTS.md CLAUDE.md GEMINI.md .github/copilot-instructions.md
2026-09-08 docs: agent rules にユーザーの裁量尊重とファイル名の命名規則を追加
2026-05-03 PR #1 のセルフレビュー指摘を反映
2026-05-03 AGENTS.md に作業前プランニングとブランチ戦略のセクションを追加
2026-05-03 Add agent rules (AGENTS.md) and sync workflow

その間に何度か手動実行(workflow_dispatch)はしていて、ログはすべて成功でした。ただしその成功は、全部「変更なし」で早期 return したものです。

if [[ -z "$(git status --porcelain)" ]]; then
  echo "変更なし、PR スキップ"
  exit 0
fi

親と子が一致していれば、コピーしても差分が出ません。差分が出なければここで抜けます。

結果として、書き換えた本体——PR を作って squash merge してブランチを削除する部分——が 4 か月間、一度も実行されていませんでした。リファクタした直後に一度通しておかなかったせいで、次に条件が揃うまで誰も気づけない状態になっていたわけです。

実差分を作って通した

確かめるには、実際に差分を作るしかありません。ダミーの空行を入れるのは気が引けたので、本当に入れたかったルールの追加で兼ねました

親の AGENTS.md に 2 つ足して push したところ、on.push.paths で自動発火し、今度は 4 つの子すべてに PR が立ちました。

api   PR #6 chore/sync-agent-rules-37a9506  MERGED  ブランチ削除済み
blog  PR #8 同上                            MERGED  ブランチ削除済み
web   PR #4 同上                            MERGED  ブランチ削除済み
infra PR #5 同上                            MERGED  ブランチ削除済み

配布後、5 リポジトリの blob SHA を突き合わせました。

AGENTS.md                       cea36eae   (親 / api / blog / web / infra すべて一致)
CLAUDE.md                       5011b049
GEMINI.md                       49ea64e8
.github/copilot-instructions.md fe51ee8d

保護された main に対して、PR 作成 → squash merge → ブランチ削除まで通ることを、private な infra を含む全リポジトリで確認できました。これでようやく「動く」と言えます。

早期 return は、実装が正しいことを証明しない

このワークフローは 4 か月間、正しく動いていました。差分がないときに何もしないのは意図した挙動です。問題は、その分岐しか通っていなかったこと。書き換えた本体は一度も実行されていませんでした。

CI が緑でも、それは「実行されたコードが正しかった」ことしか意味しません。実行されなかったコードについては何も言っていないわけです。当たり前のことですが、ログに success が並んでいると、つい全体が検証されたような気になります。

思えば、gh pr list を一度でも叩いていれば 4 か月も気づかずにいることはありませんでした。「動いているはず」を確かめるコストは、たいてい数十秒です。

同じ月に、別の自動化がエラーも出さずに止まっていたのも見つけています(GitHub Actions の cron が、ある日から 1 日 48 回中 6 回しか動かなくなった)。あちらは「走るはずのものが走らない」、こちらは「走っているのに何もしていない」。どちらも失敗として現れないのが共通点でした。

まとめ

  • 共通ファイルは親を正として子へ配る。コピペ運用はズレます
  • 子の main が保護されていても、PR を立てて即 squash mergeすれば通ります
  • GITHUB_TOKEN では PR を作れないことがあります。設定で禁止でき、そのときは PAT が要ります
  • 実装を書き換えたら、その場で一度通す。 次に条件が揃うまで誰も気づけません
  • 動いているつもりの自動化は、出力側(PR が立っているか、ファイルが揃っているか)を一度見るのが確実です

書いた仕組みが動いていることを確かめるところまでが実装だな、と反省しました。