Featured image of post GitHub Actions: Understanding the Push and Pull Request Triggers

GitHub Actions: Understanding the Push and Pull Request Triggers

Learn how to use push and pull request triggers in GitHub Actions to automate workflows effectively with branch and path filters.

Introduction

Hey there everyone đź‘‹ Welcome back to our GitHub Action Journey! In our previous article, “Understanding GitHub Actions”, we built our first workflow - a “Hello World” automation that ran every time we pushed code to our repository. That was a great introduction, but what if we want more control over when and how our workflows run?

That’s precisely what we’ll be exploring in this article! We’ll dive deep into two essential triggers—the mechanisms that determine what events kick off our workflows. We’ll start by taking a closer look at the push event, which we used in our “Hello World” example. We’ll learn how to refine this trigger, giving us more control over when our workflow runs in response to code changes.

Then, we’ll explore the pull_request trigger, one of the most commonly used triggers in real-world workflows:

  • Push events: Control when your workflow runs based on code pushes
  • Pull request events: Automate tasks when someone suggests changes to your code

With these triggers, you’ll be able to automate tasks like running tests only on specific branches or performing code reviews when a pull request is created.

So, how do we actually put these triggers to work? We’ll start with the push event, since we’ve already used it in our “Hello World” example. Let’s break down how it works and see how we can tailor it to our specific needs.

The Push Event: Your First Trigger

When we’re using GitHub Actions, workflows are the backbone of automation, letting us organize the tasks we want to automate. Think of them as detailed recipes, written in YAML, that tell GitHub Actions what to do. Each workflow, like the simple “Hello World” example from our previous article, is set in motion by a trigger - a specific event that kicks off the automation process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Workflow (YAML File)
├── Name: My Workflow
├── on: [push]  // Triggers
└── jobs:
    ├── job1:
    │   └── steps:
    │       ├── step1: ...
    │       └── step2: ...
    └── job2:
        └── steps:
            └── step1: ...

Figure 1: A simplified structure of a GitHub Actions workflow, highlighting the key components: Name, Triggers, Jobs, and Steps.

Let’s take a look back at that basic workflow:

1
2
3
4
5
6
7
8
 name: Hello World      # The name of our workflow
 on: [push]            # What caused it to run
 jobs:                 # What it does
   say-hello:
     runs-on: ubuntu-latest
     steps:
       - name: Say hello
         run: echo "Hello, GitHub Actions!"

This workflow, triggered by the push event, showed us the fundamental components: a name, a trigger (on: [push]), and the work to be done (the say-hello job with its steps). Now, we’ll explore how to refine this push event, giving us more control over when our workflow runs. A common need is to differentiate between code versions. When developing software, we often work with multiple branches - parallel versions of our code. This allows us to isolate new features or bug fixes from the stable “main” branch. In its simplest form, on: push triggers the workflow on every push to any branch. But we can be more specific.

Controlling Workflow Execution with Branches

As we just discussed, the basic on: push trigger runs our workflow whenever we push code to any branch in our repository. But GitHub Actions gives us more control than that. We can use the branches and branches-ignore keywords to specify exactly which branches should (or shouldn’t) trigger our workflow.

The branches Keyword

The branches keyword lets us create a list of branches that will trigger the workflow. Let’s say we want our workflow to run only when we push changes to the main branch or any branch whose name starts with release/. Here’s how we’d do it:

1
2
3
4
5
on:
  push:
    branches:
      - main
      - 'release/**'

Let’s break this down:

  • on: push:: This part is familiar - it tells GitHub Actions that we’re configuring the push event trigger.
  • branches:: This keyword indicates that we’re about to specify a list of branches.
  • main: This line tells GitHub Actions to trigger the workflow when changes are pushed to the main branch.
  • 'release/**': This line uses a wildcard pattern. The * means that the workflow will be triggered on any branch whose name starts with release/, such as release/1.0, release/2.3.4, or even release/alpha.

With this configuration, pushes to other branches, like a development branch or a feature branch named new-login-page, will not trigger the workflow.

