GitHub Actions Tutorial — Branch & Path Filtering

2 minute read

Branch & Path Filtering

Not every push or pull request needs to trigger your entire CI pipeline. Branch and path filters let you control exactly which changes trigger which workflows — saving runner minutes and reducing noise.


Branch Filtering

Include Specific Branches

on:
  push:
    branches:
      - main
      - develop
      - 'release/**'

This triggers only on pushes to main, develop, or any branch matching release/** (e.g., release/v2.0, release/hotfix).

Exclude Branches

on:
  push:
    branches-ignore:
      - 'docs/**'
      - 'dependabot/**'

This triggers on pushes to all branches except those matching docs/** or dependabot/**.

Rule: You can use branches or branches-ignore, but not both in the same trigger.


Tag Filtering

Trigger on Tags

on:
  push:
    tags:
      - 'v*'

This triggers when a tag matching v* is pushed (e.g., v1.0.0, v2.3.1-beta).

Exclude Tags

on:
  push:
    tags-ignore:
      - 'v*-rc*'

This triggers on all tags except release candidates.


Path Filtering

Include Specific Paths

on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - 'package-lock.json'

The workflow triggers only when changed files match at least one of these patterns. If you only change README.md, the workflow won’t run.

Exclude Paths

on:
  push:
    paths-ignore:
      - 'docs/**'
      - '**.md'
      - '.gitignore'

This triggers on pushes unless the only changes are to documentation files, markdown files, or .gitignore.

Rule: You can use paths or paths-ignore, but not both in the same trigger.


Combining Filters

You can combine branch and path filters:

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'tests/**'

This triggers only when:

  1. The push is to main AND
  2. At least one changed file matches src/** or tests/**

Both conditions must be true.


Glob Patterns

GitHub Actions uses these glob patterns for filtering:

Pattern Matches
* Any characters except /
** Any characters including / (recursive)
? Any single character
[abc] Any character in the set
[a-z] Any character in the range
! Negate a pattern (when prefixed)

Examples:

Pattern Matches
src/** src/app.js, src/lib/utils.js
*.js app.js (root only, not src/app.js)
**/*.js Any .js file at any depth
release/v[0-9]* release/v1, release/v2.0

Real-World Examples

Frontend CI — Only on Frontend Changes


name: Frontend CI

on:
  push:
    branches: [main]
    paths:
      - 'frontend/**'
      - 'shared/**'
  pull_request:
    branches: [main]
    paths:
      - 'frontend/**'
      - 'shared/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd frontend && npm test

Backend CI — Ignore Documentation


name: Backend CI

on:
  push:
    branches: [main, develop]
    paths-ignore:
      - 'docs/**'
      - '**.md'
      - '.github/ISSUE_TEMPLATE/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew test

Release Pipeline — Tags Only


name: Release

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'    # matches v1.0.0, v2.3.1

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Publishing version ${{ github.ref_name }}"


Common Mistakes

Mistake Fix
Using branches and branches-ignore together Pick one — they’re mutually exclusive
Using paths and paths-ignore together Pick one — they’re mutually exclusive
Path filter never matches Check glob patterns; *.js only matches root, use **/*.js for recursive
Workflow still runs on docs changes Ensure paths-ignore covers all doc patterns including **.md

Quick Reference

Filter Purpose Example
branches: Include branches [main, develop]
branches-ignore: Exclude branches ['docs/**']
tags: Include tags ['v*']
tags-ignore: Exclude tags ['v*-rc*']
paths: Include file paths ['src/**']
paths-ignore: Exclude file paths ['**.md']

← Previous: Push & Pull Request

Next: Schedule & Dispatch →

Updated: