GitHub Actions composite action no such file or directory
A GitHub Actions composite action no such file error is a working directory problem, not a packaging problem: your script shipped with the action and is on the runner. Every run step inside a composite action starts in the caller repository checkout, so a relative path looks for the file in the wrong tree.

What this error means
The action resolves, the step starts, and the shell exits immediately saying it cannot find a file you can see in your own repository. It happens on the first run step that invokes something the action ships, and it is consistent: the same action fails for every consumer, on every runner, on every attempt. Adding the file, committing it again or switching to a tag changes nothing, because the file was never missing. Only the path was.
/home/runner/work/_temp/8a1a7b3c.sh: line 1: ./scripts/setup.sh: No such file or directoryA composite action that produces it
This action is written for this page and has never been run. It is the smallest shape that reaches the error: an action that ships a script beside its own action.yml, and a run step that invokes it the way you would from the repository root. Nothing here is deprecated, and the YAML is valid.
The step is doing exactly what it says. It runs ./scripts/setup.sh from wherever the shell happens to start, and where the shell starts is the one thing this file never states.
name: setup
description: Installs the toolchain
runs:
using: composite
steps:
- run: ./scripts/setup.sh
shell: bashCommon causes
A relative path to a file the action ships
The common one. ./scripts/setup.sh inside an action.yml is resolved against the caller checkout, where that directory usually does not exist. It is especially easy to reach when the action started life as a run step in one repository, where the relative path was correct, and was later extracted into an action.
The action was consumed from another repository
A local action referenced as ./.github/actions/setup happens to sit inside the same checkout, so a relative path can appear to work while it is only being tested in its own repository. Publish the action and the first external consumer hits the error, because the action is now downloaded somewhere outside their workspace.
working-directory used as the fix
Setting working-directory moves the step within the caller checkout, because the runner joins it onto the workspace path. It cannot reach the action directory unless its value is itself built from github.action_path, so it usually changes which missing directory is reported rather than finding the file.
The path is right and the file is not executable
A script committed without the executable bit produces a permission error rather than a missing file, and a script with CRLF line endings produces a message naming the interpreter instead of the script. Both read as path problems at a glance, and neither is fixed by changing the path.
How to fix it
Anchor every bundled path to the action directory
Prefix each reference to a file the action ships with ${{ github.action_path }}. This is the documented property for exactly this purpose and it is set for every step inside a composite action, so it works the same whether the action is local or consumed from another repository.
- run: "${{ github.action_path }}/scripts/setup.sh"
shell: bashOr use the environment variable inside a multi-line script
Inside a block of shell, the environment variable form reads better than interpolating an expression into every line, and it keeps the value out of the script text the runner writes to disk. The contexts reference suggests this form directly, changing directory to "$GITHUB_ACTION_PATH".
- run: |
cd "$GITHUB_ACTION_PATH"
./scripts/setup.sh
./scripts/verify.sh
shell: bashKeep the caller workspace when that is what you meant
- Decide per step which tree it operates on: the action's own files, or the consumer's repository.
- Use
github.workspaceexplicitly when a step must act on the consumer checkout, rather than relying on the default. - Never mix the two in one path.
Make the bundled script runnable
Commit the executable bit with git update-index --chmod=+x, or invoke the script through an interpreter so the bit does not matter. Checking the line endings is worth a minute too: a script saved with CRLF fails with a message about the interpreter, which looks nothing like a path problem.
git update-index --chmod=+x scripts/setup.sh
git commit -m "mark setup.sh executable"Every run step starts in the caller workspace
The runner builds the working directory for a script step the same way whether the step came from a workflow or from inside a composite action. In ScriptHandler.RunAsync it takes the workspace out of the github context and joins the step's own working-directory onto it, defaulting to the empty string. Nothing in that code puts the action's own directory into the calculation, which is the whole story: a relative path written inside a composite action is resolved against the caller's workspace like any other.
The action directory is not hidden from you, it is just somewhere else. CompositeActionHandler sets it on the github context for every embedded step, under the comment "Set GITHUB_ACTION_PATH", so each step inside a composite action can reach it through github.action_path or the GITHUB_ACTION_PATH environment variable.
var workspaceDir = githubContext["workspace"] as StringContextData;
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);Where each path resolves
The contexts reference describes the property in one sentence: github.action_path is "The path where an action is located. This property is only supported in composite actions." It also gives the environment variable form, suggesting you change directory with cd "$GITHUB_ACTION_PATH".
Read the four rows below as one rule with three consequences. A relative path is relative to the caller. An absolute path built from the action path is relative to nothing. And working-directory moves you inside the caller tree, so it cannot rescue a path that needed to leave it.
| Written in action.yml | Resolved against | Finds the action's own file |
|---|---|---|
./scripts/setup.sh | the caller checkout | no |
scripts/setup.sh | the caller checkout | no |
${{ github.action_path }}/scripts/setup.sh | the action directory | yes |
$GITHUB_ACTION_PATH/scripts/setup.sh | the action directory | yes |
working-directory: scripts | the caller checkout | no |
The corrected action, and one detail people miss
The corrected file below anchors the script to the action directory and nothing else changes. It is worth setting the executable bit on the script in git, because a checkout preserves it and a script without it fails with a permission error rather than a missing file, which sends you looking in a third direction. Invoking the script through bash sidesteps that entirely.
If you need a relative path to keep working inside the script itself, set the working directory as well rather than only the command. The two are separate: naming the interpreter's argument does not move the shell.
name: setup
description: Installs the toolchain
runs:
using: composite
steps:
- run: bash "${{ github.action_path }}/scripts/setup.sh"
shell: bash
working-directory: ${{ github.action_path }}Why there is no recorded run on this page
A recorded run would be a run of our own scaffold, and the thing it would prove is a fact about that scaffold rather than a fact about composite actions. The path that fails is whatever the consumer repository happens not to contain, so the message carries a temp script name and a repository layout that are ours and nobody else's, and a reader comparing it with their own log would be comparing two different accidents. The rule underneath is fixed by two lines in the runner, quoted above from the file that contains them, and that is stable in a way a log of one repository is not. The error text at the top of this page is labeled for what it is: the shape bash prints, not a capture of a specific job.
How to prevent it
- Treat
github.action_pathas the only correct root for files an action ships. - Test a new composite action from a second repository before publishing it.
- Keep a
.gitattributesentry that forces LF on shell scripts. - Prefer
bash <path>over a bare path so a missing executable bit cannot masquerade as a missing file.
Frequently asked questions
Why can a composite action not find a script it ships?
working-directory onto github.workspace, and the action directory is never part of that path. A relative path therefore looks for the file in the consumer tree.What is github.action_path set to?
Can I use github.action_path in a JavaScript or Docker action?
Does working-directory fix a composite action path error?
github.action_path. On its own, working-directory is joined onto the caller workspace by the same code that builds the default, so it moves the step around inside the consumer checkout rather than into the action directory.