GitHub Actions Path Validation Error caching, on a green job
A GitHub Actions Path Validation Error caching line means the glob in your path input matched nothing when the post step ran, so no archive was built and nothing was uploaded. The job stays green, because the action writes that line with core.info rather than as a warning annotation.

What this error means
Every run installs from scratch and every run is successful. There is no red step, no annotation in the run summary, and the restore step near the top of the job reports a miss in the ordinary way that a first run would. The only evidence is one line inside the collapsed post step at the very bottom, which says that the paths specified for caching do not exist and that no cache is being saved. Because it is not an annotation, it does not appear in the run summary, it does not show in the job list, and searching the repository for failed runs will never surface it.
[warning]Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.Why this is not an annotation
Two pieces of code together explain the silence. In the toolkit, the check happens before the try block that wraps the rest of the save, so the error is not caught locally and propagates out of saveCache entirely. In the action, the post step catches everything and hands it to a helper called logWarning, which is not core.warning at all.
The helper prefixes the text with [warning] and writes it with core.info. core.warning would emit the workflow command that creates an annotation; core.info emits a plain line of output. So the run has no annotation, the step has no failure, and the only trace is a line of text that reads like a warning and is not one.
const cachePaths = await utils.resolvePaths(paths)
if (cachePaths.length === 0) {
throw new Error(
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
)
}
// actions/cache, src/utils/actionUtils.ts
export function logWarning(message: string): void {
const warningPrefix = "[warning]";
core.info(`${warningPrefix}${message}`);
}Common causes
The directory is created by a step that did not run
A cache step wraps a build that was skipped by an if:, or that lives in another job, or that failed earlier while the workflow carried on. The post step then runs against a workspace where the directory was never created, matches nothing, and says so quietly.
The tool writes somewhere other than where you cached
Package manager store locations move between versions and between operating systems. Caching a hard-coded home-relative path on a Windows runner, or a Linux default on macOS, gives a path that is correct in general and absent on this runner. Asking the tool for its own path is the durable fix.
A later step deleted or moved the directory
A cleanup step, a container that mounted over the location, or a build that relocates its output between phases can all leave the path empty by the time the post step runs. The post step is the last thing in the job, so it sees the end state and not the state the cache step was written for.
The path input resolved to an empty string
An expression that produced nothing, or a multi-line input where the only line was a comment, leaves the action with no patterns at all. This is the second message rather than the first, and it is worth reading which one you have before changing anything.
How to fix it
Find the line before you change the path
- Open a run and expand the last step, named Post followed by your cache step name.
- Look for a line starting
[warning]Path Validation Error; it is plain output, not an annotation. - If the line names paths not existing, the directory was absent at the end of the job.
- If it says at least one path is required, the
pathinput itself was empty and the expression that builds it is the bug.
Ask the tool where its cache is
Hard-coded store paths are the most common cause of a path that exists on one runner and not another. Resolve it at run time and feed it into the cache step, which keeps working across matrix operating systems and across tool upgrades.
- id: pip
run: echo "dir=$(pip cache dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6
with:
path: ${{ steps.pip.outputs.dir }}
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}Make the absence loud
The action will not fail for you, so add the check yourself while you are settling this. One line at the end of the job turns a silent miss into a red step that names the directory, and it costs nothing once the path is right.
- name: Assert the cache path exists
if: always()
run: |
test -d "$HOME/.cache/pip" \
|| { echo 'nothing to cache at ~/.cache/pip' >&2; exit 1; }Cache the store, not the build tree
Most paths that go missing are build outputs that a later step cleans up. Package manager stores are stable, are populated by the install you already run, and survive to the end of the job. If you are caching something a step deletes, cache the thing it was downloaded from instead.
What matching nothing actually means
The check is on the number of paths the glob resolved, not on whether the directory has anything in it. resolvePaths globs your patterns with implicit descendants switched off, so a directory that exists resolves to one path, itself, even when it is empty. An empty directory therefore saves a cache, just an uninteresting one.
Zero paths means the patterns matched no filesystem entry at all. The usual reasons are that the build which creates the directory did not run, that the directory is created somewhere else, or that a tilde in the path did not expand the way you assumed on the runner you are on.
There is a second, different message with the same prefix, and it is worth telling them apart because the fix is not the same. Path Validation Error: At least one directory or file path is required comes from input validation and means the path input itself was empty, usually because an expression that was supposed to produce it produced nothing.
| State at post-step time | Paths resolved | What the post step does |
|---|---|---|
| directory exists with files in it | one or more | builds an archive and uploads |
| directory exists and is empty | one, the directory | uploads a near-empty archive |
| directory was never created | none | writes the Path Validation Error line |
path input resolved to an empty string | not reached | writes At least one directory or file path is required |
Why the restore step cannot tell you
The restore half of the action runs at the top of the job, before anything has been built, and it is perfectly happy. It looks up a key, finds nothing, logs a miss and moves on. That is the same output it would produce on the first run of a brand new cache, which is exactly what makes this stick around: every run looks like the first run, and the first run is allowed to miss.
The asymmetry is the whole diagnosis. If your restore step reports a miss on every run and your post step reports the Path Validation Error on every run, nothing has ever been saved and the key is not the problem. If your restore step misses and your post step says it saved, then something is being written and the key is not matching it, which is a different investigation.
It also means the fastest confirmation is not in the cache step at all. Add one listing of the path immediately before the job ends and you will know within a run whether the directory is there under the name you gave it.
Why there is no recorded run on this page
A recorded run here would be a screenshot of a green job, which is evidence of nothing. That is not a limitation of the recording; it is the substance of the page. The failure mode is precisely that the run carries no failure signal, so any artefact we could capture from it looks identical to a healthy run and would need a caption explaining that the interesting part is what is absent.
It is also outside what a runner can act on. The line is written by a post step that has already decided it has nothing to archive, at a point where the job is over and the build that should have created the directory has long finished. There is no operation to retry and no moment at which retrying it would produce a different answer.
How to prevent it
- Resolve tool cache directories at run time rather than hard-coding them.
- Read the post step on the first run after adding or changing a cache.
- Keep the step that creates the directory in the same job as the cache step.
- Prefer a package manager store over a build output as the cached path.
Frequently asked questions
Why does actions/cache not fail when the path does not exist?
core.info behind a [warning] prefix rather than with core.warning. Only core.warning creates an annotation, so nothing appears in the run summary and the job exits zero.Does an empty directory trigger Path Validation Error?
Where is the Path Validation Error line in the log?
What is the difference between the two Path Validation Error messages?
path input was empty before any globbing happened. The first is a build problem, the second is an expression problem.Related guides
References
- actions/toolkit packages/cache: the paths check and where it sits relative to the try block
- actions/cache src/utils/actionUtils.ts: logWarning writes through core.info
- actions/toolkit core: which helpers create annotations and which do not
- Dependency caching reference: the path input on the cache action
- GitHub Actions documentation