How to Run GitHub Actions Only on Specific File Path Changes

You waste precious minutes and cloud credits every time your CI/CD pipeline runs a full test suite for a simple README.md typo. In a high-velocity development environment, triggering a 20-minute build process for a non-code change isn't just annoying—it's a bottleneck that inflates your GitHub Actions bill and slows down your team's feedback loop. If you are managing a monorepo or a project with a mix of documentation and source code, you need a way to tell GitHub exactly which file changes deserve a workflow run.

GitHub Actions provides a built-in solution using the paths and paths-ignore filters under the on.push and on.pull_request triggers. By defining these filters in your YAML configuration, you can ensure that your backend tests only run when /src changes, and your deployment only fires when /dist is updated. This guide shows you how to implement these filters effectively to streamline your DevOps pipeline.

TL;DR — Use the paths: key in your workflow YAML to list specific directories that trigger a run. Use paths-ignore: to exclude files like documentation or configuration. Remember that you cannot use both keys for the same event in a single workflow.

The Concept of Path-Based Triggers

💡 Analogy: Imagine a high-security office building. If a delivery person brings a package for the Mailroom, you don't need to alert the CEO or trigger a fire drill for the entire building. Path filtering is your building's security desk—it checks the "delivery address" (the file path) and only alerts the relevant "department" (the specific workflow).

GitHub Actions uses a declarative YAML syntax to define when a workflow should execute. By default, a push or pull_request event triggers a workflow whenever any file in the repository changes. Path filtering acts as a conditional gatekeeper. It evaluates the list of changed files in a commit or a pull request against a list of patterns you provide. If there is a match, the workflow proceeds; if not, GitHub skips the run entirely.

This logic relies on "Glob" patterns. If you have ever used .gitignore, you are already familiar with the basics. For example, src/** matches any file inside the src directory and its subdirectories. Understanding how GitHub interprets these patterns is the secret to building efficient CI/CD pipelines. As of the current version of the GitHub Actions engine, these filters are evaluated before any virtual environment is provisioned, meaning you save 100% of the compute time for skipped runs.

When to Use Path Filtering

There are three primary scenarios where path filtering is not just helpful, but essential for maintaining a healthy repository. First, consider the Documentation Split. Almost every modern repository includes a README.md, a /docs folder, or contributing guidelines. If a team member fixes a typo in a markdown file, your unit tests for a React app or a Go binary don't need to run. Using paths-ignore for these files keeps the build queue clear for actual logic changes.

Second, Monorepo Architectures are the biggest beneficiaries. In a monorepo, you might have a /frontend, /backend, and /infrastructure directory. It is highly inefficient to run Python Django tests when only the CSS in your React frontend has changed. By assigning specific workflows to specific directories, you isolate the CI process. This allows for parallel development across teams without stepping on each other's compute resources.

Third, think about Sensitive Configuration Changes. You might have a directory like .github/workflows or /scripts that contains sensitive deployment logic. You can create a specialized "Security Audit" workflow that triggers only when these orchestration files are modified, ensuring that changes to the "how" of your deployment are scrutinized more heavily than the "what" of your source code.

How to Configure Path Filters in YAML

Implementing path filters requires adding the paths or paths-ignore key to your on configuration. Below is the standard syntax for including specific paths. This example ensures the workflow only runs when files in the src directory or the package.json file are modified.

name: Frontend CI
on:
  push:
    branches:
      - main
    paths:
      - 'src/**'
      - 'package.json'
  pull_request:
    paths:
      - 'src/**'
      - 'package.json'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Tests
        run: npm test

Conversely, you can use paths-ignore to run the workflow for every change except certain files. This is perfect for excluding documentation or metadata that doesn't affect the build. Note that you must use single quotes if your pattern starts with a character that YAML interprets as a special character (like *).

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

When writing these patterns, the ** syntax is your most powerful tool. A single * matches characters within a directory level, while ** matches recursively across any number of subdirectories. In my experience managing a 50-service monorepo, using precise patterns like services/auth-api/** rather than services/** prevented "noisy" builds that would have cost the company thousands in unnecessary runner minutes.

Common Pitfalls and Troubleshooting

⚠️ Common Mistake: You cannot use both paths and paths-ignore for the same event in the same workflow. If you try to define both, GitHub will throw a YAML validation error when you push the workflow file.

One of the most confusing aspects of path filtering involves Required Status Checks. If you have a branch protection rule that requires a specific check to pass before merging, and that check is part of a workflow that gets skipped due to path filtering, your Pull Request will be stuck forever. GitHub sees the check as "Pending" because it never started. To fix this, you either need to use a single "wrapper" workflow that always runs but uses internal logic to skip steps, or ensure your branch protection rules are aware of the optional nature of these checks.

Another issue occurs with Tag Pushes. Path filtering does not work with tag pushes. If you push a tag to GitHub, the paths and paths-ignore filters are ignored, and the workflow will always run. This is a common point of frustration for developers trying to automate releases. If you need path filtering for tags, you must handle the logic within the job steps using a shell script that checks git diff, rather than relying on the on: trigger.

Finally, remember that path filters only look at the files changed in the specific event. For a push, it looks at the commits pushed. For a pull_request, it looks at the diff between the base and head branches. If you perform a forced push or a complex rebase, sometimes the diff GitHub sees might be larger than you expect, causing the workflow to trigger even if your local "last commit" only touched a markdown file.

Best Practices for Monorepos

To keep your monorepo clean, use shared workflow templates. While path filters help in deciding *when* to run, the *what* can still be repetitive. You can combine path filters with workflow_call to reuse logic across different directories. For example, your auth-service.yml and user-service.yml can both have different paths: filters but call the same node-build.yml template.

Always include your dependency manifest files in your path filters. If you are building a Python app, don't just watch app/**.py. You must also include requirements.txt or pyproject.toml. If you update a dependency but don't change a line of source code, your tests still need to run to ensure the new dependency version didn't break your logic. Neglecting to include these files is a leading cause of "ghost bugs" where CI passes but the production build fails.

📌 Key Takeaways:

  • Path filters use standard Glob patterns; use ** for recursive matching.
  • Use paths-ignore to skip CI for README or documentation updates.
  • Never combine paths and paths-ignore in the same trigger block.
  • Be careful with Required Status Checks in Pull Requests—skipped workflows can block merges.
  • Include dependency lock files (package-lock.json, poetry.lock) in your filters.

Frequently Asked Questions

Q. How do I trigger a GitHub Action only when a specific file changes?

A. You can use the paths key under the on.push or on.pull_request trigger. For example, to trigger only when config.json changes, use paths: ['config.json']. This ensures the workflow ignores all other file modifications in the commit.

Q. Why is my GitHub Action running even though I used paths-ignore?

A. This usually happens because you are pushing a Tag, which ignores path filters, or because at least one file in your push was NOT in the ignore list. If a push contains five files and only four are ignored, the workflow will still trigger for the fifth file.

Q. Does GitHub Actions path filtering work with submodules?

A. No, path filters only evaluate files within the immediate repository. Changes inside a submodule do not trigger the paths filter in the parent repository unless the submodule pointer (the commit hash) itself is updated and committed to the parent repo.

Optimizing your workflows with path triggers is one of the most effective "quick wins" for any DevOps engineer. By reducing unnecessary noise, you provide faster feedback to developers and maintain a leaner, more cost-effective CI/CD pipeline. For more advanced use cases, refer to the official GitHub documentation on workflow syntax.

Post a Comment