{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreia6rqvzceklm6c6vb44vyuudwuyatiflkm2tey76ea54temu7q6na",
"uri": "at://did:plc:g4wcucb6ko2frmko2x3lvgyi/app.bsky.feed.post/3mlqmotzyla72"
},
"path": "/2026/05/13/automerge-gate/",
"publishedAt": "2026-05-13T15:30:30.945Z",
"site": "https://efcl.info",
"tags": [
"automerge-gate",
"pkgdeps/automerge-gate",
"Auto Merge",
"Branch protection ruleやRuleset",
"upsidr/merge-gatekeeper",
"textlint",
"CI: add Merge Gatekeeper workflow for pull requests by azu · Pull Request #1577 · textlint/textlint",
"利用できるランナー",
"単価",
"merge-gatekeeper",
"architecture.md",
"secretlint/secretlint#1557",
"Triggering a workflow from a workflow - GitHub Docs",
"About protected branches - GitHub Docs",
"Automatically merging a pull request - GitHub Docs"
],
"textContent": "GitHubのAuto Mergeをひとつの必須チェックに集約するためのGitHub Action automerge-gate を作ったので紹介します。\n\n * GitHub: pkgdeps/automerge-gate\n\n\n\n## 背景: GitHubの必須チェック設定はPRごとの揺らぎに弱い\n\n前提として、GitHubのAuto Mergeを使うには、必須チェック未達成のPRをマージできない状態にするBranch protection ruleやRulesetの設定が必要です。 これらの保護機能でPRがブロックされる状態を作ったうえで、すべての必須チェックが成功した時点でAuto Mergeが発火する、という仕組みになっています。 逆に言うと、Auto Mergeを使うには何かしらのステータスチェックを必ず必須に入れる必要があります。\n\nそして、Branch protection ruleやRulesetは、マージに必要なステータスチェックを名前で列挙する形式です。 この方式は次のような場面で壊れやすいという問題があります。\n\n * RenovateやDependabotなど外部のGitHub Appが追加するチェックは、PRごとにあったりなかったりする\n * monorepoでパスフィルタを使っていると、ワークフローがPRによってスキップされたりされなかったりする\n * 新しいワークフローを追加する度に、Rulesetを書き換える必要がある\n\n\n\nGitHubのRulesetは複数の必須チェックをANDでつなぐ(全部成功すること)しか表現できないため、「チェック群のうちどれかが走っていればよい」みたいな条件は書けません。 そのため、PRごとに発火するチェックが違うケースだと、片方のPRでは存在しないチェックを必須にしてしまい、いつまでもマージできないという状態が発生します。\n\nこの問題への対処として、必須チェックを1つのステータスに集約するupsidr/merge-gatekeeperを使っているケースも多いです。 自分もtextlintなどのオープンソースプロジェクトや、プライベートリポジトリで使っていました。\n\n * CI: add Merge Gatekeeper workflow for pull requests by azu · Pull Request #1577 · textlint/textlint\n\n\n\n最近はmerge-gatekeeperからautomerge-gateに入れ替えて使っています。 automerge-gateも同じ「集約された1つの必須チェック」というアプローチですが、GitHubのAuto Mergeと組み合わせて使うことを前提に作られています。 必須チェックとして登録するのは`automerge-gate/all-passed`の1つだけで、ワークフローやGitHub App由来のチェックをこのアクションが集約してくれます。\n\n## 仕組み\n\nautomerge-gateには2つのモードがあります。\n\n * Private mode — フォークPRを受け取らないリポジトリ向けのコスト最適化モード。マージ意図のないPRではアクション自体が早期returnして、ランナー時間を消費しない\n * Public mode — フォークPRを受け取るリポジトリ向け。フォークPRでは`GITHUB_TOKEN`が読み取り専用になるため、ジョブ自身の`check_run`の終了コードがゲート信号になる\n\n\n\nメインのユースケースはPrivate modeで、Public modeはオープンソースプロジェクトのようなフォークPR対応用です。\n\n### Private mode (メインのユースケース)\n\nPrivate modeでは、アクションがREST APIで集約結果をcommit statusとして書き込みます。 重要なのは、PRが開かれただけでマージ意図がない状態(Auto Merge未有効 & write権限のApproveなし)では、アクション側はポーリングをせずに何も書き込まずにすぐに終了する点です。\n\nこのとき、必須チェック`automerge-gate/all-passed`はGitHubのデフォルトの`Expected — Waiting for status to be reported`のままです。 そのため、マージはブロックされた状態が維持されます。 メンテナがAuto Mergeを有効化したタイミング、またはwrite権限を持つレビュアーがApproveしたタイミングで、初めてアクションがポーリングを開始してチェックを集約しにいきます。\n\nジョブ自体は軽量で、PRの`check_run`を一定間隔(デフォルト30秒)でポーリングして集約結果を計算するだけです。 依存関係のビルドもなく、`runs-on: ubuntu-latest`の標準ランナーで十分動きます。 ポーリングしない場合のジョブは数秒で終わるので、Auto MergeがONになる前のPRではランナー時間をほぼ消費しません。\n\nこのスキップ動作によって、プライベートリポジトリでのコスト効率が改善できます。 GitHub Actionsの料金はジョブ単位の1分未満切り上げで、利用できるランナーごとに単価が異なります。 代表的なものを抜粋すると次のとおりです。\n\n`runs-on:` | スペック | 料金\n---|---|---\n`ubuntu-latest` | Linux 2-core (x64) | $0.006 / 分\n`ubuntu-24.04-arm` | Linux 2-core (arm64) | $0.005 / 分\n`ubuntu-slim` | Linux 1-core (x64) | $0.002 / 分\n\nmerge-gatekeeperは同等の集約処理をしてくれますが、Auto MergeがONかどうかに関わらず、PRが開かれた時点から他のチェックが揃うまでポーリングを続ける設計です。 そのため、1回のポーリングはCI全体が完了するのにかかる時間(例: 10分)とほぼ同じだけ走り続けます。 さらにpushするたびに同じポーリングが起きるので、`push数 × ポーリング時間`の課金が発生します。 さらにmerge-gatekeeperは内部でDockerコマンドを使うため、Dockerが使えない`ubuntu-slim`では動きません。 そのため、$0.006/分の`ubuntu-latest`を選ぶ必要がありました。\n\nautomerge-gateの場合、本体はNode.js製のActionでDockerに依存していないため、`ubuntu-slim`(1-core, $0.002/分)でも動かせます。 加えてPrivate modeでは、Auto Mergeが有効化されておらずwrite権限ありのApproveもないPRに対してはポーリング自体を開始しません。 ジョブはトリガーされるので1分切り上げの最低課金(`ubuntu-slim`なら$0.002)は発生しますが、CI完了までポーリングし続けることはなくなります。\n\nただし、merge-gatekeeperとは視覚的な違いがあります。 merge-gatekeeperの場合、常にポーリングしているので、すべてのチェックが揃えば集約チェックがグリーンになります。 一方automerge-gateのPrivate modeでは、Auto MergeまたはApproveまで`automerge-gate/all-passed`は`pending`のままです。 GitHubの表示上は`Expected — Waiting for status to be reported`と出ます。\n\nCommit statusは`(SHA, context)`の組をキーにしてGitHubが評価するので、新しいコミットがpushされても自動的に新しいSHAに対して再評価が走ります。Auto Mergeを一度有効にしたら、その後はpush毎に有効/無効を切り替える必要はありません。\n\n## 設定方法\n\n設定は次の5ステップです。\n\n 1. モードを選ぶ(Private / Public)\n 2. `.github/workflows/automerge-gate.yaml`を追加\n 3. Ruleset(またはBranch protection)で`automerge-gate/all-passed`を必須チェックに登録\n 4. リポジトリ設定で「Allow auto-merge」を有効化\n 5. PRで「Enable Auto Merge」をクリック\n\n\n\n### ワークフローファイル (Private mode)\n\n`.github/workflows/automerge-gate.yaml`は次のような内容になります。\n\n\n name: automerge-gate\n\n on:\n pull_request:\n types: [opened, synchronize, reopened, auto_merge_enabled]\n pull_request_review:\n types: [submitted]\n\n concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true\n\n jobs:\n gate:\n if: >-\n github.event_name != 'pull_request_review' ||\n github.event.review.state == 'approved'\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n statuses: write\n checks: read\n pull-requests: read\n actions: read\n steps:\n - uses: pkgdeps/automerge-gate@v4.0.0\n with:\n gate-mode: 'private'\n context: 'automerge-gate/all-passed'\n\n\nポイントは次のとおりです。\n\n * `pull_request_review`の`if:`で`approved`のみを通している。GitHubの`on:`はレビューのstateで絞り込めないので、ジョブの`if:`で弾いて空振りでもrunnerが立ち上がらないようにしている\n * `timeout-minutes: 10`がポーリングループの唯一のタイムアウト。アクション側に独立した`timeout-seconds`入力はあえて用意されておらず、設定箇所を二重化しない方針になっている\n * 権限は`statuses: write`(commit status書き込み)と`checks: read`(チェックの集約読み取り)で十分\n\n\n\nApproveをマージ意図として扱いたくないチームは、`on:`から`pull_request_review`を外せば、Auto Mergeを明示的に有効化したときだけポーリングが走るようになります。\n\n### 必須チェックの登録\n\nSettings → Rules → Rulesetsを開いて、`automerge-gate/all-passed`を必須チェックに追加します。\n\n📝 必須チェックのドロップダウンは、過去にそのリポジトリで実行されたチェック名しかオートコンプリートしないので、初回設定時は候補に出てきません。手入力で`automerge-gate/all-passed`と入れる必要があります。\n\n### Auto Mergeの有効化\n\nSettings → General → Pull Requestsで「Allow auto-merge」をチェックします。これをしないとPRに「Enable Auto Merge」ボタンが出ないので、ステップ5が動きません。\n\n## 除外パターンの指定\n\nCodecovやNetlifyのプレビュー、Renovateなど特定のチェック/Appをゲートから外したい場合は、`ignore-apps`または`ignore-checks`で除外できます。\n\n\n - uses: pkgdeps/automerge-gate@v4.0.0\n with:\n gate-mode: 'private'\n ignore-apps: |\n dependabot\n renovate\n\n\n`ignore-checks`はglob(`*` / `?`)が使えます。\n\n\n - uses: pkgdeps/automerge-gate@v4.0.0\n with:\n gate-mode: 'private'\n ignore-checks: |\n optional-*\n docs-only\n\n\n`ignore-checks`がチェックするのはGitHub APIの`check_run.name`(=`jobs.<key>.name`)です。 GitHubのUIで見える`<workflow> / <job>`形式ではない点に注意してください。 実際にどの名前で記録されているかは、次のコマンドで確認できます。\n\n\n gh api \"repos/{owner}/{repo}/commits/{sha}/check-runs\" \\\n --jq '.check_runs[] | {name, app: .app.slug, conclusion}'\n\n\n## Public modeについて\n\nオープンソースプロジェクトのようにフォークPRを受け付けるリポジトリでは、フォークPRに対して`GITHUB_TOKEN`が読み取り専用になります。 そのため、Private modeのようにcommit statusをPOSTする方法は使えません。 書き込みできないと「待機中(=ステータス未設定)」の状態も表現できないため、Private modeでやっている「マージ意図がなければスキップ」もそのままでは成り立ちません。\n\nそこでPublic modeでは、ジョブ自身の`check_run`(GitHub Actionsが自動で作るもの)の終了コードをゲート信号として扱います。 ジョブの`name:`を必須チェックの名前(`automerge-gate/all-passed`)に揃えておくことで、ジョブの結果がそのまま必須チェックの結果になります。 この点はmerge-gatekeeperとほぼ同じ仕組みです。 代わりに「スキップで節約」はできなくなるため、Public modeでは全イベントで常にポーリングする形になります。\n\n代替案として`pull_request_target`で`GITHUB_TOKEN`に書き込み権限を持たせるアプローチもあります。 しかし、フォーク由来のコードを書き込み権限付きで動かすことになり、セキュリティ上の問題が大きいです。 そのため、この方式は採らずに「ジョブ自身の終了コードを信号にする」形に落ち着きました。 詳しい設計の背景は、architecture.mdにまとまっています。\n\nワークフローは次のようになります。\n\n\n name: automerge-gate\n\n on:\n pull_request:\n types: [opened, synchronize, reopened, auto_merge_enabled]\n\n concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true\n\n jobs:\n gate:\n name: automerge-gate/all-passed # ruleset側の必須チェック名と一致させる\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n checks: read\n pull-requests: read\n actions: read\n steps:\n - uses: pkgdeps/automerge-gate@v4.0.0\n with:\n gate-mode: 'public'\n\n\nPrivate modeとの違いは次のとおりです。\n\n * 権限は`checks: read`のみでよい(commit statusを書き込まないため)\n * 「マージ意図がないPRはスキップ」というコスト最適化は行わない。常にトリガーごとにポーリングする(`GITHUB_TOKEN`が読み取り専用だと”待機中”の信号を書き込めないため)\n * ジョブの`name:`が必須チェック名そのものになる\n\n\n\n実際のPublic modeでの実行例として、secretlint/secretlint#1557のログを見てみます。 `ubuntu-24.04`の標準ランナー上で、17個のチェックを集約している様子です。\n\n\n ##[group][00:05] Poll #1 — pending, 14/15 completed\n 🟡 Agent (in_progress)\n ✅ Analyze (javascript-typescript) (success)\n ✅ Analyze (javascript) (success)\n ✅ binary-test (success)\n ✅ CodeQL (success)\n ...\n ##[group][00:38] Poll #2 — success, 17/17 completed\n ✅ Agent (success)\n ...\n ✅ Passed (17):\n\n\nCodeQL、hadolint、secretlint、各OS/Node.jsのテストなど複数ワークフロー由来のチェックが、`automerge-gate/all-passed`の1つに集約されています。 ジョブ自体はチェック結果を読んで待つだけなので、約38秒で集約完了しています。\n\nオープンソースプロジェクトのようにフォークPRを受け付ける環境でなければ、Private modeを使うのが基本になります。\n\n## merge-gatekeeperとの細かな違い: GitHub Actionsが作ったPRのデッドロック\n\nGitHub ActionsがPRを作る場合、`secrets.GITHUB_TOKEN`で作成されたPRに対しては、無限ループ防止のために他のGitHub Actionsワークフローが発火しません。\n\n * 参考: Triggering a workflow from a workflow - GitHub Docs\n\n\n\nこのとき、ゲート用のワークフローも発火しないため、必須チェックがいつまでも報告されません。 merge-gatekeeperの場合は、PRイベントでしか動かないため、このPRはマージできないままデッドロックします。\n\nautomerge-gateはPrivate/Publicどちらのモードでも、`pull_request`の`auto_merge_enabled`イベントをトリガーに含めています。 さらにPrivate modeではApprove(`pull_request_review`の`submitted`)もトリガーに含まれます。 そのため、手動でAuto Mergeを有効化するかApproveすれば、人手起点でゲートを動かせます。 完全な自動化はできませんが、デッドロック状態からは一応抜け出せる構造になっています。\n\n## 制限事項\n\nautomerge-gateには次の制限があります。\n\n * Merge Queue非対応 — GitHubの`merge_group`イベントには非対応\n * ジョブのタイムアウト — `timeout-minutes`に達するとジョブが`failure` / `cancelled`で終わり、必須チェックは赤のまま残る。GitHub Actionsの「Re-run failed jobs」でリトライするか、Auto Mergeを一度無効化して有効化し直す\n * Legacy commit status APIのみのCI — AtlantisやJenkinsの一部のような、legacy commit status APIだけを使うCIは集約対象にならない\n * 該当するCIはRulesetに直接必須チェックとして追加し、`automerge-gate/all-passed`と並列に置く必要がある\n\n\n\n## バージョニング\n\nautomerge-gateのリリースは、`v4.0.0`のような不変のSemVerタグで公開されます。 `v4`のように移動するメジャータグは意図的に作っていないので、ワークフロー側では固定バージョンを指定して、RenovateやDependabotで更新するスタイルが推奨されています。これは、移動するタグが書き換えられるサプライチェーンリスクを避けるための設計です。\n\n## まとめ\n\nautomerge-gateは、GitHubのAuto Mergeとあわせて使う集約チェックのGitHub Actionです。\n\n * Rulesetに登録する必須チェックは`automerge-gate/all-passed`の1つだけで済む\n * Renovate/DependabotやmonorepoのパスフィルタによってPRごとにチェックが増減しても、自動的に集約される\n * Private modeでは、マージ意図のないPRではポーリングをスキップするためrunner時間をほぼ消費しない\n * フォークPRを受け取るオープンソースプロジェクトなどはPublic modeで対応できる\n\n\n\nmerge-gatekeeperと似たコンセプトですが、Auto Merge前提でコストを最適化している点が違います。 また、Private/Publicの2モードに分けて`GITHUB_TOKEN`の権限差に対応している点も異なります。\n\n## 参考\n\n * pkgdeps/automerge-gate\n * upsidr/merge-gatekeeper\n * About protected branches - GitHub Docs\n * Automatically merging a pull request - GitHub Docs\n\n",
"title": "automerge-gate: GitHubのAuto Mergeをひとつの必須チェックに集約するGitHub Action",
"updatedAt": "2026-05-13T11:00:00.000Z"
}