GitHub

GitHub Actionsの条件分岐(if)|書く場所で変わるコンテキストと評価の規則

GitHub Actionsの条件分岐(if)|書く場所で変わるコンテキストと評価の規則

ifは1行で書けます。ところが、ジョブに書いたifからenvを参照しても何も起きず、ステップに書いたifからsecretsを読もうとすると値が空になります。原因は文法の誤りではなく、公式リファレンスの Context availability 表が書く場所ごとに参照できるコンテキストを別々に定めているためです。この記事では、評価される場所と暗黙のsuccess()、ジョブとステップで違う参照範囲、4つのステータス関数の使い分け、比較で足をすくわれる型キャスト、プルリクエストでブランチ判定が必ず偽になる罠までを扱いました。ワークフローの基本構造はGitHub Actionsとは?できること・使い方とCI/CD自動化の解説記事、テスト実行の組み方はGitHub Actionsでビルド・自動テストを設定する方法を先に押さえてください。

まとめ:ifで先に決める4つの前提

第一に、ifを置ける場所はジョブとステップの2つです。同じ式を書いても、ジョブに置いたときとステップに置いたときで参照できるコンテキストが変わります。ジョブのほうが狭く、envもmatrixもstepsも届きません。

第二に、ステータスチェック関数を1つも書かなければ、success()が既定として黙って付きます。逆にalways()やfailure()を書いた瞬間、その暗黙の前提は外れました。失敗しても走らせたい後始末が動かない原因の大半はここにあります。

第三に、always()は公式が明確に避けるよう促している関数です。取り消し操作でも止まらないため、詰まった処理がタイムアウトまで居座ります。中止以外は必ず走らせたい、という意図なら否定形のcancelled()を囲んで書くのが既定の選択になります。

第四に、比較演算子は両辺を数値へ寄せてから突き合わせる仕様です。真偽値のtrueは1になり、文字列のほうはJSON数値として解釈できずNaNになります。この2つは決して一致しません。環境変数もステップの出力も常に文字列で返るため、引用符付きで比べる必要があります。

ifが評価される2つの場所と暗黙のsuccess()という前提

まず、どのタイミングで式が読まれるのかを揃えておきます。ここがずれていると、後段のコンテキストの話が単なる暗記になってしまうためです。

ジョブのifは開始前に一度・ステップのifは直前に評価される

ジョブに書いたifは、そのジョブを起こすかどうかの判定に使われます。偽ならジョブごと skipped になり、中のステップは1つも動きません。ステップに書いたifは、そのステップの直前に読まれます。偽ならそのステップだけが skipped で飛ばされ、ジョブ自体は成功として続きました。

この差が効いてくるのは、依存ジョブを持つときです。ジョブが skipped になると、そのジョブをneedsに指定した後続も連鎖して skipped になります。ステップの skip はジョブの結果を汚さないため、後続ジョブから見れば成功したジョブと区別が付きません。切り分けの粒度をジョブに置くのかステップに置くのかで、下流への伝わり方が変わると考えてください。

ステータス関数を書かない限り暗黙のsuccess()が付く規則

ステップにifを書かないとき、そこにはsuccess()が既定として適用されます。先行ステップが1つでも落ちれば、以降のステップは自動的に飛ばされる、という見慣れた挙動の正体がこれです。

問題は、ifに自前の条件だけを書いたときに起きます。イベント名の一致だけを書けば、ステータス関数を明示していないのでsuccess()は残り、先行が失敗すればこのステップも飛びます。ところがalways()を先頭に足して同じ条件をつなぐと暗黙の前提が外れ、先行の失敗を無視して走り出しました。後始末やレポート収集を仕込むときは、この切り替えを意識して書き分けてください。

区切り記号を省ける条件と感嘆符で始めたときに落ちる仕様を理解する

ifの値は式として読まれるため、区切り記号を省いてif: success()とだけ書けます。ただし例外は1つです。式が!で始まる場合、YAMLのパーサがこれをタグ指定として解釈してしまい、読み込みの段階で落ちます。

否定形をそのまま裸で書く形は動きません。区切り記号で囲んでください。否定を式の途中へ移してcancelled() == falseと書く逃げ方もありますが、公式の書き方に揃えたほうが後任が読みやすくなります。

jobs:
  build:
    if: github.event_name == 'push'
    runs-on: ubuntu-24.04
    steps:
      - run: make build
      - name: 失敗したときだけ通知する
        if: failure()
        run: bash scripts/notify.sh
      - name: 中止以外では必ず後始末する
        if: ${{ !cancelled() }}
        run: bash scripts/cleanup.sh

ジョブifとステップifで参照できるコンテキストの違いと回避策

ここがifで最初につまずく場所です。式の文法は同じでも、書いた位置によって解決できる名前の集合が違います。参照できないコンテキストは構文エラーにならず、静かに空として評価されるため、気付くのに時間がかかります。

書く場所 参照できるコンテキスト
ジョブのif github・needs・vars・inputs
ステップのif 左記+strategy・matrix等6種
ジョブのenv 左記の大半+secrets(stepsは不可)
ステップのenv ジョブのenv+job・runner・steps

ジョブのifが見られるのは4つのコンテキストだけという制約を確認する

ジョブのifから届くのはgithubとneedsとvarsとinputsの4つです。strategyとmatrixは含まれません。マトリックスの値でジョブごと落とそうとしても、参照が空になるため意図した絞り込みにならないのです。

マトリックスの値で分岐させたいなら、選択肢は2つあります。1つはステップのifへ判定を下ろす方法。もう1つはincludeで組み合わせごとにフラグ列を足し、ステップ側でそのフラグを読む方法です。組み合わせの展開規則そのものはGitHub Actionsのマトリックスビルド|組み合わせ展開とinclude・excludeの調整で扱っています。

secretsはどちらのifからも参照できず環境変数へ写して判定する

secretsはジョブのifにもステップのifにも出てきません。「シークレットが登録されていればデプロイする」といった条件は、ifへ直接は書けない仕様だと理解してください。

回避の道筋はenvです。ジョブ直下のenvはsecretsを参照できるので、シークレットの有無だけを真偽値として環境変数へ写します。ステップのifはenvを読めるため、そこで判定できるようになりました。写すのは値そのものではなく有無の判定結果に留めるのが要点で、値を素通しすると条件式やログの経路から漏れます。置き場所の選定と漏えい経路はGitHub ActionsのSecrets管理|置き場所の選定と漏えい経路・OIDC移行の判断を参照してください。

jobs:
  deploy:
    runs-on: ubuntu-24.04
    env:
      HAS_DEPLOY_KEY: ${{ secrets.DEPLOY_KEY != '' }}
    steps:
      - name: 鍵が登録されているときだけ配布する
        if: env.HAS_DEPLOY_KEY == 'true'
        run: make deploy

ジョブ側で分岐させたい値はvarsかoutputsのどちらかへ寄せる

ジョブのifで使える変数は実質varsとneedsの出力です。リポジトリや組織に登録した設定値ならvarsで直接読めます。実行時に決まる値なら、前段ジョブのoutputsへ載せてneeds経由で受け取る形になりました。

この2つで足りない場合、その判定はジョブ単位で切るべき条件ではない可能性があります。ステップ側へ下ろすか、ワークフローそのものを分けるほうが読みやすい構成に落ち着くはずです。

4つのステータス関数の使い分けと踏みやすい落とし穴を避ける設計

ステータスチェック関数は4つです。名前は素直ですが、どこまでを見て真偽を決めるのかがステップとジョブで違います。

4つの関数が見る範囲はステップとジョブで別物になる規則を理解する

