Introduction
One of the key features of GitHub Actions is its support for conditional execution, which allows you to control when certain steps or jobs within a workflow should run. This feature is particularly useful when managing complex workflows with multiple jobs, steps, and configurations.
Conditional Expressions
Conditional expressions form the foundation of conditional execution in GitHub Actions. In this chapter, I will cover the basics of conditional expressions, the operators you can use, and how to work with contexts and functions.
Basics of Conditional Expressions
Conditional expressions in GitHub Actions are written using the GitHub Actions expression syntax, which is based on JavaScript. These expressions are typically used with the if
keyword to determine whether a step or job should run. The result of the expression must be a boolean value, either true
or false
.
A simple example of a conditional expression is:
if: success()
In this case, the expression checks if the previous steps were successful using the success()
function. If they were, the step or job will run; otherwise, it will be skipped.
Operators in Conditional Expressions
GitHub Actions expressions support a variety of operators, which can be used to create more complex expressions. These operators include:
- Logical operators
&&
(and),||
(or),!
(not) - Equality operators
==
(equal),!=
(not equal) - Comparison operators
<
(less than),>
(greater than),<=
(less than or equal to),>=
(greater than or equal to)
Here's an example that combines several operators:
if: github.event_name == 'pull_request' && github.event.action == 'opened'
This expression checks if the current event is a pull request being opened.
Using Contexts and Functions in Expressions
GitHub Actions expressions can access various context objects and built-in functions, which provide useful information and functionality. Some of these include:
github
: Provides information about the current repository, event, workflow run, and more.env
: Contains environment variables.secrets
: Holds repository secrets.steps
: Gives access to outputs from previous steps.needs
: Allows you to access outputs from dependent jobs.
Functions like success()
, failure()
, cancelled()
, always()
, etc.
Here's an example that uses the github
context object:
if: github.ref == 'refs/heads/main' && success()
This expression checks if the current branch is the main
branch and if the previous steps were successful. If both conditions are met, the step or job will run.
Skipping Actions
In certain scenarios, you may want to skip specific steps or jobs within your workflow. This can be achieved using conditional expressions in combination with the if
keyword. In this chapter, I will explore various use cases for skipping actions and how to implement them.
Utilizing the if Keyword
The if
keyword allows you to conditionally run a step or job based on the result of a conditional expression. If the expression specified with if
evaluates to true
, the step or job will run. If the expression evaluates to false
, the step or job will be skipped.
Here's an example that demonstrates the use of the if
keyword:
steps:
- name: Run tests
run: npm test
if: success()
In this example, the "Run tests" step will only be executed if the previous steps were successful.
Skipping Steps Based on Branches or Tags
You can use conditional expressions to run steps only for specific branches or tags. This can be helpful when certain steps should only be executed for specific releases or branches. Here's an example:
steps:
- name: Deploy to production
run: npm run deploy
if: github.ref == 'refs/heads/main'
In this example, the "Deploy to production" step will only be executed if the current branch is main
.
Skipping Steps Based on Modified Files
Another use case for conditional execution is running a step only when certain files have been modified. This can be useful when you want to trigger a step or job only if specific files have been changed in a commit or pull request.
You can achieve this using the contains()
and fromJson()
functions, along with a custom action that lists the changed files. Here's an example:
steps:
- name: Get changed files
id: changes
uses: trilom/file-changes-action@v1
with:
output: 'json'
- name: Run tests for important files
run: npm run test:important
if: contains(fromJson(steps.changes.outputs.files), 'path/to/important/file')
In this example, the "Run tests for important files" step will only be executed if the specified file (path/to/important/file
) has been modified.
Matrix Builds
Matrix builds are a powerful feature of GitHub Actions that allow you to run the same job with multiple configurations. This is particularly useful when you want to test your code on different platforms or with various versions of a programming language or dependency. In this chapter, I will explore the concept of matrix builds, how to use conditional matrix builds, and how to combine matrix builds with the if
keyword for advanced scenarios.
Understanding Matrix Builds
Matrix builds in GitHub Actions enable you to define a set of configurations that your job should run with. This is done by specifying a matrix
key in the job definition, followed by a list of variables and their possible values. GitHub Actions will then run the job for each combination of these values.
Here's a basic example of a matrix build:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [12, 14, 16]
steps:
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
In this example, the test
job will run on three different operating systems (Ubuntu, macOS, and Windows) and with three different Node.js versions (12, 14, and 16), resulting in a total of nine job runs.
Using Conditional Matrix Builds
Sometimes, you may want to use conditional expressions within a matrix build to include or exclude specific configurations based on certain conditions. This can be done using the include
and exclude
keys within the matrix
definition.
Here's an example that demonstrates the use of conditional matrix builds:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [12, 14, 16]
include:
- os: ubuntu-latest
node-version: 18
if: github.ref == 'refs/heads/main'
In this example, an additional configuration (Node.js version 18 on Ubuntu) will be included in the matrix only if the current branch is main
.
Combining Matrix Builds with if for Advanced Scenarios
For more advanced scenarios, you can use the if
keyword within a job or step to further control the execution of your matrix build. By combining conditional expressions with the matrix
context object, you can create complex and flexible workflows.
Here's an example that demonstrates the use of the if
keyword with a matrix build:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [12, 14, 16]
steps:
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Publish coverage report
run: npm run coverage
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 14
In this example, the "Publish coverage report" step will only be executed if the current configuration is running on Ubuntu with Node.js version 14. This can be useful when you want to perform certain tasks, such as publishing a coverage report or deploying your application, only once in a matrix build or for a specific configuration.