Controlling Job Execution with Conditions in GitHub Actions

·

5 min read


cicube.io

Introduction

Conditional execution of jobs is one of those powerful features you find in GitHub Actions, underutilized at times. It enables you to block the execution of jobs unless certain conditions are met-something which is super useful if you want to control a workflow flow based on outputs or external factors. Whether for multiple environments, complex build pipelines, or job dependencies, conditions enable you to further define your automation.

Using Conditions to Control Job Execution

GitHub Actions similar to jobs.<job_id>.if syntax, you define conditions based on a wide range of inputs where a job would run. Amongst these are job outputs, GitHub contexts-like repository names or branches, environment variables amongst others. Adding a condition makes your workflow more efficient while skipping jobs by marking them as "success" if skipped.

Here's a simple example - we only want to run a deployment job if the repository is a production repository:

jobs:
  production-deploy:
    if: github.repository == 'my-org/prod-repo'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Deploying to production"

In this case, the production-deploy job will only execute if the workflow is triggered in the repository my-org/prod-repo, and be skipped for any other cases.

Combining Job Dependencies and Conditions

This is often the case when you want to make a job execute depending on the result of the execution of another job, deployment job after a build job. You can easily define job dependencies using the keyword needs in these cases, or you could combine the usage of needs with conditions to fine-tune this behavior.

Consider an example where we have the three jobs: build, test, and deploy. Here, we want deploy to run only if build succeeds and the test job either succeeds or is skipped.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building project"

  test:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - run: echo "Running tests"
      - id: set_test_output
        run: echo "::set-output name=should_deploy::yes"
    outputs:
      should_deploy: ${{ steps.set_test_output.outputs.should_deploy }}

  deploy:
    runs-on: ubuntu-latest
    needs: [build, test]
    if: needs.test.outputs.should_deploy == 'yes' && (needs.test.result == 'success' || needs.test.result == 'skipped')
    steps:
      - run: echo "Deploying project"

Overview: How It Works:

  1. In it, the job build is executed and upon success of this job, the test job is triggered.

  2. The test job outputs a should_deploy, which will be used in the deploy job to decide if it should be executed or not.

  3. Here, the deploy job is dependent on success of the build job and either success or skip of the test job. The deployment proceeds when the output of the test job is set to 'yes'.

This will be an elastic way of dealing with job dependencies, which come with complementary conditions.

Handling of Skipped Jobs and Conditional Logic

One common challenge within workflows, is how to deal with skipped jobs. By default a job may be "skipped", yet its status can still be reported out as "success", yet you may wish for other jobs to behave differently based on this status. That's where using conditions like success(), failure(), cancelled() or always() can help.

Suppose you then have a test job which, under some circumstances is going to skip. Since the following job, notify should-only execute if prior test job succeed or is skipped.

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Running tests"
      - id: set_skip_condition
        run: echo "::set-output name=should_skip::no"
    outputs:
      should_skip: ${{ steps.set_skip_condition.outputs.should_skip }}

  notify:
    runs-on: ubuntu-latest
    needs: test
    if: needs.test.result == 'success' || needs.test.result == 'skipped'
    steps:
      - run: echo "Sending notification"

Breakdown:

  1. The test job sets an output called should_skip.

  2. The notify job will only run if the test job succeeds or is skipped.

  3. This way, it would still generally be expected that the workflow proceeds, even if a job was skipped.


Monitoring GitHub Actions Workflows

CICube is a GitHub Actions monitoring tool that provides you with detailed insights into your workflows to further optimize your CI/CD pipeline. With CICube, you will be able to track your workflow runs, understand where the bottlenecks are, and tease out the best from your build times. Go to cicube.io now and create a free account to better optimize your GitHub Actions workflows!

CICube GitHub Actions Workflow Duration Monitoring


Lessons Learned: Always Be Careful with Conditional Logic

When I was investigating the feature of conditional execution of jobs, that is when all those unexpected behaviors kicked in. For example, I used some combined conditions with if. Sometimes, the workflow didn't behave the way it was expected.

Sometimes you might want to make sure a job runs under several conditions. You can use always() as a way of forcing an evaluation alongside other conditions. Here's such an example where we wish the finalize job to run even if previous jobs were skipped, provided another condition is met:

jobs:
  finalize:
    runs-on: ubuntu-latest
    needs: [build, test]
    if: always() && (needs.build.result == 'success' || needs.test.result == 'skipped')
    steps:
      - run: echo "Finalizing workflow"

This will ensure that finalize is definitely executed regardless of the result of all the other jobs, but only of course, if at least one of the jobs succeeded or was skipped.

Conclusion

Conditional execution of jobs in GitHub Actions allows you to get much more efficient and maintainable continuous integration/continuous deployment pipelines. Because you're going to have some context, like outputs or external factors determining whether it's worth running the job, you will save not just time but also resources. You would need to consider how you will handle skipped jobs. Know about status checks like success(), failure() or always().

You'll be able to make your workflows more intelligent and responsive with conditional logic so that the need for redundant jobs or manual intervention will be reduced when different scenarios pop up.