Mastering Conditional Execution in GitHub Actions

In a professional CI/CD pipeline, you rarely want every single job or step to run every time a workflow is triggered. For example, you might want to run tests on every pull request but only deploy to production when code is merged into the main branch. This is where conditional execution comes into play.

GitHub Actions provides the if conditional to control whether a job or step should run based on specific criteria. By mastering these conditions, you can save compute minutes, speed up your feedback loop, and prevent accidental deployments.

Understanding the "if" Conditional

The if keyword can be applied at two levels: the Job level and the Step level. When an if expression evaluates to true, the job or step runs. If it evaluates to false, it is skipped entirely.

[Event Triggered]
      |
      v
[Evaluate Job Condition] --(False)--> [Skip Job]
      | (True)
      v
[Evaluate Step Condition] --(False)--> [Skip Step]
      | (True)
      v
[Execute Step Logic]
    

Common Expressions and Syntax

GitHub Actions uses a specific expression syntax for conditions. While you usually wrap expressions in ${{ }} elsewhere in a workflow, the if conditional automatically evaluates its content as an expression, so the curly braces are optional.

  • github.ref: The branch or tag ref that triggered the workflow (e.g., refs/heads/main).
  • github.event_name: The name of the event (e.g., push, pull_request).
  • success(): Returns true if no previous step has failed (default behavior).
  • failure(): Returns true if any previous step of a job has failed.
  • always(): Forces a step to run even if previous steps failed or were cancelled.
  • cancelled(): Returns true if the workflow was manually cancelled.

Practical Code Examples

Example 1: Running a Step Only on the Main Branch

This is the most common use case. You might want to build your application on every branch but only upload the artifact if you are on the main branch.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Application
        run: npm run build
      - name: Deploy to Production
        if: github.ref == 'refs/heads/main'
        run: npm run deploy
    

Example 2: Running a Job Based on Event Type

You can prevent an entire job from running unless the event is a pull_request. This is useful for expensive security scans that you only want to run during code reviews.

jobs:
  security-scan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit
    

Example 3: Error Handling with failure()

If a build fails, you might want to send a notification to a Slack channel. The failure() function ensures this step only triggers when something goes wrong.

    - name: Notify Slack on Failure
      if: failure()
      run: curl -X POST -d "text=Build Failed!" ${{ secrets.SLACK_WEBHOOK }}
    

Real-World Use Cases

  • Environment Specifics: Deploying to a "Staging" environment if the branch is develop and to "Production" if the branch is main.
  • Selective Testing: Running heavy integration tests only when a specific label (like "run-ui-tests") is added to a Pull Request.
  • Cleanup Operations: Using always() to ensure that temporary cloud resources or Docker containers are torn down, regardless of whether the tests passed or failed.
  • Skipping Documentation: Using conditions to skip documentation site builds if only the /src folder was modified and the /docs folder remained unchanged.

Common Mistakes to Avoid

  • Incorrect Ref Format: Forgetting that github.ref includes the full prefix. Use refs/heads/main instead of just main, or use github.ref_name for the short version.
  • Using Quotes for Booleans: Writing if: 'true' (string) instead of if: true (boolean).
  • Context Availability: Trying to use env variables in a job-level if condition. Job-level conditions are evaluated before environment variables are initialized.
  • Overusing always(): Using always() on a step that depends on a previous step's output. If the previous step failed, the output might be missing, causing the "always" step to crash.

Interview Notes for Developers

  • Question: How do you make a step run even if the previous step fails?
  • Answer: Use the if: failure() or if: always() status check functions.
  • Question: Is the ${{ }} syntax required in an if block?
  • Answer: No, it is optional because GitHub Actions automatically treats the content of an if key as an expression.
  • Question: How can you check if a workflow was triggered by a specific user?
  • Answer: You can use the github.actor context, for example: if: github.actor == 'octocat'.

Summary

Conditional execution is a fundamental pillar of efficient CI/CD. By using the if keyword at the job or step level, combined with GitHub's rich set of contexts and status check functions, you can create highly dynamic and intelligent workflows. Remember to use github.ref for branch filtering, failure() for error handling, and always() for essential cleanup tasks. This concludes Topic 11: Conditional Execution of Jobs and Steps. In the next part of our series, we will explore how to use matrix builds to run tests across multiple environments simultaneously.