The branches-ignore Keyword

Sometimes, it’s easier to specify which branches should not trigger the workflow. That’s where branches-ignore comes in. Let’s say we want our workflow to run on all branches except those starting with feature/:

1
2
3
4
on:
  push:
    branches-ignore:
      - 'feature/**'

In this case, pushes to branches like feature/new-ui or feature/bug-fix will not trigger the workflow, while pushes to main, development, or any other branch will trigger it.

Combining branches and branches-ignore

We can even use branches and branches-ignore together for more complex scenarios. For example:

1
2
3
4
5
6
7
on:
  push:
    branches:
      - main
      - 'releases/**'
    branches-ignore:
      - 'releases/**-alpha'

This configuration would trigger the workflow on pushes to main and branches starting with releases/, except for branches starting with releases/ that also end with -alpha. So, releases/1.0 would trigger the workflow, but releases/2.0-alpha would not.

Let’s Practice!

Now it’s your turn to try it out! Let’s modify our “Hello World” workflow to run only when we push changes to the main branch:

  1. Open your main.yml file (it’s in the .github/workflows directory of your repository).
  2. Update the on: push section to include the branches keyword:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
name: Hello World
on:
  push:
    branches:
      - main

jobs:
  say-hello:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello, GitHub Actions!"
      - name: Tell us the time
        run: date
  1. Save the main.yml file.
  2. Stage, commit, and push the changes to your GitHub repository (you can use the Source Control panel in VS Code or the command line).

Head over to the “Actions” tab of your repository, and you’ll see that the workflow has run again, but this time it was only triggered because you pushed to the main branch!

Controlling Workflow Execution with File Paths

In any repository, not all files are created equal. Some files—such as the source code in your src directory or critical JavaScript files—are essential to your project’s functionality, while others, like documentation (e.g., Markdown files), may be less critical when it comes to triggering tests, security scans, or other resource-intensive workflows. GitHub Actions gives you the flexibility to react differently to changes in various files using the paths and paths-ignore keywords. This allows you to tailor your workflows so that they only run when changes occur in the parts of your repository that truly matter.

For example, suppose you want your workflow to trigger only when there are changes to:

  • A specific file (say, src/app.js), because it’s the heart of your application.
  • Any file within the src directory, where most of your code lives.
  • Or any JavaScript file in the entire repository, since these files are critical to your application logic.

You can set this up as follows:

1
2
3
4
5
6
on:
  push:
    paths:
      - 'src/app.js'      # Triggers if this specific file changes
      - 'src/**'          # Triggers if any file in the src directory (or its subdirectories) changes
      - '**/*.js'         # Triggers if any JavaScript file, anywhere in the repository, changes

Key Points:

  • Specific vs. General:

    • Using 'src/app.js' is very specific—only changes to that file will trigger the workflow.
    • Using 'src/**' is more general and matches any file within the src directory (and its subdirectories).
    • The pattern '**/*.js' matches any file ending in .js regardless of its location in the repository.
  • Pattern Matching:

    • The ** wildcard in a pattern matches any number of directories or files. For example, '**/*.js' explicitly indicates “any directory followed by any file ending with .js.” This is a common and clear convention, even though GitHub Actions might also understand simpler patterns like '**.js'.
  • OR Relationship:

    • The patterns listed under paths are evaluated with an OR condition. This means that if a push contains changes matching any one of these patterns, the workflow will trigger. You do not need a change to match all the patterns; matching one is sufficient.

By using these options, you gain precise control over when your workflow runs—ensuring that resource-intensive processes like testing or security scanning only occur when changes are made to the parts of your repository that truly warrant them.

The paths-ignore Keyword

Just as you can specify which file changes should trigger your workflow, you can also define which changes should not trigger it using the paths-ignore keyword. For instance, if you want your workflow to run on all pushes except those that only modify Markdown files or files in the docs directory, you can use:

1
2
3
4
5
on:
  push:
    paths-ignore:
      - '**/*.md'
      - 'docs/**'

