Configuring GitHub Actions to respond to both push and pull_request events is a common pattern in continuous integration and deployment pipelines. However, this configuration introduces a significant operational inefficiency: double execution of workflow jobs. When a developer pushes a commit to a feature branch and subsequently opens a pull request (PR) targeting the main branch, the workflow triggers twice—once for the push event and once for the pull request event. This redundancy doubles computational costs, increases wait times for status checks, and raises the probability of spurious or flaky test failures. Properly structuring the on clause to filter branches and events is essential for maintaining an efficient, cost-effective, and reliable CI/CD pipeline.
The Pitfall of Generic Triggers
A naive approach to configuring GitHub Actions often results in a workflow definition that listens to all push and pull request activity without distinction. The following configuration is frequently seen in repositories:
yaml
on: [push, pull_request]
This syntax instructs GitHub Actions to run the workflow on any push to any branch or when a pull request is opened or updated. While this appears comprehensive, it creates a scenario where every commit pushed to a feature branch triggers a workflow run, and the subsequent opening of a pull request for that same commit triggers a second, identical workflow run.
The consequences of this double execution are tangible. First, it incurs double the billing cost for GitHub Actions minutes. Second, it introduces latency; developers must wait for two separate sets of status checks to complete before merging code. Third, running the same tests twice increases the surface area for intermittent failures, potentially causing false negatives that delay merges unnecessarily.
Optimizing the On Clause
The solution to double execution lies in restricting the push event to specific branches while allowing the pull_request event to trigger on all relevant PRs. The core logic is that pull request workflows should handle all non-main branch changes, while push workflows should only handle direct pushes to protected or main branches.
A corrected configuration looks like this:
yaml
on:
push:
branches:
- main
pull_request:
In this structure, the workflow runs on any push to the main branch or when a pull request is opened or updated, regardless of the source branch. This ensures that changes proposed via a pull request are tested once. Direct pushes to main (if permitted by branch protection rules) are also tested.
For projects that utilize semantic versioning or release tags, the configuration can be extended to include tag pushes. This is particularly useful for workflows that contain release jobs:
yaml
on:
push:
branches:
- main
tags:
- '**'
pull_request:
This setup ensures that the workflow runs on pushes to main, on any new tag, and on pull request activities. The result is a single, authoritative workflow run for each logical change, eliminating redundancy.
Understanding Pull Request Event Types
The pull_request trigger is more granular than a simple push event. It allows developers to specify exactly which pull request activities should initiate a workflow. By default, if no types are specified, GitHub Actions triggers on three specific events:
opened: When a new pull request is created.reopened: When a previously closed pull request is reopened.synchronize: When new commits are pushed to the branch associated with the pull request.
These defaults cover the majority of continuous integration use cases. However, GitHub Actions allows further customization to automate other aspects of the code review process. For example, workflows can be triggered when a pull request is closed, labeled, assigned, or edited:
yaml
on:
pull_request:
types: [opened, closed, labeled, assigned, edited]
Such configurations enable automation beyond testing. A workflow could trigger cleanup tasks when a PR is closed, assign reviewers based on labels, or notify specific teams when a PR is edited. Understanding these event types allows for more sophisticated automation that responds to the lifecycle of a pull request rather than just its code changes.
The Critical Distinction: Target Branch Filtering
A fundamental concept in GitHub Actions trigger configuration is the difference between source and target branches in the context of pull requests.
Push events are triggered by changes to the branch where the commit was made. If you push to feature-x, the push event fires for feature-x. Pull request events, however, are triggered based on the target branch of the pull request. When you open a pull request from feature-x to main, the pull_request event is associated with the main branch because that is where the changes are being proposed.
This distinction is critical for filtering. When you specify branches: [main] under a pull_request trigger, you are telling GitHub Actions to run the workflow only for pull requests that target main. You are not filtering by the source branch of the PR. This provides precise control over which workflows run for which protection rules. If a PR targets develop, and your workflow is configured to only run on pull_request to main, the workflow will not run for that PR. This ensures that workflows are only executed when the changes are relevant to the protected branch, saving resources and reducing noise.
Community Insights and Workarounds
The issue of double execution has been widely discussed in the GitHub community. Many users initially configure workflows with on: [push, pull_request] and later encounter the inefficiency. Community discussions highlight that this behavior is technically "correct" from GitHub’s perspective—the system is executing exactly what was asked—but it is often suboptimal for the user.
One common observation is that the Checks tab in a pull request will list jobs from both the push event (triggered by the latest commit on the source branch) and the pull_request event. This can be confusing, as it appears that the same tests are running twice. In reality, they are two separate workflow runs triggered by different events.
Some users have attempted to create complex workarounds to avoid this, such as using JavaScript scripts to check if a branch is already part of a pull request before running push-based jobs. While technically possible, these solutions add unnecessary complexity and maintenance overhead. The straightforward solution of restricting push triggers to the main branch (and tags) is the recommended best practice, as it aligns with standard Git workflows and GitHub’s intended design.
Implementing Intelligent Automation
Moving beyond basic triggers, developers can combine branch and path filters to create highly efficient workflows. For example, a workflow can be configured to run only when specific files change, or only when changes are pushed to specific branches.
branchesandbranches-ignore: Control which branches trigger the workflow.pathsandpaths-ignore: Control which file changes trigger the workflow.
By combining these filters, you can ensure that a deployment workflow only runs when changes are pushed to the main branch and only when files in the /src directory are modified. This level of granularity ensures that automation responds intelligently to changes, saving compute resources and providing feedback exactly when needed.
Conclusion
Configuring GitHub Actions to handle both push and pull request events requires careful consideration of event triggers and branch filters. The default behavior of triggering on all pushes and pull requests leads to duplicate workflow executions, increased costs, and delayed feedback. By restricting push triggers to main branches and tags, and allowing pull request triggers to handle feature branches, developers can eliminate redundancy and create efficient CI/CD pipelines.
Understanding the difference between source and target branches in pull requests is essential for precise control. Pull request triggers filter on the target branch, ensuring that workflows run only when changes are proposed to protected branches. Combined with event type filtering and path-based triggers, developers can build intelligent automation systems that respond to the specific needs of their projects.
As GitHub Actions continues to evolve, mastering these fundamental trigger configurations remains a critical skill for DevOps engineers and developers. Future enhancements, such as scheduled triggers and manual dispatches, build upon these foundational concepts, offering even greater flexibility for complex automation scenarios.