GitHub Actionsで「ワークフローが動かない」「同じPRで2回走る」と言われる原因は、ジョブの中身ではなくonの書き方に集中します。ブランチだけ指定してタグのpushで発火しない、pathsで絞ったせいでPRが永久にマージできない、ワークフローが作ったコミットで次のワークフローが起動しない。どれも仕様どおりの挙動です。本記事では発火条件の設計に絞り、イベントの選び分け、フィルタの併用ルール、モノレポでの制御、そしてpull_request_targetの権限リスクまでを実装できる粒度で扱います。GitHub Actions自体の概要とワークフローの基本構造はGitHub Actionsでできること・使い方の解説に譲ります。
まとめ:onの設計で先に決める3つの分岐と結論
先に結論を書きます。onの設計は3つの問いで決まります。1つ目は「誰の変更で走らせるか」。自リポジトリのブランチ更新だけならpush、レビュー前の検証ならpull_request、外部フォークからのPRに対して書き込み権限やシークレットが要るときだけpull_request_targetを検討します。
2つ目は「どのrefと、どのファイルに反応するか」。branchesとtagsは片方だけ書くともう片方では発火しません。branchesとbranches-ignore、pathsとpaths-ignoreは同一イベント内で併用できず、除外を混ぜたいときはpathsのリストへ先頭に否定演算子を付けた行を並べます。
3つ目は「そのチェックを必須にするか」。ここが最大の落とし穴で、必須ステータスチェックに指定したワークフローをpathsで飛ばすと、チェックはPendingのまま残りPRがマージできなくなります。必須にする予定のワークフローにパスフィルタを掛けるなら、同名の成功返しワークフローを併置するか、フィルタをジョブ側のifへ移してください。
迷ったときの初期値は、onにpush(既定ブランチのみ)とpull_request(フィルタなし)の2つだけを書き、実行時間が問題になってから絞り込む順序です。先に細かく絞ると、動かない原因の切り分けに時間を取られます。実行時間そのものの費用感は2026年改定後の分単価と無料枠の解説で確認できます。
主要イベントの選び分け:pushとpull_requestとreleaseの役割
onに書けるイベントは30種類以上ありますが、受託開発の現場で実際に使い分けが問題になるのは4つに絞られます。ここではそれぞれが「いつ」「どの文脈で」走るのかを整理します。
pushはブランチとタグの両方に反応するrefの絞り込みが前提
pushはコミットがリポジトリのrefへ到達した時点で発火します。注意したいのは対象がブランチだけではない点で、タグのpushでも同じイベントです。フィルタを何も書かなければ、機能ブランチへの中間コミットでもリリースタグの作成でも同じワークフローが走ります。
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- 'apps/api/**'
- '.github/workflows/api.yml'
この例ではmainへの更新かvで始まるタグのどちらかに一致し、かつ変更ファイルがapps配下のapiディレクトリかこのワークフロー定義そのものであるときに走ります。ref側の条件はor、パス側の条件はandで効く点を押さえてください。ビルドとテストを実際に組む手順はビルド・自動テストの設定方法の記事にまとめています。
pull_request既定3種はopened・synchronize・reopened
pull_requestは活動タイプを書かなければ、PRが作成されたとき、ヘッドブランチが更新されたとき、再オープンされたときの3種類で走ります。ラベル付与やレビュー要求では走りません。逆にラベルを起点にしたい場合はtypesで明示します。
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
branches:
- main
paths-ignore:
- 'docs/**'
もう1つ押さえたいのは実行される中身です。pull_requestのワークフローはヘッドブランチそのものではなく、ヘッドをベースへ仮マージしたref(pull配下のmergeブランチ)の上で走ります。手元では通るのにCIだけ落ちる場合、ベース側の変更を取り込んだ結果である可能性を先に疑ってください。なお下書き(Draft)のPRでも既定の活動タイプは発火します。下書きの間は走らせたくないなら、ジョブ側でPRのdraftプロパティを判定するか、typesにready_for_reviewを足す設計になります。
releaseとタグpushを二重に走らせないための切り分けの原則
リリース時の配布処理でよく起きるのが二重実行です。タグを作ってからGitHub上でリリースを公開する運用だと、pushのタグ条件とreleaseの両方が反応します。同じ成果物を2回作って同じ場所へ上書きする事故になりやすいので、どちらか一方に寄せてください。
判断基準は単純です。リリースノートや添付ファイルの情報を使うならrelease(活動タイプはpublishedが実用的)、タグ名だけで完結するならpushのタグ条件です。リリース側の作法はリリースノート作成とタグ連携の解説が参考になります。マージキューを使っている組織では、キュー投入時に走るmerge_group(活動タイプはchecks_requested)も選択肢へ入ります。時刻で起動させたい処理は定期実行(cron)の設計解説にまとめています。
フィルタは直感に反する制約をいくつか持っています。ここを知らないまま書くと「絞ったつもりが全部走る」「絞ったら何も走らない」の両方が起きます。
branchesとbranches-ignoreを同一イベントで併用できない仕様
公式ドキュメントは、同一イベントに対してbranchesとbranches-ignoreの両方を使うことはできないと明記しています。tagsとtags-ignore、pathsとpaths-ignoreも同じ扱いです。「mainとreleaseブランチは対象、ただしdraft用のブランチは除外」のような条件を書きたいときは、肯定側のリストに否定演算子付きの行を足す形へ寄せます。
on:
push:
branches:
- 'main'
- 'release/**'
- '!release/draft-**'
パターンにはグロブが使えます。アスタリスク1つはスラッシュを越えず、2つ並べると越えるという違いがあるため、階層を持つブランチ名を扱うときは後者を選びます。行の評価順は関係なく、否定行は常に肯定行の結果から差し引かれる挙動です。
pushでbranchesだけ書くとタグのpushでは発火しない理由
これは仕様として明文化されている挙動です。branchesまたはbranches-ignoreだけを定義した場合、定義していない側のref(つまりタグ)に影響するイベントではワークフローが走りません。逆も同じで、tagsだけを書けばブランチへのpushは無視されます。両方に反応させたいときは、両方のキーを並べて書く必要があります。
リリース用ワークフローが「タグを打っても動かない」という相談の大半はこれが原因です。既定ブランチ向けのCIをコピーしてタグ条件を足したつもりが、元のbranches指定が残っていて、タグ側の条件を書き忘れている構図になります。
pathsとpaths-ignoreの排他と否定演算子で例外を書く方法
パスフィルタは、そのpushやPRに含まれる変更ファイルのうち1つでも条件に一致すれば発火する評価です。pathsがホワイトリスト、paths-ignoreがブラックリストにあたり、両方を同時には書けません。ドキュメント以外の全変更を対象にしたいなら後者、特定ディレクトリだけを対象にしたいなら前者、という選択になります。
on:
pull_request:
paths:
- 'packages/**'
- '!packages/**/*.md'
- '!packages/**/__snapshots__/**'
ここで気を付けたいのは、パスフィルタが評価するのは「そのイベントに含まれる差分」である点です。PRのsynchronizeでは直前のpush分ではなくPR全体の差分が対象になるため、初回でドキュメントだけを直し、次のpushでコードを直すと2回目から発火します。フィルタが効かないと感じたときは、まず変更ファイル一覧が想定どおりかを確認してください。
モノレポの発火制御:pathsで絞ると必須チェックが保留で残る
ここからがモノレポ運用の本題です。1つのリポジトリに複数のサービスを置くと、無関係な変更で全ジョブが走る無駄を消したくなります。ところがパスフィルタとブランチ保護の組み合わせには、公式に既知として書かれている相性問題があります。
pathsフィルタで飛ばした必須チェックがPendingで残る仕組み
公式のトラブルシューティングは、パスフィルタ・ブランチフィルタ・コミットメッセージによってワークフローがスキップされた場合、そのワークフローに紐づくチェックはPendingの状態で残ると説明しています。必須ステータスチェックに指定されていれば、PRはそのチェックの完了を待ち続け、マージできません。スキップは「成功」ではなく「未報告」として扱われるためです。
同じ文書は、ジョブ側の条件分岐でスキップされた場合はSuccessとして報告されるとも書いています。つまり同じ「走らせない」でも、ワークフロー単位で飛ばすか、ジョブ単位で飛ばすかによって結果は正反対です。必須チェックの設定そのものは必須ステータスチェックの設定方法の記事で確認できます。
同名ワークフローを併置して必須チェックへ成功を返す公式の回避手順
公式が案内する回避策は、同じ名前・同じジョブ名を持つ2本目のワークフローを用意し、1本目とは逆のパス条件で成功だけを返す構成です。片方が走らないときにもう片方が必ず走るため、必須チェックは常に報告されます。
name: api-ci
on:
pull_request:
paths-ignore:
- 'apps/api/**'
jobs:
api-test:
runs-on: ubuntu-latest
steps:
- run: echo 'api に変更が無いため成功として報告する'
本体側は同じnameと同じジョブ名api-testを持ち、pathsでapi配下を指定します。2本の定義が食い違うと片方が報告されない状態に戻るため、パス条件は必ず対にして管理してください。この方式は定義が二重化する代わりに、無関係な変更でランナー時間を消費しない利点が残ります。
ジョブ側のifで分岐させて発火自体は起こす設計が適する使いどころ
もう1つの選択は、ワークフロー自体はフィルタ無しで必ず起動させ、重い処理をジョブのifで切る方法です。差分の判定には変更ファイルを列挙するアクションを使い、その出力を後続ジョブの条件に渡します。ジョブとステップで参照できるコンテキストが違う点を含め、条件式の書き方はGitHub Actionsの条件分岐(if)|書く場所で変わるコンテキストと評価の規則にまとめました。
jobs:
detect:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v7
- id: filter
run: echo "api=true" >> "$GITHUB_OUTPUT"
api-test:
needs: detect
if: needs.detect.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- run: npm test
この構成なら必須チェックはSuccessで報告され、Pendingで止まる問題は起きません。代償は、判定ジョブ分のランナー起動が毎回発生することと、YAMLの依存関係が増えることです。アクションのバージョン表記は2026年8月時点の最新メジャーであるv7系を例に置いていますが、実運用ではコミットSHAで固定する方法の解説のとおりSHA固定を推奨します。
判断の目安は、必須チェックにするかどうかです。必須にするなら本項のジョブif方式、必須にしないならワークフロー単位のpaths方式で構いません。定義の重複を嫌う組織ほど前者へ寄ります。
pull_request_targetの権限リスクと採用できる条件の線引き
フォークからのPRでラベルを付けたい、コメントを書きたい、シークレットを使いたい。この要求からpull_request_targetへ手を伸ばす場面がありますが、扱いを誤ると資格情報の流出に直結します。
ベース側の文脈で走るためシークレットと書き込み権限が付く条件
公式ドキュメントは、このイベントがマージコミットではなくベースリポジトリの既定ブランチの文脈で実行されると説明しています。実行されるワークフロー定義はベース側のものであり、シークレットにもアクセス可能です。そのうえで、このトリガーで信頼できないコードを実行するとセキュリティ脆弱性につながる可能性があると警告し、キャッシュ汚染や書き込み権限・シークレットへの意図しないアクセスを例に挙げています。
危険なのはイベント自体ではなく、そこでPRのヘッドを明示的にチェックアウトして実行する書き方です。ベース側の定義で走っていても、ビルドスクリプトや依存パッケージのインストールフックは外部の投稿者が書き換えられます。
forkからのPRでGITHUB_TOKENが読み取り専用になる境界
通常のpull_requestでは、フォークからのPRに与えられるGITHUB_TOKENは読み取り専用へ落とされ、シークレットも渡りません。縮退が起きる条件とスコープの絞り方はGitHub Actionsのpermissions|GITHUB_TOKENの既定権限と最小権限の設計で解説しています。これは事故を防ぐための境界であり、ラベル付与やコメント投稿が失敗するのはこの制約が働いているためです。pull_request_targetはその境界を外す選択にあたります。トークンやシークレットの置き場所そのものはenv・vars・secretsの使い分けの記事で整理しています。
未検証コードを実行しない構成に限って採用する判断時の安全基準
採用条件は言い切れます。pull_request_targetを使ってよいのは、PRのヘッドをチェックアウトせず、外部由来のコードを一切実行しない処理だけです。具体的には、ラベル付与、コメント投稿、Projectsへの追加、初回投稿者への案内メッセージのような、イベントのメタデータだけで完結する処理に限られます。
見送るべき場面も明確です。フォークからのPRでビルドやテストを回したい、E2Eテストで本物の認証情報を使いたい、という要求にこのイベントを充てるのは避けてください。その場合はpull_requestのまま走らせて権限が要る後処理だけを別ワークフローへ分離するか、承認を挟む運用へ切り替えます。処理を分けて再利用したいときはworkflow_callによる呼び出しの解説の構成が使えます。
発火しない・二重で走るときの切り分け手順とチーム運用の決め方
最後に、設定が正しく見えるのに想定どおり動かないケースを潰します。ここに挙げる3つは仕様であって不具合ではないため、ログを追っても原因にたどり着けません。
GITHUB_TOKENによるpushが新しい実行を起こさない仕様
公式ドキュメントは、GITHUB_TOKENによってトリガーされたイベントは新しいワークフロー実行を作らないと明記しています。ワークフローがリポジトリのGITHUB_TOKENでコードをpushした場合、pushで走る設定があっても次の実行は起きません。再帰的な無限ループを防ぐための仕様です。
バージョン更新の自動コミットやフォーマッタの自動修正で「その後のCIが走らない」のはこれが理由です。意図して連鎖させたい場合は、GitHub Appのインストールアクセストークンか個人アクセストークンを使ってpushする構成へ切り替えます。トークンの管理コストが増えるため、連鎖が本当に必要かを先に検討してください。
skip ciのコミット指定がpushとpull_requestにしか効かない
コミットメッセージにskip ciなどの語を角括弧で囲んで入れると実行を飛ばせますが、これが効くのはpushとpull_requestだけです。公式ドキュメントは、たとえばpull_request_targetで起動するワークフローはコミットメッセージでは止められないと明示しています。トレーラー行としてskip-checks:trueを書く方式も同様の適用範囲です。
そして前節と同じく、この方法で飛ばしたワークフローのチェックもPendingで残ります。必須チェックを設定しているリポジトリでは、スキップした結果としてマージできなくなる点に注意してください。
受託開発でトリガーをどう決めるか判断軸と採用条件と見送りの線引き
ここは判断を言い切ります。プロジェクト開始時点で入れるべきなのは、既定ブランチへのpushとpull_requestの2本だけです。パスフィルタは、ランナーの実行時間が月次の想定枠を超え始めたか、CIの待ち時間がレビューの体感を落とし始めたか、このどちらかが観測できてから入れます。先に入れると、動かない理由の切り分けが増えるだけで得るものがありません。
フィルタを入れる段になったら、必須チェックの有無で方式を決めます。必須にしているチェックはジョブif方式、必須にしていない補助的なチェックはワークフローpaths方式です。pull_request_targetは、外部コントリビュータを受け入れるOSSか、それに準じた運用でない限り、初期構成に入れる理由がありません。
見送るべき場面も挙げます。リポジトリが単一サービスで、テスト一式が数分で終わるなら、パスフィルタの導入は保守対象を増やすだけです。またモノレポでも、サービス間の依存が強くて片方だけのテストでは品質が担保できないなら、絞り込みは逆効果です。CI・CD全体の設計思想と導入判断はCI/CDの仕組みと導入判断の基準で整理しています。既存のワークフローが増えすぎて手が回らない、社内に維持できる人がいないという状態であれば、保守運用・内製化支援のサービスで構成の棚卸しから引き継ぎまで対応しています。
よくある質問
onに複数のイベントを書いても問題ありませんか?
onの下にイベントを並べれば、いずれか1つが条件を満たした時点でワークフローが走ります。ただしイベントごとに使えるコンテキストが変わるため、イベントのペイロードを参照している箇所は注意が必要です。たとえばPR番号はpull_requestでは取得できますがpushでは存在しません。共通のジョブで両方を扱うなら、参照する値をイベント種別で分岐させるか、そもそもワークフローを分けたほうが読みやすくなります。
pathsフィルタが効かず毎回ワークフローが走るのはなぜですか?
まず変更ファイルの一覧を確認してください。パスフィルタはそのイベントに含まれる差分を対象に判定し、1つでも一致すれば発火します。PRの場合はPR全体の差分が対象になるため、過去のコミットで対象ファイルを触っていれば以降のpushでも走り続ける状態です。次に確認したいのはインデントで、pathsはonの直下ではなくイベント名の下、branchesと同じ階層に書きます。位置を1段間違えると、構文エラーにならないまま条件が無視される書き方になります。
pull_requestとpull_request_targetはどちらを使うべきですか?
原則はpull_requestです。pull_request_targetは、フォークからのPRに対してシークレットや書き込み権限が必要で、かつPRのコードを一切実行しない処理に限って選びます。ラベル付与やコメント投稿がこれにあたります。逆にビルドやテストのようにPRの内容を実行する処理へ充てると、外部から任意のコードでシークレットを読み出される構図です。公式ドキュメントも、このトリガーで信頼できないコードを実行すると脆弱性につながると警告しています。
ワークフローが自分のpushでもう一度走らないのは不具合ですか?
仕様です。GITHUB_TOKENを使って行われたpushやPR作成は、新しいワークフロー実行を作りません。無限ループを避けるための制限で、workflow_dispatchとrepository_dispatchが例外として扱われます。手動起動側の設計はGitHub Actionsの手動実行(workflow_dispatch)|inputsの型定義とCLI・APIからの起動を参照してください。自動コミットの後にCIを続けたい場合は、GitHub Appのインストールアクセストークンか個人アクセストークンでpushする構成に変えます。その分だけ資格情報の管理と権限設計の手間が増えるため、後続処理を同じワークフローの中で完結させられないかを先に検討してください。
タグをpushしてもリリース用ワークフローが動かない原因と確認手順
onのpushにbranchesだけを書いていないか確認してください。ブランチ側のフィルタだけを定義した場合、タグに影響するイベントではワークフローが実行されない仕様です。タグでも走らせたいならtagsを併記します。もう1つの原因は、リポジトリのブランチ保護やアクション権限の設定で実行が抑止されているケースです。実行履歴に何も残らないなら発火していないと判断でき、履歴はあるが即座に失敗しているなら権限側の問題として切り分けられます。
関連記事
- GitHub Actionsとは?できること・使い方とCI/CD自動化の運用例をわかりやすく解説:ワークフローの基本構造と機能の全体像を扱っています。
- GitHub Actionsでビルド・自動テストを設定する方法|CI/CDワークフローの作り方:トリガーを決めた後に組む処理本体の手順です。
- GitHub Actionsの環境変数|env・vars・secretsの使い分けと受け渡し:シークレットと構成変数の置き場所を整理しています。
- Require status checks to passとは?GitHubの必須ステータスチェックを設定する方法:パスフィルタと組み合わせる前に読む設定手順です。
- Reusable Workflows(GitHub Actions)とは?workflow_callでの作成・呼び出しとinputs/secrets/outputsを解説:権限の要る処理を分離するときの構成が分かります。
- CI/CDとは?仕組み・パイプライン・導入すべき企業の判断基準を解説:発火条件を決める前提となる全体設計の考え方です。