With this configuration, if a push only includes changes to files ending in .md or files within the docs directory (or its subdirectories), the workflow will not run. Any other changes will trigger it.

Combining paths and paths-ignore

For even finer control, you can combine both paths and paths-ignore in your workflow configuration. This allows you to specify exactly which changes should trigger the workflow and which should be ignored. For example, if you want your workflow to trigger for any changes in the src directory except those within a src/test subdirectory, you can write:

1
2
3
4
5
6
on:
  push:
    paths:
      - 'src/**'
    paths-ignore:
      - 'src/test/**'

This configuration will trigger the workflow on any changes within the src directory, except for changes to files within the src/test subdirectory.

Let’s Practice!

Let’s add to our previous example. We’ll modify our workflow to run only when changes are pushed to the main branch AND when those changes include modifications to JavaScript files.

  1. Open your main.yml file.

  2. Update the on: push section to include both branches and paths:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    name: Hello World
    
    on:
      push:
        branches:
          - main
        paths:
          - '**/*.js'
    
    jobs:
      say-hello:
        runs-on: ubuntu-latest
        steps:
          - name: Say hello
            run: echo "Hello, GitHub Actions!"
    
          - name: Tell us the time
            run: date
    
  3. Save the main.yml file.

  4. Now, let’s create a simple JavaScript file to test our updated workflow. In the root of your repository, create a new file named test.js and add the following line of JavaScript code:

    1
    
    console.log("Hello from test.js!");
    

    This is just a basic JavaScript command that will print a message to the console.

  5. Stage, commit, and push the changes to your GitHub repository. You can use the Source Control panel in VS Code or the command line:

    1
    2
    3
    
    git add .
    git commit -m "Add test.js file"
    git push
    

Now, when you go to the “Actions” tab, you’ll see that the workflow was only triggered because you modified a JavaScript file on the main branch. If you make changes to other file types or on other branches, the workflow won’t run!

The pull_request Event: Automating Code Reviews

So far, we’ve explored how to trigger workflows using the push event, which is fantastic for automating tasks based on code changes being directly added to our repository. But in many development workflows, especially when collaborating with others, changes aren’t directly pushed to the main branch. Instead, they go through a review process using pull requests.

Think of a pull request as a formal proposal to merge changes from one branch into another. It’s a way of saying, “Hey, I’ve made some changes on this branch, and I think they should be incorporated into the main branch. Can someone please review them?”

GitHub Actions provides the pull_request event trigger, which allows us to automate tasks specifically for this code review process. This means we can set up workflows that automatically run tests, check code style, or perform other checks whenever a pull request is created or updated.

Github Actions Workflow trigger by branch and JS file changes

Understanding pull_request Events

Now, pull_request events are a bit different than the push events we talked about earlier. Unlike push, which simply signals a code push, pull_request involves a lot more activities related to proposing and reviewing code changes, especially when collaborating with others. So, when we’re working with pull_request triggers, it’s important to understand that each event actually has a specific type attached to it. This type tells us what kind of activity happened with the pull request.

Let’s start with the simplest way to use a pull_request trigger:

1
2
on:
  pull_request:

When used in this way, without specifying any types, GitHub Actions will, by default, only trigger our workflow if one of the following three event types occurs:

  • opened: This is when someone first creates a pull request. It’s usually a good time to run some initial tests or checks.
  • reopened: This is for when a pull request that was closed gets reopened. Just like when it’s first opened, you’d probably want to re-run your checks.
  • synchronize: Now, this one’s a bit more advanced, so we won’t go into all the details just yet. Think of it as a way to keep an eye on updates to the pull request’s code. We’ll explore this further in future articles.

These default types cover the most common situations where you’d want some automation to run when working with pull requests.

However, there are many other pull_request event types available. Here are a few you might find useful:

  • closed: This happens when a pull request is closed. It could be merged or just closed without merging - either way, this event is triggered.
  • assigned / unassigned: This is triggered when someone gets assigned to a pull request or unassigned from it.
  • labeled / unlabeled: This lets you know when labels are added to or removed from a pull request.
  • edited: This event occurs if someone changes the pull request’s title or description.