ステップに書いた場合、success()は同じジョブ内の先行ステップがすべて成功したときに真を返します。failure()は先行ステップのいずれかが失敗したときに真、cancelled()は実行が取り消されたときに真、always()は常に真です。

ジョブに書いた場合、見る対象はneedsでつながる先行ジョブへ移ります。failure()は依存の連なりのどこかで失敗が起きていれば真になるため、直前の1本だけを見ているつもりだと判定を読み違えます。特定のジョブの結果だけで分けたいときは、関数ではなくneedsの結果を名指しで比べるほうが確実です。

always()を避けて否定形のcancelled()を使うよう公式が促す

公式リファレンスはalways()について、致命的な失敗が起こりうる処理には使わないよう明記しています。理由は、取り消しを掛けても該当のジョブやステップが止まらず、ワークフローがタイムアウトするまでハングしてしまうためです。手で止めたはずの実行が居座り続け、同時実行の枠と課金分数を食い続けます。

公式が挙げる代替はcancelled()を否定した形です。失敗しても走らせたいが、人が止めたときは素直に止まってほしい、という意図をそのまま表せます。テストが落ちてもレポートだけは回収したい、といった用途はこちらで足ります。成果物の受け渡し自体の設計はGitHub Actionsのアーティファクト|受け渡しの設計と同名不可・保持期間の決め方にまとめました。実行を途中で打ち切る側の制御はGitHub Actionsのconcurrency|groupの設計とcancel-in-progress・queueの選び方と組み合わせて考えてください。

needsのresultは4値でskippedを成功と混同しない書き方

needsの結果が返す値は success・failure・cancelled・skipped の4つです。否定形のcancelled()でジョブを起こすと、先行が失敗していても走り出します。ですから、拾い上げる条件と実行してよい条件は分けて書いてください。

やりがちなのは、中止以外という条件だけを書いたデプロイジョブです。テストが落ちても本番へ配りにいってしまいます。needsの結果を明示的に比べる1行を足せば防げました。skipped は失敗ではないため、条件付きで飛ばした前段を許容したいときは失敗との不一致で書く選び方もあります。どのジョブをどの順で結び、失敗とスキップがどこまで伝わるのかはGitHub Actionsのneedsによるジョブ依存の設計で扱っています。

jobs:
  deploy:
    needs: [build, test]
    if: ${{ !cancelled() && needs.build.result == 'success' && needs.test.result != 'failure' }}
    runs-on: ubuntu-24.04
    steps:
      - run: make release

ifの比較で意図しない結果になる型キャストの規則と対処の基本

式の評価は緩い等価で行われ、型が違えば両辺を数値へ寄せてから比べます。この変換の順序を知らないと、目で見て正しい条件が動かない状況に出会います。

文字列のtrueと真偽値のtrueが一致しないキャストの順序

変換の規則は決まっています。nullは0、真偽値のtrueは1でfalseは0、文字列はJSONの数値として解釈を試み、失敗すればNaN、空文字列は0、配列とオブジェクトはNaNです。

ここに文字列と真偽値の比較を当てはめてみます。真偽値の側は1、文字列の側は数値として読めないのでNaN。NaNはどの値とも一致しないため、この比較は偽になりました。文字列同士の比較なら大文字小文字を区別せずに突き合わせるので、綴りが同じなら大文字でも真になります。真偽値と文字列を混ぜないことが唯一の予防策です。

環境変数と出力は常に文字列で返るため引用符付きで比べる正しい方法

envの値とステップのoutputsの値は、どう書いても文字列として届きます。真偽値と直接比べる書き方は先ほどの規則で必ず偽になるため、if: env.RUN_E2E == 'true'と引用符を付けてください。

例外にあたるのが手動実行の入力値です。inputsコンテキスト経由ならboolean型の入力が真偽値のまま届くため、引用符なしで比べる書き方が正解になります。同じ値でもイベントのペイロード側から読むと文字列に戻るので、経路によって書き方が変わる点に注意してください。入力の型定義そのものはGitHub Actionsの手動実行(workflow_dispatch)|inputsの型定義とCLI・APIからの起動で扱っています。

