Filter workflows by path in GitHub Actions
Add `paths` or `paths-ignore` to expensive workflows so a docs-only or unrelated change doesn't trigger the full test suite.
Do this
Expensive workflows (E2E, full integration suites) declare a paths filter listing the source directories that can actually affect them, or a paths-ignore list for docs and markdown. A README change no longer spins up the entire suite. Three syntax rules govern the filter: paths and paths-ignore are only evaluated on the push, pull_request, and pull_request_target events (on any other trigger they do nothing, and they are skipped for tag pushes); * matches any character except / while ** matches across directories; and a ! negation excludes what an earlier pattern matched, with the last matching pattern winning, which is why an exclusion like !**/*.md has to come last. That last rule is GitHub's own; dorny/paths-filter is a different matcher that ORs its patterns, so a ! pattern there does not mean the same thing.1234
# A REQUIRED check must never be filtered at the workflow level: a workflow that
# does not run reports no status at all, and the PR blocks forever waiting for it.
# So leave the trigger unfiltered, compute what changed in a cheap job, and gate
# the expensive job with if:. A job skipped by a conditional reports Success,
# which satisfies branch protection.
#
# Not a required check? Then you do not need any of this. Filter the trigger
# itself and the workflow simply does not run:
#
# on:
# pull_request:
# paths:
# - "src/**"
# - "package.json"
# - "pnpm-lock.yaml"
# - ".github/workflows/e2e.yml" # re-run if the workflow itself changes
on:
pull_request:
jobs:
changes:
runs-on: ubuntu-latest
# dorny reads the PR's file list from the REST API, so it needs
# pull-requests: read. Declaring a permissions block at all sets every scope
# you DON'T list to none, and new repos default to a restricted token that
# lacks it anyway - either way, without this line the step fails with
# "Resource not accessible by integration" and takes the required check down
# with it. No checkout needed on pull_request events.
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.filter.outputs.src }}
steps:
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
# dorny is not GitHub's matcher: it ORs its patterns, so a file that
# matches ANY of them sets the output. Do not paste a "!" exclusion in
# here expecting GitHub's last-match-wins rule - "!**/*.md" would match
# every file that is not markdown, and the filter would fire on
# everything. List what you DO care about.
filters: |
src:
- "src/**"
- "package.json"
- "pnpm-lock.yaml"
e2e:
needs: changes
if: ${{ needs.changes.outputs.src == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10 # ubuntu-latest ships no pnpm; required unless package.json sets "packageManager"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:e2eAvoid this
A docs-only change spins up the full suite, spending wall-clock and runner minutes for nothing.
on:
pull_request: # a one-word README fix triggers the full E2E suiteSeen in the wild
Real workflow files from public projects.
on:
push:
branches:
- main
paths:
- "packages/**"
- .prettierrc
- .prettierignore
- tsconfig.json
- oxlint.json
- "!**/*.md" pull_request:
paths-ignore:
- compiler/** pull_request:
paths:
- crates/**
- examples/**
- Cargo.toml
- .github/workflows/test-rust.ymlWhy it matters
Running an expensive suite on changes that can't affect it is pure waste, both wall-clock the developer waits on and runner-minutes you pay for. Path filters are a high-impact, low-risk trigger fix. The one thing to watch: if a required status check is path-filtered, a skipped run never reports its status, and GitHub has no branch-protection setting that treats a missing check as passed, so the PR waits forever. Either don't make the path-filtered workflow a required check, or gate the work with a job-level if: condition instead of a workflow-level paths filter: GitHub reports a job skipped by a conditional as Success, which satisfies the required check, whereas a skipped workflow reports nothing at all.
When to use
Use it when
Workflows with several jobs and a clear set of source paths that gate them, especially E2E and integration suites.
Be careful when
Be careful path-filtering a required status check: a filtered-out workflow never reports, and GitHub can't treat a missing check as satisfied, so the PR stays blocked. Prefer path-filtering non-required expensive workflows. If the check must stay required, gate the work with a job-level if: condition instead: a job skipped by a conditional reports Success and satisfies the check, unlike a workflow skipped by paths. To compute the condition that if: tests, run dorny/paths-filter in a cheap upstream job (pinned to its commit SHA) and read its per-filter output, as in the YAML above.
Verify on your repo
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.
Inspect this repo's .github/workflows for expensive workflows (E2E, integration suites) that trigger on every change with no path scoping. For each, look at the `on:` block for `paths:` or `paths-ignore:` and flag multi-job workflows that have neither. Add a `paths` allowlist of the source directories that can actually affect the workflow (include the workflow file itself) so a docs-only change does not spin up the full suite. Important: do not path-filter a required status check, since a skipped workflow never reports and blocks the PR forever, so instead gate the expensive work with a job-level `if:` condition (a job skipped by a conditional reports Success). Show me the diff and open a PR rather than applying it blindly.
Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax, https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.Prefer to check by hand?
Look at the
on:block of each expensive workflow forpaths:orpaths-ignore:.Flag workflows with several jobs and no path filtering at all.
For any required check, don't path-filter the whole workflow: either drop it from the required list, or gate the expensive work with a job-level
if:(a job skipped by a conditional reportsSuccess), so PRs aren't blocked waiting on a check that never runs.
Automate this with StarSling.
Rather than audit this by hand, install the StarSling GitHub App: it opens a pull request to move your workflows onto StarSling runners, then its agents keep reading your jobs, run logs, and machine telemetry and open pull requests that fix what this page covers, along with caching, parallelization, and test sharding across the rest of your pipeline. Every change arrives as a PR you review and merge.
More best practices for GitHub Actions
Where to go next in the CI best-practices catalog.
- Trigger Scope
Cancel superseded runs with concurrency groups
Add a GitHub Actions
concurrencygroup scoped to the ref and turn oncancel-in-progressfor PR runs (it defaults tofalse), so a new push cancels the now-obsolete run instead of leaving both to occupy runners. - Parallelization
Build and test only what changed in GitHub Actions
Scope your slowest build/test job to only the packages a PR actually changed, using your monorepo tool's affected mode, with a mandatory full-run fallback so a resolution error never silently skips work.
FAQ
Should I use paths or paths-ignore?
paths is an allowlist (run only when these change) and is safest for a suite with a well-defined source set. paths-ignore is a denylist (skip when only these change) and fits when you mainly want to exclude docs and markdown. Don't set both on the same event.
Will a path filter break a required status check?
It can. When a workflow is skipped by a paths filter, its check context never reports, and branch protection waits for it forever, GitHub has no setting that counts a missing check as passed. The safe fix: don't require a path-filtered workflow, or gate the expensive job with a job-level if: condition instead. GitHub reports a job skipped by a conditional as Success (which satisfies the required check), whereas a skipped workflow reports nothing.
Why is my paths filter being ignored?
GitHub only evaluates paths and paths-ignore on the push, pull_request, and pull_request_target events, and it skips them for tag pushes. On a schedule, workflow_dispatch, or workflow_call trigger the filter does nothing and the workflow runs anyway. The other common cause is the glob: * stops at a /, so src/* matches only the files directly under src, while src/** matches the whole tree beneath it.
Sources
1GitHub Actions · paths / paths-ignore filters (opens in new tab)
2GitHub Actions · filter pattern cheat sheet (workflow syntax) (opens in new tab)
3dorny/paths-filter (opens in new tab)
4GitHub · troubleshooting required status checks (opens in new tab)
Last updated 2026-07-15
Don't run the full suite on a docs change.
One line to install. Faster runs on day one, and agents that open reviewable PRs to keep your pipeline following practices like this one.