We can use the types keyword to tell GitHub Actions which of these event types we care about. For instance, if we only want our workflow to run when a pull request gets labeled or unlabeled, here’s what we could use:

1
2
3
on:
  pull_request:
    types: [labeled, unlabeled]

Or, if we want to be really thorough and trigger our workflow on all the event types we’ve talked about, we would use something like this:

1
2
3
on:
  pull_request:
    types: [opened, synchronize, reopened, closed, assigned, unassigned, labeled, unlabeled, edited]

Just remember, every pull_request event will always have one of these types attached to it. It’s like a little tag that tells us what happened. The types keyword helps us pick and choose which types we want our workflow to respond to. And if we don’t specify any types, like in our first example, GitHub Actions will just assume we mean opened, reopened, and synchronize.

Fine-Tuning Your pull_request Triggers: Branches and Paths

Similar to how we used filters with the push event, we can fine-tune our pull_request triggers using branches, branches-ignore, paths, and paths-ignore. Here’s the key difference when using them with pull_request events:

Understanding the Pull Request Process

Remember those branches we talked about earlier - main and our release branches? When developers want to contribute changes, they typically follow these steps:

  1. Create a Branch: They start by branching off from their target branch (like main or a release branch). This new branch - called the source (or head) branch - gives them a safe space to work on their changes.
  2. Make Changes: They make their code changes and commit them to their source branch.
  3. Open a Pull Request: They create a pull request on GitHub, connecting their source branch (with the new changes) to the target (or base) branch (where they want their changes to end up).
  4. Review and Merge: Other developers review the code, potentially suggesting changes. Once everything’s approved, the pull request gets merged - which means all the changes from the source branch become part of the target branch.

Filtering Based on Branches

Here are a couple of examples:

Example 1: Triggering only on pull requests targeting the main branch:

1
2
3
4
on:
  pull_request:
    branches:
      - main

This setup is straightforward. Our workflow will only run when someone creates or updates a pull request that’s aimed at merging changes into the main branch.

Example 2: Triggering on pull requests targeting branches starting with release/, except for release/old-version:

1
2
3
4
5
6
on:
  pull_request:
    branches:
      - 'release/**'
    branches-ignore:
      - 'release/old-version'

Here’s a slightly more complex scenario: Our workflow will run when the pull request is meant to be merged into any branch that starts with release/, unless that branch is named exactly release/old-version.

Filtering Based on File Paths

Remember, a pull request is always about merging changes from one branch into another. While branch filters look at where your changes are going, paths and paths-ignore look at what you’re actually changing in your source branch.

Let’s say someone’s working on a feature branch. If they’re only updating documentation, do we need to run our full test suite? Probably not. But if they’re modifying our JavaScript files? That’s when we want our tests to kick in.

Here’s how we handle that:

1
2
3
4
5
6
7
8
# Example 3: Running checks only when source code changes
on:
  pull_request:
    paths:
      - 'src/**'
      - '**/*.js'
    paths-ignore:
      - 'src/docs/**'

This workflow says: “If someone changes any file in the src directory or any JavaScript file - except documentation - we need to run our checks.”

Combining Branch and Path Filters

But here’s where it gets really powerful - we can combine both filters! Let’s say we want to be extra careful with code going into our main branch:

1
2
3
4
5
6
7
8
# Example 4: Running checks for source code changes going into main
on:
  pull_request:
    branches:
      - main
    paths:
      - 'src/**'
      - '**/*.js'

Now our workflow only runs when someone’s trying to merge JavaScript or source code changes into the main branch. Documentation updates or changes to other branches? We might ignore them or handle those differently.

Let’s Practice!

Alright, let’s apply what we’ve learned! We’ll create a workflow that demonstrates how to use pull request filters effectively.

Goal: We want to create a workflow that only triggers under two specific conditions:

  1. When someone opens a pull request targeting our main branch
  2. When that pull request includes changes to JavaScript files in our src directory

