My issue

GitLab CI used to interpret file path patterns differently in different contexts:

  • in cache:paths or artifacts:paths, like glob patterns

  • in cache:key:files, like git pathspecs

Globs and pathspecs behave differently, e.g. for **/foo.

$ tree
.
├── dir
│   └── foo
└── foo

2 directories, 2 files

$ shopt -s globstar  # enable globstar expansion

$ ls **/foo
dir/foo  foo

$ git ls-files '**/foo'
dir/foo

This caught me out: I used a pattern like **/foo in cache:key:files, expecting it to catch foo files at every level, but it didn’t.

I raised an issue, suggesting the docs make this clear. It was closed in favour of https://gitlab.com/gitlab-org/gitlab/-/work_items/460235. But that addressed a different issue: commit hashing versus content hashing. So has my issue actually been fixed?

Digging into the history

When I raised the issue in June 2025:

  • cache:key:files called last_commit_id_for_path(path), which delegated to Gitaly (RPC access to Git), which interpreted the path as a pathspec

  • wildcard support seems to be accidental

  • switched cache:key:files from commit to content hashing

  • cache:key:files paths are matched literally (no wildcard support)

  • preserved the old commit hashing behaviour under a new key cache:key:files_commits

  • restored wildcard support to cache:key:files, which broke with 203233

  • wildcards are supported by creating regexes from user-provided globs (fiddly!)

  • fixed a bug when matching wildcards, so **/foo does now match at top-level

  • reverted 209633 because it allowed a file to include itself

  • better version of 209633

  • restored wildcard support and fixed wildcard matching, as in 209633

  • but also prevented a file including itself

Has my issue been fixed?

I also skimmed the GitLab source.

In cache:key:files, patterns are now interpreted like globs. So **/foo does match foo at the top-level.

But cache:key:files_commits still behaves as it did originally: it calls last_commit_id_for_path(path), which delegates to Gitaly, which interprets the path as a pathspec. So **/foo still does not match foo at the top-level.

The bottom line: pattern interpretation is still inconsistent and confusing.