dorny/paths-filter outputs always false in GitHub Actions
dorny/paths-filter outputs always false in GitHub Actions is almost never a broken glob. The action could not work out which files changed, fell back to a smaller comparison, and set every filter from that fallback, and it says so in the log two lines above the result.

What this error means
The filter step is green and nothing in the run is red. The steps you gated on it never run, including on a commit that obviously touched the directory the filter names. Above the results the action prints how it decided to detect changes, and that line is the diagnosis: it names the base it compared against, or it warns that it could not find one. Below it the results block lists one line per filter, Filter <name> = false, with Matching files: none in each group. Two problems end here. Either the change set the action computed was not the one you meant, or it was right and the condition reading the output compares the wrong things. The log below is that fallback inverted: in that report it set every filter true, because the one commit it fell back to had touched everything.
Run dorny/paths-filter@v2.10.2
Get current git ref
/usr/bin/git branch --show-current
master
Warning: 'before' field is missing in event payload - changes will be detected from last commit
Change detection in last commit
Detected 821 changed files
Results:
Filter my_first_path = trueA minimal workflow that produces it
This file is written for this page and has never been run. It is the smallest shape that reproduces the fallback: a manual trigger, no base input, and a filter over a directory. Dispatched from the default branch, the base resolves to that same branch, so the action looks for the previous commit on the event payload and a workflow_dispatch payload carries none. It warns, then narrows to the last commit. Every filter then describes that one commit rather than your branch.
name: changed
on:
workflow_dispatch:
jobs:
changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
src:
- 'src/**'
- if: steps.filter.outputs.src == true
run: npm testCommon causes
The event carried no previous commit to diff against
The common one, and the one the log names. It fires when the base resolves to the branch you are already on, which is what a workflow_dispatch, schedule or workflow_run from the default branch looks like: those payloads carry no before commit, so the action narrows to the last commit unless you give it a base. A squash merge lands as one commit, so even on push the fallback can describe a change set that has nothing to do with the branch you merged.
The condition compares a string to something that is not that string
The outputs are the text true and false. A comparison against a bare boolean, or no comparison at all, is wrong in a way that looks like the filter failed. When the log shows the comparison you meant, this is the half that is left, and the log agrees with it.
The history the diff needs is not in the checkout
On any path that uses git rather than the REST API, the action needs the commits it is comparing. A default checkout is shallow, and the action fetches more history only up to initial-fetch-depth, which defaults to 100. Skip actions/checkout and the git paths cannot run at all.
How to fix it
Read the line above the results before changing anything
- Open the filter step in the run and look at the group above
Results:. - If it says the
beforefield is missing, the action compared one commit and the fix is abase. - If it names two refs, the comparison was right and the condition reading the output is wrong.
Give the action a base it can resolve for your event
On pull_request the base is the pull request base and you need nothing. Everywhere else, set base explicitly to the branch or commit you actually want to compare against. Setting base to the branch that was pushed is the documented way to compare against the previous commit on a long-lived branch.
- uses: dorny/paths-filter@v4
id: filter
with:
base: main
filters: .github/filters.ymlCompare the output to the quoted string
Use single quotes, the way the README does, and keep the step id and the filter key identical in both places. If you want the count instead of the boolean, the action already gives you one as <name>_count.
- if: steps.filter.outputs.src == 'true'
run: npm test
- if: steps.filter.outputs.src_count != '0'
run: echo 'at least one file matched'Check out enough history for the comparison you asked for
When the action is diffing with git, give it the commits. A full history is the blunt instrument and it is slow on a large repository; raising initial-fetch-depth is the cheaper one, because the action doubles its fetch until it finds the merge-base.
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dorny/paths-filter@v4
id: filter
with:
initial-fetch-depth: 200
filters: .github/filters.ymlThe action picks a comparison before it matches anything
The README sets out one behavior per trigger and they are not interchangeable. On pull_request, "changes are detected against the pull request base branch" using the REST API. On a feature branch, "changes are detected against the merge-base with the configured base branch or the default branch" using git, and the repository "must be already checked out". When base names the branch that was pushed, changes are "detected against the most recent commit on the same branch before the push".
When none of those apply the action says so before it prints a result. The warning below is the exact string in src/main.ts, and it is the line to look for in the step log. It is a warning, so the step still succeeds and every output is still set.
Warning: 'before' field is missing in event payload - changes will be detected from last commitThe output is a string, so the comparison has to be one too
Every filter sets an output named after the filter, and the README documents its two values as text: 'true' if any changed file matches, 'false' if none does. It also sets <name>_count, and a changes output that is "JSON array with names of all filters matching any of the changed files".
So if: steps.filter.outputs.src == true compares a string against a boolean literal, which is not the comparison the README writes: its own example is if: steps.changes.outputs.src == 'true', with single quotes. The mirror image is if: steps.filter.outputs.src with nothing after it, which is truthy for the text false too.
- uses: dorny/paths-filter@v4
id: filter
with:
base: ${{ github.event.repository.default_branch }}
filters: |
src:
- 'src/**'
- if: steps.filter.outputs.src == 'true'
run: npm testThe built-in paths filter is a different mechanism
GitHub has its own path filter on the trigger, and it answers a different question: whether the workflow runs at all, rather than which job inside it runs. It is narrower than it looks: the filters apply "when using the push and pull_request events", and "Path filters are not evaluated for pushes of tags". The rules are strict. "You cannot use both the paths and paths-ignore filters for the same event in a workflow", and if you want both behaviors you use paths with the ! prefix for the exclusions. Order matters: "A matching negative pattern (prefixed with !) after a positive match will exclude the path."
A workflow skipped by a path filter leaves its checks "in a \"Pending\" state", and the docs add that "A pull request that requires those checks to be successful will be blocked from merging." That is why most monorepos filter inside the job with this action instead. The globs are not interchangeable either: src/* matches the files directly in src, and src/** matches the whole tree under it.
Why there is no recorded run on this page
Every other failure page here carries a log from a job we ran. This one does not. The decision happens inside the action, against a git history and an event payload, so a script on a runner cannot produce it on demand and there is nothing transient to retry or repair. The workflows above are illustrative.
How to prevent it
- Set
baseon every trigger exceptpull_request, where the base is unambiguous. - Compare outputs to
'true'in quotes, and keep the filter keys in one.github/filters.yml. - Echo the
changesoutput once while wiring a new filter up, so you see the array the action built. - Prefer filtering inside the job over the trigger-level
pathswhen a required check depends on the workflow.
Frequently asked questions
Why does dorny/paths-filter return false for every filter?
Results: in the step log: if the action warned that the before field was missing, it compared a single commit, and if that commit touched nothing your filters name, every output is false.Why is every filter true after a merge instead?
workflow_run trigger and every filter flipped to true; the log shows the action warning that the before field was missing, detecting 821 changed files from the last commit, and setting every filter from that. A merge commit that touches everything makes the fallback match everything.Do I need actions/checkout before dorny/paths-filter?
pull_request the action asks the REST API instead and needs pull-requests: read.Can I use paths and paths-ignore together for the same event?
paths and paths-ignore filters for the same event in a workflow." Use paths and prefix the exclusions with !. Order is significant: a negative pattern after a positive match excludes the path, and a positive one after it puts the path back.