containsとstartsWithは大文字小文字を区別しない前提で書く

containsとstartsWithとendsWithは、引数を文字列へキャストしたうえで大文字小文字を無視して判定します。コミットメッセージに特定の語が含まれるかを見る条件は、大文字で書かれた同じ語にも当たると理解しておいてください。

逆に、この寛容さを設計へ組み込むこともできます。列挙した候補のどれかに一致するかを見たいなら、fromJSONで文字列をJSONの配列へ戻し、containsで包含を判定する書き方が短くまとまります。個別の等価比較を並べるより、候補を1か所に集められる分だけ後から読みやすい形です。

      - name: 環境変数は文字列として比べる
        if: env.RUN_E2E == 'true'
        run: npm run e2e
      - name: 候補の配列に含まれるときだけ流す
        if: contains(fromJSON('["main","release","hotfix"]'), github.ref_name)
        run: make deploy

ブランチと変更ファイルで実行を絞るときのifの書き分けと実装例

「main だけデプロイする」「API を触ったときだけ流す」という要求は頻出します。ところが、ifで素直に書こうとするとgithubコンテキストの仕様に引っかかります。

pull_requestではgithub.refがマージ用のrefへ変わる仕様

github.refは「ワークフローを起こしたブランチまたはタグの完全なref」です。push ならrefs/heads/mainのような値が入ります。ところが、マージされていないプルリクエストではrefs/pull/[番号]/mergeという別の形になりました。

そのため完全なrefを main と直接比べる条件は、main を対象にしたプルリクエストでは決して真になりません。プルリクエストのCIでだけ動かない条件があるとき、まずここを疑ってください。

ref_nameとbase_refとhead_refの3つを使い分ける判断基準

github.ref_nameは短いref名を返します。接頭辞を書かずに済むため、単純なブランチ判定はこちらのほうが読みやすい書き方です。

github.base_refはプルリクエストの向き先、github.head_refは元のブランチで、どちらも pull_request イベントのときだけ値が入ります。それ以外のイベントでは空になるため、複数のイベントで共用するワークフローに書くと条件が崩れました。定期実行では既定ブランチの文脈で動く点も併せて押さえておくと安全で、GitHub Actionsの定期実行(cron)|timezone指定・遅延と欠落・60日停止の実務で扱った時刻起点の実行と組み合わせるときに効いてきます。

変更ファイルで絞るならトリガー側のpathsを第一候補にする

ifの式には「どのファイルが変わったか」を直接読む手段がありません。変更ファイルで絞るなら、まずトリガー側のpathsを検討してください。発火そのものを起こさない分だけ、待ち時間も課金分数も減ります。フィルタの構文と、絞ったときに必須チェックが保留のまま残る問題はGitHub Actionsのトリガー(on)|イベント選定とbranches・pathsフィルタの設計にまとめました。

それでもif側で判定したい場面はあります。発火はさせたうえで重い処理だけを飛ばしたい、必須チェックを保留にしたくない、といった場合です。手順は、差分を出すステップで結果をoutputsへ書き、後続のステップがその値を文字列として読む形になります。判定そのものをシェルへ押し出すため、式の制約からも外れて扱いやすくなりました。

      - id: changed
        run: |
          if git diff --name-only HEAD^ | grep -q "^api"; then
            echo "api=true" >> "$GITHUB_OUTPUT"
          else
            echo "api=false" >> "$GITHUB_OUTPUT"
          fi
      - name: APIが変わったときだけ配布する
        if: steps.changed.outputs.api == 'true'
        run: make deploy-api

受託開発でifをどこまで書いてよいかを決める判断の基準を整理する

