GitLab CI Error Guide: 'fatal: reference is not a tree' Shallow Clone Failure
Fix GitLab's 'reference is not a tree' and 'did not receive expected object': raise GIT_DEPTH, unshallow, fetch the ref, and adjust the project clone depth.
- #gitlab-cicd
- #troubleshooting
- #errors
- #git
Exact Error Message
The job fails inside a git operation, not your script:
$ git checkout 4f9a2c1
fatal: reference is not a tree: 4f9a2c1d8e3b5a7f9c0e1d2b3a4c5e6f7a8b9c0d
ERROR: Job failed: exit code 128
Closely related variants from the same root cause:
fatal: did not receive expected object 4f9a2c1d8e3b5a7f9c0e1d2b3a4c5e6f7a8b9c0d
fatal: not a git repository (or any of the parent directories): .git
The exit code is 128 — git’s own “fatal error” code — which tells you the failure is in git plumbing, not in your build commands.
What the Error Means
By default, GitLab runners do a shallow clone: they fetch only the most recent commit (or a small number, set by GIT_DEPTH) to keep checkouts fast. A shallow clone is a truncated history — git only has the objects within the depth window.
fatal: reference is not a tree: <sha> means you asked git to use a commit (checkout, merge-base, diff, describe, a tag) whose object was never fetched because it falls outside the shallow window. Git knows the SHA exists conceptually but doesn’t have the actual tree object on disk, so it refuses.
fatal: did not receive expected object is the same story during fetch — a referenced object wasn’t transferred. fatal: not a git repository appears when GIT_STRATEGY: none skipped the clone entirely but a later command still expects a working .git.
Common Causes
GIT_DEPTHtoo shallow. The default depth (often 20) doesn’t include the older commit, tag, or merge base your job needs.- Checking out an old commit or tag that’s beyond the fetched window — common in deploy/rollback jobs that target a specific historical SHA.
git describe/git merge-base/git diffagainst a base (e.g. comparing tomainor a release tag) when that base isn’t in the shallow clone.- Submodules whose pinned commits live outside the shallow depth of the superproject.
- A force-push mid-pipeline that rewrote history, so the SHA the pipeline was created against no longer exists in the fetched range.
GIT_STRATEGY: noneused on a job that nonetheless runs git commands.
How to Reproduce the Error
Pin a deploy job to an old SHA while the clone is shallow:
deploy-rollback:
variables:
GIT_DEPTH: "1" # only the tip commit is fetched
script:
- git checkout v1.2.0 # tag is older than depth 1 → not fetched
fatal: reference is not a tree: a1b2c3d...
Or run a tag-based version command on a shallow clone:
version:
variables:
GIT_DEPTH: "5"
script:
- git describe --tags # needs older tag objects outside the window
fatal: No tags can describe '<sha>'.
fatal: reference is not a tree: <sha>
Diagnostic Commands
Add a git-inspection block to the job to see exactly what was fetched:
script:
- echo "GIT_DEPTH=$GIT_DEPTH GIT_STRATEGY=$GIT_STRATEGY"
- git rev-parse --is-shallow-repository # true => shallow clone
- git log --oneline -n 30 # how many commits do we actually have?
- git rev-list --count HEAD # total reachable commit count
- git tag # are the tags present?
What each tells you:
echo "$GIT_DEPTH"— confirms the active depth (empty means project default applies).git rev-parse --is-shallow-repository— printstrueif the clone is truncated.git log --oneline -n 30— shows how far back history actually goes; if the SHA/tag you need isn’t listed, it wasn’t fetched.
To confirm the fix interactively, deepen the clone in the job and retry:
git fetch --unshallow # convert to a full clone
# or fetch just what you need:
git fetch --depth=100 origin
git fetch origin refs/tags/v1.2.0
git checkout v1.2.0 # now succeeds
git fetch --unshallow pulls the entire remaining history; git fetch origin <ref> grabs a specific branch/tag without a full deepen.
You can also check the project-level setting: Settings > CI/CD > General pipelines > Git shallow clone depth. A small value there is the global cause when individual jobs don’t override GIT_DEPTH.
Step-by-Step Resolution
1. Raise GIT_DEPTH on the affected job (or globally) so the needed commit is in range:
deploy-rollback:
variables:
GIT_DEPTH: "100" # deep enough to include the target tag/commit
script:
- git checkout v1.2.0
2. Use a full clone when you need arbitrary history (tags, git describe, diffs against any base). Set depth to 0, which disables shallow cloning:
variables:
GIT_DEPTH: "0" # 0 = fetch full history (no shallow clone)
Set it globally at the top of .gitlab-ci.yml if most jobs need full history, or per-job to keep fast jobs shallow.
3. Fetch the specific ref instead of deepening everything — cheaper for one-off needs:
version:
script:
- git fetch origin "refs/tags/*:refs/tags/*" # fetch all tags
- git fetch --unshallow || true # or unshallow if shallow
- git describe --tags
4. Handle submodules by fetching them with adequate depth via the runner variable:
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_DEPTH: 0 # full depth for submodules too
5. Avoid GIT_STRATEGY: none on git-using jobs. If a job runs git commands, give it a clone:
build:
variables:
GIT_STRATEGY: fetch # not "none" — ensure .git exists
6. Set the project default appropriately. If many pipelines need history, raise Settings > CI/CD > General pipelines > Git shallow clone depth (set to 0 for full clones) rather than overriding GIT_DEPTH in every job.
Prevention and Best Practices
- Make depth explicit for any job that touches tags, merge bases, or historical commits — don’t rely on the (small) default.
- Use
GIT_DEPTH: "0"for release/versioning and deploy-rollback jobs that may reference arbitrary history. - Keep fast jobs shallow (
GIT_DEPTH: "1"or the default) and only deepen the specific jobs that need it, so you don’t slow every pipeline. - Fetch tags deliberately (
git fetch origin "refs/tags/*:refs/tags/*") when runninggit describe— shallow clones omit tags by default. - Mirror the project setting to your needs: check the Git shallow clone depth value under CI/CD settings if jobs fail without an explicit
GIT_DEPTH. - Beware mid-pipeline force-pushes to the running ref; they can orphan the SHA the pipeline was built against.
Related Errors
- GitLab CI Error Guide: ‘ERROR: Job failed: exit code 1’ — generic script failures (note this git error is exit 128, not 1).
- GitLab CI Error Guide: script ‘No such file or directory’ — when a needed file is missing because of how the repo was checked out.
- GitLab CI Error Guide: ‘Invalid CI config’ — config rejected before the clone even starts.
Frequently Asked Questions
What does reference is not a tree actually mean?
You asked git to use a commit (via checkout, diff, describe, or a tag) whose object isn’t on disk. Because the runner did a shallow clone, that commit falls outside the fetched depth window. Git knows the SHA but never received the tree object, so it errors out with exit code 128.
How do I do a full (non-shallow) clone in GitLab CI?
Set GIT_DEPTH: "0" — a depth of zero disables shallow cloning and fetches the full history. Put it in a job’s variables: for one job, or at the top level of .gitlab-ci.yml for all jobs. You can also set the project default to 0 under Settings > CI/CD > General pipelines > Git shallow clone depth.
Why does git describe --tags fail in CI but work locally?
Shallow clones don’t fetch tags or older history by default, so git describe can’t find a tag to describe. Either raise GIT_DEPTH (or set it to 0) and fetch tags with git fetch origin "refs/tags/*:refs/tags/*", or run the command after git fetch --unshallow.
What’s the difference between GIT_DEPTH and the project’s clone depth setting?
The project setting (Settings > CI/CD > General pipelines > Git shallow clone depth) is the global default for all pipelines. GIT_DEPTH in .gitlab-ci.yml overrides it per-job or per-pipeline. If jobs fail without an explicit GIT_DEPTH, the project default is too shallow.
My pipeline started failing with this error after a force-push. Why? A force-push rewrote the branch history while the pipeline was running, so the SHA the pipeline was created against no longer exists in the newly fetched range. Re-run the pipeline against the new HEAD, and avoid force-pushing to a ref with active pipelines.
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.