Skip to content
Latchkey LogoLatchkey home

GitHub Actions invalid pattern paths filter, and the quoting rule

A GitHub Actions invalid pattern paths filter is most often not a filter problem at all: the documentation states that a pattern beginning with certain characters must be quoted, and that an unquoted one creates a parse error which prevents the workflow from running. That is YAML refusing the file, before any filter has been consulted.

Filter pattern characters with their documented meaning and which ones need quoting in YAML
Three characters are special to YAML at the start of a value, and two of the pattern operators act on the preceding character.

What this error means

Either the workflow stops running entirely, which means the file was refused, or it runs at the wrong times, which means the file was accepted and a pattern is not matching what you think. Those are different problems and the first thing to do is decide which one you have, by looking at whether any run exists for the commit at all. A workflow that has simply stopped appearing is the refused case, and no amount of reasoning about glob semantics will help with it.

Quoted from the filter pattern cheat sheet in the GitHub Actions workflow syntax reference
# documented as invalid: creates a parse error that prevents the workflow running
paths:
  - **/README.md

# valid
paths:
  - '**/README.md'

The quoting rule, quoted

The documentation is unusually direct about this. It says the asterisk, the opening bracket and the exclamation mark are special characters in YAML, and that if you start a pattern with any of them you must enclose the pattern in quotes. It adds that if you use a flow sequence with a pattern containing brackets, the pattern must be quoted there too.

It then shows the failure by example, with a pattern beginning with two asterisks written unquoted and labeled as invalid, adding that it creates a parse error which prevents your workflow from running. That is the sentence that matters, and it tells you the consequence is total rather than partial. The workflow does not misbehave, it stops.

The reason is ordinary YAML. An asterisk at the start of a plain scalar begins an alias, an opening bracket begins a flow sequence, and an exclamation mark begins a tag. None of those is what you meant, and two of them produce a document that cannot be parsed at all.

So the first move on any pattern problem is to quote every pattern in the block, which costs nothing and removes the entire category. Quoting a pattern that did not need it changes nothing about how it matches.