Here’s the Plan: We’ll walk through this process step by step:

  1. Create a new branch to work on our changes
  2. Set up a src directory and add a JavaScript file to it
  3. Update our workflow file to use pull request filters
  4. Push our changes and open a pull request
  5. Observe how our workflow responds to the pull request

Note that we won’t actually merge our changes - we’re just interested in seeing how the workflow responds when we open the pull request.

Steps:

  1. Create a New Branch

First, let’s create a new branch called test-pr-filters. In Git, branches let us work on changes separately from our main code, and it’s good practice to give them descriptive names - in our case, we’re testing pull request filters.

1
2
# Create and switch to our new branch
git checkout -b test-pr-filters

Git Branch Commands: You might see different commands for working with branches in Git:

  • git branch <name> creates a branch
  • git checkout <name> switches to a branch
  • git checkout -b <name> does both in one step (what we used above)
  • git switch -c <name> -c (short for --create) is the newer (Git 2.23+) it also does both in one step but in a more intuitive way

We used checkout -b since you’ll often see it in documentation, but feel free to use switch -c if you prefer.

If you’re using VS Code, you can also create branches through the interface - check our previous article Setting Up Your First GitHub Repository for step-by-step instructions.

  1. Set Up the Source Directory

Now we’ll create a src directory and add a JavaScript file to it. Many projects keep their source code in a src directory - it’s a common convention that helps organize code files.

1
2
3
4
5
# Create the src directory
mkdir src

# Create and add content to our JavaScript file
echo 'console.log("Testing pull request filters!");' > src/test.js

Quick Note: We’re using mkdir to create a directory and echo with > to create and write to our file. If you prefer, you can also:

  • Create the src folder manually in your file explorer
  • Create test.js in your favorite code editor
  • Copy our existing test.js file into the new src directory

The important thing is that we end up with a JavaScript file inside a src directory, as this matches the path filter we’ll set up in our workflow.

If you’re using VS Code, you can right-click in the explorer panel to create both the directory and file - refer to our previous article for the detailed steps.

  1. Update the Workflow File

Let’s modify our workflow file to use pull request filters. We’ll update main.yml in the .github/workflows directory to run only when someone opens a pull request targeting our main branch with changes to JavaScript files in the src directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
name: PR Filter Demo

on:
  pull_request:
    branches:
      - main
    paths:
      - 'src/**.js'

jobs:
  check-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Check PR details
        run: |
          echo "This workflow ran because:"
          echo "1. A pull request is targeting main"
          echo "2. JavaScript files in src/ were modified"          

About the Workflow:

  • The pull_request trigger responds to pull request activities
  • branches: [main] ensures we only run on PRs targeting main
  • paths: ['src/**.js'] matches any .js file in the src directory
  • The * means “any subdirectory” (so it would also match src/utils/file.js)
  • Our job simply prints a message confirming why it ran

This workflow will help us verify that our filters are working correctly when we open our pull request.

  1. Push Changes

At this point, the new branch exists only on your local machine.

For GitHub (or any remote repository), both branches need to be available on GitHub before merging so that GitHub can see and compare them.

To merge our local branch test-pr-filters with the main branch, we first need to push it to GitHub.

Pushing the branch does not merge it with main—it simply makes it available remotely.

This way, when we open a pull request later, GitHub will be able to compare both branches properly.

1
2
3
4
5
6
# Stage and commit all our changes
git add .
git commit -m "Add src directory and update workflow with PR filters"

# Push our changes to GitHub
git push -u origin test-pr-filters

About These Commands:

  • git add . stages all our changes (the new src directory and updated workflow file)
  • git commit -m "..." creates a commit with a descriptive message.
  • git push -u origin test-pr-filters →
  • The -u flag (short for --set-upstream) tells Git to remember which remote branch to push to in the future.
  • This way, for future git push commands, you don’t have to specify the remote branch every time—it will automatically push to test-pr-filters on GitHub.
  1. Open a Pull Request

Now that we’ve pushed our branch to GitHub, let’s open a pull request to see our workflow filters in action.

When you go to your repository on GitHub, you should see a “Compare & pull request” button, as shown below:

GitHub Button Compare & pull request

This appears because GitHub notices your newly pushed branch.

Steps to Open a Pull Request:

  1. Click the Compare & pull request button and you’ll see a form to create your pull request, like this:

    GitHub pull request form details

  2. In the pull request form:

    • Set Base branch: This is the branch where we want to merge our changes (main).

    • Set Compare branch: This is the branch with the changes we’ve made (test-pr-filters).

    • Title: GitHub automatically uses the most recent commit message as the title for the pull request. You can leave it as is or modify it for clarity.

    • Description: Provide a brief explanation of the purpose of the pull request. For example:

      “This pull request tests the GitHub Actions workflow trigger by creating a pull request that targets the main branch. The pull request includes changes to a JavaScript file in the src directory and serves to verify whether the workflow responds to pull requests with these criteria.”

  3. Click the green “Create pull request” button.

Note:

Don’t merge the pull request yet. At this point, we want to observe whether the pull request successfully triggers our GitHub Actions workflow. We’ll merge the pull request in a later step.

If you prefer using VS Code’s interface for pushing changes and creating pull requests, refer to our previous guide Setting Up Your First GitHub Repository for a step-by-step walkthrough.

  1. Observe the Workflow Results

After creating the pull request, navigate to the “Actions” tab in your GitHub repository. You should see the “PR Filter Demo” workflow running, as shown below:

GitHub Actions triggered by a pull request

Click on the workflow run to view the details. If the workflow is configured correctly, you should see an output similar to this:

1
2
3
This workflow ran because:
1. A pull request is targeting main
2. JavaScript files in src/ were modified

Why This Happened:

  • The pull request targets the main branch, as specified in our workflow trigger.
  • Changes were made to JavaScript files in the src directory, satisfying the paths condition.

Further Testing the Filters

To verify the robustness of your filters, try these scenarios:

  • Create a pull request targeting a different branch: The workflow shouldn’t run.
  • Modify non-JavaScript files in the src directory: The workflow shouldn’t run.
  • Change files outside the src directory: The workflow shouldn’t run.

Each of these cases confirms that our workflow responds only when both conditions are met.

  1. Merge the Pull Request

After confirming that the workflow triggered successfully, you can now merge the pull request to clean up your repository and leave it in a good state for future use.

Pull Request Overview

When viewing the pull request, you’ll see a summary similar to this:

GitHub Merge pull request

What This Means:

  1. “All checks passed”: This confirms that our GitHub Actions workflow ran successfully without issues.
  2. “This branch has no conflicts with the base branch”: Indicates that test-pr-filters can be merged into main automatically.

Steps to Merge:

  1. Click the green “Merge pull request” button.
  2. Confirm the merge when prompted.
  3. Delete the test-pr-filters branch if prompted, as it is no longer needed.

Why We’re Merging:

Although the goal of this guide was to test the workflow trigger, merging the branches ensures the repository remains organized. Future articles and readers following along won’t encounter multiple stale branches.

Recap

🎉 Congratulations! You’ve successfully developed a strong understanding of two of the most commonly used triggers in GitHub Actions: push and pull_request. This sets a solid foundation for automating and improving your GitHub workflows.

In this guide, we’ve:

  1. Explored the push and pull_request triggers, understanding how they work and when to use them.
  2. Fine-tuned workflows with branches and path filters, giving you precise control over when your workflows run.
  3. Successfully set up workflows that respond to both push and pull requests, targeting specific branches and files.

These triggers lay the foundation for more advanced tasks, such as code reviews, testing, and deployments, all built on the same principles.

But this is just the beginning! 🚀 In our next article, we’ll dive into two more advanced triggers:

  • Scheduled triggers for running workflows at specific times.
  • Manual triggers for workflows you can launch on demand.

Until then, take some time to experiment with the branches and path filters we’ve covered here. You’ll discover just how much control and flexibility they provide for automating your GitHub projects.