Skip to content

Fix: GitHub Actions if Condition Not Working (Steps and Jobs Being Skipped or Always Running)

FixDevs ·

Quick Answer

How to fix GitHub Actions if conditions that don't evaluate correctly — why steps are skipped or always run, how to use context expressions, fix boolean checks, and handle job outputs in conditions.

The Error

A step or job with an if: condition does not behave as expected:

- name: Deploy to production
  if: github.ref == 'refs/heads/main'
  run: ./deploy.sh
# Step is always skipped, or always runs regardless of branch

Or a condition on a job output doesn’t work:

jobs:
  check:
    outputs:
      changed: ${{ steps.check.outputs.changed }}

  deploy:
    needs: check
    if: needs.check.outputs.changed == 'true'  # Never evaluates to true
    runs-on: ubuntu-latest

Or success(), failure(), or always() conditions are not behaving as documented:

- name: Notify on failure
  if: failure()  # Never runs even when previous step fails
  run: ./notify.sh

Why This Happens

GitHub Actions if: expressions use a specific syntax that differs from typical programming languages. Common mistakes:

  • Forgetting the ${{ }} expression syntaxif: evaluates the expression directly without wrapping it. Unlike run: or env:, the if: field interprets its value as an expression automatically. Sometimes wrapping in ${{ }} is needed, sometimes it is not.
  • String comparison vs booleangithub.event_name == 'push' is correct, but github.event.pull_request.draft == false may not work as expected because false is a boolean in YAML but github.event.pull_request.draft is a JSON boolean.
  • Incorrect context accessneeds.job-name.outputs.key requires the exact job ID, not the display name.
  • Status check functionssuccess(), failure(), always() only work at the step level when used with if: and only reflect the status of the overall job up to that point.
  • Job output not set — a job output must be explicitly set with echo "key=value" >> $GITHUB_OUTPUT for it to be available in downstream jobs.

Fix 1: Correct if: Syntax for Common Conditions

# Branch check
- name: Deploy
  if: github.ref == 'refs/heads/main'

# Event type check
- name: Run on push only
  if: github.event_name == 'push'

# Multiple conditions (AND)
- name: Deploy to prod
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'

# Multiple conditions (OR)
- name: Run on main or release
  if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')

# Negation
- name: Skip on forks
  if: github.repository_owner == 'myorg'

# Check if a variable is not empty
- name: Run if secret exists
  if: secrets.DEPLOY_KEY != ''
  # Note: secrets cannot be directly evaluated in if conditions for security
  # Use an env variable workaround:
  env:
    HAS_SECRET: ${{ secrets.DEPLOY_KEY != '' }}
  if: env.HAS_SECRET == 'true'

The correct way to check secrets in conditions:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Check if deploy key exists
        id: check-secret
        run: |
          if [ -n "${{ secrets.DEPLOY_KEY }}" ]; then
            echo "has_key=true" >> $GITHUB_OUTPUT
          else
            echo "has_key=false" >> $GITHUB_OUTPUT
          fi

      - name: Deploy
        if: steps.check-secret.outputs.has_key == 'true'
        run: ./deploy.sh

Fix 2: Fix Step Status Conditions

Use status functions to run steps conditionally based on prior step results:

steps:
  - name: Build
    id: build
    run: npm run build  # This might fail

  # Runs only if ALL previous steps succeeded (default behavior)
  - name: Test
    run: npm test

  # Runs only if the 'build' step failed
  - name: Notify build failure
    if: failure() && steps.build.conclusion == 'failure'
    run: ./notify-failure.sh

  # Always runs regardless of previous step status
  - name: Upload logs
    if: always()
    uses: actions/upload-artifact@v4
    with:
      name: build-logs
      path: logs/

  # Runs if the overall job failed (any step failed)
  - name: Send failure alert
    if: failure()
    run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Build failed!"}'

  # Run on failure BUT only on the main branch
  - name: Alert on main failure
    if: failure() && github.ref == 'refs/heads/main'
    run: ./alert.sh

Step conclusion vs outcome:

- name: Risky step
  id: risky
  continue-on-error: true  # Don't fail the job if this step fails
  run: ./risky-command.sh

# steps.risky.outcome = 'success' | 'failure' | 'cancelled' | 'skipped'
# steps.risky.conclusion = same values, but reflects continue-on-error

- name: Handle risky failure
  # Use outcome when continue-on-error is set — conclusion is always 'success' with continue-on-error
  if: steps.risky.outcome == 'failure'
  run: echo "Risky step failed but we continue"

Fix 3: Fix Job-Level if Conditions

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

  deploy:
    needs: build
    runs-on: ubuntu-latest

    # Job-level conditions use job context
    if: |
      github.ref == 'refs/heads/main' &&
      github.event_name == 'push' &&
      needs.build.result == 'success'

    steps:
      - run: ./deploy.sh

Job result values: success, failure, cancelled, skipped

  notify:
    needs: [build, deploy]
    runs-on: ubuntu-latest
    if: always()  # Always run this job

    steps:
      - name: Report status
        run: |
          echo "Build: ${{ needs.build.result }}"
          echo "Deploy: ${{ needs.deploy.result }}"

Fix 4: Fix Job Output Not Available in if Conditions

