Expressions, contexts and if conditions

GitHub Actions · Practical · 12 min

Conditions and context data are how one workflow behaves differently for PRs, branches and failures.

An expression ${{ ... }} is evaluated by GitHub, not by the shell. It can read contexts, objects describing the run:

  • github: event name, ref, sha, actor, repository and the full webhook payload in github.event.
  • env, vars, secrets, inputs: values you defined.
  • steps.<id>.outputs and steps.<id>.outcome: results of earlier steps (give the step an id).
  • needs.<job>.outputs and needs.<job>.result: results of jobs listed in needs.
  • matrix, runner, job.

Expressions support ==, !=, &&, ||, ! and functions such as contains(), startsWith(), format(), join(), toJSON(), fromJSON() and hashFiles(). String comparison is case-insensitive.

In if: keys the ${{ }} wrapper is optional. A step's if implicitly includes success(), meaning "all previous steps succeeded". The status functions change that:

  • failure(): run only if an earlier step failed (notifications, uploading logs).
  • always(): run regardless, even after cancellation.
  • cancelled(): run only when the run was cancelled.
  • !cancelled(): run after success or failure but not cancellation, usually better than always() for cleanup.

Debugging tip: dump a context with echo '${{ toJSON(github.event) }}' to see exactly what you can use, but never do this with secrets.

Example

steps:
  - id: tests
    run: npm test
  - if: failure() && steps.tests.outcome == 'failure'
    run: echo "Tests failed on ${{ github.ref_name }}"
  - if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'preview')
    run: echo "Building preview for PR ${{ github.event.number }}"
  - if: ${{ !cancelled() }}
    run: echo "always tidy up (unless cancelled)"
  - if: startsWith(github.ref, 'refs/tags/v')
    run: echo "release ${{ github.ref_name }}" 

Remember

  • Contexts expose the event, steps, needs, matrix and your variables
  • if: implies success() unless you use a status function
  • Prefer !cancelled() to always() for cleanup steps

Watch out for

  • Writing if: ! startsWith(...) without ${{ }}: a leading ! is YAML syntax, so wrap it in ${{ }}.

Check yourself

  1. A step has if: github.ref == 'refs/heads/main'. An earlier step failed. Does it run?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions