2.4 Branch, Tag, and Path Filtering Patterns
Key Takeaways
- The branches and branches-ignore filters are mutually exclusive within the same event trigger; attempting to configure both results in a YAML validation error.
- Glob pattern matching in GitHub Actions supports * (matches characters except /), ** (recursive path matching including /), ? (single character), and ! (negation).
- Filter patterns are evaluated sequentially from top to bottom; a negative pattern prefixed with ! excludes previously matched refs or paths, and defining only negative patterns includes all non-matching targets.
- Path filtering (paths and paths-ignore) inspects the modified files in a push or pull request; if all changed files match paths-ignore (or none match paths), the workflow does not run.
- Combining required status checks with path filtering can cause pull requests to hang in a pending state when changes do not trigger the required check; solving this requires generic check wrappers or multi-job workflows.
Branch, Tag, and Path Filtering Patterns
In enterprise repositories and monorepos containing multiple services, running every workflow on every commit is inefficient, slow, and costly. GitHub Actions provides filtering mechanisms to execute workflows only when specific branches, tags, or file paths are modified.
Understanding glob pattern syntax, negation precedence, mutual exclusivity rules, and path filtering edge cases is essential for passing the GH-200 exam and architecting scalable enterprise CI/CD pipelines.
1. Branch and Tag Filtering Mechanics
Branch and tag filters scope workflow execution to specific Git references for push and pull_request events.
Mutual Exclusivity Rule
For a single event, you cannot specify both an inclusion filter and an exclusion filter simultaneously:
- You cannot define both
branchesandbranches-ignorefor the same event. - You cannot define both
tagsandtags-ignorefor the same event. - You cannot define both
pathsandpaths-ignorefor the same event.
Attempting to define both triggers a workflow syntax validation error:
# ❌ INVALID SYNTAX: Mutually exclusive keys in the same trigger
on:
push:
branches:
- 'main'
- 'releases/**'
branches-ignore: # ERROR: Cannot use branches and branches-ignore together!
- 'releases/**-alpha'
# ✅ VALID SYNTAX: Using negation patterns within branches
on:
push:
branches:
- 'main'
- 'releases/**'
- '!releases/**-alpha' # Correct way to exclude specific branches
Push vs. Pull Request Branch Filtering Semantics
An important distinction on the GH-200 exam is how branches is evaluated across different event types:
- For
pushevents:branchesfilters against the target branch to which commits were pushed (github.ref). - For
pull_requestevents:branchesfilters against the base branch (target of the PR), NOT the head/source branch containing the author's commits!
on:
pull_request:
branches:
- main # Workflow runs only when PR targets main, regardless of PR source branch name
2. Glob Pattern Syntax & Matching Rules
GitHub Actions uses standard globbing syntax for matching branches, tags, and file paths:
| Glob Pattern | Matching Behavior | Example Match | Non-Match |
|---|---|---|---|
* | Matches zero or more characters except directory separator / | releases/v1 (with releases/*) | releases/v1/patch |
** | Matches zero or more characters including directory separator / | src/api/v1/auth.js (with src/**.js or src/**/auth.js) | tests/auth.js |
? | Matches exactly one character (excluding /) | v1.2 (with v?.?) | v10.20 |
+ | Matches one or more of the preceding pattern (in character classes) | v1 (with v[0-9]+) | v |
[abc] | Matches any single character enclosed within the brackets | app-a (with app-[abc]) | app-d |
[a-z] | Matches any single character within the specified range | v1.2.3 (with v[0-9].[0-9].[0-9]) | va.b.c |
! | Negation prefix: Excludes matching patterns from the filter list | !docs/** | N/A |
[!IMPORTANT] YAML Quoting Rule for Special Characters: In YAML syntax, unquoted strings starting with
*or!are interpreted as YAML anchors/aliases or custom type tags, resulting in parsing errors. Always enclose glob expressions containing*,!,?, or brackets in single or double quotes (e.g.,'releases/**','!docs/**').
3. Pattern Evaluation Order and Precedence Rules
GitHub Actions evaluates glob patterns in a defined sequential order:
- Sequential Top-to-Bottom Evaluation: Patterns are evaluated in the exact order they appear in the YAML list.
- Inclusion Followed by Negation: If a target matches a positive pattern, it is marked for inclusion. If a subsequent negative pattern (
!) matches the target, it is removed from inclusion. - Negation-Only Lists: If a filter contains only negative patterns (
!), GitHub Actions implicitly treats the filter as matching everything except the negated patterns. For example:
on:
push:
paths:
- '!docs/**'
- '!**.md'
This configuration triggers on any push where at least one modified file is NOT inside docs/ and does NOT end with .md.
Case Sensitivity
Branch, tag, and path filtering in GitHub Actions is strictly case-sensitive. A path filter for 'src/**.ts' will not match 'SRC/index.ts'.
4. Path Filtering in Practice & The Required Status Check Dilemma
Path filtering allows workflows to execute only when specific files or directories are modified.
How Path Changes Are Evaluated
- For
pushevents, GitHub Actions evaluates all modified, added, or deleted files betweengithub.event.beforeandgithub.event.after. - For
pull_requestevents, GitHub Actions evaluates all changed files across the entire pull request diff against the base branch. - Trigger Threshold: If at least one changed file matches the
pathsfilter, the workflow triggers. If all changed files matchpaths-ignore(or none matchpaths), the workflow is skipped.
name: Backend API CI
on:
push:
branches: [main]
paths:
- 'services/api/**'
- 'shared/libs/**'
- 'package.json'
- '!services/api/**/*.md'
pull_request:
branches: [main]
paths:
- 'services/api/**'
- 'shared/libs/**'
The "Required Status Check" Hanging Dilemma
A classic enterprise issue on the GH-200 exam involves Branch Protection Rules combined with path filters:
- A repository requires a status check named
build-and-testto pass before a pull request can be merged intomain. - The workflow that produces
build-and-testhaspaths: ['src/**']. - A developer opens a pull request that updates only
docs/README.md. - The Problem: Because no files under
src/**changed, GitHub Actions skips the workflow entirely. As a result,build-and-testnever reports a status (neither success nor failure). - The Impact: The pull request displays
Expected — Waiting for status to be reported, permanently blocking the developer from merging.
+-------------------------------------------------------------------------+
| REQUIRED STATUS CHECK DEADLOCK |
| |
| [Branch Protection Rule] ---> Requires check: 'ci/test' |
| [Pull Request #42] ---> Modifies ONLY 'docs/architecture.md' |
| [Workflow paths filter] ---> paths: ['src/**'] |
| |
| [Result] ---> Workflow skipped. 'ci/test' is NEVER sent. |
| [PR State] -> Stalled at: 'Expected — Waiting for status to report' |
+-------------------------------------------------------------------------+
Recommended Solutions
- Strategy 1: Universal Workflow with Internal Conditional Steps: Remove top-level
pathsfrom theon:trigger. Run the workflow on all PRs, but use a path-filtering step (likedorny/paths-filter) to conditionally skip heavy build steps while still reporting a successful top-level check. - Strategy 2: Status Check Aggregator / Wrapper: Create a lightweight generic job that always runs and reports success when non-code files are changed.
A workflow defines the following trigger configuration: on: push: branches: - 'releases/' - '!releases/-alpha' - '!releases/**-beta' Which of the following branch pushes will successfully trigger the workflow?
An author attempts to validate a workflow file with the following configuration: on: push: branches: - 'main' - 'releases/' branches-ignore: - 'releases/-draft' What will occur when this workflow file is pushed to GitHub?
A repository enforces a branch protection rule requiring the build-and-test status check to pass before merging into main. The workflow is configured with on: pull_request: paths: ['src/**']. A developer opens a pull request that only modifies docs/architecture.md. What happens to the pull request?