Ken Muse

Understanding Push Triggers and Branches in GitHub


Most things just work with GitHub Actions. You specify the triggering event and the steps just execute. Once branching and branching strategies get involved, things can get more complicated quickly. A common question I’ve heard is “how do you know when a push trigger will run once the Actions workflow is on multiple branches?” Hopefully, today’s post will give you some insights. Sometimes having examples helps!

TL;DR – you need a workflow to exist in the the target branch and the workflow in that branch must contain a branch filter that matches the target.

The default branch

First thing to understand – the workflows expect to exist on the default branch. In fact, it’s usually the starting point for a trigger. If your default branch is main, then you generally need to have the workflows in the .github/workflows directory in your repository on that branch. Some events only trigger from the default branch ( such as workflow_dispatch, repository_dispatch, and schedule). Others, like push, use the workflow defined on their branch.

The next few examples should help clarify that process.

The simple case

Let’s consider the case where we have this workflow defined on the main branch:

1name: Explorations
2on:
3  push:
4    branches: [ "main", "feature" ]
5jobs:
6  build:
7    runs-on: ubuntu-latest
8    steps:
9      - run: echo Hello from ${{ github.ref_name }}!

Next, create a branch from main called feature:

Feature branch

In this case, a push to main or to feature will trigger the workflow. This is because the branch operation copied the workflow file and made it available to the feature branch.

If this YAML is modified in the feature branch, the workflow process will run that version of the YAML for pushes to feature.

More complicated

What happens if the main branch does not include a trigger for feature?

1name: Explorations
2on:
3  push:
4    branches: [ "main" ]
5jobs:
6  build:
7    runs-on: ubuntu-latest
8    steps:
9      - run: echo Hello from ${{ github.ref_name }}!

As expected, push to the feature branch won’t trigger the workflow. However, if we update this workflow file on feature, that changes:

1name: Explorations
2on:
3  push:
4    branches: [ "main", "feature" ]
5jobs:
6  build:
7    runs-on: ubuntu-latest
8    steps:
9      - run: echo Hello from ${{ github.ref_name }}!

The feature branch is related to the default branch, so triggers can occur. Similarly, if we alter the workflow file on feature to only trigger on the feature branch, it continues to work.

Children of the branch

What about a situation where a branch such as gamma has been created and we later add the workflow reference on main:

1name: Explorations
2on:
3  push:
4    branches: [ "main", "gamma", "feature" ]
5jobs:
6  build:
7    runs-on: ubuntu-latest
8    steps:
9      - run: echo Hello from ${{ github.ref_name }}!

Child of beta without workflow

If the workflow didn’t exist at the time gamma was created, then the trigger doesn’t exist. The workflow needs to be present on the branch to execute the trigger. Like before, we can create a workflow file on gamma which triggers on that branch. This enables gamma to handle the push events. The Git graph would look like this:

Child of beta with workflow

What if the workflow is created before the branch operation, as show in the next diagram?

Child of beta with workflow created on main

In this case, creating the branches after the workflow file gives us the desired result. The workflow file now exists on beta and gamma, so the version on gamma will run as expected.

The orphan experience

What if we try to create a new branch that is separate and distinct from our default branch (main), but the name of that branch exists in the workflow on main? As an example, assume that on the main branch, we have this file:

1name: Explorations
2on:
3  push:
4    branches: [ "orphan" ]
5jobs:
6  build:
7    runs-on: ubuntu-latest
8    steps:
9      - run: echo Hello from ${{ github.ref_name }}!

An orphan branch can be created in the repository from the command line. For example:

1git checkout --orphan orphan
2ls >> Test.txt
3git add Test.txt
4git commit -a -m "Orphan branch commit"
5git push origin orphan

We now have a two unrelated branches – the default branch and an orphan branch called orphan.

Orphan branch

What do you think will happen? At this point, hopefully you’ve realized the answer is nothing. Because the orphan branch isn’t associated with the default branch, it doesn’t contain a copy of the workflow. As a result, the push event is not triggered. If we create a workflow in the orphan branch, then everything works as expected.

What that means

In short, workflows will trigger on the push event when the workflow is present on the matching branch. The logic is simple and makes the process intuitive.

For this reason, you may want to consider using a CODEOWNERS file to protect changes to the workflows. While this won’t prevent a new workflow from running, it will ensure that it gets reviewed before it can merge to a protected branch.

In addition, consider environment secrets if you need to avoid exposing certain secrets to the workflow without approval (or gating). Repository and org secrets are available to workflows automatically. Environment secrets, however, require specific criteria to be met in order to expose those secrets. While this might not matter as much for accessing a package management server, it’s a very important consideration for secrets related to your deployment environments (or which expose credentials with elevated privileges).

Happy DevOp’ing!