Job outputs must be explicitly defined and set:

jobs:
  check-changes:
    runs-on: ubuntu-latest
    outputs:
      # Declare the output at the job level
      has_changes: ${{ steps.diff.outputs.has_changes }}
      changed_files: ${{ steps.diff.outputs.changed_files }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Need at least 2 commits to diff

      - name: Check for changes
        id: diff
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD -- src/)
          if [ -n "$CHANGED" ]; then
            echo "has_changes=true" >> $GITHUB_OUTPUT
            echo "changed_files<<EOF" >> $GITHUB_OUTPUT
            echo "$CHANGED" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
          else
            echo "has_changes=false" >> $GITHUB_OUTPUT
          fi

  deploy:
    needs: check-changes
    runs-on: ubuntu-latest

    # Access job output with: needs.<job-id>.outputs.<output-name>
    if: needs.check-changes.outputs.has_changes == 'true'

    steps:
      - name: Deploy
        run: echo "Deploying because src/ changed"

Common mistake — comparing boolean to string:

# Wrong — 'true' (string) != true (YAML boolean)
if: needs.check.outputs.has_changes == true

# Correct — compare to string 'true'
if: needs.check.outputs.has_changes == 'true'

# Also correct — use the expression to coerce
if: fromJSON(needs.check.outputs.has_changes) == true

Fix 5: Fix Environment and Variable Conditions

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Requires approval if configured

    steps:
      - name: Set environment flag
        id: env-check
        run: |
          echo "env_name=${{ github.event.deployment.environment }}" >> $GITHUB_OUTPUT

      # Check environment variable set in a previous step
      - name: Production only step
        if: steps.env-check.outputs.env_name == 'production'
        run: ./production-only.sh

      # Check vars context (repository/org variables)
      - name: Use variable
        if: vars.FEATURE_FLAG == 'enabled'
        run: echo "Feature is enabled"

Using env context in conditions (set in the workflow):

env:
  DEPLOY_ENV: production

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # env context from workflow-level env
      - name: Deploy to prod
        if: env.DEPLOY_ENV == 'production'
        run: ./deploy-prod.sh

Fix 6: Fix Pull Request and Push Event Conditions

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Only on PRs to main
      - name: PR checks
        if: github.event_name == 'pull_request' && github.base_ref == 'main'
        run: ./pr-checks.sh

      # Only on direct pushes (not PRs)
      - name: Push-only step
        if: github.event_name == 'push'
        run: ./push-only.sh

      # Only when PR is not a draft
      - name: Full test suite
        if: |
          github.event_name == 'push' ||
          (github.event_name == 'pull_request' && !github.event.pull_request.draft)
        run: npm run test:all

      # Check if PR is from a fork (fork PRs don't have access to secrets)
      - name: Deploy preview
        if: |
          github.event_name == 'pull_request' &&
          github.event.pull_request.head.repo.full_name == github.repository
        run: ./deploy-preview.sh

Fix 7: Debug if Conditions with Verbose Logging

steps:
  - name: Debug context values
    run: |
      echo "github.ref: ${{ github.ref }}"
      echo "github.event_name: ${{ github.event_name }}"
      echo "github.repository: ${{ github.repository }}"
      echo "github.actor: ${{ github.actor }}"
      echo "github.base_ref: ${{ github.base_ref }}"

  - name: Debug entire context (careful with secrets)
    run: echo '${{ toJSON(github) }}'

  - name: Debug needs context
    run: echo '${{ toJSON(needs) }}'

  - name: Conditional with debug
    id: my-condition
    run: |
      # Evaluate condition manually for debugging
      REF="${{ github.ref }}"
      EVENT="${{ github.event_name }}"
      echo "ref=${REF}, event=${EVENT}"
      if [ "$REF" = "refs/heads/main" ] && [ "$EVENT" = "push" ]; then
        echo "should_deploy=true" >> $GITHUB_OUTPUT
      else
        echo "should_deploy=false" >> $GITHUB_OUTPUT
        echo "Condition not met: ref=${REF}, event=${EVENT}"
      fi

  - name: Deploy
    if: steps.my-condition.outputs.should_deploy == 'true'
    run: ./deploy.sh

Still Not Working?

Check expression syntax carefully. GitHub Actions expressions use == (not ===), &&, ||, !. Function calls use startsWith(), endsWith(), contains(), fromJSON(), toJSON():

# String functions
if: startsWith(github.ref, 'refs/tags/')
if: endsWith(github.ref, '/main')
if: contains(github.event.pull_request.labels.*.name, 'ready-to-deploy')

Check for YAML quoting issues. Conditions with && or || need quoting in some YAML contexts:

# May need quoting
if: "github.ref == 'refs/heads/main' && github.event_name == 'push'"

# Or use block scalar
if: |
  github.ref == 'refs/heads/main' &&
  github.event_name == 'push'

Add workflow_dispatch to test manually:

on:
  push:
  workflow_dispatch:  # Allows manual trigger from GitHub UI for testing
    inputs:
      debug:
        type: boolean
        default: false

For related GitHub Actions issues, see Fix: GitHub Actions Cache Not Working and Fix: GitHub Actions Permission Denied.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles