GitHub Actions Conditional Steps

Conditional steps let you run specific parts of your workflow only when certain conditions are true. Instead of creating separate workflow files for different situations, you add simple rules that decide whether each step or job should execute. This makes your workflows flexible, efficient, and easier to maintain.

The if Keyword

The if keyword works on both jobs and steps. When the condition evaluates to true, the job or step runs. When it evaluates to false, it is skipped without causing a failure.

steps:
  - name: Run only on main branch
    if: github.ref_name == 'main'
    run: echo "This is the main branch"

Notice that inside if conditions, you can write expressions with or without the ${{ }} wrapper — GitHub treats the entire value as an expression automatically. Both of these work:

if: github.ref_name == 'main'
if: ${{ github.ref_name == 'main' }}

Conditional Jobs vs Conditional Steps

You can apply if conditions to both jobs and individual steps. The effect is the same — skip when the condition is false — but the scope differs:

Conditional job — the entire job is skipped:

jobs:
  deploy:
    if: github.ref_name == 'main'
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh
Conditional step — only that step is skipped:

steps:
  - name: Send Slack alert
    if: failure()
    run: curl -X POST "${{ secrets.SLACK_WEBHOOK }}"

Branch-Based Conditions

A common pattern is running different commands depending on which branch triggered the workflow:

steps:
  - name: Deploy to production
    if: github.ref_name == 'main'
    run: ./scripts/deploy-prod.sh

  - name: Deploy to staging
    if: github.ref_name == 'staging'
    run: ./scripts/deploy-staging.sh

  - name: Run preview deployment
    if: startsWith(github.ref_name, 'feature/')
    run: ./scripts/deploy-preview.sh
Branch diagram:

main branch push     → deploy-prod step runs
staging branch push  → deploy-staging step runs
feature/login push   → deploy-preview step runs

Event-Based Conditions

Run a step only when a specific event triggers the workflow:

steps:
  - name: Comment on pull request
    if: github.event_name == 'pull_request'
    uses: actions/github-script@v7
    with:
      script: |
        github.rest.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: "Tests are running!"
        })

Status-Based Conditions

The most common conditional pattern handles failures gracefully. GitHub provides built-in status functions:

steps:
  - name: Run tests
    id: tests
    run: npm test

  - name: Upload test logs on failure
    if: failure()
    run: ./upload-logs.sh

  - name: Notify team on success
    if: success()
    run: echo "All tests passed!"

  - name: Archive results regardless
    if: always()
    run: ./archive-results.sh
Flow when tests pass:
  Run tests ✓ → skip "Upload logs" → "Notify team" ✓ → "Archive" ✓

Flow when tests fail:
  Run tests ✗ → "Upload logs" ✓ → skip "Notify team" → "Archive" ✓

Combining Multiple Conditions

Combine conditions using && (and) and || (or) operators:

steps:
  - name: Deploy to production
    if: github.ref_name == 'main' && github.event_name == 'push'
    run: ./deploy.sh

  - name: Alert on any failure or cancellation
    if: failure() || cancelled()
    run: ./alert.sh

Checking Who Triggered the Workflow

Run a step only when a specific user or role triggered the workflow:

steps:
  - name: Run admin task
    if: github.actor == 'alice'
    run: ./admin-task.sh

  - name: Skip bot-triggered steps
    if: "!contains(github.actor, '[bot]')"
    run: ./notify-team.sh

The second example skips the notification step when an automated bot (like Dependabot) triggers the workflow. Bots typically have [bot] in their username.

Checking Job Dependencies in Conditions

When jobs depend on each other with needs, use the needs context in conditions to check a previous job's outcome:

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

  notify:
    needs: build
    runs-on: ubuntu-latest
    if: needs.build.result == 'failure'
    steps:
      - run: echo "Build failed! Sending alert."

Using Outputs in Conditions

A step can write a value and a later step checks that value before deciding to run:

steps:
  - name: Check if tests changed
    id: check
    run: |
      if git diff --name-only HEAD~1 | grep -q 'test/'; then
        echo "changed=true" >> $GITHUB_OUTPUT
      else
        echo "changed=false" >> $GITHUB_OUTPUT
      fi

  - name: Run tests
    if: steps.check.outputs.changed == 'true'
    run: npm test

The test step only runs when test files actually changed. This saves time on commits that only update documentation or configuration files.

Condition Evaluation Summary

Condition                          | When it runs
-----------------------------------|-----------------------------
github.ref_name == 'main'          | Push to main branch only
github.event_name == 'push'        | Push events only
success()                          | Previous steps all passed
failure()                          | At least one step failed
always()                           | Every time, no exceptions
cancelled()                        | Workflow was cancelled
contains(github.actor, '[bot]')    | A bot triggered the workflow
startsWith(github.ref, 'refs/tags')| A tag was pushed

Leave a Comment

Your email address will not be published. Required fields are marked *