Leading characterWhat YAML starts readingMust be quoted
*an alias to an anchoryes
[a flow sequenceyes
!a tagyes
A letter, a digit or a dota plain scalarno, but harmless

Common causes

A pattern starts with an unquoted special character

The asterisk, the opening bracket and the exclamation mark all begin something else in YAML. The documentation labels an unquoted leading double asterisk as invalid and says it creates a parse error that prevents the workflow running.

A negation appears before the patterns it should subtract from

Negation acts on previous positive patterns only. A list that opens with an exclusion excludes nothing, and the result is indistinguishable from the exclusion being ignored.

The pattern does not match the whole path

Path patterns match the whole path starting from the repository root. A pattern written as though it were relative to a subdirectory matches nothing, quietly.

A single asterisk was used where a double was needed

A single asterisk does not cross a slash. This is the difference between matching files directly inside a directory and matching everything beneath it, and in our experience it accounts for most filters that match less than expected.

A question mark was read as a single character wildcard

It matches zero or one of the preceding character rather than any one character. A pattern carried over from shell globbing therefore means something different here than it did there.

How to fix it

Establish whether the file was refused

  1. Look at the Actions tab for the commit and see whether any run exists.
  2. If nothing exists and the workflow has simply stopped appearing, the file was refused.
  3. If runs exist but at the wrong times, the file parsed and a pattern is not matching.

Quote every pattern in the block

Quoting is unconditional and free. A block in which every pattern is quoted cannot fail on a leading special character, and quoting a pattern that did not need it changes nothing about how it matches.

.github/workflows/ci.yml
on:
  push:
    branches:
      - 'main'
      - 'release/**'
    paths:
      - 'src/**'
      - '!src/**/*.md'

Put negations after the patterns they subtract from

Order the list positives first, negations last. Since an exclamation mark only negates what precedes it, a negation at the top of the list has no effect at all.

Write patterns from the repository root

Path patterns match the whole path. Begin each one at the root rather than at the directory you happen to be thinking about, and use the double asterisk wherever the match needs to cross a slash.

Verify with test pushes rather than by reasoning

Push one commit that should trigger the workflow and one that should not, and read the Actions tab after each. Since the deciding component is not published, an observed result is the only reliable answer.

Two operators that do not mean what glob users expect

If the file is accepted and the filter matches the wrong things, the pattern semantics are worth reading carefully, because two of them differ from ordinary shell globbing in a way that reverses expectations.

The question mark matches zero or one of the preceding character, and the plus matches one or more of the preceding character. In most glob dialects a question mark matches exactly one character of any kind. Here it is a repetition operator applied to whatever came before it, which is why the documented example for matching both a JavaScript and a JSX file name puts the question mark after the letter it makes optional.

The bracket class matches one alphanumeric character listed in the brackets or included in a range, and the documentation limits ranges to lower case letters, upper case letters and digits. A range outside those is not something to rely on.

The two asterisk forms follow the usual convention and are the two worth memorising: a single asterisk matches any characters except a slash, and a double asterisk matches any characters including slashes. The documentation also states that path patterns must match the whole path and start from the repository root, which is the rule behind most patterns that quietly match nothing.

.github/workflows/ci.yml
on:
  push:
    paths:
      - '**.js'          # every .js file, at any depth
      - 'docs/*'         # files directly in docs, not in its subdirectories
      - 'docs/**'        # docs and everything beneath it
      - '*.jsx?'         # page.js and page.jsx

Negation, and the layer that decides it

The exclamation mark negates previous positive patterns when it is the first character of a pattern, and the documentation adds that it has no special meaning if it is not the first character. Both halves matter. A negation placed before the patterns it is meant to subtract from removes nothing, because there is nothing before it to subtract from, and this is the commonest reason a filter appears to ignore an exclusion.

Now the scoping, which is the part this page will not guess at. The event filters are not evaluated by anything published. The workflow schema that ships in the runner reaches the event mapping with loose keys, so the filters inside it are never validated there, and the same is true of the editor copy. The component that decides whether a push matches your patterns is a GitHub service whose source is not available.

That means we can tell you the documented pattern semantics, which are published and quoted above, and we can tell you that an unquoted pattern breaks the file, which is also published. We are not going to tell you what a malformed but parseable pattern does, because that is decided somewhere we cannot read. Validate patterns by pushing test commits, not by reasoning about an implementation nobody outside GitHub can see.

.github/workflows/ci.yml
# excludes nothing: the negation has no preceding pattern to subtract from
paths:
  - '!docs/**'
  - 'src/**'

# excludes the markdown files under src
paths:
  - 'src/**'
  - '!src/**/*.md'

Why there is no recorded run on this page

In the refused case there is nothing to record, because the file does not parse and no run is created. In the accepted case the observable artifact is a run that exists or does not exist for a given push, and a recording of ours would be a list of our commits against our patterns.

The more important reason is the scoping above. Recording what our repository did with an unusual pattern would produce a confident looking demonstration of behavior we have just said is decided by a component we cannot read. That is exactly how an observation gets mistaken for a contract, and this page would rather be short on that point than wrong about it.

Everything that is published is quoted instead: the quoting requirement and its consequence, the pattern semantics including the two operators that act on the preceding character, the negation rule, and the requirement that path patterns match the whole path from the repository root. And there is nothing to repair, since no runner is ever involved in a trigger that did not fire.

How to prevent it

  • Quote every filter pattern, unconditionally, as a style rule.
  • Keep negated patterns at the end of the list, after the positives.
  • Write path patterns from the repository root and prefer the double asterisk when crossing directories.
  • Confirm any filter change with two test pushes before relying on it.

Frequently asked questions

Why does an unquoted pattern break my workflow entirely?
Because the asterisk, the opening bracket and the exclamation mark start something else in YAML, so the file does not parse. The documentation shows an unquoted leading double asterisk as invalid and says it creates a parse error that prevents your workflow from running.
What does the question mark match in a filter pattern?
Zero or one of the preceding character, not any single character as in most shell globbing. The plus behaves the same way, matching one or more of the preceding character. Patterns carried over from a shell therefore mean something different here.
Why is my negated pattern ignored?
Most often because it comes before the patterns it should subtract from. An exclamation mark negates previous positive patterns only, and it has no special meaning at all when it is not the first character of the pattern.
Can I find out exactly how a malformed pattern is handled?
Not from anything published. The event filters are not validated by the workflow schema, which reaches the event mapping with loose keys, and the component that matches a push against your patterns is a GitHub service whose source is not available. Test with real pushes instead.

Related guides

References

Testing a path filter means pushing until it behaves. Latchkey runs each attempt at $0.0025/min. Start free → 30-day trial · No credit card