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.
Last updated: August 2026

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 branches and branches-ignore for the same event.
  • You cannot define both tags and tags-ignore for the same event.
  • You cannot define both paths and paths-ignore for 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 push events: branches filters against the target branch to which commits were pushed (github.ref).
  • For pull_request events: branches filters 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 PatternMatching BehaviorExample MatchNon-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 bracketsapp-a (with app-[abc])app-d
[a-z]Matches any single character within the specified rangev1.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:

  1. Sequential Top-to-Bottom Evaluation: Patterns are evaluated in the exact order they appear in the YAML list.
  2. 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.
  3. 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 push events, GitHub Actions evaluates all modified, added, or deleted files between github.event.before and github.event.after.
  • For pull_request events, 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 paths filter, the workflow triggers. If all changed files match paths-ignore (or none match paths), 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:

  1. A repository requires a status check named build-and-test to pass before a pull request can be merged into main.
  2. The workflow that produces build-and-test has paths: ['src/**'].
  3. A developer opens a pull request that updates only docs/README.md.
  4. The Problem: Because no files under src/** changed, GitHub Actions skips the workflow entirely. As a result, build-and-test never reports a status (neither success nor failure).
  5. 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 paths from the on: trigger. Run the workflow on all PRs, but use a path-filtering step (like dorny/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.
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D