Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 9 min read

GitLab CI Error Guide: './script.sh: No such file or directory' Command Not Found

Fix GitLab's 'No such file or directory' and 'command not found': chmod +x, wrong paths, CRLF line endings, missing shebangs, and tools not in the image.

  • #gitlab-cicd
  • #troubleshooting
  • #errors
  • #scripts

Exact Error Message

The job log shows one of these just before it fails:

$ ./deploy.sh
/bin/bash: line 123: ./deploy.sh: No such file or directory
ERROR: Job failed: exit code 127

Or, when the command itself is missing from PATH:

$ terraform plan
/bin/sh: 1: terraform: not found
ERROR: Job failed: exit code 127
$ jq '.version' package.json
bash: jq: command not found
ERROR: Job failed: exit code 127

The tell-tale sign is exit code 127 — the shell’s dedicated code for “command not found” — as opposed to the generic exit code 1 that means a command ran but returned an error.

What the Error Means

There are two distinct flavours hiding under similar wording:

  1. ./script.sh: No such file or directory — the shell found something to run but couldn’t actually execute it. Either the file isn’t there, isn’t executable, or its #! interpreter line points at a binary that doesn’t exist (classic CRLF symptom: #!/bin/bash\r looks for an interpreter literally named bash\r).
  2. <cmd>: not found / command not found — the shell searched every directory in PATH and never found a binary by that name. The tool simply isn’t installed in the container image, or it lives somewhere not on PATH.

Both end in exit code 127, which is how you distinguish this class of failure from a command that ran and merely failed (exit 1).

Common Causes

  • Script isn’t executable. Git tracked the file without the +x bit, so ./deploy.sh can’t be run directly.
  • Wrong path or working directory. The script lives in scripts/deploy.sh but the job runs ./deploy.sh from the repo root.
  • The file isn’t in the job at all. It was produced by a previous job and never passed forward via artifacts: / needs:, or GIT_STRATEGY: none skipped the checkout.
  • CRLF line endings. A file edited on Windows has \r\n; the shebang becomes #!/bin/bash\r, and the kernel looks for an interpreter that doesn’t exist.
  • Missing shebang. Running ./script.sh with no #! line means the kernel can’t pick an interpreter.
  • Tool not installed in the image. terraform, jq, aws, kubectl, etc. aren’t in the base image you chose.
  • sh vs bash. Many slim images default to sh (dash/busybox), which lacks bash-isms ([[ ]], arrays) and may not even include bash.

How to Reproduce the Error

A non-executable script reproduces flavour 1 instantly:

deploy:
  image: alpine:3.20
  script:
    - ./deploy.sh      # committed without chmod +x
/bin/sh: ./deploy.sh: not found

A missing tool reproduces flavour 2:

plan:
  image: alpine:3.20    # no terraform, no jq, no bash
  script:
    - jq --version
/bin/sh: jq: not found

Diagnostic Commands

Add a debug block to the start of the failing job so the log shows the ground truth before the failing command:

script:
  - pwd && ls -la                 # where am I, what files exist here?
  - ls -la scripts/              # is the script in the path I expect?
  - file ./deploy.sh             # is it a script or something else?
  - head -1 ./deploy.sh          # what does the shebang say?
  - cat -A ./deploy.sh | head    # ^ shows $ for LF, ^M$ for CRLF
  - which jq || echo "jq not on PATH"
  - echo "$PATH"
  - ./deploy.sh

What each line tells you:

  • pwd && ls -la — confirms the working directory and whether the file is present and executable (look for the x bits in -rwxr-xr-x).
  • file ./deploy.sh — reports the file type and, for scripts, the interpreter.
  • head -1 ./deploy.sh — shows the shebang so you can spot a missing or wrong one.
  • cat -A ./deploy.sh — exposes hidden characters: a line ending in ^M$ is CRLF, the most insidious cause of a “valid” script that “doesn’t exist.”
  • which <cmd> and echo "$PATH" — confirm whether a tool is installed and reachable.

For a deeper trace, set CI_DEBUG_TRACE: "true" to see the full generated shell wrapper and exactly which line the runner reached.

Step-by-Step Resolution

1. Make the script executable and commit the mode bit:

chmod +x deploy.sh
git update-index --chmod=+x deploy.sh   # ensure the +x is recorded in git
git commit -am "make deploy.sh executable"

Alternatively, sidestep the executable bit by invoking the interpreter explicitly:

script:
  - bash scripts/deploy.sh        # runs even without +x or a shebang

2. Fix the path / working directory. Use the real path, or cd first:

script:
  - cd scripts && ./deploy.sh
  # or
  - ./scripts/deploy.sh

3. Strip CRLF line endings if cat -A showed ^M$:

before_script:
  - apt-get update && apt-get install -y dos2unix
  - dos2unix scripts/*.sh

Better, prevent it at the source with a .gitattributes entry so the repo always stores LF:

*.sh text eol=lf

4. Add or fix the shebang as the first line of the script:

#!/usr/bin/env bash
set -euo pipefail

5. Install the missing tool or pick the right image. Either install it in before_script:

before_script:
  - apk add --no-cache jq curl bash      # alpine
  # or: apt-get update && apt-get install -y jq curl   # debian/ubuntu

…or, far cleaner, use an image that already ships the tool:

plan:
  image: hashicorp/terraform:1.9     # terraform pre-installed
  script:
    - terraform plan

6. Use bash explicitly if you need bash-isms. On a slim image where sh is the default shell, either install bash and set the shell, or switch to an image with bash built in. If the file must run via bash, call bash script.sh rather than ./script.sh.

Prevention and Best Practices

  • Commit the executable bit (git update-index --chmod=+x) and verify with ls -la in CI the first time.
  • Enforce LF endings with a .gitattributes rule (*.sh text eol=lf) so Windows checkouts never corrupt shebangs.
  • Always start scripts with #!/usr/bin/env bash and set -euo pipefail for predictable behaviour.
  • Choose an image that already contains your tools instead of installing them on every run — it’s faster and removes a class of “not found” failures.
  • Pin image tags so a tool that exists today isn’t dropped when latest rebuilds.
  • Reference scripts from the repo root or cd deliberately; don’t assume the working directory.

Frequently Asked Questions

What’s the difference between No such file or directory and command not found? No such file or directory (for a path like ./deploy.sh) usually means the file is missing, not executable, or has a broken shebang — the shell found a path but couldn’t run it. command not found means the shell searched PATH and the binary isn’t installed anywhere. Both produce exit code 127.

My script exists and I can see it with ls, but I still get No such file or directory. Why? Almost always CRLF line endings. The shebang #!/bin/bash becomes #!/bin/bash\r, so the kernel looks for an interpreter literally named bash\r and fails. Run cat -A script.sh — a trailing ^M$ confirms it. Fix with dos2unix and a .gitattributes eol=lf rule.

Why does my bash script fail on an Alpine image? Alpine’s default shell is busybox sh, and bash may not be installed at all. Either apk add bash and call bash script.sh, or use a Debian/Ubuntu-based image. Bash-only syntax like [[ ]] or arrays will break under sh.

A tool works locally but is not found in CI. How do I fix it? Your local machine has the tool installed; the CI container doesn’t. Install it in before_script (apk add / apt-get install) or, better, choose an image: that already includes it (e.g. hashicorp/terraform, amazon/aws-cli). Run which <tool> in the job to confirm.

Do I have to chmod +x my scripts? Only if you invoke them directly as ./script.sh. If you call the interpreter explicitly — bash script.sh or python script.py — the executable bit doesn’t matter. For direct invocation, commit the +x bit with git update-index --chmod=+x.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.