Running GitHub Action steps and jobs only on push to master
GitHub Actions is now an easy way to automate your software’s continuous integration and continuous delivery pipelines when you’ve already have your code in GitHub. Currently at the time of writing this post, within the free tier of GitHub you get 2,000 monthly minutes of actions time which is generous amount for any personal or small business projects.
There’s lots of uses for GitHub Actions but one of the most common scenarios is to build and publish your software.
Let’s look at an example of a dotnet class library that we package up and push up to NuGet.Org.
name: Continuous Integration Workflow
on: [push, pull_request]
jobs:
build:
name: Build, Test, Pack, Push
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- run: dotnet restore
- run: dotnet build --no-restore --configuration Release
- run: dotnet test --no-build --configuration Release
- run: dotnet pack --output nupkgs --no-build --configuration Release
- run: dotnet nuget push nupkgs\*.nupkg
This workflow is setup to run on every push
and pull_request
made to the repository, however, we don’t really want the nuget push
step to push packages up on a pull request or anything else other than the master
branch.
We could separate out the workflow to be a pull request workflow and a push workflow, however, we’d be duplicating lots of parts of our workflow and it would be a lot harder to maintain.
Conditional steps
On a step we can set extra properties, one of the most common properties what we can use is name
, this is way to describe the step when the script is ambiguous.
Another property is if
, this is a property that takes in an expression which if evaluated to true will run the step.
The below step is an example of a step that will alway return true
for the if
expression.
- run: echo I will always run
if: ${{ true }}
However alternately we can use false
which will mean this step never gets run.
- run: echo I will always run
if: ${{ false }}
The expressions can become fairly complex with expressions built up based on the environment and the current running context. More information on what you can include in expressions can be found on the GitHub - Context and expression syntax for Actions.
Exclude steps on pull request
Now we know about conditional steps, we can start to exclude our nuget push
being run on a pull request. One of the variables we can use in out expression is github.event_name
, this variable is the name of the event that triggered the workflow run. In our example this can either be push
or pull_request
. So what we can do is check that the event is always a push
on our step.
Below is an example of a step that only runs on a push
- run: echo I will only run on push
if: ${{ github.event_name == 'push' }}
Only run steps on master
Another useful variable is github.ref
this is the branch or tag ref that triggered the workflow run. These are in the format refs/heads/{branch}
.
We can do similar to the above and check that we’re on the master branch.
- run: echo I will only run on the master branch
if: ${{ github.ref == 'refs/heads/master' }}
Combining expressions
We’ve now got both expressions for building our step so it does not run on a pull request and only on the master branch. However, we need to join these expressions together. For this we can use some logical operators, the operator that we want to use is the &&
(and operator) but there is also ||
(or operator), these operators are similar to some common languages like C# and JavaScript.
So we can simply combined these expressions together like the below.
- run: echo I will only run on the master branch and not on pull request
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
We can also rearrange the properties so they read a bit better
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
run: echo I will only run on the master branch and not on pull request
Creating meaningful variables
It’s always nice to be able to read what the code does, and having lots of long expressions doesn’t help anyone out. Within the workflows we can create environment variables with descriptive names and use these in our if expressions. This also allows us to reuse the expressions multiple times over the workflow without duplicating the code. Let’s take the following workflow for example.
jobs:
build:
name: Build, Test, Pack, Push
runs-on: windows-latest
env:
PUSH_PACKAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
steps:
- name: Step 1
if: ${{ env.PUSH_PACKAGES }}
run: echo Step 2 running based on PUSH_PACKAGES value of $PUSH_PACKAGES
- name: Step 2
if: ${{ env.PUSH_PACKAGES }}
run: echo Step 2 running based on PUSH_PACKAGES value of $PUSH_PACKAGES
Steps 1 and 2 only run based on the environment variable PUSH_PACKAGES
, this is referenced in an expression with the env.{var-name}
syntax. The PUSH_PACKAGES
environment variable however is only set once at the start of our job.
Our final dotnet class library build/push workflow
Given all the information above, we can now plumb in the extra fields we know to only allow our nuget push to execute on master and not on a pull request.
name: Continuous Integration Workflow
on: [push, pull_request]
jobs:
build:
name: Build, Test, Pack, Push
runs-on: windows-latest
env:
PUSH_PACKAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
steps:
- uses: actions/checkout@master
- run: dotnet restore
- run: dotnet build --no-restore --configuration Release
- run: dotnet test --no-build --configuration Release
- run: dotnet pack --output nupkgs --no-build --configuration Release
- if: ${{ env.PUSH_PACKAGES }}
run: dotnet nuget push nupkgs\*.nupkg
Conditional jobs
So what about conditional jobs? These are practically the same as a step setup, we can add an extra property of if
with an expression and if the expression evaluates to true then the job will run.
jobs:
build:
name: Build, Test, Pack, Push
runs-on: windows-latest
if: ${{ true }}
steps:
- run: echo Hello World