ここからは、条件をどこまで作り込むかという設計の話です。ifは無料で足せるように見えますが、引き渡し後に払うコストがあります。

ifが増えるほど動かなかった理由の説明に時間がかかる保守上の背景

skipped になったステップは、ログに「なぜ飛んだのか」を書き残しません。式のどの項が偽だったのかは表示されないため、読む側は条件を頭の中で再評価することになります。入れ子になった論理演算子が5つ6つと並べば、この作業は数分では終わりません。

納品先の運用担当が自力で追えるかどうかが分岐点です。手元で挙動を確かめられる環境があれば負担は下がるので、GitHub Actionsをローカル実行するact|導入とイメージ選択・再現できる範囲のような検証手段を引き渡しの範囲に含めておくと、問い合わせの往復が減ります。

採用してよいのは本番切り分けと通知と後始末の3つに絞る運用方針

言い切ります。ifを積極的に使ってよいのは3つの用途だけです。1つ目は本番と検証の切り分けで、github.ref_nameやneedsの結果で配布先を分ける条件。2つ目は失敗時の通知で、failure()を条件にした連絡系の処理。3つ目は後始末とレポート回収で、中止以外を条件にした片付けです。

この3つは、条件が偽になったときの結果が読む側にとって自明です。「本番ブランチではないから配らなかった」「落ちなかったから通知しなかった」と、ログを読まずに説明が付きます。逆に言えば、説明に式の再評価が要る条件は採用の基準を満たしていません。

ワークフロー分割やトリガー側の指定で足りるなら見送る判断基準

見送るべき場面もはっきりしています。条件がイベントの種類だけで決まるなら、ifではなくワークフローファイルを分けてください。ファイル名で用途が読めるようになり、必須チェックの設定も素直になります。変更ファイルで絞るだけなら、前述のとおりトリガー側の指定が先です。マトリックスの一部だけを外したいならexcludeが正攻法で、ifで空回りさせるとジョブ枠と待ち時間を無駄に消費します。

判断の順序は、トリガー側で絞れるか、ワークフローを分けられるか、それでも残る条件だけをifへ、の3段です。この順で削ると、条件式は自然と数本に収まります。パイプライン全体をどう区切るかから見直したい場合はCI/CDとは?仕組み・パイプライン・導入すべき企業の判断基準を解説が入口です。CI基盤の棚卸しや、条件が絡み合ったワークフローの引き取りをお考えでしたら、保守運用 / 内製化支援で現行の構成を読み解くところからご相談いただけます。

よくある質問

ifの式に区切り記号は必要ですか?

省略できます。if: success()のように式だけを書いて構いません。ただし!で始まる式はYAMLがタグとして解釈して読み込みに失敗するため、区切り記号で囲んでください。

always()と否定形のcancelled()はどちらを使うべきですか?

既定は否定形のcancelled()です。公式は、致命的な失敗が起こりうる処理にalways()を使うとタイムアウトまでワークフローがハングすると警告しています。取り消し操作で素直に止めたいなら否定形を選んでください。

ジョブのifからenvやsecretsを参照できますか?

どちらもできません。ジョブのifが参照できるのはgithubとneedsとvarsとinputsの4つだけです。シークレットの有無で分けたいときは、ジョブ直下のenvへ真偽の判定結果を写し、ステップのifから読む形にします。

環境変数を真偽値と比べると動かないのはなぜですか?

環境変数は文字列で返るためです。比較では真偽値のtrueが1へ、文字列の側がNaNへ変換され、両者は一致しません。if: env.FLAG == 'true'と引用符を付けて文字列同士で比べてください。

ステップをスキップしたジョブは失敗扱いになりますか?

なりません。ステップのifが偽で飛ばされた場合、そのステップは skipped ですがジョブは成功として終わります。一方でジョブのifが偽なら、ジョブ自体が skipped になり、それをneedsに指定した後続も連鎖して飛びます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  3. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  4. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次