自動マージが発火しない——GITHUB_TOKENの罠
GITHUB_TOKENで作ったPRはpull_requestイベントを発火しないGitHubの再帰防止仕様で、自動マージが無言で止まった実例をdiff付きで解説する。
この記事の結論: pull_requestイベント頼みの自動化は、GITHUB_TOKENが作ったPRだと発火しないことがある。pushイベント側で完結する救済経路もセットで用意しないと、気づかないまま放置される。
リナ社長(高校生社長のやり手ギャル)× タクヤ(入社3年目)— 今回は、自動マージのはずが人力マージの痕跡が残ってた事件の回。(登場人物について)
リナ社長「タクヤ、これ見て。マージコミットのメッセージ、なんか見たことない形になってない?」
タクヤ「Merge branch 'claude/sweet-einstein-wvvqgl': Astro 7記事を追加……ですね。うちのauto-merge-blog.ymlはgh pr merge --squashでマージするはずなので、squashコミットなら『PRタイトル (#番号)』の形式になるはずです。これは違いますね」
リナ社長「じゃあこれ、自動マージじゃなくて誰かが手でマージしたってこと?」
タクヤ「その可能性が高いです。7月6日の20:17に、この形式のマージコミットが2つ続けて入ってます。1e3a98e(Astro 7記事)と400e8e7(Biome比較記事)。両方とも通常のgit mergeの痕跡です」
事件発覚: 見慣れないマージコミット
うちのブログ運用は、クラウド実行環境のエージェントが記事を書いてpushし、.github/workflows/auto-merge-blog.ymlがブログ記事のみの変更を検知して自動でsquashマージ、Cloudflareが自動デプロイする、という流れになっている。
ところが7月6日20:17にマージされた2本の記事は、squashマージの痕跡ではなく通常マージの痕跡だった。つまり、本来ならpull_requestイベントで自動マージされるはずのPRが、何らかの理由で自動マージジョブを起動できず、ブランチが放置されかけていた。
タクヤ「放置されかけてた、ってことは誰かが気づいて手でマージしたってことですよね。それ誰です?」
リナ社長「うーん、そこはログに残ってないから断定はできないんだけど、少なくとも自動マージが刺さってなかったのは事実。原因調べよ」
原因調査: GITHUB_TOKENの再帰防止
タクヤ「調べてみたら、GitHub Actionsの仕様に行き着きました。GITHUB_TOKENで作成したPRやpushは、デフォルトでは新たなワークフロー実行をトリガーしない仕様があるんです。無限ループ防止の再帰防止機構ですね」
リナ社長「待って、それって……」
タクヤ「はい。うちのauto-merge-blog.ymlはpull_requestイベントのopenedとかで発火する設計でした。でもエージェントがクラウド環境からGITHUB_TOKEN相当の権限でPRを作っても、そのPR作成イベント自体がワークフローを起動しない。だから自動マージジョブが一度も走らなかったんです」
リナ社長「つまり、自動マージの仕組み自体は正しく書けてたのに、そもそも起動する入り口が塞がれてたってこと?」
タクヤ「そういうことです。バグというよりGitHub側の安全機構に引っかかった形ですね」
なお、これは以前の記事github-workflow-scope-error.mdで扱った「OAuth Appにworkflowスコープが無くgit push自体が拒否される」問題とは別の話だ。あちらはpushが拒否される問題、今回はpushやPR作成は通るが後続イベントが発火しない問題で、原因も現象も別物になる。
対策1: フォールバック手順をauto-blog-prompt.mdに追記
クラウド実行環境はmasterへの直接pushを許可していないことがある。その場合エージェントはブランチにpushしてPRを作るしかない。そこで自動投稿の指示書auto-blog-prompt.mdを、コミットbd0d986(20:34)で以下のように直した。
-4. 以下の手順でpushする(失敗時は最大3回リトライ):
+4. 以下の手順でpushする:
git pull --rebase origin master
git commit -m "content: <記事タイトル>を追加"
git push origin master
-- push失敗時はエラー内容を確認し、`git pull --rebase origin master` をやり直してから再push
+- `git push origin master` が失敗した場合は `git pull --rebase origin master` をやり直してから再push(最大3回)
+- **3回とも拒否された場合(クラウド実行環境はmasterへの直接pushを許可していないことがある)はブランチ経由にフォールバックする**:
+
+git push origin HEAD:refs/heads/claude/auto-blog-<YYYYMMDD>
+gh pr create --base master --head claude/auto-blog-<YYYYMMDD> \
+ --title "content: <記事タイトル>を追加" \
+ --body "自動ブログ投稿ルーティンによる記事追加(ブログ記事のみの変更)。auto-merge-blog.yml が自動マージする。"
+
+- ブログ記事のみの変更であれば `.github/workflows/auto-merge-blog.yml` がPRを自動でsquashマージし、Cloudflareへデプロイされる
+- `gh pr create` が失敗しても、ブランチpushが成功していれば同ワークフローがpushイベント側で自動マージするため、ブランチ名をログに出力して**正常終了**とする(ブランチを放置したまま黙って終わらない)
- リトライ時に `git remote set-url` でURLにトークンを埋め込まないこと
-5. push成功 → 完了ログ(ファイル名・タイトル・トラック区分・コミットハッシュ)を出力
- push失敗(3回とも) → エラー内容をログに出力して終了
+5. push成功(master直接またはブランチ経由) → 完了ログ(ファイル名・タイトル・トラック区分・コミットハッシュ。ブランチ経由の場合はブランチ名とPR番号も)を出力
+ すべて失敗 → エラー内容をログに出力して終了
リナ社長「あれ、ちょっと待って。ブランチ+PRにフォールバックしても、GITHUB_TOKENで作ったPRならpull_requestイベント自体が発火しないんじゃないの? だったら結局、自動マージされないまま止まるだけじゃない?」
タクヤ「……その通りです。bd0d986だけだと同じ穴にまたはまります。気づいて、9分後のe1c0418で手を入れました」
対策2: pushイベント側で完結する救済ジョブ
発覚(20:17)から26分ほどで対策を入れた。bd0d986のさらに9分後、コミットe1c0418(20:43)でauto-merge-blog.ymlに、pull_requestイベントの発火を待たずにpushイベント側だけで完結する経路を追加している。
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [master]
+ push:
+ branches: ['claude/**']
そして既存のauto-mergeジョブはpull_requestイベント専用に条件を絞り込み、新しくauto-merge-branch-pushジョブを追加した。
+ auto-merge-branch-push:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: ブログ記事のみの変更ならPR作成して自動マージ
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BRANCH: ${{ github.ref_name }}
+ REPO: ${{ github.repository }}
+ run: |
+ git fetch origin master
+ files=$(git diff --name-only "origin/master...HEAD")
+ if [ -z "$files" ]; then
+ echo "masterとの差分がありません。スキップします。"
+ exit 0
+ fi
+ echo "変更ファイル一覧:"
+ echo "$files"
+ nonblog=$(echo "$files" | grep -v '^src/content/blog/' || true)
+ if [ -n "$nonblog" ]; then
+ echo "ブログ記事以外の変更を含むため自動マージしません:"
+ echo "$nonblog"
+ exit 0
+ fi
+ # エージェント自身がPRを作成済みならPR経路(auto-mergeジョブ)に任せる(二重マージ防止)
+ open_prs=$(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json number --jq length)
+ if [ "$open_prs" != "0" ]; then
+ echo "openなPRが既にあります。PR経路に任せます。"
+ exit 0
+ fi
+ subject=$(git log -1 --format=%s HEAD)
+ gh pr create --repo "$REPO" --base master --head "$BRANCH" \
+ --title "$subject" \
+ --body "claude/** ブランチへの直接push(ブログ記事のみの変更)を自動マージ経路に載せるため、workflowが自動作成したPR。"
+ gh pr merge --repo "$REPO" --squash --delete-branch "$BRANCH"
ポイントは、pull_requestイベントの発火を待たず、pushイベントを受けた同一ジョブの中でgh pr createからgh pr merge --squash --delete-branchまで完結させていること。後続イベント待ちにしないことで、再帰防止の壁を回避している。
リナ社長「open_prsのチェック、これ何のためです?」
タクヤ「二重マージ防止です。エージェント自身が既にPRを作れてた場合、このジョブでも同じブランチをまた処理すると二重にマージ処理が走りかねない。だからgh pr list --head "$BRANCH" --state openで先にPRの有無を見て、あればPR経路に譲るようにしてます」
リナ社長「地味だけど大事なガードだね」
これでbd0d986のフォールバック手順とe1c0418の救済ジョブが繋がった。
リナ社長「これでPR作成すら失敗しても、ブランチさえpushできてればpushイベント側の救済ジョブが拾ってくれる、と」
タクヤ「そうです。二段構えになりました」
リナ社長「で、これウチの何が変わるの? 技術的な話は分かったけど、結局何が防げるようになったわけ?」
タクヤ「『自動投稿したのに記事が公開されない』という、ブログ運営フローが無言で止まる穴が塞がれました。今までは気づかなければブランチが溜まり続けて、週3本ペースの更新頻度が落ちる恐れがありました。対策自体はワークフロー1ファイルへの追加だけで済んでいます」
タクヤ「技術的には妥当な対策だと思います」
リナ社長「でも私が引っかかるのは、今回だって『たまたまマージコミットの形式がおかしいのに気づいた』から発覚しただけってこと。気づかなかったらどうなってた? 通知とか監視の仕組みが無いのが本質的な課題だと思うんだけど」
タクヤ「……それは正直、刺さります。今回の修正は事後対応で、ブランチが放置されたこと自体を検知する仕組みはまだ無いです。次はそこですね」
GITHUB_TOKENで作ったPRやpushは、ワークフローの再帰防止でイベントが発火しないことがある。pull_requestイベント頼みの自動化を組むときは、pushイベント側で完結する救済経路もセットで用意しておく必要がある。
みんなのbot、pull_requestイベント頼みになってない?
コメント