Computer scienceSystem administration and DevOpsCI/CD processesGitHub Actions

Conditional execution

8 minutes read

In this topic, we will discuss conditionals in GitHub Actions workflows. Then learn about how jobs can depend on one another during a run. Finally, you will learn to stop a workflow from failing if an error occurs.

If conditional

Conditionals in GitHub Actions workflows are similar to other any programming language. if conditional prevents a job or a step from executing unless specific conditions are met.

  • To execute a job within the workflow or notjob.<job_id>.if

  • To execute a step inside a job or not job.<job_id>.steps.if

Variables accessible in these conditions are the same ones that a user can access on either the job level or the step level. You can use the following variables as conditions to evaluate the if conditional.

  • Context variables

  • Environment variables

  • Output variables (from other jobs)

You can also use expressions to validate conditions with the help of operators. For example: && (And), || (Or), < (Less than), > (Greater than), <=(Less than or equal), >=(Greater than or equal), == (Equal), and != (Not equal). Visit the official documentation to learn more about operators.

Let's look at a workflow file to see how the if conditional can be used.

name: Conditional Workflow
on: push
jobs:
  number_generator:
    runs-on: ubuntu-latest
    steps:
      - id: generator
        run: echo "num=$RANDOM % 2" 
      - if: steps.generator.outputs.num == 0
        run: echo zero
      - if: steps.generator.outputs.num == 1
        run: echo one

Running this snippet will give the following result:

result of if conditional using

A random number is generated — 28232. When 28232 is modded with 2, the output is zero. Therefore, the second step's condition is fulfilled, and an output is produced. The third step is omitted as the conditions to run the step are not met. You can even skip a job entirely. For instance, since issues and PRs are similar under the hood and have a similar event payload in GitHub Actions, you can check if the event is a PR before proceeding:

on:
  issue_comment:
    types: [created, deleted]

jobs:
  pr_comments:
    if: github.event.issue.pull_request # only runs for PR comments
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "You have a new comment on PR ${{ github.event.issue.number }}"

In the following run, we created new comments on an issue and a PR. As you can see, the job ran successfully for the PR and was skipped for the Issue:

Screenshot showing workflow runs triggered when comments were added on both issues and PRs.

GitHub automatically evaluates if conditional as an expression. Therefore we can omit the expression syntax (${{ }}) in if conditionals.

Using predefined functions with if conditional

In addition to variables, you can also use predefined functions in conditions.

  • startsWith(searchString, searchValue) returns true when searchStrin starts with searchValue;

  • contains(search, item) returns true if the search contains the expected item;

  • endsWith(searchString, searchValue) returns true if searchString ends with searchValue.

Let's look at another example that uses these predefined functions.

name: Predefined function
on: push
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - id: step1
        run: "echo test=hello >> $GITHUB_OUTPUT"
      - name: step2
        if: contains(steps.step1.outputs.test, 'hello')
        run: echo is executed
      - name: step3
        if: contains(steps.step1.outputs.test, 'bye')
        run: echo not executed

In this workflow, only step 2 is executed. Step 3 is skipped as the conditions are not met.

$GITHUB_OUTPUT was used in step 1 to set a step's output parameter. You will need to define an id for the step to later retrieve the output value.

Using state validations

You can also use state validations in conditions;

  • success() returns true if no previous steps have failed or have been canceled;

  • failure() returns true if any previous step failed;

  • always() returns true no matter if a step failed or the workflow was canceled;

  • canceled() returns true if the workflow was canceled.

But what if there is a step that must be executed regardless of whether the previous step succeeds or fails? A common example of this scenario is when the result of a task should be notified by email or messaging application. Because you want to be notified whether the execution of the job results in success or failure.

In this case, you can set the expression of GitHub Actions to the if property of the step with always() that must be performed unconditionally.

For example, let's set up a job so that the first step succeeds or fails randomly. Then the second step always outputs the first step's result. In order to check the output result of a specific step, let's use steps.<step_id>.outcome.

name: State validations
on: push
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - id: generator
        run: if [[ $((RANDOM % 2)) == 0 ]]; then exit 0; else exit 1; fi
      - if: ${{ always() }}
        run: echo ${{ steps.generator.outcome }}

If the first step succeeds, the second step will print success. But if the first step fails, the second step will print failure. This shows that the second step is executed regardless of the result of the first step.

Occasionally, you may want to set up a backup step that is performed only if a step fails. In this case, you can set the GitHub Actions expression called in the if properties with failure() of the corresponding backup step.

For example, let's set up a job so that the first step fails unconditionally, and the second step takes its place.

name: failure
on: push
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: original
        run: exit 1
      - name: backup
        if: ${{ failure() }}
        run: echo backup

In contrast, let's modify the workflow so that the first step passes unconditionally.

name: fixed-failure
on: push
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: original
        run: exit 0
      - name: backup
        if: ${{ failure() }}
        run: echo backup

After running this workflow, you will see that the second step was not performed.

Job dependencies

By default, jobs are executed in parallel. Now let's create a workflow where you need to validate the state of a previous job to proceed to the next job. The needs keyword will help to make sure certain jobs depend on another job.

Let's take a look at an example:

jobs:
  Job1:
  Job2:
  Job3:
    needs: [Job1, Job2]

In this example, you can see that Job3 depends on Job1 and Job2. This means that both jobs need to be completed successfully before Job3 can run. Another important point to note is that if a job fails, all the jobs that need it are skipped unless the jobs use a conditional expression to continue. In this example, if either Job1 or Job2 fails, then Job3 will not run.

Ignoring failures in jobs and steps (continue-on-error)

In GitHub Actions, by default, if a step fails during the execution of a job, subsequent steps are not executed and the job is aborted. In most cases, the default handling of these GitHub Actions makes sense, as unnecessary steps can be omitted.

But in a real project, there may be an unstable phase where a step may fail. A typical example – there may be a job or step whose success or failure is unknown. In this situation, the whole team would be in trouble if the entire task was brought to a halt whenever a step failed.

continue-on-error supports properties. If this property is set to true, the job or workflow will not be interrupted even if the corresponding step fails. The remaining steps will continue to be executed.

For example, let's look at the following task.

name: continue-on-error
on: push
jobs:
  ignore:
    runs-on: ubuntu-latest
    steps:
      - id: step1
        continue-on-error: true
        run: exit 1
      - run: echo "I don't care!"

The first step always fails, but the second step prints I don't care! because the continue-on-error property is set to true.

Conclusion

GitHub Actions allows you to control the execution of jobs and steps in various ways. You can use if conditional to prevent a job or step from running if certain conditions are not met. And using state validations within conditionals can give you even more flexibility. Also, jobs run in parallel by default. But they may be dependent on each other. The needs keyword can help you manage such dependencies and run jobs sequentially. Just in case a step fails, continue-on-error will prevent interruptions and the job will not halt.

10 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo